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