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