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-2015 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  /**
074   * Connection through which task scheduling will take place.
075   */
076  protected LDAPConnection connection;
077
078  /**
079   * Keeps track of message IDs.
080   */
081  private final AtomicInteger nextMessageID = new AtomicInteger(0);
082
083  /**
084   * Creates a new TaskClient for interacting with the task backend remotely.
085   * @param conn for accessing the task backend
086   */
087  public TaskClient(LDAPConnection conn) {
088    this.connection = conn;
089  }
090
091  /**
092   * Returns the ID of the task entry for a given list of task attributes.
093   * @param taskAttributes the task attributes.
094   * @return the ID of the task entry for a given list of task attributes.
095   */
096  public static String getTaskID(List<RawAttribute> taskAttributes)
097  {
098    String taskID = null;
099
100    RawAttribute recurringIDAttr = getAttribute(ATTR_RECURRING_TASK_ID,
101        taskAttributes);
102
103    if (recurringIDAttr != null) {
104      taskID = recurringIDAttr.getValues().get(0).toString();
105    } else {
106      RawAttribute taskIDAttr = getAttribute(ATTR_TASK_ID,
107          taskAttributes);
108      taskID = taskIDAttr.getValues().get(0).toString();
109    }
110
111    return taskID;
112  }
113
114  private static RawAttribute getAttribute(String attrName,
115      List<RawAttribute> taskAttributes)
116  {
117    for (RawAttribute attr : taskAttributes)
118    {
119      if (attr.getAttributeType().equalsIgnoreCase(attrName))
120      {
121        return attr;
122      }
123    }
124    return null;
125  }
126
127  /**
128   * Returns the DN of the task entry for a given list of task attributes.
129   * @param taskAttributes the task attributes.
130   * @return the DN of the task entry for a given list of task attributes.
131   */
132  public static String getTaskDN(List<RawAttribute> taskAttributes)
133  {
134    String entryDN = null;
135    String taskID = getTaskID(taskAttributes);
136    RawAttribute recurringIDAttr = getAttribute(ATTR_RECURRING_TASK_ID,
137        taskAttributes);
138
139    if (recurringIDAttr != null) {
140      entryDN = ATTR_RECURRING_TASK_ID + "=" +
141      taskID + "," + RECURRING_TASK_BASE_RDN + "," + DN_TASK_ROOT;
142    } else {
143      entryDN = ATTR_TASK_ID + "=" + taskID + "," +
144      SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT;
145    }
146    return entryDN;
147  }
148
149  private static boolean isScheduleRecurring(TaskScheduleInformation information)
150  {
151    return information.getRecurringDateTime() != null;
152  }
153
154  /**
155   * This is a commodity method that returns the common attributes (those
156   * related to scheduling) of a task entry for a given
157   * {@link TaskScheduleInformation} object.
158   * @param information the scheduling information.
159   * @return the schedule attributes of the task entry.
160   */
161  public static ArrayList<RawAttribute> getTaskAttributes(
162      TaskScheduleInformation information)
163  {
164    String taskID = null;
165    boolean scheduleRecurring = isScheduleRecurring(information);
166
167    if (scheduleRecurring) {
168      taskID = information.getTaskId();
169      if (taskID == null || taskID.length() == 0) {
170        taskID = information.getTaskClass().getSimpleName() + "-" + UUID.randomUUID();
171      }
172    } else {
173      // Use a formatted time/date for the ID so that is remotely useful
174      SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
175      taskID = df.format(new Date());
176    }
177
178    ArrayList<RawAttribute> attributes = new ArrayList<>();
179
180    ArrayList<String> ocValues = new ArrayList<>(4);
181    ocValues.add("top");
182    ocValues.add(ConfigConstants.OC_TASK);
183    if (scheduleRecurring) {
184      ocValues.add(ConfigConstants.OC_RECURRING_TASK);
185    }
186    ocValues.add(information.getTaskObjectclass());
187    attributes.add(new LDAPAttribute(ATTR_OBJECTCLASS, ocValues));
188
189    if (scheduleRecurring) {
190      attributes.add(new LDAPAttribute(ATTR_RECURRING_TASK_ID, taskID));
191    }
192    attributes.add(new LDAPAttribute(ATTR_TASK_ID, taskID));
193
194    String classValue = information.getTaskClass().getName();
195    attributes.add(new LDAPAttribute(ATTR_TASK_CLASS, classValue));
196
197    // add the start time if necessary
198    Date startDate = information.getStartDateTime();
199    if (startDate != null) {
200      String startTimeString = StaticUtils.formatDateTimeString(startDate);
201      attributes.add(new LDAPAttribute(ATTR_TASK_SCHEDULED_START_TIME, startTimeString));
202    }
203
204    if (scheduleRecurring) {
205      String recurringPatternValues = information.getRecurringDateTime();
206      attributes.add(new LDAPAttribute(ATTR_RECURRING_TASK_SCHEDULE, recurringPatternValues));
207    }
208
209    // add dependency IDs
210    List<String> dependencyIds = information.getDependencyIds();
211    if (dependencyIds != null && !dependencyIds.isEmpty()) {
212      attributes.add(new LDAPAttribute(ATTR_TASK_DEPENDENCY_IDS, dependencyIds));
213
214      // add the dependency action
215      FailedDependencyAction fda = information.getFailedDependencyAction();
216      if (fda == null) {
217        fda = FailedDependencyAction.defaultValue();
218      }
219      attributes.add(new LDAPAttribute(ATTR_TASK_FAILED_DEPENDENCY_ACTION, fda.name()));
220    }
221
222    // add completion notification email addresses
223    List<String> compNotifEmailAddresss = information.getNotifyUponCompletionEmailAddresses();
224    if (compNotifEmailAddresss != null && !compNotifEmailAddresss.isEmpty()) {
225      attributes.add(new LDAPAttribute(ATTR_TASK_NOTIFY_ON_COMPLETION, compNotifEmailAddresss));
226    }
227
228    // add error notification email addresses
229    List<String> errNotifEmailAddresss = information.getNotifyUponErrorEmailAddresses();
230    if (errNotifEmailAddresss != null && !errNotifEmailAddresss.isEmpty()) {
231      attributes.add(new LDAPAttribute(ATTR_TASK_NOTIFY_ON_ERROR, errNotifEmailAddresss));
232    }
233
234    information.addTaskAttributes(attributes);
235
236    return attributes;
237  }
238
239  /**
240   * Schedule a task for execution by writing an entry to the task backend.
241   *
242   * @param information to be scheduled
243   * @return String task ID assigned the new task
244   * @throws IOException if there is a stream communication problem
245   * @throws LDAPException if there is a problem getting information
246   *         out to the directory
247   * @throws DecodeException if there is a problem with the encoding
248   * @throws TaskClientException if there is a problem with the task entry
249   */
250  public synchronized TaskEntry schedule(TaskScheduleInformation information)
251          throws LDAPException, IOException, DecodeException, TaskClientException
252  {
253    LDAPReader reader = connection.getLDAPReader();
254    LDAPWriter writer = connection.getLDAPWriter();
255
256    ArrayList<Control> controls = new ArrayList<>();
257    ArrayList<RawAttribute> attributes = getTaskAttributes(information);
258
259    ByteString entryDN = ByteString.valueOfUtf8(getTaskDN(attributes));
260    AddRequestProtocolOp addRequest = new AddRequestProtocolOp(entryDN, attributes);
261    LDAPMessage requestMessage =
262         new LDAPMessage(nextMessageID.getAndIncrement(), addRequest, controls);
263
264    // Send the request to the server and read the response.
265    LDAPMessage responseMessage;
266    writer.writeMessage(requestMessage);
267
268    responseMessage = reader.readMessage();
269    if (responseMessage == null)
270    {
271      throw new LDAPException(
272              LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
273              ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get());
274    }
275
276    if (responseMessage.getProtocolOpType() !=
277        LDAPConstants.OP_TYPE_ADD_RESPONSE)
278    {
279      throw new LDAPException(
280              LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
281              ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE.get(
282                responseMessage.getProtocolOpName()));
283    }
284
285    AddResponseProtocolOp addResponse =
286         responseMessage.getAddResponseProtocolOp();
287    if (addResponse.getResultCode() != 0) {
288      throw new LDAPException(
289              LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
290              addResponse.getErrorMessage());
291    }
292    return getTaskEntry(getTaskID(attributes));
293  }
294
295  /**
296   * Gets all the ds-task entries from the task root.
297   *
298   * @return list of entries from the task root
299   * @throws IOException if there is a stream communication problem
300   * @throws LDAPException if there is a problem getting information
301   *         out to the directory
302   * @throws DecodeException if there is a problem with the encoding
303   */
304  public synchronized List<TaskEntry> getTaskEntries()
305          throws LDAPException, IOException, DecodeException {
306    List<Entry> entries = new ArrayList<>();
307
308    writeSearch(new SearchRequestProtocolOp(
309        ByteString.valueOfUtf8(ConfigConstants.DN_TASK_ROOT),
310            SearchScope.WHOLE_SUBTREE,
311            DereferenceAliasesPolicy.NEVER,
312            Integer.MAX_VALUE,
313            Integer.MAX_VALUE,
314            false,
315            LDAPFilter.decode("(objectclass=ds-task)"),
316            new LinkedHashSet<String>()));
317
318    LDAPReader reader = connection.getLDAPReader();
319    byte opType;
320    do {
321      LDAPMessage responseMessage = reader.readMessage();
322      if (responseMessage == null) {
323        throw new LDAPException(
324                LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
325                ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get());
326      } else {
327        opType = responseMessage.getProtocolOpType();
328        if (opType == LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY) {
329          SearchResultEntryProtocolOp searchEntryOp =
330                  responseMessage.getSearchResultEntryProtocolOp();
331          SearchResultEntry entry = searchEntryOp.toSearchResultEntry();
332          entries.add(entry);
333        }
334      }
335    }
336    while (opType != LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE);
337    List<TaskEntry> taskEntries = new ArrayList<>(entries.size());
338    for (Entry entry : entries) {
339      taskEntries.add(new TaskEntry(entry));
340    }
341    return Collections.unmodifiableList(taskEntries);
342  }
343
344  /**
345   * Gets the entry of the task whose ID is <code>id</code> from the directory.
346   *
347   * @param id of the entry to retrieve
348   * @return Entry for the task
349   * @throws IOException if there is a stream communication problem
350   * @throws LDAPException if there is a problem getting information
351   *         out to the directory
352   * @throws DecodeException if there is a problem with the encoding
353   * @throws TaskClientException if there is no task with the requested id
354   */
355  public synchronized TaskEntry getTaskEntry(String id)
356          throws LDAPException, IOException, DecodeException, TaskClientException
357  {
358    Entry entry = null;
359
360    writeSearch(new SearchRequestProtocolOp(
361        ByteString.valueOfUtf8(ConfigConstants.DN_TASK_ROOT),
362            SearchScope.WHOLE_SUBTREE,
363            DereferenceAliasesPolicy.NEVER,
364            Integer.MAX_VALUE,
365            Integer.MAX_VALUE,
366            false,
367            LDAPFilter.decode("(" + ATTR_TASK_ID + "=" + id + ")"),
368            new LinkedHashSet<String>()));
369
370    LDAPReader reader = connection.getLDAPReader();
371    byte opType;
372    do {
373      LDAPMessage responseMessage = reader.readMessage();
374      if (responseMessage == null) {
375        LocalizableMessage message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
376        throw new LDAPException(UNAVAILABLE.intValue(), message);
377      } else {
378        opType = responseMessage.getProtocolOpType();
379        if (opType == LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY) {
380          SearchResultEntryProtocolOp searchEntryOp =
381                  responseMessage.getSearchResultEntryProtocolOp();
382          entry = searchEntryOp.toSearchResultEntry();
383        }
384      }
385    }
386    while (opType != LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE);
387    if (entry == null) {
388      throw new TaskClientException(ERR_TASK_CLIENT_UNKNOWN_TASK.get(id));
389    }
390    return new TaskEntry(entry);
391  }
392
393
394  /**
395   * Changes that the state of the task in the backend to a canceled state.
396   *
397   * @param  id if the task to cancel
398   * @throws IOException if there is a stream communication problem
399   * @throws LDAPException if there is a problem getting information
400   *         out to the directory
401   * @throws DecodeException if there is a problem with the encoding
402   * @throws TaskClientException if there is no task with the requested id
403   */
404  public synchronized void cancelTask(String id)
405          throws TaskClientException, IOException, DecodeException, LDAPException
406  {
407    LDAPReader reader = connection.getLDAPReader();
408    LDAPWriter writer = connection.getLDAPWriter();
409
410    TaskEntry entry = getTaskEntry(id);
411    TaskState state = entry.getTaskState();
412    if (state != null) {
413      if (!TaskState.isDone(state)) {
414
415        ByteString dn = ByteString.valueOfUtf8(entry.getDN().toString());
416
417        ArrayList<RawModification> mods = new ArrayList<>();
418
419        String newState;
420        if (TaskState.isPending(state)) {
421          newState = TaskState.CANCELED_BEFORE_STARTING.name();
422        } else {
423          newState = TaskState.STOPPED_BY_ADMINISTRATOR.name();
424        }
425        LDAPAttribute attr = new LDAPAttribute(ATTR_TASK_STATE, newState);
426        mods.add(new LDAPModification(ModificationType.REPLACE, attr));
427
428        ModifyRequestProtocolOp modRequest =
429                new ModifyRequestProtocolOp(dn, mods);
430        LDAPMessage requestMessage =
431             new LDAPMessage(nextMessageID.getAndIncrement(), modRequest, null);
432
433        writer.writeMessage(requestMessage);
434
435        LDAPMessage responseMessage = reader.readMessage();
436
437        if (responseMessage == null) {
438          LocalizableMessage message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
439          throw new LDAPException(UNAVAILABLE.intValue(), message);
440        }
441
442        if (responseMessage.getProtocolOpType() !=
443                LDAPConstants.OP_TYPE_MODIFY_RESPONSE)
444        {
445          throw new LDAPException(
446                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
447                  ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE.get(
448                    responseMessage.getProtocolOpName()));
449        }
450
451        ModifyResponseProtocolOp modResponse =
452                responseMessage.getModifyResponseProtocolOp();
453        LocalizableMessage errorMessage = modResponse.getErrorMessage();
454        if (errorMessage != null) {
455          throw new LDAPException(
456                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
457                  errorMessage);
458        }
459      } else if (TaskState.isRecurring(state)) {
460
461        ByteString dn = ByteString.valueOfUtf8(entry.getDN().toString());
462        DeleteRequestProtocolOp deleteRequest =
463          new DeleteRequestProtocolOp(dn);
464
465        LDAPMessage requestMessage = new LDAPMessage(
466          nextMessageID.getAndIncrement(), deleteRequest, null);
467
468        writer.writeMessage(requestMessage);
469
470        LDAPMessage responseMessage = reader.readMessage();
471
472        if (responseMessage == null) {
473          LocalizableMessage message = ERR_TASK_CLIENT_UNEXPECTED_CONNECTION_CLOSURE.get();
474          throw new LDAPException(UNAVAILABLE.intValue(), message);
475        }
476
477        if (responseMessage.getProtocolOpType() !=
478                LDAPConstants.OP_TYPE_DELETE_RESPONSE)
479        {
480          throw new LDAPException(
481                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
482                  ERR_TASK_CLIENT_INVALID_RESPONSE_TYPE.get(
483                    responseMessage.getProtocolOpName()));
484        }
485
486        DeleteResponseProtocolOp deleteResponse =
487                responseMessage.getDeleteResponseProtocolOp();
488        LocalizableMessage errorMessage = deleteResponse.getErrorMessage();
489        if (errorMessage != null) {
490          throw new LDAPException(
491                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
492                  errorMessage);
493        }
494      } else {
495        throw new TaskClientException(
496                ERR_TASK_CLIENT_UNCANCELABLE_TASK.get(id));
497      }
498    } else {
499      throw new TaskClientException(
500              ERR_TASK_CLIENT_TASK_STATE_UNKNOWN.get(id));
501    }
502  }
503
504
505  /**
506   * Writes a search to the directory writer.
507   * @param searchRequest to write
508   * @throws IOException if there is a stream communication problem
509   */
510  private void writeSearch(SearchRequestProtocolOp searchRequest)
511          throws IOException {
512    LDAPWriter writer = connection.getLDAPWriter();
513    LDAPMessage requestMessage = new LDAPMessage(
514            nextMessageID.getAndIncrement(),
515            searchRequest,
516            new ArrayList<Control>());
517
518    // Send the request to the server and read the response.
519    writer.writeMessage(requestMessage);
520  }
521
522}