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 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.backends.task;
018
019import static org.forgerock.util.Reject.*;
020import static org.opends.messages.BackendMessages.*;
021import static org.opends.server.config.ConfigConstants.*;
022import static org.opends.server.util.StaticUtils.*;
023
024import java.io.File;
025import java.io.FileFilter;
026import java.net.InetAddress;
027import java.nio.file.Path;
028import java.util.Collections;
029import java.util.GregorianCalendar;
030import java.util.Iterator;
031import java.util.List;
032import java.util.ListIterator;
033import java.util.Set;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.forgerock.opendj.config.server.ConfigChangeResult;
038import org.forgerock.opendj.config.server.ConfigException;
039import org.forgerock.opendj.ldap.ByteString;
040import org.forgerock.opendj.ldap.ConditionResult;
041import org.forgerock.opendj.ldap.DN;
042import org.forgerock.opendj.ldap.ModificationType;
043import org.forgerock.opendj.ldap.ResultCode;
044import org.forgerock.opendj.ldap.SearchScope;
045import org.forgerock.opendj.ldap.schema.AttributeType;
046import org.forgerock.util.Reject;
047import org.opends.server.admin.server.ConfigurationChangeListener;
048import org.opends.server.admin.std.server.TaskBackendCfg;
049import org.opends.server.api.Backend;
050import org.opends.server.api.Backupable;
051import org.opends.server.config.ConfigEntry;
052import org.opends.server.core.AddOperation;
053import org.opends.server.core.DeleteOperation;
054import org.opends.server.core.DirectoryServer;
055import org.opends.server.core.ModifyDNOperation;
056import org.opends.server.core.ModifyOperation;
057import org.opends.server.core.SearchOperation;
058import org.opends.server.core.ServerContext;
059import org.opends.server.types.Attribute;
060import org.opends.server.types.BackupConfig;
061import org.opends.server.types.BackupDirectory;
062import org.opends.server.types.CanceledOperationException;
063import org.opends.server.types.DirectoryException;
064import org.opends.server.types.Entry;
065import org.opends.server.types.IndexType;
066import org.opends.server.types.InitializationException;
067import org.opends.server.types.LDIFExportConfig;
068import org.opends.server.types.LDIFImportConfig;
069import org.opends.server.types.LDIFImportResult;
070import org.opends.server.types.LockManager.DNLock;
071import org.opends.server.types.Modification;
072import org.opends.server.types.RestoreConfig;
073import org.opends.server.types.SearchFilter;
074import org.opends.server.util.BackupManager;
075import org.opends.server.util.LDIFException;
076import org.opends.server.util.LDIFReader;
077import org.opends.server.util.LDIFWriter;
078import org.opends.server.util.StaticUtils;
079
080/**
081 * This class provides an implementation of a Directory Server backend that may
082 * be used to execute various kinds of administrative tasks on a one-time or
083 * recurring basis.
084 */
085public class TaskBackend
086       extends Backend<TaskBackendCfg>
087       implements ConfigurationChangeListener<TaskBackendCfg>, Backupable
088{
089
090  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
091
092
093
094  /** The current configuration state. */
095  private TaskBackendCfg currentConfig;
096
097  /** The DN of the configuration entry for this backend. */
098  private DN configEntryDN;
099
100  /**
101   * The DN of the entry that will serve as the parent for all recurring task
102   * entries.
103   */
104  private DN recurringTaskParentDN;
105
106  /**
107   * The DN of the entry that will serve as the parent for all scheduled task
108   * entries.
109   */
110  private DN scheduledTaskParentDN;
111
112  /** The DN of the entry that will serve as the root for all task entries. */
113  private DN taskRootDN;
114
115  /** The set of base DNs defined for this backend. */
116  private DN[] baseDNs;
117
118  /**
119   * The length of time in seconds after a task is completed that it should be
120   * removed from the set of scheduled tasks.
121   */
122  private long retentionTime;
123
124  /** The e-mail address to use for the sender from notification messages. */
125  private String notificationSenderAddress;
126
127  /** The path to the task backing file. */
128  private String taskBackingFile;
129
130  /**
131   * The task scheduler that will be responsible for actually invoking scheduled
132   * tasks.
133   */
134  private TaskScheduler taskScheduler;
135
136  private ServerContext serverContext;
137
138  /**
139   * Creates a new backend with the provided information.  All backend
140   * implementations must implement a default constructor that use
141   * <CODE>super()</CODE> to invoke this constructor.
142   */
143  public TaskBackend()
144  {
145    super();
146
147    // Perform all initialization in initializeBackend.
148  }
149
150
151
152  /** {@inheritDoc} */
153  @Override
154  public void configureBackend(TaskBackendCfg cfg, ServerContext serverContext) throws ConfigException
155  {
156    Reject.ifNull(cfg);
157    this.serverContext = serverContext;
158
159    final DN[] baseDNs = new DN[cfg.getBaseDN().size()];
160    cfg.getBaseDN().toArray(baseDNs);
161
162    ConfigEntry configEntry = DirectoryServer.getConfigEntry(cfg.dn());
163
164    configEntryDN = configEntry.getDN();
165
166
167    // Make sure that the provided set of base DNs contains exactly one value.
168    // We will only allow one base for task entries.
169    if (baseDNs.length == 0)
170    {
171      throw new ConfigException(ERR_TASKBE_NO_BASE_DNS.get());
172    }
173    else if (baseDNs.length > 1)
174    {
175      LocalizableMessage message = ERR_TASKBE_MULTIPLE_BASE_DNS.get();
176      throw new ConfigException(message);
177    }
178    else
179    {
180      this.baseDNs = baseDNs;
181
182      taskRootDN = baseDNs[0];
183
184      String recurringTaskBaseString = RECURRING_TASK_BASE_RDN + "," +
185                                       taskRootDN;
186      try
187      {
188        recurringTaskParentDN = DN.valueOf(recurringTaskBaseString);
189      }
190      catch (Exception e)
191      {
192        logger.traceException(e);
193
194        // This should never happen.
195        LocalizableMessage message = ERR_TASKBE_CANNOT_DECODE_RECURRING_TASK_BASE_DN.get(
196            recurringTaskBaseString, getExceptionMessage(e));
197        throw new ConfigException(message, e);
198      }
199
200      String scheduledTaskBaseString = SCHEDULED_TASK_BASE_RDN + "," +
201                                       taskRootDN;
202      try
203      {
204        scheduledTaskParentDN = DN.valueOf(scheduledTaskBaseString);
205      }
206      catch (Exception e)
207      {
208        logger.traceException(e);
209
210        // This should never happen.
211        LocalizableMessage message = ERR_TASKBE_CANNOT_DECODE_SCHEDULED_TASK_BASE_DN.get(
212            scheduledTaskBaseString, getExceptionMessage(e));
213        throw new ConfigException(message, e);
214      }
215    }
216
217
218    // Get the retention time that will be used to determine how long task
219    // information stays around once the associated task is completed.
220    retentionTime = cfg.getTaskRetentionTime();
221
222
223    // Get the notification sender address.
224    notificationSenderAddress = cfg.getNotificationSenderAddress();
225    if (notificationSenderAddress == null)
226    {
227      try
228      {
229        notificationSenderAddress = "opendj-task-notification@" +
230             InetAddress.getLocalHost().getCanonicalHostName();
231      }
232      catch (Exception e)
233      {
234        notificationSenderAddress = "opendj-task-notification@opendj.org";
235      }
236    }
237
238
239    // Get the path to the task data backing file.
240    taskBackingFile = cfg.getTaskBackingFile();
241
242    currentConfig = cfg;
243  }
244
245
246
247  /** {@inheritDoc} */
248  @Override
249  public void openBackend()
250         throws ConfigException, InitializationException
251  {
252    // Create the scheduler and initialize it from the backing file.
253    taskScheduler = new TaskScheduler(serverContext, this);
254    taskScheduler.start();
255
256
257    // Register with the Directory Server as a configurable component.
258    currentConfig.addTaskChangeListener(this);
259
260
261    // Register the task base as a private suffix.
262    try
263    {
264      DirectoryServer.registerBaseDN(taskRootDN, this, true);
265    }
266    catch (Exception e)
267    {
268      logger.traceException(e);
269
270      LocalizableMessage message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(
271          taskRootDN, getExceptionMessage(e));
272      throw new InitializationException(message, e);
273    }
274  }
275
276
277
278  /** {@inheritDoc} */
279  @Override
280  public void closeBackend()
281  {
282    currentConfig.removeTaskChangeListener(this);
283
284    try
285    {
286      taskScheduler.stopScheduler();
287    }
288    catch (Exception e)
289    {
290      logger.traceException(e);
291    }
292
293    try
294    {
295      LocalizableMessage message = INFO_TASKBE_INTERRUPTED_BY_SHUTDOWN.get();
296
297      taskScheduler.interruptRunningTasks(TaskState.STOPPED_BY_SHUTDOWN,
298                                          message, true);
299    }
300    catch (Exception e)
301    {
302      logger.traceException(e);
303    }
304
305    try
306    {
307      DirectoryServer.deregisterBaseDN(taskRootDN);
308    }
309    catch (Exception e)
310    {
311      logger.traceException(e);
312    }
313  }
314
315
316
317  /** {@inheritDoc} */
318  @Override
319  public DN[] getBaseDNs()
320  {
321    return baseDNs;
322  }
323
324
325
326  /** {@inheritDoc} */
327  @Override
328  public long getEntryCount()
329  {
330    if (taskScheduler != null)
331    {
332      return taskScheduler.getEntryCount();
333    }
334
335    return -1;
336  }
337
338
339
340  /** {@inheritDoc} */
341  @Override
342  public boolean isIndexed(AttributeType attributeType, IndexType indexType)
343  {
344    // All searches in this backend will always be considered indexed.
345    return true;
346  }
347
348
349
350  /** {@inheritDoc} */
351  @Override
352  public ConditionResult hasSubordinates(DN entryDN)
353         throws DirectoryException
354  {
355    long ret = numSubordinates(entryDN, false);
356    if(ret < 0)
357    {
358      return ConditionResult.UNDEFINED;
359    }
360    return ConditionResult.valueOf(ret != 0);
361  }
362
363  /** {@inheritDoc} */
364  @Override
365  public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException {
366    checkNotNull(baseDN, "baseDN must not be null");
367    return numSubordinates(baseDN, true) + 1;
368  }
369
370  /** {@inheritDoc} */
371  @Override
372  public long getNumberOfChildren(DN parentDN) throws DirectoryException {
373    checkNotNull(parentDN, "parentDN must not be null");
374    return numSubordinates(parentDN, false);
375  }
376
377  private long numSubordinates(DN entryDN, boolean subtree) throws DirectoryException
378  {
379    if (entryDN == null)
380    {
381      return -1;
382    }
383
384    if (entryDN.equals(taskRootDN))
385    {
386      // scheduled and recurring parents.
387      if(!subtree)
388      {
389        return 2;
390      }
391      else
392      {
393        return taskScheduler.getScheduledTaskCount() +
394            taskScheduler.getRecurringTaskCount() + 2;
395      }
396    }
397    else if (entryDN.equals(scheduledTaskParentDN))
398    {
399      return taskScheduler.getScheduledTaskCount();
400    }
401    else if (entryDN.equals(recurringTaskParentDN))
402    {
403      return taskScheduler.getRecurringTaskCount();
404    }
405
406    DN parentDN = DirectoryServer.getParentDNInSuffix(entryDN);
407    if (parentDN == null)
408    {
409      return -1;
410    }
411
412    if (parentDN.equals(scheduledTaskParentDN) &&
413        taskScheduler.getScheduledTask(entryDN) != null)
414    {
415      return 0;
416    }
417    else if (parentDN.equals(recurringTaskParentDN) &&
418        taskScheduler.getRecurringTask(entryDN) != null)
419    {
420      return 0;
421    }
422    else
423    {
424      return -1;
425    }
426  }
427
428
429
430  /** {@inheritDoc} */
431  @Override
432  public Entry getEntry(DN entryDN)
433         throws DirectoryException
434  {
435    if (entryDN == null)
436    {
437      return null;
438    }
439
440    DNLock lock = taskScheduler.readLockEntry(entryDN);
441    try
442    {
443      if (entryDN.equals(taskRootDN))
444      {
445        return taskScheduler.getTaskRootEntry();
446      }
447      else if (entryDN.equals(scheduledTaskParentDN))
448      {
449        return taskScheduler.getScheduledTaskParentEntry();
450      }
451      else if (entryDN.equals(recurringTaskParentDN))
452      {
453        return taskScheduler.getRecurringTaskParentEntry();
454      }
455
456      DN parentDN = DirectoryServer.getParentDNInSuffix(entryDN);
457      if (parentDN == null)
458      {
459        return null;
460      }
461
462      if (parentDN.equals(scheduledTaskParentDN))
463      {
464        return taskScheduler.getScheduledTaskEntry(entryDN);
465      }
466      else if (parentDN.equals(recurringTaskParentDN))
467      {
468        return taskScheduler.getRecurringTaskEntry(entryDN);
469      }
470      else
471      {
472        // If we've gotten here then this is not an entry
473        // that should exist in the task backend.
474        return null;
475      }
476    }
477    finally
478    {
479      lock.unlock();
480    }
481  }
482
483
484
485  /** {@inheritDoc} */
486  @Override
487  public void addEntry(Entry entry, AddOperation addOperation)
488         throws DirectoryException
489  {
490    Entry e = entry.duplicate(false);
491
492    // Get the DN for the entry and then get its parent.
493    DN entryDN = e.getName();
494    DN parentDN = DirectoryServer.getParentDNInSuffix(entryDN);
495
496    if (parentDN == null)
497    {
498      LocalizableMessage message = ERR_TASKBE_ADD_DISALLOWED_DN.
499          get(scheduledTaskParentDN, recurringTaskParentDN);
500      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
501    }
502
503    // If the parent DN is equal to the parent for scheduled tasks, then try to
504    // treat the provided entry like a scheduled task.
505    if (parentDN.equals(scheduledTaskParentDN))
506    {
507      Task task = taskScheduler.entryToScheduledTask(e, addOperation);
508      taskScheduler.scheduleTask(task, true);
509      return;
510    }
511
512    // If the parent DN is equal to the parent for recurring tasks, then try to
513    // treat the provided entry like a recurring task.
514    if (parentDN.equals(recurringTaskParentDN))
515    {
516      RecurringTask recurringTask = taskScheduler.entryToRecurringTask(e);
517      taskScheduler.addRecurringTask(recurringTask, true);
518      return;
519    }
520
521    // We won't allow the entry to be added.
522    LocalizableMessage message = ERR_TASKBE_ADD_DISALLOWED_DN.
523        get(scheduledTaskParentDN, recurringTaskParentDN);
524    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
525  }
526
527
528
529  /** {@inheritDoc} */
530  @Override
531  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
532         throws DirectoryException
533  {
534    // Get the parent for the provided entry DN.  It must be either the
535    // scheduled or recurring task parent DN.
536    DN parentDN = DirectoryServer.getParentDNInSuffix(entryDN);
537    if (parentDN == null)
538    {
539      LocalizableMessage message = ERR_TASKBE_DELETE_INVALID_ENTRY.get(entryDN);
540      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
541    }
542    else if (parentDN.equals(scheduledTaskParentDN))
543    {
544      // It's a scheduled task.  Make sure that it exists.
545      Task t = taskScheduler.getScheduledTask(entryDN);
546      if (t == null)
547      {
548        LocalizableMessage message = ERR_TASKBE_DELETE_NO_SUCH_TASK.get(entryDN);
549        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
550      }
551
552
553      // Look at the state of the task.  We will allow pending and completed
554      // tasks to be removed, but not running tasks.
555      TaskState state = t.getTaskState();
556      if (TaskState.isPending(state))
557      {
558        if (t.isRecurring()) {
559          taskScheduler.removePendingTask(t.getTaskID());
560          long scheduledStartTime = t.getScheduledStartTime();
561          long currentSystemTime = System.currentTimeMillis();
562          if (scheduledStartTime < currentSystemTime) {
563            scheduledStartTime = currentSystemTime;
564          }
565          GregorianCalendar calendar = new GregorianCalendar();
566          calendar.setTimeInMillis(scheduledStartTime);
567          taskScheduler.scheduleNextRecurringTaskIteration(t,
568                  calendar);
569        } else {
570          taskScheduler.removePendingTask(t.getTaskID());
571        }
572      }
573      else if (TaskState.isDone(t.getTaskState()))
574      {
575        taskScheduler.removeCompletedTask(t.getTaskID());
576      }
577      else
578      {
579        LocalizableMessage message = ERR_TASKBE_DELETE_RUNNING.get(entryDN);
580        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
581      }
582    }
583    else if (parentDN.equals(recurringTaskParentDN))
584    {
585      // It's a recurring task.  Make sure that it exists.
586      RecurringTask rt = taskScheduler.getRecurringTask(entryDN);
587      if (rt == null)
588      {
589        LocalizableMessage message = ERR_TASKBE_DELETE_NO_SUCH_RECURRING_TASK.get(entryDN);
590        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
591      }
592
593      taskScheduler.removeRecurringTask(rt.getRecurringTaskID());
594    }
595    else
596    {
597      LocalizableMessage message = ERR_TASKBE_DELETE_INVALID_ENTRY.get(entryDN);
598      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
599    }
600  }
601
602
603
604  /** {@inheritDoc} */
605  @Override
606  public void replaceEntry(Entry oldEntry, Entry newEntry,
607      ModifyOperation modifyOperation) throws DirectoryException
608  {
609    DN entryDN = newEntry.getName();
610    DNLock entryLock = null;
611    if (! taskScheduler.holdsSchedulerLock())
612    {
613      entryLock = DirectoryServer.getLockManager().tryWriteLockEntry(entryDN);
614      if (entryLock == null)
615      {
616        throw new DirectoryException(ResultCode.BUSY,
617                                     ERR_TASKBE_MODIFY_CANNOT_LOCK_ENTRY.get(entryDN));
618      }
619    }
620
621    try
622    {
623      // Get the parent for the provided entry DN.  It must be either the
624      // scheduled or recurring task parent DN.
625      DN parentDN = DirectoryServer.getParentDNInSuffix(entryDN);
626      if (parentDN == null)
627      {
628        LocalizableMessage message = ERR_TASKBE_MODIFY_INVALID_ENTRY.get(entryDN);
629        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
630      }
631      else if (parentDN.equals(scheduledTaskParentDN))
632      {
633        // It's a scheduled task.  Make sure that it exists.
634        Task t = taskScheduler.getScheduledTask(entryDN);
635        if (t == null)
636        {
637          LocalizableMessage message = ERR_TASKBE_MODIFY_NO_SUCH_TASK.get(entryDN);
638          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
639        }
640
641        // Look at the state of the task.  We will allow anything to be altered
642        // for a pending task.  For a running task, we will only allow the state
643        // to be altered in order to cancel it.  We will not allow any
644        // modifications for completed tasks.
645        TaskState state = t.getTaskState();
646        if (TaskState.isPending(state) && !t.isRecurring())
647        {
648          Task newTask = taskScheduler.entryToScheduledTask(newEntry,
649              modifyOperation);
650          taskScheduler.removePendingTask(t.getTaskID());
651          taskScheduler.scheduleTask(newTask, true);
652          return;
653        }
654        else if (TaskState.isRunning(state))
655        {
656          // If the task is running, we will only allow it to be cancelled.
657          // This will only be allowed using the replace modification type on
658          // the ds-task-state attribute if the value starts with "cancel" or
659          // "stop".  In that case, we'll cancel the task.
660          boolean acceptable = isReplaceEntryAcceptable(modifyOperation);
661
662          if (acceptable)
663          {
664            LocalizableMessage message = INFO_TASKBE_RUNNING_TASK_CANCELLED.get();
665            t.interruptTask(TaskState.STOPPED_BY_ADMINISTRATOR, message);
666            return;
667          }
668          else
669          {
670            LocalizableMessage message = ERR_TASKBE_MODIFY_RUNNING.get(entryDN);
671            throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
672          }
673        }
674        else if (TaskState.isPending(state) && t.isRecurring())
675        {
676          // Pending recurring task iterations can only be canceled.
677          boolean acceptable = isReplaceEntryAcceptable(modifyOperation);
678          if (acceptable)
679          {
680            Task newTask = taskScheduler.entryToScheduledTask(newEntry,
681              modifyOperation);
682            if (newTask.getTaskState() ==
683              TaskState.CANCELED_BEFORE_STARTING)
684            {
685              taskScheduler.removePendingTask(t.getTaskID());
686              long scheduledStartTime = t.getScheduledStartTime();
687              long currentSystemTime = System.currentTimeMillis();
688              if (scheduledStartTime < currentSystemTime) {
689                scheduledStartTime = currentSystemTime;
690              }
691              GregorianCalendar calendar = new GregorianCalendar();
692              calendar.setTimeInMillis(scheduledStartTime);
693              taskScheduler.scheduleNextRecurringTaskIteration(
694                      newTask, calendar);
695            }
696            else if (newTask.getTaskState() ==
697              TaskState.STOPPED_BY_ADMINISTRATOR)
698            {
699              LocalizableMessage message = INFO_TASKBE_RUNNING_TASK_CANCELLED.get();
700              t.interruptTask(TaskState.STOPPED_BY_ADMINISTRATOR, message);
701            }
702              return;
703          }
704          else
705          {
706            LocalizableMessage message = ERR_TASKBE_MODIFY_RECURRING.get(entryDN);
707            throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
708          }
709        }
710        else
711        {
712          LocalizableMessage message = ERR_TASKBE_MODIFY_COMPLETED.get(entryDN);
713          throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
714        }
715      }
716      else if (parentDN.equals(recurringTaskParentDN))
717      {
718        // We don't currently support altering recurring tasks.
719        LocalizableMessage message = ERR_TASKBE_MODIFY_RECURRING.get(entryDN);
720        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
721      }
722      else
723      {
724        LocalizableMessage message = ERR_TASKBE_MODIFY_INVALID_ENTRY.get(entryDN);
725        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
726      }
727    }
728    finally
729    {
730      if (entryLock != null)
731      {
732        entryLock.unlock();
733      }
734    }
735  }
736
737
738
739  /**
740   * Helper to determine if requested modifications are acceptable.
741   * @param modifyOperation associated with requested modifications.
742   * @return <CODE>true</CODE> if requested modifications are
743   *         acceptable, <CODE>false</CODE> otherwise.
744   */
745  private boolean isReplaceEntryAcceptable(ModifyOperation modifyOperation)
746  {
747    for (Modification m : modifyOperation.getModifications()) {
748      if (m.isInternal()) {
749        continue;
750      }
751
752      if (m.getModificationType() != ModificationType.REPLACE) {
753        return false;
754      }
755
756      Attribute a = m.getAttribute();
757      AttributeType at = a.getAttributeDescription().getAttributeType();
758      if (!at.hasName(ATTR_TASK_STATE)) {
759        return false;
760      }
761
762      Iterator<ByteString> iterator = a.iterator();
763      if (!iterator.hasNext()) {
764        return false;
765      }
766
767      ByteString v = iterator.next();
768      if (iterator.hasNext()) {
769        return false;
770      }
771
772      String valueString = toLowerCase(v.toString());
773      if (!valueString.startsWith("cancel")
774          && !valueString.startsWith("stop")) {
775        return false;
776      }
777    }
778
779    return true;
780  }
781
782
783
784  /** {@inheritDoc} */
785  @Override
786  public void renameEntry(DN currentDN, Entry entry,
787                                   ModifyDNOperation modifyDNOperation)
788         throws DirectoryException
789  {
790    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
791        ERR_BACKEND_MODIFY_DN_NOT_SUPPORTED.get(currentDN, getBackendID()));
792  }
793
794
795
796  /** {@inheritDoc} */
797  @Override
798  public void search(SearchOperation searchOperation)
799         throws DirectoryException, CanceledOperationException {
800    // Look at the base DN and scope for the search operation to decide which
801    // entries we need to look at.
802    boolean searchRoot            = false;
803    boolean searchScheduledParent = false;
804    boolean searchScheduledTasks  = false;
805    boolean searchRecurringParent = false;
806    boolean searchRecurringTasks  = false;
807
808    DN           baseDN       = searchOperation.getBaseDN();
809    SearchScope  searchScope  = searchOperation.getScope();
810    SearchFilter searchFilter = searchOperation.getFilter();
811
812    if (baseDN.equals(taskRootDN))
813    {
814      switch (searchScope.asEnum())
815      {
816        case BASE_OBJECT:
817          searchRoot = true;
818          break;
819        case SINGLE_LEVEL:
820          searchScheduledParent = true;
821          searchRecurringParent = true;
822          break;
823        case WHOLE_SUBTREE:
824          searchRoot            = true;
825          searchScheduledParent = true;
826          searchRecurringParent = true;
827          searchScheduledTasks  = true;
828          searchRecurringTasks  = true;
829          break;
830        case SUBORDINATES:
831          searchScheduledParent = true;
832          searchRecurringParent = true;
833          searchScheduledTasks  = true;
834          searchRecurringTasks  = true;
835          break;
836      }
837    }
838    else if (baseDN.equals(scheduledTaskParentDN))
839    {
840      switch (searchScope.asEnum())
841      {
842        case BASE_OBJECT:
843          searchScheduledParent = true;
844          break;
845        case SINGLE_LEVEL:
846          searchScheduledTasks = true;
847          break;
848        case WHOLE_SUBTREE:
849          searchScheduledParent = true;
850          searchScheduledTasks  = true;
851          break;
852        case SUBORDINATES:
853          searchScheduledTasks  = true;
854          break;
855      }
856    }
857    else if (baseDN.equals(recurringTaskParentDN))
858    {
859      switch (searchScope.asEnum())
860      {
861        case BASE_OBJECT:
862          searchRecurringParent = true;
863          break;
864        case SINGLE_LEVEL:
865          searchRecurringTasks = true;
866          break;
867        case WHOLE_SUBTREE:
868          searchRecurringParent = true;
869          searchRecurringTasks  = true;
870          break;
871        case SUBORDINATES:
872          searchRecurringTasks  = true;
873          break;
874      }
875    }
876    else
877    {
878      DN parentDN = DirectoryServer.getParentDNInSuffix(baseDN);
879      if (parentDN == null)
880      {
881        LocalizableMessage message = ERR_TASKBE_SEARCH_INVALID_BASE.get(baseDN);
882        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
883      }
884      else if (parentDN.equals(scheduledTaskParentDN))
885      {
886        DNLock lock = taskScheduler.readLockEntry(baseDN);
887        try
888        {
889          Entry e = taskScheduler.getScheduledTaskEntry(baseDN);
890          if (e == null)
891          {
892            LocalizableMessage message = ERR_TASKBE_SEARCH_NO_SUCH_TASK.get(baseDN);
893            throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message,
894                                         scheduledTaskParentDN, null);
895          }
896
897          if ((searchScope == SearchScope.BASE_OBJECT || searchScope == SearchScope.WHOLE_SUBTREE)
898              && searchFilter.matchesEntry(e))
899          {
900            searchOperation.returnEntry(e, null);
901          }
902
903          return;
904        }
905        finally
906        {
907          lock.unlock();
908        }
909      }
910      else if (parentDN.equals(recurringTaskParentDN))
911      {
912        DNLock lock = taskScheduler.readLockEntry(baseDN);
913        try
914        {
915          Entry e = taskScheduler.getRecurringTaskEntry(baseDN);
916          if (e == null)
917          {
918            LocalizableMessage message = ERR_TASKBE_SEARCH_NO_SUCH_RECURRING_TASK.get(baseDN);
919            throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message,
920                                         recurringTaskParentDN, null);
921          }
922
923          if ((searchScope == SearchScope.BASE_OBJECT || searchScope == SearchScope.WHOLE_SUBTREE)
924              && searchFilter.matchesEntry(e))
925          {
926            searchOperation.returnEntry(e, null);
927          }
928
929          return;
930        }
931        finally
932        {
933          lock.unlock();
934        }
935      }
936      else
937      {
938        LocalizableMessage message = ERR_TASKBE_SEARCH_INVALID_BASE.get(baseDN);
939        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
940      }
941    }
942
943
944    if (searchRoot)
945    {
946      Entry e = taskScheduler.getTaskRootEntry();
947      if (searchFilter.matchesEntry(e) && !searchOperation.returnEntry(e, null))
948      {
949        return;
950      }
951    }
952
953
954    if (searchScheduledParent)
955    {
956      Entry e = taskScheduler.getScheduledTaskParentEntry();
957      if (searchFilter.matchesEntry(e) && !searchOperation.returnEntry(e, null))
958      {
959        return;
960      }
961    }
962
963
964    if (searchScheduledTasks
965        && !taskScheduler.searchScheduledTasks(searchOperation))
966    {
967      return;
968    }
969
970
971    if (searchRecurringParent)
972    {
973      Entry e = taskScheduler.getRecurringTaskParentEntry();
974      if (searchFilter.matchesEntry(e) && !searchOperation.returnEntry(e, null))
975      {
976        return;
977      }
978    }
979
980
981    if (searchRecurringTasks
982        && !taskScheduler.searchRecurringTasks(searchOperation))
983    {
984      return;
985    }
986  }
987
988
989
990  /** {@inheritDoc} */
991  @Override
992  public Set<String> getSupportedControls()
993  {
994    return Collections.emptySet();
995  }
996
997  /** {@inheritDoc} */
998  @Override
999  public Set<String> getSupportedFeatures()
1000  {
1001    return Collections.emptySet();
1002  }
1003
1004  /** {@inheritDoc} */
1005  @Override
1006  public boolean supports(BackendOperation backendOperation)
1007  {
1008    switch (backendOperation)
1009    {
1010    case LDIF_EXPORT:
1011    case BACKUP:
1012    case RESTORE:
1013      return true;
1014
1015    default:
1016      return false;
1017    }
1018  }
1019
1020  /** {@inheritDoc} */
1021  @Override
1022  public void exportLDIF(LDIFExportConfig exportConfig)
1023         throws DirectoryException
1024  {
1025    File taskFile = getFileForPath(taskBackingFile);
1026
1027    // Read from.
1028    LDIFReader ldifReader;
1029    try
1030    {
1031      ldifReader = new LDIFReader(new LDIFImportConfig(taskFile.getPath()));
1032    }
1033    catch (Exception e)
1034    {
1035      LocalizableMessage message = ERR_TASKS_CANNOT_EXPORT_TO_FILE.get(e);
1036      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
1037    }
1038
1039    // Write to.
1040    LDIFWriter ldifWriter;
1041    try
1042    {
1043      ldifWriter = new LDIFWriter(exportConfig);
1044    }
1045    catch (Exception e)
1046    {
1047      logger.traceException(e);
1048
1049      LocalizableMessage message = ERR_TASKS_CANNOT_EXPORT_TO_FILE.get(
1050          stackTraceToSingleLineString(e));
1051      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1052                                   message);
1053    }
1054
1055    // Copy record by record.
1056    try
1057    {
1058      while (true)
1059      {
1060        Entry e = null;
1061        try
1062        {
1063          e = ldifReader.readEntry();
1064          if (e == null)
1065          {
1066            break;
1067          }
1068        }
1069        catch (LDIFException le)
1070        {
1071          if (! le.canContinueReading())
1072          {
1073            LocalizableMessage message = ERR_TASKS_CANNOT_EXPORT_TO_FILE.get(e);
1074            throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, le);
1075          }
1076          continue;
1077        }
1078        ldifWriter.writeEntry(e);
1079      }
1080    }
1081    catch (Exception e)
1082    {
1083      logger.traceException(e);
1084    }
1085    finally
1086    {
1087      close(ldifWriter, ldifReader);
1088    }
1089  }
1090
1091  /** {@inheritDoc} */
1092  @Override
1093  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext sContext) throws DirectoryException
1094  {
1095    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
1096        ERR_BACKEND_IMPORT_NOT_SUPPORTED.get(getBackendID()));
1097  }
1098
1099  /** {@inheritDoc} */
1100  @Override
1101  public void createBackup(BackupConfig backupConfig) throws DirectoryException
1102  {
1103    new BackupManager(getBackendID()).createBackup(this, backupConfig);
1104  }
1105
1106  /** {@inheritDoc} */
1107  @Override
1108  public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
1109  {
1110    new BackupManager(getBackendID()).removeBackup(backupDirectory, backupID);
1111  }
1112
1113  /** {@inheritDoc} */
1114  @Override
1115  public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
1116  {
1117    new BackupManager(getBackendID()).restoreBackup(this, restoreConfig);
1118  }
1119
1120  /** {@inheritDoc} */
1121  @Override
1122  public boolean isConfigurationAcceptable(TaskBackendCfg config,
1123                                           List<LocalizableMessage> unacceptableReasons,
1124                                           ServerContext serverContext)
1125  {
1126    return isConfigAcceptable(config, unacceptableReasons, null);
1127  }
1128
1129
1130
1131  /** {@inheritDoc} */
1132  @Override
1133  public boolean isConfigurationChangeAcceptable(TaskBackendCfg configEntry,
1134                                            List<LocalizableMessage> unacceptableReasons)
1135  {
1136    return isConfigAcceptable(configEntry, unacceptableReasons,
1137                              taskBackingFile);
1138  }
1139
1140
1141
1142  /**
1143   * Indicates whether the provided configuration is acceptable for this task
1144   * backend.
1145   *
1146   * @param  config               The configuration for which to make the
1147   *                              determination.
1148   * @param  unacceptableReasons  A list into which the unacceptable reasons
1149   *                              should be placed.
1150   * @param  taskBackingFile      The currently-configured task backing file, or
1151   *                              {@code null} if it should not be taken into
1152   *                              account.
1153   *
1154   * @return  {@code true} if the configuration is acceptable, or {@code false}
1155   *          if not.
1156   */
1157  private static boolean isConfigAcceptable(TaskBackendCfg config,
1158                                            List<LocalizableMessage> unacceptableReasons,
1159                                            String taskBackingFile)
1160  {
1161    boolean configIsAcceptable = true;
1162
1163
1164    try
1165    {
1166      String tmpBackingFile = config.getTaskBackingFile();
1167      if (taskBackingFile == null ||
1168          !taskBackingFile.equals(tmpBackingFile))
1169      {
1170        File f = getFileForPath(tmpBackingFile);
1171        if (f.exists())
1172        {
1173          // This is only a problem if it's different from the active one.
1174          if (taskBackingFile != null)
1175          {
1176            unacceptableReasons.add(
1177                    ERR_TASKBE_BACKING_FILE_EXISTS.get(tmpBackingFile));
1178            configIsAcceptable = false;
1179          }
1180        }
1181        else
1182        {
1183          File p = f.getParentFile();
1184          if (p == null)
1185          {
1186            unacceptableReasons.add(ERR_TASKBE_INVALID_BACKING_FILE_PATH.get(
1187                    tmpBackingFile));
1188            configIsAcceptable = false;
1189          }
1190          else if (! p.exists())
1191          {
1192            unacceptableReasons.add(ERR_TASKBE_BACKING_FILE_MISSING_PARENT.get(
1193                    p.getPath(),
1194                    tmpBackingFile));
1195            configIsAcceptable = false;
1196          }
1197          else if (! p.isDirectory())
1198          {
1199            unacceptableReasons.add(
1200                    ERR_TASKBE_BACKING_FILE_PARENT_NOT_DIRECTORY.get(
1201                            p.getPath(),
1202                            tmpBackingFile));
1203            configIsAcceptable = false;
1204          }
1205        }
1206      }
1207    }
1208    catch (Exception e)
1209    {
1210      logger.traceException(e);
1211
1212      unacceptableReasons.add(ERR_TASKBE_ERROR_GETTING_BACKING_FILE.get(
1213              getExceptionMessage(e)));
1214
1215      configIsAcceptable = false;
1216    }
1217
1218    return configIsAcceptable;
1219  }
1220
1221
1222
1223  /** {@inheritDoc} */
1224  @Override
1225  public ConfigChangeResult applyConfigurationChange(TaskBackendCfg configEntry)
1226  {
1227    final ConfigChangeResult ccr = new ConfigChangeResult();
1228
1229
1230    String tmpBackingFile = taskBackingFile;
1231    try
1232    {
1233      {
1234        tmpBackingFile = configEntry.getTaskBackingFile();
1235        if (! taskBackingFile.equals(tmpBackingFile))
1236        {
1237          File f = getFileForPath(tmpBackingFile);
1238          if (f.exists())
1239          {
1240            ccr.addMessage(ERR_TASKBE_BACKING_FILE_EXISTS.get(tmpBackingFile));
1241            ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
1242          }
1243          else
1244          {
1245            File p = f.getParentFile();
1246            if (p == null)
1247            {
1248              ccr.addMessage(ERR_TASKBE_INVALID_BACKING_FILE_PATH.get(tmpBackingFile));
1249              ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
1250            }
1251            else if (! p.exists())
1252            {
1253              ccr.addMessage(ERR_TASKBE_BACKING_FILE_MISSING_PARENT.get(p, tmpBackingFile));
1254              ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
1255            }
1256            else if (! p.isDirectory())
1257            {
1258              ccr.addMessage(ERR_TASKBE_BACKING_FILE_PARENT_NOT_DIRECTORY.get(p, tmpBackingFile));
1259              ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
1260            }
1261          }
1262        }
1263      }
1264    }
1265    catch (Exception e)
1266    {
1267      logger.traceException(e);
1268
1269      ccr.addMessage(ERR_TASKBE_ERROR_GETTING_BACKING_FILE.get(getExceptionMessage(e)));
1270      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
1271    }
1272
1273
1274    long tmpRetentionTime = configEntry.getTaskRetentionTime();
1275
1276
1277    if (ccr.getResultCode() == ResultCode.SUCCESS)
1278    {
1279      // Everything looks OK, so apply the changes.
1280      if (retentionTime != tmpRetentionTime)
1281      {
1282        retentionTime = tmpRetentionTime;
1283
1284        ccr.addMessage(INFO_TASKBE_UPDATED_RETENTION_TIME.get(retentionTime));
1285      }
1286
1287
1288      if (! taskBackingFile.equals(tmpBackingFile))
1289      {
1290        taskBackingFile = tmpBackingFile;
1291        taskScheduler.writeState();
1292
1293        ccr.addMessage(INFO_TASKBE_UPDATED_BACKING_FILE.get(taskBackingFile));
1294      }
1295    }
1296
1297
1298    String tmpNotificationAddress = configEntry.getNotificationSenderAddress();
1299    if (tmpNotificationAddress == null)
1300    {
1301      try
1302      {
1303        tmpNotificationAddress = "opendj-task-notification@" +
1304             InetAddress.getLocalHost().getCanonicalHostName();
1305      }
1306      catch (Exception e)
1307      {
1308        tmpNotificationAddress = "opendj-task-notification@opendj.org";
1309      }
1310    }
1311    notificationSenderAddress = tmpNotificationAddress;
1312
1313
1314    currentConfig = configEntry;
1315    return ccr;
1316  }
1317
1318
1319
1320  /**
1321   * Retrieves the DN of the configuration entry for this task backend.
1322   *
1323   * @return  The DN of the configuration entry for this task backend.
1324   */
1325  public DN getConfigEntryDN()
1326  {
1327    return configEntryDN;
1328  }
1329
1330
1331
1332  /**
1333   * Retrieves the path to the backing file that will hold the scheduled and
1334   * recurring task definitions.
1335   *
1336   * @return  The path to the backing file that will hold the scheduled and
1337   *          recurring task definitions.
1338   */
1339  public String getTaskBackingFile()
1340  {
1341    File f = getFileForPath(taskBackingFile);
1342    return f.getPath();
1343  }
1344
1345
1346
1347  /**
1348   * Retrieves the sender address that should be used for e-mail notifications
1349   * of task completion.
1350   *
1351   * @return  The sender address that should be used for e-mail notifications of
1352   *          task completion.
1353   */
1354  public String getNotificationSenderAddress()
1355  {
1356    return notificationSenderAddress;
1357  }
1358
1359
1360
1361  /**
1362   * Retrieves the length of time in seconds that information for a task should
1363   * be retained after processing on it has completed.
1364   *
1365   * @return  The length of time in seconds that information for a task should
1366   *          be retained after processing on it has completed.
1367   */
1368  public long getRetentionTime()
1369  {
1370    return retentionTime;
1371  }
1372
1373
1374
1375  /**
1376   * Retrieves the DN of the entry that is the root for all task information in
1377   * the Directory Server.
1378   *
1379   * @return  The DN of the entry that is the root for all task information in
1380   *          the Directory Server.
1381   */
1382  public DN getTaskRootDN()
1383  {
1384    return taskRootDN;
1385  }
1386
1387
1388
1389  /**
1390   * Retrieves the DN of the entry that is the immediate parent for all
1391   * recurring task information in the Directory Server.
1392   *
1393   * @return  The DN of the entry that is the immediate parent for all recurring
1394   *          task information in the Directory Server.
1395   */
1396  public DN getRecurringTasksParentDN()
1397  {
1398    return recurringTaskParentDN;
1399  }
1400
1401
1402
1403  /**
1404   * Retrieves the DN of the entry that is the immediate parent for all
1405   * scheduled task information in the Directory Server.
1406   *
1407   * @return  The DN of the entry that is the immediate parent for all scheduled
1408   *          task information in the Directory Server.
1409   */
1410  public DN getScheduledTasksParentDN()
1411  {
1412    return scheduledTaskParentDN;
1413  }
1414
1415
1416
1417  /**
1418   * Retrieves the scheduled task for the entry with the provided DN.
1419   *
1420   * @param  taskEntryDN  The DN of the entry for the task to retrieve.
1421   *
1422   * @return  The requested task, or {@code null} if there is no task with the
1423   *          specified entry DN.
1424   */
1425  public Task getScheduledTask(DN taskEntryDN)
1426  {
1427    return taskScheduler.getScheduledTask(taskEntryDN);
1428  }
1429
1430
1431
1432  /**
1433   * Retrieves the recurring task for the entry with the provided DN.
1434   *
1435   * @param  taskEntryDN  The DN of the entry for the recurring task to
1436   *                      retrieve.
1437   *
1438   * @return  The requested recurring task, or {@code null} if there is no task
1439   *          with the specified entry DN.
1440   */
1441  public RecurringTask getRecurringTask(DN taskEntryDN)
1442  {
1443    return taskScheduler.getRecurringTask(taskEntryDN);
1444  }
1445
1446
1447
1448  /** {@inheritDoc} */
1449  @Override
1450  public File getDirectory()
1451  {
1452    return getFileForPath(taskBackingFile).getParentFile();
1453  }
1454
1455  private FileFilter getFilesToBackupFilter()
1456  {
1457    return new FileFilter()
1458    {
1459      @Override
1460      public boolean accept(File file)
1461      {
1462        return file.getName().equals(getFileForPath(taskBackingFile).getName());
1463      }
1464    };
1465  }
1466
1467  /** {@inheritDoc} */
1468  @Override
1469  public ListIterator<Path> getFilesToBackup() throws DirectoryException
1470  {
1471    return BackupManager.getFiles(getDirectory(), getFilesToBackupFilter(), getBackendID()).listIterator();
1472  }
1473
1474  /** {@inheritDoc} */
1475  @Override
1476  public boolean isDirectRestore()
1477  {
1478    return true;
1479  }
1480
1481  /** {@inheritDoc} */
1482  @Override
1483  public Path beforeRestore() throws DirectoryException
1484  {
1485    // save current files
1486    return BackupManager.saveCurrentFilesToDirectory(this, getBackendID());
1487  }
1488
1489  /** {@inheritDoc} */
1490  @Override
1491  public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException
1492  {
1493    // restore was successful, delete the save directory
1494    StaticUtils.recursiveDelete(saveDirectory.toFile());
1495  }
1496
1497}
1498