001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2009-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.tools.tasks;
018
019import static org.forgerock.opendj.ldap.ResultCode.*;
020import static org.opends.messages.ToolMessages.*;
021import static org.opends.server.config.ConfigConstants.*;
022
023import java.io.IOException;
024import java.text.SimpleDateFormat;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Date;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.UUID;
031import java.util.concurrent.atomic.AtomicInteger;
032
033import org.forgerock.i18n.LocalizableMessage;
034import org.forgerock.opendj.ldap.ByteString;
035import org.forgerock.opendj.ldap.DecodeException;
036import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
037import org.forgerock.opendj.ldap.ModificationType;
038import org.forgerock.opendj.ldap.SearchScope;
039import org.opends.server.backends.task.FailedDependencyAction;
040import org.opends.server.backends.task.TaskState;
041import org.opends.server.config.ConfigConstants;
042import org.opends.server.protocols.ldap.AddRequestProtocolOp;
043import org.opends.server.protocols.ldap.AddResponseProtocolOp;
044import org.opends.server.protocols.ldap.DeleteRequestProtocolOp;
045import org.opends.server.protocols.ldap.DeleteResponseProtocolOp;
046import org.opends.server.protocols.ldap.LDAPAttribute;
047import org.opends.server.protocols.ldap.LDAPConstants;
048import org.opends.server.protocols.ldap.LDAPFilter;
049import org.opends.server.protocols.ldap.LDAPMessage;
050import org.opends.server.protocols.ldap.LDAPModification;
051import org.opends.server.protocols.ldap.LDAPResultCode;
052import org.opends.server.protocols.ldap.ModifyRequestProtocolOp;
053import org.opends.server.protocols.ldap.ModifyResponseProtocolOp;
054import org.opends.server.protocols.ldap.SearchRequestProtocolOp;
055import org.opends.server.protocols.ldap.SearchResultEntryProtocolOp;
056import org.opends.server.tools.LDAPConnection;
057import org.opends.server.tools.LDAPReader;
058import org.opends.server.tools.LDAPWriter;
059import org.opends.server.types.Control;
060import org.opends.server.types.Entry;
061import org.opends.server.types.LDAPException;
062import org.opends.server.types.RawAttribute;
063import org.opends.server.types.RawModification;
064import org.opends.server.types.SearchResultEntry;
065import org.opends.server.util.StaticUtils;
066
067/**
068 * Helper class for interacting with the task backend on behalf of utilities
069 * that are capable of being scheduled.
070 */
071public class TaskClient {
072
073  /** Connection through which task scheduling will take place. */
074  private LDAPConnection connection;
075  /** Keeps track of message IDs. */
076  private final AtomicInteger nextMessageID = new AtomicInteger(0);
077
078  /**
079   * Creates a new TaskClient for interacting with the task backend remotely.
080   * @param conn for accessing the task backend
081   */
082  public TaskClient(LDAPConnection conn) {
083    this.connection = conn;
084  }
085
086  /**
087   * Returns the ID of the task entry for a given list of task attributes.
088   * @param taskAttributes the task attributes.
089   * @return the ID of the task entry for a given list of task attributes.
090   */
091  public static String getTaskID(List<RawAttribute> taskAttributes)
092  {
093    RawAttribute recurringIDAttr = getAttribute(ATTR_RECURRING_TASK_ID, taskAttributes);
094    if (recurringIDAttr != null) {
095      return recurringIDAttr.getValues().get(0).toString();
096    }
097    RawAttribute taskIDAttr = getAttribute(ATTR_TASK_ID, taskAttributes);
098    return taskIDAttr.getValues().get(0).toString();
099  }
100
101  private static RawAttribute getAttribute(String attrName,
102      List<RawAttribute> taskAttributes)
103  {
104    for (RawAttribute attr : taskAttributes)
105    {
106      if (attr.getAttributeType().equalsIgnoreCase(attrName))
107      {
108        return attr;
109      }
110    }
111    return null;
112  }
113
114  /**
115   * Returns the DN of the task entry for a given list of task attributes.
116   * @param taskAttributes the task attributes.
117   * @return the DN of the task entry for a given list of task attributes.
118   */
119  public static String getTaskDN(List<RawAttribute> taskAttributes)
120  {
121    String entryDN = null;
122    String taskID = getTaskID(taskAttributes);
123    RawAttribute recurringIDAttr = getAttribute(ATTR_RECURRING_TASK_ID,
124        taskAttributes);
125
126    if (recurringIDAttr != null) {
127      entryDN = ATTR_RECURRING_TASK_ID + "=" +
128      taskID + "," + RECURRING_TASK_BASE_RDN + "," + DN_TASK_ROOT;
129    } else {
130      entryDN = ATTR_TASK_ID + "=" + taskID + "," +
131      SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT;
132    }
133    return entryDN;
134  }
135
136  private static boolean isScheduleRecurring(TaskScheduleInformation information)
137  {
138    return information.getRecurringDateTime() != null;
139  }
140
141  /**
142   * This is a commodity method that returns the common attributes (those
143   * related to scheduling) of a task entry for a given
144   * {@link TaskScheduleInformation} object.
145   * @param information the scheduling information.
146   * @return the schedule attributes of the task entry.
147   */
148  public static ArrayList<RawAttribute> getTaskAttributes(
149      TaskScheduleInformation information)
150  {
151    String taskID = null;
152    boolean scheduleRecurring = isScheduleRecurring(information);
153
154    if (scheduleRecurring) {
155      taskID = information.getTaskId();
156      if (taskID == null || taskID.length() == 0) {
157        taskID = information.getTaskClass().getSimpleName() + "-" + UUID.randomUUID();
158      }
159    } else {
160      // Use a formatted time/date for the ID so that is remotely useful
161      SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
162      taskID = df.format(new Date());
163    }
164
165    ArrayList<RawAttribute> attributes = new ArrayList<>();
166
167    ArrayList<String> ocValues = new ArrayList<>(4);
168    ocValues.add("top");
169    ocValues.add(ConfigConstants.OC_TASK);
170    if (scheduleRecurring) {
171      ocValues.add(ConfigConstants.OC_RECURRING_TASK);
172    }
173    ocValues.add(information.getTaskObjectclass());
174    attributes.add(new LDAPAttribute(ATTR_OBJECTCLASS, ocValues));
175
176    if (scheduleRecurring) {
177      attributes.add(new LDAPAttribute(ATTR_RECURRING_TASK_ID, taskID));
178    }
179    attributes.add(new LDAPAttribute(ATTR_TASK_ID, taskID));
180
181    String classValue = information.getTaskClass().getName();
182    attributes.add(new LDAPAttribute(ATTR_TASK_CLASS, classValue));
183
184    // add the start time if necessary
185    Date startDate = information.getStartDateTime();
186    if (startDate != null) {
187      String startTimeString = StaticUtils.formatDateTimeString(startDate);
188      attributes.add(new LDAPAttribute(ATTR_TASK_SCHEDULED_START_TIME, startTimeString));
189    }
190
191    if (scheduleRecurring) {
192      String recurringPatternValues = information.getRecurringDateTime();
193      attributes.add(new LDAPAttribute(ATTR_RECURRING_TASK_SCHEDULE, recurringPatternValues));
194    }
195
196    // add dependency IDs
197    List<String> dependencyIds = information.getDependencyIds();
198    if (dependencyIds != null && !dependencyIds.isEmpty()) {
199      attributes.add(new LDAPAttribute(ATTR_TASK_DEPENDENCY_IDS, dependencyIds));
200
201      // add the dependency action
202      FailedDependencyAction fda = information.getFailedDependencyAction();
203      if (fda == null) {
204        fda = FailedDependencyAction.defaultValue();
205      }
206      attributes.add(new LDAPAttribute(ATTR_TASK_FAILED_DEPENDENCY_ACTION, fda.name()));
207    }
208
209    // add completion notification email addresses
210    List<String> compNotifEmailAddresss = information.getNotifyUponCompletionEmailAddresses();
211    if (compNotifEmailAddresss != null && !compNotifEmailAddresss.isEmpty()) {
212      attributes.add(new LDAPAttribute(ATTR_TASK_NOTIFY_ON_COMPLETION, compNotifEmailAddresss));
213    }
214
215    // add error notification email addresses
216    List<String> errNotifEmailAddresss = information.getNotifyUponErrorEmailAddresses();
217    if (errNotifEmailAddresss != null && !errNotifEmailAddresss.isEmpty()) {
218      attributes.add(new LDAPAttribute(ATTR_TASK_NOTIFY_ON_ERROR, errNotifEmailAddresss));
219    }
220
221    information.addTaskAttributes(attributes);
222
223    return attributes;
224  }
225
226  /**
227   * Schedule a task for execution by writing an entry to the task backend.
228   *
229   * @param information to be scheduled
230   * @return String task ID assigned the new task
231   * @throws IOException if there is a stream communication problem
232   * @throws LDAPException if there is a problem getting information
233   *         out to the directory
234   * @throws DecodeException if there is a problem with the encoding
235   * @throws TaskClientException if there is a problem with the task entry
236   */
237  public synchronized TaskEntry schedule(TaskScheduleInformation information)
238          throws LDAPException, IOException, DecodeException, TaskClientException
239  {
240    LDAPReader reader = connection.getLDAPReader();
241    LDAPWriter writer = connection.getLDAPWriter();
242
243    ArrayList<Control> controls = new ArrayList<>();
244    ArrayList<RawAttribute> attributes = getTaskAttributes(information);
245
246    ByteString entryDN = ByteString.valueOfUtf8(getTaskDN(attributes));
247    AddRequestProtocolOp addRequest = new AddRequestProtocolOp(entryDN, attributes);
248    LDAPMessage requestMessage =
249         new LDAPMessage(nextMessageID.getAndIncrement(), addRequest, controls);
250
251    // Send the request to the server and read the response.
252    LDAPMessage responseMessage;
253    writer.writeMessage(requestMessage);
254
255    responseMessage = reader.readMessage();
256    if (responseMessage == null)
257    {
258      throw new LDAPException(
259              LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
260              ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get());
261    }
262
263    if (responseMessage.getProtocolOpType() !=
264        LDAPConstants.OP_TYPE_ADD_RESPONSE)
265    {
266      throw new LDAPException(
267              LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
268              ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE.get(
269                responseMessage.getProtocolOpName()));
270    }
271
272    AddResponseProtocolOp addResponse =
273         responseMessage.getAddResponseProtocolOp();
274    if (addResponse.getResultCode() != 0) {
275      throw new LDAPException(
276              LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
277              addResponse.getErrorMessage());
278    }
279    return getTaskEntry(getTaskID(attributes));
280  }
281
282  /**
283   * Gets all the ds-task entries from the task root.
284   *
285   * @return list of entries from the task root
286   * @throws IOException if there is a stream communication problem
287   * @throws LDAPException if there is a problem getting information
288   *         out to the directory
289   * @throws DecodeException if there is a problem with the encoding
290   */
291  public synchronized List<TaskEntry> getTaskEntries()
292          throws LDAPException, IOException, DecodeException {
293    List<Entry> entries = new ArrayList<>();
294
295    writeSearch(new SearchRequestProtocolOp(
296        ByteString.valueOfUtf8(ConfigConstants.DN_TASK_ROOT),
297            SearchScope.WHOLE_SUBTREE,
298            DereferenceAliasesPolicy.NEVER,
299            Integer.MAX_VALUE,
300            Integer.MAX_VALUE,
301            false,
302            LDAPFilter.decode("(objectclass=ds-task)"),
303            new LinkedHashSet<String>()));
304
305    LDAPReader reader = connection.getLDAPReader();
306    byte opType;
307    do {
308      LDAPMessage responseMessage = reader.readMessage();
309      if (responseMessage == null) {
310        throw new LDAPException(
311                LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
312                ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get());
313      }
314      opType = responseMessage.getProtocolOpType();
315      if (opType == LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY)
316      {
317        SearchResultEntryProtocolOp searchEntryOp = responseMessage.getSearchResultEntryProtocolOp();
318        SearchResultEntry entry = searchEntryOp.toSearchResultEntry();
319        entries.add(entry);
320      }
321    }
322    while (opType != LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE);
323
324    List<TaskEntry> taskEntries = new ArrayList<>(entries.size());
325    for (Entry entry : entries) {
326      taskEntries.add(new TaskEntry(entry));
327    }
328    return Collections.unmodifiableList(taskEntries);
329  }
330
331  /**
332   * Gets the entry of the task whose ID is <code>id</code> from the directory.
333   *
334   * @param id of the entry to retrieve
335   * @return Entry for the task
336   * @throws IOException if there is a stream communication problem
337   * @throws LDAPException if there is a problem getting information
338   *         out to the directory
339   * @throws DecodeException if there is a problem with the encoding
340   * @throws TaskClientException if there is no task with the requested id
341   */
342  public synchronized TaskEntry getTaskEntry(String id)
343          throws LDAPException, IOException, DecodeException, TaskClientException
344  {
345    Entry entry = null;
346
347    writeSearch(new SearchRequestProtocolOp(
348        ByteString.valueOfUtf8(ConfigConstants.DN_TASK_ROOT),
349            SearchScope.WHOLE_SUBTREE,
350            DereferenceAliasesPolicy.NEVER,
351            Integer.MAX_VALUE,
352            Integer.MAX_VALUE,
353            false,
354            LDAPFilter.decode("(" + ATTR_TASK_ID + "=" + id + ")"),
355            new LinkedHashSet<String>()));
356
357    LDAPReader reader = connection.getLDAPReader();
358    byte opType;
359    do {
360      LDAPMessage responseMessage = reader.readMessage();
361      if (responseMessage == null) {
362        LocalizableMessage message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
363        throw new LDAPException(UNAVAILABLE.intValue(), message);
364      }
365      opType = responseMessage.getProtocolOpType();
366      if (opType == LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY)
367      {
368        SearchResultEntryProtocolOp searchEntryOp = responseMessage.getSearchResultEntryProtocolOp();
369        entry = searchEntryOp.toSearchResultEntry();
370      }
371    }
372    while (opType != LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE);
373    if (entry == null) {
374      throw new TaskClientException(ERR_TASK_CLIENT_UNKNOWN_TASK.get(id));
375    }
376    return new TaskEntry(entry);
377  }
378
379  /**
380   * Changes that the state of the task in the backend to a canceled state.
381   *
382   * @param  id if the task to cancel
383   * @throws IOException if there is a stream communication problem
384   * @throws LDAPException if there is a problem getting information
385   *         out to the directory
386   * @throws DecodeException if there is a problem with the encoding
387   * @throws TaskClientException if there is no task with the requested id
388   */
389  public synchronized void cancelTask(String id)
390          throws TaskClientException, IOException, DecodeException, LDAPException
391  {
392    LDAPReader reader = connection.getLDAPReader();
393    LDAPWriter writer = connection.getLDAPWriter();
394
395    TaskEntry entry = getTaskEntry(id);
396    TaskState state = entry.getTaskState();
397    if (state == null)
398    {
399      throw new TaskClientException(ERR_TASK_CLIENT_TASK_STATE_UNKNOWN.get(id));
400    }
401    if (!TaskState.isDone(state))
402    {
403      cancelNotDoneTask(entry, state, writer, reader);
404    }
405    else if (TaskState.isRecurring(state))
406    {
407      cancelRecurringTask(entry, writer, reader);
408    }
409    else
410    {
411      throw new TaskClientException(ERR_TASK_CLIENT_UNCANCELABLE_TASK.get(id));
412    }
413  }
414
415  private void cancelNotDoneTask(TaskEntry entry, TaskState state, LDAPWriter writer, LDAPReader reader)
416      throws IOException, LDAPException
417  {
418    ByteString dn = ByteString.valueOfUtf8(entry.getDN().toString());
419
420    ArrayList<RawModification> mods = new ArrayList<>();
421
422    String newState;
423    if (TaskState.isPending(state))
424    {
425      newState = TaskState.CANCELED_BEFORE_STARTING.name();
426    }
427    else
428    {
429      newState = TaskState.STOPPED_BY_ADMINISTRATOR.name();
430    }
431    LDAPAttribute attr = new LDAPAttribute(ATTR_TASK_STATE, newState);
432    mods.add(new LDAPModification(ModificationType.REPLACE, attr));
433
434    ModifyRequestProtocolOp modRequest = new ModifyRequestProtocolOp(dn, mods);
435    LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), modRequest, null);
436
437    writer.writeMessage(requestMessage);
438
439    LDAPMessage responseMessage = reader.readMessage();
440    if (responseMessage == null)
441    {
442      LocalizableMessage message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
443      throw new LDAPException(UNAVAILABLE.intValue(), message);
444    }
445    if (responseMessage.getProtocolOpType() != LDAPConstants.OP_TYPE_MODIFY_RESPONSE)
446    {
447      throw new LDAPException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE
448          .get(responseMessage.getProtocolOpName()));
449    }
450
451    ModifyResponseProtocolOp modResponse = responseMessage.getModifyResponseProtocolOp();
452    LocalizableMessage errorMessage = modResponse.getErrorMessage();
453    if (errorMessage != null)
454    {
455      throw new LDAPException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, errorMessage);
456    }
457  }
458
459  private void cancelRecurringTask(TaskEntry entry, LDAPWriter writer, LDAPReader reader)
460      throws IOException, LDAPException
461  {
462    ByteString dn = ByteString.valueOfUtf8(entry.getDN().toString());
463    DeleteRequestProtocolOp deleteRequest = new DeleteRequestProtocolOp(dn);
464    LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), deleteRequest, null);
465    writer.writeMessage(requestMessage);
466
467    LDAPMessage responseMessage = reader.readMessage();
468    if (responseMessage == null)
469    {
470      LocalizableMessage message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
471      throw new LDAPException(UNAVAILABLE.intValue(), message);
472    }
473    if (responseMessage.getProtocolOpType() != LDAPConstants.OP_TYPE_DELETE_RESPONSE)
474    {
475      LocalizableMessage msg = ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE.get(responseMessage.getProtocolOpName());
476      throw new LDAPException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, msg);
477    }
478
479    DeleteResponseProtocolOp deleteResponse = responseMessage.getDeleteResponseProtocolOp();
480    LocalizableMessage errorMessage = deleteResponse.getErrorMessage();
481    if (errorMessage != null)
482    {
483      throw new LDAPException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, errorMessage);
484    }
485  }
486
487  /**
488   * Writes a search to the directory writer.
489   * @param searchRequest to write
490   * @throws IOException if there is a stream communication problem
491   */
492  private void writeSearch(SearchRequestProtocolOp searchRequest)
493          throws IOException {
494    LDAPWriter writer = connection.getLDAPWriter();
495    LDAPMessage requestMessage = new LDAPMessage(
496            nextMessageID.getAndIncrement(),
497            searchRequest,
498            new ArrayList<Control>());
499
500    // Send the request to the server and read the response.
501    writer.writeMessage(requestMessage);
502  }
503}