001/*
002 * Copyright 2008-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2015-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tasks;
037
038
039
040import java.util.LinkedList;
041import java.util.List;
042
043import com.unboundid.ldap.sdk.Entry;
044import com.unboundid.ldap.sdk.Filter;
045import com.unboundid.ldap.sdk.LDAPConnection;
046import com.unboundid.ldap.sdk.LDAPException;
047import com.unboundid.ldap.sdk.Modification;
048import com.unboundid.ldap.sdk.ModificationType;
049import com.unboundid.ldap.sdk.ResultCode;
050import com.unboundid.ldap.sdk.SearchResult;
051import com.unboundid.ldap.sdk.SearchResultEntry;
052import com.unboundid.ldap.sdk.SearchScope;
053import com.unboundid.util.Debug;
054import com.unboundid.util.ThreadSafety;
055import com.unboundid.util.ThreadSafetyLevel;
056
057import static com.unboundid.ldap.sdk.unboundidds.tasks.TaskMessages.*;
058
059
060
061/**
062 * This class provides a number of utility methods for interacting with tasks in
063 * Ping Identity, UnboundID, or Nokia/Alcatel-Lucent 8661 server instances.
064 * <BR>
065 * <BLOCKQUOTE>
066 *   <B>NOTE:</B>  This class, and other classes within the
067 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
068 *   supported for use against Ping Identity, UnboundID, and
069 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
070 *   for proprietary functionality or for external specifications that are not
071 *   considered stable or mature enough to be guaranteed to work in an
072 *   interoperable way with other types of LDAP servers.
073 * </BLOCKQUOTE>
074 * <BR>
075 * It provides methods for the following:
076 * <UL>
077 *   <LI>Retrieving information about all scheduled, running, and
078 *       recently-completed tasks in the server.</LI>
079 *   <LI>Retrieving a specific task by its task ID.</LI>
080 *   <LI>Scheduling a new task.</LI>
081 *   <LI>Waiting for a scheduled task to complete.</LI>
082 *   <LI>Canceling a scheduled task.</LI>
083 *   <LI>Deleting a scheduled task.</LI>
084 * </UL>
085 * <H2>Example</H2>
086 * The following example demonstrates the process for retrieving information
087 * about all tasks within the server and printing their contents using the
088 * generic API:
089 * <PRE>
090 * List&lt;Task&gt; allTasks = TaskManager.getTasks(connection);
091 * for (Task task : allTasks)
092 * {
093 *   String taskID = task.getTaskID();
094 *   String taskName = task.getTaskName();
095 *   TaskState taskState = task.getState();
096 *   Map&lt;TaskProperty,List&lt;Object&gt;&gt; taskProperties =
097 *        task.getTaskPropertyValues();
098 * }
099 * </PRE>
100 */
101@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
102public final class TaskManager
103{
104  /**
105   * Prevent this class from being instantiated.
106   */
107  private TaskManager()
108  {
109    // No implementation is required.
110  }
111
112
113
114  /**
115   * Constructs the DN that should be used for the entry with the specified
116   * task ID.
117   *
118   * @param  taskID  The task ID for which to construct the entry DN.
119   *
120   * @return  The constructed task entry DN.
121   */
122  private static String getTaskDN(final String taskID)
123  {
124    // In general, constructing DNs is bad, but we'll do it here because we know
125    // we're dealing specifically with the Ping Identity, UnboundID, or
126    // Nokia/Alcatel-Lucent 8661 Directory Server and we can ensure that this
127    // location will not change without extremely good reasons.
128    return Task.ATTR_TASK_ID + '=' + taskID + ',' +
129           Task.SCHEDULED_TASKS_BASE_DN;
130  }
131
132
133
134  /**
135   * Retrieves the task with the specified task ID using the given connection.
136   *
137   * @param  connection  The connection to the Directory Server from which to
138   *                     retrieve the task.  It must not be {@code null}.
139   * @param  taskID      The task ID for the task to retrieve.  It must not be
140   *                     {@code null}.
141   *
142   * @return  The requested task, or {@code null} if no such task exists in the
143   *          server.  An attempt will be made to instantiate the task as the
144   *          most appropriate task type, but if this is not possible then it
145   *          will be a generic {@code Task} object.
146   *
147   * @throws  LDAPException  If a problem occurs while communicating with the
148   *                         Directory Server over the provided connection.
149   *
150   * @throws  TaskException  If the retrieved entry cannot be parsed as a task.
151   */
152  public static Task getTask(final String taskID,
153                            final LDAPConnection connection)
154         throws LDAPException, TaskException
155  {
156    try
157    {
158      final Entry taskEntry = connection.getEntry(getTaskDN(taskID));
159      if (taskEntry == null)
160      {
161        return null;
162      }
163
164      return Task.decodeTask(taskEntry);
165    }
166    catch (final LDAPException le)
167    {
168      Debug.debugException(le);
169      if (le.getResultCode() == ResultCode.NO_SUCH_OBJECT)
170      {
171        return null;
172      }
173
174      throw le;
175    }
176  }
177
178
179
180  /**
181   * Retrieves all of the tasks defined in the Directory Server using the
182   * provided connection.
183   *
184   * @param  connection  The connection to the Directory Server instance from
185   *                     which to retrieve the defined tasks.
186   *
187   * @return  A list of all tasks defined in the associated Directory Server.
188   *
189   * @throws  LDAPException  If a problem occurs while communicating with the
190   *                         Directory Server over the provided connection.
191   */
192  public static List<Task> getTasks(final LDAPConnection connection)
193         throws LDAPException
194  {
195    final Filter filter =
196         Filter.createEqualityFilter("objectClass", Task.OC_TASK);
197
198    final SearchResult result = connection.search(Task.SCHEDULED_TASKS_BASE_DN,
199         SearchScope.SUB, filter);
200
201    final LinkedList<Task> tasks = new LinkedList<>();
202    for (final SearchResultEntry e : result.getSearchEntries())
203    {
204      try
205      {
206        tasks.add(Task.decodeTask(e));
207      }
208      catch (final TaskException te)
209      {
210        Debug.debugException(te);
211
212        // We got an entry that couldn't be parsed as a task.  This is an error,
213        // but we don't want to spoil the ability to retrieve other tasks that
214        // could be decoded, so we'll just ignore it for now.
215      }
216    }
217
218    return tasks;
219  }
220
221
222
223  /**
224   * Schedules a new instance of the provided task in the Directory Server.
225   *
226   * @param  task        The task to be scheduled.
227   * @param  connection  The connection to the Directory Server in which the
228   *                     task is to be scheduled.
229   *
230   * @return  A {@code Task} object representing the task that was scheduled and
231   *          re-read from the server.
232   *
233   * @throws  LDAPException  If a problem occurs while communicating with the
234   *                         Directory Server, or if it rejects the task.
235   *
236   * @throws  TaskException  If the entry read back from the server after the
237   *                         task was created could not be parsed as a task.
238   */
239  public static Task scheduleTask(final Task task,
240                                  final LDAPConnection connection)
241         throws LDAPException, TaskException
242  {
243    final Entry taskEntry = task.createTaskEntry();
244    connection.add(task.createTaskEntry());
245
246    final Entry newTaskEntry = connection.getEntry(taskEntry.getDN());
247    if (newTaskEntry == null)
248    {
249      // This should never happen.
250      throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
251    }
252
253    return Task.decodeTask(newTaskEntry);
254  }
255
256
257
258  /**
259   * Submits a request to cancel the task with the specified task ID.  Note that
260   * some tasks may not support being canceled.  Further, for tasks that do
261   * support being canceled it may take time for the cancel request to be
262   * processed and for the task to actually be canceled.
263   *
264   * @param  taskID      The task ID of the task to be canceled.
265   * @param  connection  The connection to the Directory Server in which to
266   *                     perform the operation.
267   *
268   * @throws  LDAPException  If a problem occurs while communicating with the
269   *                         Directory Server.
270   */
271  public static void cancelTask(final String taskID,
272                                final LDAPConnection connection)
273         throws LDAPException
274  {
275    // Note:  we should use the CANCELED_BEFORE_STARTING state when we want to
276    // cancel a task regardless of whether it's pending or running.  If the
277    // task is running, the server will convert it to STOPPED_BY_ADMINISTRATOR.
278    final Modification mod =
279         new Modification(ModificationType.REPLACE, Task.ATTR_TASK_STATE,
280                          TaskState.CANCELED_BEFORE_STARTING.getName());
281    connection.modify(getTaskDN(taskID), mod);
282  }
283
284
285
286  /**
287   * Attempts to delete the task with the specified task ID.
288   *
289   * @param  taskID      The task ID of the task to be deleted.
290   * @param  connection  The connection to the Directory Server in which to
291   *                     perform the operation.
292   *
293   * @throws  LDAPException  If a problem occurs while communicating with the
294   *                         Directory Server.
295   */
296  public static void deleteTask(final String taskID,
297                                final LDAPConnection connection)
298         throws LDAPException
299  {
300    connection.delete(getTaskDN(taskID));
301  }
302
303
304
305  /**
306   * Waits for the specified task to complete.
307   *
308   * @param  taskID         The task ID of the task to poll.
309   * @param  connection     The connection to the Directory Server containing
310   *                        the desired task.
311   * @param  pollFrequency  The minimum length of time in milliseconds between
312   *                        checks to see if the task has completed.  A value
313   *                        less than or equal to zero will cause the client to
314   *                        check as quickly as possible.
315   * @param  maxWaitTime    The maximum length of time in milliseconds to wait
316   *                        for the task to complete before giving up.  A value
317   *                        less than or equal to zero indicates that it will
318   *                        keep checking indefinitely until the task has
319   *                        completed.
320   *
321   * @return  Task  The decoded task after it has completed, or after the
322   *                maximum wait time has expired.
323   *
324   * @throws  LDAPException  If a problem occurs while communicating with the
325   *                         Directory Server.
326   *
327   * @throws  TaskException  If a problem occurs while attempting to parse the
328   *                         task entry as a task, or if the specified task
329   *                         entry could not be found.
330   */
331  public static Task waitForTask(final String taskID,
332                                 final LDAPConnection connection,
333                                 final long pollFrequency,
334                                 final long maxWaitTime)
335         throws LDAPException, TaskException
336  {
337    final long stopWaitingTime;
338    if (maxWaitTime > 0)
339    {
340      stopWaitingTime = System.currentTimeMillis() + maxWaitTime;
341    }
342    else
343    {
344      stopWaitingTime = Long.MAX_VALUE;
345    }
346
347    while (true)
348    {
349      final Task t = getTask(taskID, connection);
350      if (t == null)
351      {
352        throw new TaskException(ERR_TASK_MANAGER_WAIT_NO_SUCH_TASK.get(taskID));
353      }
354
355      if (t.isCompleted())
356      {
357        return t;
358      }
359
360      final long timeRemaining = stopWaitingTime - System.currentTimeMillis();
361      if (timeRemaining <= 0)
362      {
363        return t;
364      }
365
366      try
367      {
368        Thread.sleep(Math.min(pollFrequency, timeRemaining));
369      }
370      catch (final InterruptedException ie)
371      {
372        Debug.debugException(ie);
373        Thread.currentThread().interrupt();
374        throw new TaskException(ERR_TASK_MANAGER_WAIT_INTERRUPTED.get(taskID),
375                                ie);
376      }
377    }
378  }
379}