001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-2024 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) 2008-2024 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.LDAPInterface;
047import com.unboundid.ldap.sdk.LDAPException;
048import com.unboundid.ldap.sdk.Modification;
049import com.unboundid.ldap.sdk.ModificationType;
050import com.unboundid.ldap.sdk.ResultCode;
051import com.unboundid.ldap.sdk.SearchResult;
052import com.unboundid.ldap.sdk.SearchResultEntry;
053import com.unboundid.ldap.sdk.SearchScope;
054import com.unboundid.util.Debug;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.Nullable;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059
060import static com.unboundid.ldap.sdk.unboundidds.tasks.TaskMessages.*;
061
062
063
064/**
065 * This class provides a number of utility methods for interacting with tasks in
066 * Ping Identity, UnboundID, or Nokia/Alcatel-Lucent 8661 server instances.
067 * <BR>
068 * <BLOCKQUOTE>
069 *   <B>NOTE:</B>  This class, and other classes within the
070 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
071 *   supported for use against Ping Identity, UnboundID, and
072 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
073 *   for proprietary functionality or for external specifications that are not
074 *   considered stable or mature enough to be guaranteed to work in an
075 *   interoperable way with other types of LDAP servers.
076 * </BLOCKQUOTE>
077 * <BR>
078 * It provides methods for the following:
079 * <UL>
080 *   <LI>Retrieving information about all scheduled, running, and
081 *       recently-completed tasks in the server.</LI>
082 *   <LI>Retrieving a specific task by its task ID.</LI>
083 *   <LI>Scheduling a new task.</LI>
084 *   <LI>Waiting for a scheduled task to complete.</LI>
085 *   <LI>Canceling a scheduled task.</LI>
086 *   <LI>Deleting a scheduled task.</LI>
087 * </UL>
088 * <H2>Example</H2>
089 * The following example demonstrates the process for retrieving information
090 * about all tasks within the server and printing their contents using the
091 * generic API:
092 * <PRE>
093 * List&lt;Task&gt; allTasks = TaskManager.getTasks(connection);
094 * for (Task task : allTasks)
095 * {
096 *   String taskID = task.getTaskID();
097 *   String taskName = task.getTaskName();
098 *   TaskState taskState = task.getState();
099 *   Map&lt;TaskProperty,List&lt;Object&gt;&gt; taskProperties =
100 *        task.getTaskPropertyValues();
101 * }
102 * </PRE>
103 */
104@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
105public final class TaskManager
106{
107  /**
108   * Prevent this class from being instantiated.
109   */
110  private TaskManager()
111  {
112    // No implementation is required.
113  }
114
115
116
117  /**
118   * Constructs the DN that should be used for the entry with the specified
119   * task ID.
120   *
121   * @param  taskID  The task ID for which to construct the entry DN.
122   *
123   * @return  The constructed task entry DN.
124   */
125  @NotNull()
126  private static String getTaskDN(@NotNull final String taskID)
127  {
128    // In general, constructing DNs is bad, but we'll do it here because we know
129    // we're dealing specifically with the Ping Identity, UnboundID, or
130    // Nokia/Alcatel-Lucent 8661 Directory Server and we can ensure that this
131    // location will not change without extremely good reasons.
132    return Task.ATTR_TASK_ID + '=' + taskID + ',' +
133           Task.SCHEDULED_TASKS_BASE_DN;
134  }
135
136
137
138  /**
139   * Retrieves the task with the specified task ID using the given connection.
140   *
141   * @param  connection  The connection to the Directory Server from which to
142   *                     retrieve the task.  It must not be {@code null}.
143   * @param  taskID      The task ID for the task to retrieve.  It must not be
144   *                     {@code null}.
145   *
146   * @return  The requested task, or {@code null} if no such task exists in the
147   *          server.  An attempt will be made to instantiate the task as the
148   *          most appropriate task type, but if this is not possible then it
149   *          will be a generic {@code Task} object.
150   *
151   * @throws  LDAPException  If a problem occurs while communicating with the
152   *                         Directory Server over the provided connection.
153   *
154   * @throws  TaskException  If the retrieved entry cannot be parsed as a task.
155   */
156  @Nullable()
157  public static Task getTask(@NotNull final String taskID,
158                             @NotNull final LDAPConnection connection)
159         throws LDAPException, TaskException
160  {
161    return getTask(taskID, (LDAPInterface) connection);
162  }
163
164
165
166  /**
167   * Retrieves the task with the specified task ID using the given connection.
168   *
169   * @param  connection  The connection to the Directory Server from which to
170   *                     retrieve the task.  It must not be {@code null}.
171   * @param  taskID      The task ID for the task to retrieve.  It must not be
172   *                     {@code null}.
173   *
174   * @return  The requested task, or {@code null} if no such task exists in the
175   *          server.  An attempt will be made to instantiate the task as the
176   *          most appropriate task type, but if this is not possible then it
177   *          will be a generic {@code Task} object.
178   *
179   * @throws  LDAPException  If a problem occurs while communicating with the
180   *                         Directory Server over the provided connection.
181   *
182   * @throws  TaskException  If the retrieved entry cannot be parsed as a task.
183   */
184  @Nullable()
185  public static Task getTask(@NotNull final String taskID,
186                             @NotNull final LDAPInterface connection)
187         throws LDAPException, TaskException
188  {
189    try
190    {
191      final Entry taskEntry = connection.getEntry(getTaskDN(taskID));
192      if (taskEntry == null)
193      {
194        return null;
195      }
196
197      return Task.decodeTask(taskEntry);
198    }
199    catch (final LDAPException le)
200    {
201      Debug.debugException(le);
202      if (le.getResultCode() == ResultCode.NO_SUCH_OBJECT)
203      {
204        return null;
205      }
206
207      throw le;
208    }
209  }
210
211
212
213  /**
214   * Retrieves all of the tasks defined in the Directory Server using the
215   * provided connection.
216   *
217   * @param  connection  The connection to the Directory Server instance from
218   *                     which to retrieve the defined tasks.
219   *
220   * @return  A list of all tasks defined in the associated Directory Server.
221   *
222   * @throws  LDAPException  If a problem occurs while communicating with the
223   *                         Directory Server over the provided connection.
224   */
225  @NotNull()
226  public static List<Task> getTasks(@NotNull final LDAPConnection connection)
227         throws LDAPException
228  {
229    return getTasks((LDAPInterface) connection);
230  }
231
232
233
234  /**
235   * Retrieves all of the tasks defined in the Directory Server using the
236   * provided connection.
237   *
238   * @param  connection  The connection to the Directory Server instance from
239   *                     which to retrieve the defined tasks.
240   *
241   * @return  A list of all tasks defined in the associated Directory Server.
242   *
243   * @throws  LDAPException  If a problem occurs while communicating with the
244   *                         Directory Server over the provided connection.
245   */
246  @NotNull()
247  public static List<Task> getTasks(@NotNull final LDAPInterface connection)
248         throws LDAPException
249  {
250    final Filter filter =
251         Filter.createEqualityFilter("objectClass", Task.OC_TASK);
252
253    final SearchResult result = connection.search(Task.SCHEDULED_TASKS_BASE_DN,
254         SearchScope.SUB, filter);
255
256    final LinkedList<Task> tasks = new LinkedList<>();
257    for (final SearchResultEntry e : result.getSearchEntries())
258    {
259      try
260      {
261        tasks.add(Task.decodeTask(e));
262      }
263      catch (final TaskException te)
264      {
265        Debug.debugException(te);
266
267        // We got an entry that couldn't be parsed as a task.  This is an error,
268        // but we don't want to spoil the ability to retrieve other tasks that
269        // could be decoded, so we'll just ignore it for now.
270      }
271    }
272
273    return tasks;
274  }
275
276
277
278  /**
279   * Schedules a new instance of the provided task in the Directory Server.
280   *
281   * @param  task        The task to be scheduled.
282   * @param  connection  The connection to the Directory Server in which the
283   *                     task is to be scheduled.
284   *
285   * @return  A {@code Task} object representing the task that was scheduled and
286   *          re-read from the server.
287   *
288   * @throws  LDAPException  If a problem occurs while communicating with the
289   *                         Directory Server, or if it rejects the task.
290   *
291   * @throws  TaskException  If the entry read back from the server after the
292   *                         task was created could not be parsed as a task.
293   */
294  @NotNull()
295  public static Task scheduleTask(@NotNull final Task task,
296                                  @NotNull final LDAPConnection connection)
297         throws LDAPException, TaskException
298  {
299    return scheduleTask(task, (LDAPInterface) connection);
300  }
301
302
303
304  /**
305   * Schedules a new instance of the provided task in the Directory Server.
306   *
307   * @param  task        The task to be scheduled.
308   * @param  connection  The connection to the Directory Server in which the
309   *                     task is to be scheduled.
310   *
311   * @return  A {@code Task} object representing the task that was scheduled and
312   *          re-read from the server.
313   *
314   * @throws  LDAPException  If a problem occurs while communicating with the
315   *                         Directory Server, or if it rejects the task.
316   *
317   * @throws  TaskException  If the entry read back from the server after the
318   *                         task was created could not be parsed as a task.
319   */
320  @NotNull()
321  public static Task scheduleTask(@NotNull final Task task,
322                                  @NotNull final LDAPInterface connection)
323         throws LDAPException, TaskException
324  {
325    final Entry taskEntry = task.createTaskEntry();
326    connection.add(task.createTaskEntry());
327
328    final Entry newTaskEntry = connection.getEntry(taskEntry.getDN());
329    if (newTaskEntry == null)
330    {
331      // This should never happen.
332      throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
333    }
334
335    return Task.decodeTask(newTaskEntry);
336  }
337
338
339
340  /**
341   * Submits a request to cancel the task with the specified task ID.  Note that
342   * some tasks may not support being canceled.  Further, for tasks that do
343   * support being canceled it may take time for the cancel request to be
344   * processed and for the task to actually be canceled.
345   *
346   * @param  taskID      The task ID of the task to be canceled.
347   * @param  connection  The connection to the Directory Server in which to
348   *                     perform the operation.
349   *
350   * @throws  LDAPException  If a problem occurs while communicating with the
351   *                         Directory Server.
352   */
353  public static void cancelTask(@NotNull final String taskID,
354                                @NotNull final LDAPConnection connection)
355         throws LDAPException
356  {
357    cancelTask(taskID, (LDAPInterface) connection);
358  }
359
360
361
362  /**
363   * Submits a request to cancel the task with the specified task ID.  Note that
364   * some tasks may not support being canceled.  Further, for tasks that do
365   * support being canceled it may take time for the cancel request to be
366   * processed and for the task to actually be canceled.
367   *
368   * @param  taskID      The task ID of the task to be canceled.
369   * @param  connection  The connection to the Directory Server in which to
370   *                     perform the operation.
371   *
372   * @throws  LDAPException  If a problem occurs while communicating with the
373   *                         Directory Server.
374   */
375  public static void cancelTask(@NotNull final String taskID,
376                                @NotNull final LDAPInterface connection)
377         throws LDAPException
378  {
379    // Note:  we should use the CANCELED_BEFORE_STARTING state when we want to
380    // cancel a task regardless of whether it's pending or running.  If the
381    // task is running, the server will convert it to STOPPED_BY_ADMINISTRATOR.
382    final Modification mod =
383         new Modification(ModificationType.REPLACE, Task.ATTR_TASK_STATE,
384                          TaskState.CANCELED_BEFORE_STARTING.getName());
385    connection.modify(getTaskDN(taskID), mod);
386  }
387
388
389
390  /**
391   * Attempts to delete the task with the specified task ID.
392   *
393   * @param  taskID      The task ID of the task to be deleted.
394   * @param  connection  The connection to the Directory Server in which to
395   *                     perform the operation.
396   *
397   * @throws  LDAPException  If a problem occurs while communicating with the
398   *                         Directory Server.
399   */
400  public static void deleteTask(@NotNull final String taskID,
401                                @NotNull final LDAPConnection connection)
402         throws LDAPException
403  {
404    deleteTask(taskID, (LDAPInterface) connection);
405  }
406
407
408
409  /**
410   * Attempts to delete the task with the specified task ID.
411   *
412   * @param  taskID      The task ID of the task to be deleted.
413   * @param  connection  The connection to the Directory Server in which to
414   *                     perform the operation.
415   *
416   * @throws  LDAPException  If a problem occurs while communicating with the
417   *                         Directory Server.
418   */
419  public static void deleteTask(@NotNull final String taskID,
420                                @NotNull final LDAPInterface connection)
421         throws LDAPException
422  {
423    connection.delete(getTaskDN(taskID));
424  }
425
426
427
428  /**
429   * Waits for the specified task to complete.
430   *
431   * @param  taskID         The task ID of the task to poll.
432   * @param  connection     The connection to the Directory Server containing
433   *                        the desired task.
434   * @param  pollFrequency  The minimum length of time in milliseconds between
435   *                        checks to see if the task has completed.  A value
436   *                        less than or equal to zero will cause the client to
437   *                        check as quickly as possible.
438   * @param  maxWaitTime    The maximum length of time in milliseconds to wait
439   *                        for the task to complete before giving up.  A value
440   *                        less than or equal to zero indicates that it will
441   *                        keep checking indefinitely until the task has
442   *                        completed.
443   *
444   * @return  Task  The decoded task after it has completed, or after the
445   *                maximum wait time has expired.
446   *
447   * @throws  LDAPException  If a problem occurs while communicating with the
448   *                         Directory Server.
449   *
450   * @throws  TaskException  If a problem occurs while attempting to parse the
451   *                         task entry as a task, or if the specified task
452   *                         entry could not be found.
453   */
454  @NotNull()
455  public static Task waitForTask(@NotNull final String taskID,
456                                 @NotNull final LDAPConnection connection,
457                                 final long pollFrequency,
458                                 final long maxWaitTime)
459         throws LDAPException, TaskException
460  {
461    return waitForTask(taskID, (LDAPInterface) connection, pollFrequency,
462         maxWaitTime);
463  }
464
465
466
467  /**
468   * Waits for the specified task to complete.
469   *
470   * @param  taskID         The task ID of the task to poll.
471   * @param  connection     The connection to the Directory Server containing
472   *                        the desired task.
473   * @param  pollFrequency  The minimum length of time in milliseconds between
474   *                        checks to see if the task has completed.  A value
475   *                        less than or equal to zero will cause the client to
476   *                        check as quickly as possible.
477   * @param  maxWaitTime    The maximum length of time in milliseconds to wait
478   *                        for the task to complete before giving up.  A value
479   *                        less than or equal to zero indicates that it will
480   *                        keep checking indefinitely until the task has
481   *                        completed.
482   *
483   * @return  Task  The decoded task after it has completed, or after the
484   *                maximum wait time has expired.
485   *
486   * @throws  LDAPException  If a problem occurs while communicating with the
487   *                         Directory Server.
488   *
489   * @throws  TaskException  If a problem occurs while attempting to parse the
490   *                         task entry as a task, or if the specified task
491   *                         entry could not be found.
492   */
493  @NotNull()
494  public static Task waitForTask(@NotNull final String taskID,
495                                 @NotNull final LDAPInterface connection,
496                                 final long pollFrequency,
497                                 final long maxWaitTime)
498         throws LDAPException, TaskException
499  {
500    final long stopWaitingTime;
501    if (maxWaitTime > 0)
502    {
503      stopWaitingTime = System.currentTimeMillis() + maxWaitTime;
504    }
505    else
506    {
507      stopWaitingTime = Long.MAX_VALUE;
508    }
509
510    while (true)
511    {
512      final Task t = getTask(taskID, connection);
513      if (t == null)
514      {
515        throw new TaskException(ERR_TASK_MANAGER_WAIT_NO_SUCH_TASK.get(taskID));
516      }
517
518      if (t.isCompleted())
519      {
520        return t;
521      }
522
523      final long timeRemaining = stopWaitingTime - System.currentTimeMillis();
524      if (timeRemaining <= 0)
525      {
526        return t;
527      }
528
529      try
530      {
531        Thread.sleep(Math.min(pollFrequency, timeRemaining));
532      }
533      catch (final InterruptedException ie)
534      {
535        Debug.debugException(ie);
536        Thread.currentThread().interrupt();
537        throw new TaskException(ERR_TASK_MANAGER_WAIT_INTERRUPTED.get(taskID),
538                                ie);
539      }
540    }
541  }
542}