/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS */ package org.opends.server.backends.task; import static org.forgerock.util.Reject.*; import static org.opends.messages.BackendMessages.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import java.io.*; import java.net.InetAddress; import java.security.MessageDigest; import java.util.*; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import javax.crypto.Mac; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ConditionResult; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.util.Reject; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.server.TaskBackendCfg; import org.opends.server.api.Backend; import org.opends.server.config.ConfigEntry; import org.opends.server.core.*; import org.opends.server.types.*; import org.opends.server.types.LockManager.DNLock; import org.opends.server.util.DynamicConstants; import org.opends.server.util.LDIFException; import org.opends.server.util.LDIFReader; import org.opends.server.util.LDIFWriter; /** * This class provides an implementation of a Directory Server backend that may * be used to execute various kinds of administrative tasks on a one-time or * recurring basis. */ public class TaskBackend extends Backend implements ConfigurationChangeListener { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The current configuration state. */ private TaskBackendCfg currentConfig; /** The DN of the configuration entry for this backend. */ private DN configEntryDN; /** * The DN of the entry that will serve as the parent for all recurring task * entries. */ private DN recurringTaskParentDN; /** * The DN of the entry that will serve as the parent for all scheduled task * entries. */ private DN scheduledTaskParentDN; /** The DN of the entry that will serve as the root for all task entries. */ private DN taskRootDN; /** The set of base DNs defined for this backend. */ private DN[] baseDNs; /** * The length of time in seconds after a task is completed that it should be * removed from the set of scheduled tasks. */ private long retentionTime; /** The e-mail address to use for the sender from notification messages. */ private String notificationSenderAddress; /** The path to the task backing file. */ private String taskBackingFile; /** * The task scheduler that will be responsible for actually invoking scheduled * tasks. */ private TaskScheduler taskScheduler; /** * Creates a new backend with the provided information. All backend * implementations must implement a default constructor that use * super() to invoke this constructor. */ public TaskBackend() { super(); // Perform all initialization in initializeBackend. } /** {@inheritDoc} */ @Override public void configureBackend(TaskBackendCfg cfg, ServerContext serverContext) throws ConfigException { Reject.ifNull(cfg); final DN[] baseDNs = new DN[cfg.getBaseDN().size()]; cfg.getBaseDN().toArray(baseDNs); ConfigEntry configEntry = DirectoryServer.getConfigEntry(cfg.dn()); configEntryDN = configEntry.getDN(); // Make sure that the provided set of base DNs contains exactly one value. // We will only allow one base for task entries. if (baseDNs.length == 0) { throw new ConfigException(ERR_TASKBE_NO_BASE_DNS.get()); } else if (baseDNs.length > 1) { LocalizableMessage message = ERR_TASKBE_MULTIPLE_BASE_DNS.get(); throw new ConfigException(message); } else { this.baseDNs = baseDNs; taskRootDN = baseDNs[0]; String recurringTaskBaseString = RECURRING_TASK_BASE_RDN + "," + taskRootDN; try { recurringTaskParentDN = DN.valueOf(recurringTaskBaseString); } catch (Exception e) { logger.traceException(e); // This should never happen. LocalizableMessage message = ERR_TASKBE_CANNOT_DECODE_RECURRING_TASK_BASE_DN.get( recurringTaskBaseString, getExceptionMessage(e)); throw new ConfigException(message, e); } String scheduledTaskBaseString = SCHEDULED_TASK_BASE_RDN + "," + taskRootDN; try { scheduledTaskParentDN = DN.valueOf(scheduledTaskBaseString); } catch (Exception e) { logger.traceException(e); // This should never happen. LocalizableMessage message = ERR_TASKBE_CANNOT_DECODE_SCHEDULED_TASK_BASE_DN.get( scheduledTaskBaseString, getExceptionMessage(e)); throw new ConfigException(message, e); } } // Get the retention time that will be used to determine how long task // information stays around once the associated task is completed. retentionTime = cfg.getTaskRetentionTime(); // Get the notification sender address. notificationSenderAddress = cfg.getNotificationSenderAddress(); if (notificationSenderAddress == null) { try { notificationSenderAddress = "opendj-task-notification@" + InetAddress.getLocalHost().getCanonicalHostName(); } catch (Exception e) { notificationSenderAddress = "opendj-task-notification@opendj.org"; } } // Get the path to the task data backing file. taskBackingFile = cfg.getTaskBackingFile(); currentConfig = cfg; } /** {@inheritDoc} */ @Override public void openBackend() throws ConfigException, InitializationException { // Create the scheduler and initialize it from the backing file. taskScheduler = new TaskScheduler(this); taskScheduler.start(); // Register with the Directory Server as a configurable component. currentConfig.addTaskChangeListener(this); // Register the task base as a private suffix. try { DirectoryServer.registerBaseDN(taskRootDN, this, true); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get( taskRootDN, getExceptionMessage(e)); throw new InitializationException(message, e); } } /** {@inheritDoc} */ @Override public void closeBackend() { currentConfig.removeTaskChangeListener(this); try { taskScheduler.stopScheduler(); } catch (Exception e) { logger.traceException(e); } try { LocalizableMessage message = INFO_TASKBE_INTERRUPTED_BY_SHUTDOWN.get(); taskScheduler.interruptRunningTasks(TaskState.STOPPED_BY_SHUTDOWN, message, true); } catch (Exception e) { logger.traceException(e); } try { DirectoryServer.deregisterBaseDN(taskRootDN); } catch (Exception e) { logger.traceException(e); } } /** {@inheritDoc} */ @Override public DN[] getBaseDNs() { return baseDNs; } /** {@inheritDoc} */ @Override public long getEntryCount() { if (taskScheduler != null) { return taskScheduler.getEntryCount(); } return -1; } /** {@inheritDoc} */ @Override public boolean isIndexed(AttributeType attributeType, IndexType indexType) { // All searches in this backend will always be considered indexed. return true; } /** {@inheritDoc} */ @Override public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException { long ret = numSubordinates(entryDN, false); if(ret < 0) { return ConditionResult.UNDEFINED; } return ConditionResult.valueOf(ret != 0); } /** {@inheritDoc} */ @Override public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException { checkNotNull(baseDN, "baseDN must not be null"); return numSubordinates(baseDN, true) + 1; } /** {@inheritDoc} */ @Override public long getNumberOfChildren(DN parentDN) throws DirectoryException { checkNotNull(parentDN, "parentDN must not be null"); return numSubordinates(parentDN, false); } private long numSubordinates(DN entryDN, boolean subtree) throws DirectoryException { if (entryDN == null) { return -1; } if (entryDN.equals(taskRootDN)) { // scheduled and recurring parents. if(!subtree) { return 2; } else { return taskScheduler.getScheduledTaskCount() + taskScheduler.getRecurringTaskCount() + 2; } } else if (entryDN.equals(scheduledTaskParentDN)) { return taskScheduler.getScheduledTaskCount(); } else if (entryDN.equals(recurringTaskParentDN)) { return taskScheduler.getRecurringTaskCount(); } DN parentDN = entryDN.getParentDNInSuffix(); if (parentDN == null) { return -1; } if (parentDN.equals(scheduledTaskParentDN) && taskScheduler.getScheduledTask(entryDN) != null) { return 0; } else if (parentDN.equals(recurringTaskParentDN) && taskScheduler.getRecurringTask(entryDN) != null) { return 0; } else { return -1; } } /** {@inheritDoc} */ @Override public Entry getEntry(DN entryDN) throws DirectoryException { if (entryDN == null) { return null; } DNLock lock = taskScheduler.readLockEntry(entryDN); try { if (entryDN.equals(taskRootDN)) { return taskScheduler.getTaskRootEntry(); } else if (entryDN.equals(scheduledTaskParentDN)) { return taskScheduler.getScheduledTaskParentEntry(); } else if (entryDN.equals(recurringTaskParentDN)) { return taskScheduler.getRecurringTaskParentEntry(); } DN parentDN = entryDN.getParentDNInSuffix(); if (parentDN == null) { return null; } if (parentDN.equals(scheduledTaskParentDN)) { return taskScheduler.getScheduledTaskEntry(entryDN); } else if (parentDN.equals(recurringTaskParentDN)) { return taskScheduler.getRecurringTaskEntry(entryDN); } else { // If we've gotten here then this is not an entry // that should exist in the task backend. return null; } } finally { lock.unlock(); } } /** {@inheritDoc} */ @Override public void addEntry(Entry entry, AddOperation addOperation) throws DirectoryException { Entry e = entry.duplicate(false); // Get the DN for the entry and then get its parent. DN entryDN = e.getName(); DN parentDN = entryDN.getParentDNInSuffix(); if (parentDN == null) { LocalizableMessage message = ERR_TASKBE_ADD_DISALLOWED_DN. get(scheduledTaskParentDN, recurringTaskParentDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } // If the parent DN is equal to the parent for scheduled tasks, then try to // treat the provided entry like a scheduled task. if (parentDN.equals(scheduledTaskParentDN)) { Task task = taskScheduler.entryToScheduledTask(e, addOperation); taskScheduler.scheduleTask(task, true); return; } // If the parent DN is equal to the parent for recurring tasks, then try to // treat the provided entry like a recurring task. if (parentDN.equals(recurringTaskParentDN)) { RecurringTask recurringTask = taskScheduler.entryToRecurringTask(e); taskScheduler.addRecurringTask(recurringTask, true); return; } // We won't allow the entry to be added. LocalizableMessage message = ERR_TASKBE_ADD_DISALLOWED_DN. get(scheduledTaskParentDN, recurringTaskParentDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } /** {@inheritDoc} */ @Override public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) throws DirectoryException { // Get the parent for the provided entry DN. It must be either the // scheduled or recurring task parent DN. DN parentDN = entryDN.getParentDNInSuffix(); if (parentDN == null) { LocalizableMessage message = ERR_TASKBE_DELETE_INVALID_ENTRY.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } else if (parentDN.equals(scheduledTaskParentDN)) { // It's a scheduled task. Make sure that it exists. Task t = taskScheduler.getScheduledTask(entryDN); if (t == null) { LocalizableMessage message = ERR_TASKBE_DELETE_NO_SUCH_TASK.get(entryDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } // Look at the state of the task. We will allow pending and completed // tasks to be removed, but not running tasks. TaskState state = t.getTaskState(); if (TaskState.isPending(state)) { if (t.isRecurring()) { taskScheduler.removePendingTask(t.getTaskID()); long scheduledStartTime = t.getScheduledStartTime(); long currentSystemTime = System.currentTimeMillis(); if (scheduledStartTime < currentSystemTime) { scheduledStartTime = currentSystemTime; } GregorianCalendar calendar = new GregorianCalendar(); calendar.setTimeInMillis(scheduledStartTime); taskScheduler.scheduleNextRecurringTaskIteration(t, calendar); } else { taskScheduler.removePendingTask(t.getTaskID()); } } else if (TaskState.isDone(t.getTaskState())) { taskScheduler.removeCompletedTask(t.getTaskID()); } else { LocalizableMessage message = ERR_TASKBE_DELETE_RUNNING.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } else if (parentDN.equals(recurringTaskParentDN)) { // It's a recurring task. Make sure that it exists. RecurringTask rt = taskScheduler.getRecurringTask(entryDN); if (rt == null) { LocalizableMessage message = ERR_TASKBE_DELETE_NO_SUCH_RECURRING_TASK.get(entryDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } taskScheduler.removeRecurringTask(rt.getRecurringTaskID()); } else { LocalizableMessage message = ERR_TASKBE_DELETE_INVALID_ENTRY.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } /** {@inheritDoc} */ @Override public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyOperation) throws DirectoryException { DN entryDN = newEntry.getName(); DNLock entryLock = null; if (! taskScheduler.holdsSchedulerLock()) { entryLock = DirectoryServer.getLockManager().tryWriteLockEntry(entryDN); if (entryLock == null) { throw new DirectoryException(ResultCode.BUSY, ERR_TASKBE_MODIFY_CANNOT_LOCK_ENTRY.get(entryDN)); } } try { // Get the parent for the provided entry DN. It must be either the // scheduled or recurring task parent DN. DN parentDN = entryDN.getParentDNInSuffix(); if (parentDN == null) { LocalizableMessage message = ERR_TASKBE_MODIFY_INVALID_ENTRY.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } else if (parentDN.equals(scheduledTaskParentDN)) { // It's a scheduled task. Make sure that it exists. Task t = taskScheduler.getScheduledTask(entryDN); if (t == null) { LocalizableMessage message = ERR_TASKBE_MODIFY_NO_SUCH_TASK.get(entryDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } // Look at the state of the task. We will allow anything to be altered // for a pending task. For a running task, we will only allow the state // to be altered in order to cancel it. We will not allow any // modifications for completed tasks. TaskState state = t.getTaskState(); if (TaskState.isPending(state) && !t.isRecurring()) { Task newTask = taskScheduler.entryToScheduledTask(newEntry, modifyOperation); taskScheduler.removePendingTask(t.getTaskID()); taskScheduler.scheduleTask(newTask, true); return; } else if (TaskState.isRunning(state)) { // If the task is running, we will only allow it to be cancelled. // This will only be allowed using the replace modification type on // the ds-task-state attribute if the value starts with "cancel" or // "stop". In that case, we'll cancel the task. boolean acceptable = isReplaceEntryAcceptable(modifyOperation); if (acceptable) { LocalizableMessage message = INFO_TASKBE_RUNNING_TASK_CANCELLED.get(); t.interruptTask(TaskState.STOPPED_BY_ADMINISTRATOR, message); return; } else { LocalizableMessage message = ERR_TASKBE_MODIFY_RUNNING.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } else if (TaskState.isPending(state) && t.isRecurring()) { // Pending recurring task iterations can only be canceled. boolean acceptable = isReplaceEntryAcceptable(modifyOperation); if (acceptable) { Task newTask = taskScheduler.entryToScheduledTask(newEntry, modifyOperation); if (newTask.getTaskState() == TaskState.CANCELED_BEFORE_STARTING) { taskScheduler.removePendingTask(t.getTaskID()); long scheduledStartTime = t.getScheduledStartTime(); long currentSystemTime = System.currentTimeMillis(); if (scheduledStartTime < currentSystemTime) { scheduledStartTime = currentSystemTime; } GregorianCalendar calendar = new GregorianCalendar(); calendar.setTimeInMillis(scheduledStartTime); taskScheduler.scheduleNextRecurringTaskIteration( newTask, calendar); } else if (newTask.getTaskState() == TaskState.STOPPED_BY_ADMINISTRATOR) { LocalizableMessage message = INFO_TASKBE_RUNNING_TASK_CANCELLED.get(); t.interruptTask(TaskState.STOPPED_BY_ADMINISTRATOR, message); } return; } else { LocalizableMessage message = ERR_TASKBE_MODIFY_RECURRING.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } else { LocalizableMessage message = ERR_TASKBE_MODIFY_COMPLETED.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } else if (parentDN.equals(recurringTaskParentDN)) { // We don't currently support altering recurring tasks. LocalizableMessage message = ERR_TASKBE_MODIFY_RECURRING.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } else { LocalizableMessage message = ERR_TASKBE_MODIFY_INVALID_ENTRY.get(entryDN); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } finally { if (entryLock != null) { entryLock.unlock(); } } } /** * Helper to determine if requested modifications are acceptable. * @param modifyOperation associated with requested modifications. * @return true if requested modifications are * acceptable, false otherwise. */ private boolean isReplaceEntryAcceptable(ModifyOperation modifyOperation) { boolean acceptable = true; for (Modification m : modifyOperation.getModifications()) { if (m.isInternal()) { continue; } if (m.getModificationType() != ModificationType.REPLACE) { acceptable = false; break; } Attribute a = m.getAttribute(); AttributeType at = a.getAttributeType(); if (!at.hasName(ATTR_TASK_STATE)) { acceptable = false; break; } Iterator iterator = a.iterator(); if (!iterator.hasNext()) { acceptable = false; break; } ByteString v = iterator.next(); String valueString = toLowerCase(v.toString()); if (!valueString.startsWith("cancel") && !valueString.startsWith("stop")) { acceptable = false; break; } if (iterator.hasNext()) { acceptable = false; break; } } return acceptable; } /** {@inheritDoc} */ @Override public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOperation) throws DirectoryException { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_BACKEND_MODIFY_DN_NOT_SUPPORTED.get(currentDN, getBackendID())); } /** {@inheritDoc} */ @Override public void search(SearchOperation searchOperation) throws DirectoryException, CanceledOperationException { // Look at the base DN and scope for the search operation to decide which // entries we need to look at. boolean searchRoot = false; boolean searchScheduledParent = false; boolean searchScheduledTasks = false; boolean searchRecurringParent = false; boolean searchRecurringTasks = false; DN baseDN = searchOperation.getBaseDN(); SearchScope searchScope = searchOperation.getScope(); SearchFilter searchFilter = searchOperation.getFilter(); if (baseDN.equals(taskRootDN)) { switch (searchScope.asEnum()) { case BASE_OBJECT: searchRoot = true; break; case SINGLE_LEVEL: searchScheduledParent = true; searchRecurringParent = true; break; case WHOLE_SUBTREE: searchRoot = true; searchScheduledParent = true; searchRecurringParent = true; searchScheduledTasks = true; searchRecurringTasks = true; break; case SUBORDINATES: searchScheduledParent = true; searchRecurringParent = true; searchScheduledTasks = true; searchRecurringTasks = true; break; } } else if (baseDN.equals(scheduledTaskParentDN)) { switch (searchScope.asEnum()) { case BASE_OBJECT: searchScheduledParent = true; break; case SINGLE_LEVEL: searchScheduledTasks = true; break; case WHOLE_SUBTREE: searchScheduledParent = true; searchScheduledTasks = true; break; case SUBORDINATES: searchScheduledTasks = true; break; } } else if (baseDN.equals(recurringTaskParentDN)) { switch (searchScope.asEnum()) { case BASE_OBJECT: searchRecurringParent = true; break; case SINGLE_LEVEL: searchRecurringTasks = true; break; case WHOLE_SUBTREE: searchRecurringParent = true; searchRecurringTasks = true; break; case SUBORDINATES: searchRecurringTasks = true; break; } } else { DN parentDN = baseDN.getParentDNInSuffix(); if (parentDN == null) { LocalizableMessage message = ERR_TASKBE_SEARCH_INVALID_BASE.get(baseDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } else if (parentDN.equals(scheduledTaskParentDN)) { DNLock lock = taskScheduler.readLockEntry(baseDN); try { Entry e = taskScheduler.getScheduledTaskEntry(baseDN); if (e == null) { LocalizableMessage message = ERR_TASKBE_SEARCH_NO_SUCH_TASK.get(baseDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, scheduledTaskParentDN, null); } if ((searchScope == SearchScope.BASE_OBJECT || searchScope == SearchScope.WHOLE_SUBTREE) && searchFilter.matchesEntry(e)) { searchOperation.returnEntry(e, null); } return; } finally { lock.unlock(); } } else if (parentDN.equals(recurringTaskParentDN)) { DNLock lock = taskScheduler.readLockEntry(baseDN); try { Entry e = taskScheduler.getRecurringTaskEntry(baseDN); if (e == null) { LocalizableMessage message = ERR_TASKBE_SEARCH_NO_SUCH_RECURRING_TASK.get(baseDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, recurringTaskParentDN, null); } if ((searchScope == SearchScope.BASE_OBJECT || searchScope == SearchScope.WHOLE_SUBTREE) && searchFilter.matchesEntry(e)) { searchOperation.returnEntry(e, null); } return; } finally { lock.unlock(); } } else { LocalizableMessage message = ERR_TASKBE_SEARCH_INVALID_BASE.get(baseDN); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } } if (searchRoot) { Entry e = taskScheduler.getTaskRootEntry(); if (searchFilter.matchesEntry(e) && !searchOperation.returnEntry(e, null)) { return; } } if (searchScheduledParent) { Entry e = taskScheduler.getScheduledTaskParentEntry(); if (searchFilter.matchesEntry(e) && !searchOperation.returnEntry(e, null)) { return; } } if (searchScheduledTasks && !taskScheduler.searchScheduledTasks(searchOperation)) { return; } if (searchRecurringParent) { Entry e = taskScheduler.getRecurringTaskParentEntry(); if (searchFilter.matchesEntry(e) && !searchOperation.returnEntry(e, null)) { return; } } if (searchRecurringTasks && !taskScheduler.searchRecurringTasks(searchOperation)) { return; } } /** {@inheritDoc} */ @Override public Set getSupportedControls() { return Collections.emptySet(); } /** {@inheritDoc} */ @Override public Set getSupportedFeatures() { return Collections.emptySet(); } /** {@inheritDoc} */ @Override public boolean supports(BackendOperation backendOperation) { switch (backendOperation) { case LDIF_EXPORT: case BACKUP: case RESTORE: return true; default: return false; } } /** {@inheritDoc} */ @Override public void exportLDIF(LDIFExportConfig exportConfig) throws DirectoryException { File taskFile = getFileForPath(taskBackingFile); // Read from. LDIFReader ldifReader; try { ldifReader = new LDIFReader(new LDIFImportConfig(taskFile.getPath())); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_CANNOT_EXPORT_TO_FILE.get(e); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Write to. LDIFWriter ldifWriter; try { ldifWriter = new LDIFWriter(exportConfig); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKS_CANNOT_EXPORT_TO_FILE.get( stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } // Copy record by record. try { while (true) { Entry e = null; try { e = ldifReader.readEntry(); if (e == null) { break; } } catch (LDIFException le) { if (! le.canContinueReading()) { LocalizableMessage message = ERR_TASKS_CANNOT_EXPORT_TO_FILE.get(e); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, le); } else { continue; } } ldifWriter.writeEntry(e); } } catch (Exception e) { logger.traceException(e); } finally { close(ldifWriter, ldifReader); } } /** {@inheritDoc} */ @Override public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext sContext) throws DirectoryException { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_BACKEND_IMPORT_NOT_SUPPORTED.get(getBackendID())); } /** {@inheritDoc} */ @Override public void createBackup(BackupConfig backupConfig) throws DirectoryException { // Get the properties to use for the backup. We don't care whether or not // it's incremental, so there's no need to get that. String backupID = backupConfig.getBackupID(); BackupDirectory backupDirectory = backupConfig.getBackupDirectory(); boolean compress = backupConfig.compressData(); boolean encrypt = backupConfig.encryptData(); boolean hash = backupConfig.hashData(); boolean signHash = backupConfig.signHash(); // Create a hash map that will hold the extra backup property information // for this backup. HashMap backupProperties = new HashMap(); // Get the crypto manager and use it to obtain references to the message // digest and/or MAC to use for hashing and/or signing. CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); Mac mac = null; MessageDigest digest = null; String digestAlgorithm = null; String macKeyID = null; if (hash) { if (signHash) { try { macKeyID = cryptoManager.getMacEngineKeyEntryID(); backupProperties.put(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID); mac = cryptoManager.getMacEngine(macKeyID); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKS_BACKUP_CANNOT_GET_MAC.get( macKeyID, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } else { digestAlgorithm = cryptoManager.getPreferredMessageDigestAlgorithm(); backupProperties.put(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm); try { digest = cryptoManager.getPreferredMessageDigest(); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKS_BACKUP_CANNOT_GET_DIGEST.get( digestAlgorithm, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } } // Create an output stream that will be used to write the archive file. At // its core, it will be a file output stream to put a file on the disk. If // we are to encrypt the data, then that file output stream will be wrapped // in a cipher output stream. The resulting output stream will then be // wrapped by a zip output stream (which may or may not actually use // compression). String filename = null; OutputStream outputStream; try { filename = TASKS_BACKUP_BASE_FILENAME + backupID; File archiveFile = new File(backupDirectory.getPath() + File.separator + filename); if (archiveFile.exists()) { int i=1; while (true) { archiveFile = new File(backupDirectory.getPath() + File.separator + filename + "." + i); if (archiveFile.exists()) { i++; } else { filename = filename + "." + i; break; } } } outputStream = new FileOutputStream(archiveFile, false); backupProperties.put(BACKUP_PROPERTY_ARCHIVE_FILENAME, filename); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKS_BACKUP_CANNOT_CREATE_ARCHIVE_FILE. get(filename, backupDirectory.getPath(), getExceptionMessage(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // If we should encrypt the data, then wrap the output stream in a cipher // output stream. if (encrypt) { try { outputStream = cryptoManager.getCipherOutputStream(outputStream); } catch (CryptoManagerException e) { logger.traceException(e); LocalizableMessage message = ERR_TASKS_BACKUP_CANNOT_GET_CIPHER.get( stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Wrap the file output stream in a zip output stream. ZipOutputStream zipStream = new ZipOutputStream(outputStream); LocalizableMessage message = ERR_TASKS_BACKUP_ZIP_COMMENT.get( DynamicConstants.PRODUCT_NAME, backupID); zipStream.setComment(String.valueOf(message)); if (compress) { zipStream.setLevel(Deflater.DEFAULT_COMPRESSION); } else { zipStream.setLevel(Deflater.NO_COMPRESSION); } // Take tasks file and write it to the zip stream. If we // are using a hash or MAC, then calculate that as well. byte[] buffer = new byte[8192]; File tasksFile = getFileForPath(taskBackingFile); String baseName = tasksFile.getName(); // We'll put the name in the hash, too. if (hash) { if (signHash) { mac.update(getBytes(baseName)); } else { digest.update(getBytes(baseName)); } } InputStream inputStream = null; try { ZipEntry zipEntry = new ZipEntry(baseName); zipStream.putNextEntry(zipEntry); inputStream = new FileInputStream(tasksFile); while (true) { int bytesRead = inputStream.read(buffer); if (bytesRead < 0 || backupConfig.isCancelled()) { break; } if (hash) { if (signHash) { mac.update(buffer, 0, bytesRead); } else { digest.update(buffer, 0, bytesRead); } } zipStream.write(buffer, 0, bytesRead); } zipStream.closeEntry(); inputStream.close(); } catch (Exception e) { logger.traceException(e); close(inputStream, zipStream); message = ERR_TASKS_BACKUP_CANNOT_BACKUP_TASKS_FILE.get(baseName, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } // We're done writing the file, so close the zip stream (which should also // close the underlying stream). try { zipStream.close(); } catch (Exception e) { logger.traceException(e); message = ERR_TASKS_BACKUP_CANNOT_CLOSE_ZIP_STREAM.get( filename, backupDirectory.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Get the digest or MAC bytes if appropriate. byte[] digestBytes = null; byte[] macBytes = null; if (hash) { if (signHash) { macBytes = mac.doFinal(); } else { digestBytes = digest.digest(); } } // Create the backup info structure for this backup and add it to the backup // directory. // FIXME -- Should I use the date from when I started or finished? BackupInfo backupInfo = new BackupInfo(backupDirectory, backupID, new Date(), false, compress, encrypt, digestBytes, macBytes, null, backupProperties); try { backupDirectory.addBackup(backupInfo); backupDirectory.writeBackupDirectoryDescriptor(); } catch (Exception e) { logger.traceException(e); message = ERR_TASKS_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( backupDirectory.getDescriptorPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } /** {@inheritDoc} */ @Override public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException { BackupInfo backupInfo = backupDirectory.getBackupInfo(backupID); if (backupInfo == null) { LocalizableMessage message = ERR_BACKUP_MISSING_BACKUPID.get(backupID, backupDirectory.getPath()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } HashMap backupProperties = backupInfo.getBackupProperties(); String archiveFilename = backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); File archiveFile = new File(backupDirectory.getPath(), archiveFilename); try { backupDirectory.removeBackup(backupID); } catch (ConfigException e) { logger.traceException(e); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } try { backupDirectory.writeBackupDirectoryDescriptor(); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( backupDirectory.getDescriptorPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Remove the archive file. archiveFile.delete(); } /** {@inheritDoc} */ @Override public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException { // First, make sure that the requested backup exists. BackupDirectory backupDirectory = restoreConfig.getBackupDirectory(); String backupPath = backupDirectory.getPath(); String backupID = restoreConfig.getBackupID(); BackupInfo backupInfo = backupDirectory.getBackupInfo(backupID); boolean verifyOnly = restoreConfig.verifyOnly(); if (backupInfo == null) { LocalizableMessage message = ERR_TASKS_RESTORE_NO_SUCH_BACKUP.get(backupID, backupPath); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } // Read the backup info structure to determine the name of the file that // contains the archive. Then make sure that file exists. String backupFilename = backupInfo.getBackupProperty(BACKUP_PROPERTY_ARCHIVE_FILENAME); if (backupFilename == null) { LocalizableMessage message = ERR_TASKS_RESTORE_NO_BACKUP_FILE.get(backupID, backupPath); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } File backupFile = new File(backupPath + File.separator + backupFilename); try { if (! backupFile.exists()) { LocalizableMessage message = ERR_TASKS_RESTORE_NO_SUCH_FILE.get(backupID, backupFile.getPath()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } } catch (DirectoryException de) { throw de; } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_CHECK_FOR_ARCHIVE.get( backupID, backupFile.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // If the backup is hashed, then we need to get the message digest to use // to verify it. byte[] unsignedHash = backupInfo.getUnsignedHash(); MessageDigest digest = null; if (unsignedHash != null) { String digestAlgorithm = backupInfo.getBackupProperty(BACKUP_PROPERTY_DIGEST_ALGORITHM); if (digestAlgorithm == null) { LocalizableMessage message = ERR_TASKS_RESTORE_UNKNOWN_DIGEST.get(backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } try { digest = DirectoryServer.getCryptoManager().getMessageDigest( digestAlgorithm); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_GET_DIGEST.get(backupID, digestAlgorithm); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // If the backup is signed, then we need to get the MAC to use to verify it. byte[] signedHash = backupInfo.getSignedHash(); Mac mac = null; if (signedHash != null) { String macKeyID = backupInfo.getBackupProperty(BACKUP_PROPERTY_MAC_KEY_ID); if (macKeyID == null) { LocalizableMessage message = ERR_TASKS_RESTORE_UNKNOWN_MAC.get(backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } try { mac = DirectoryServer.getCryptoManager().getMacEngine(macKeyID); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_GET_MAC.get( backupID, macKeyID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Create the input stream that will be used to read the backup file. At // its core, it will be a file input stream. InputStream inputStream; try { inputStream = new FileInputStream(backupFile); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_OPEN_BACKUP_FILE.get( backupID, backupFile.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // If the backup is encrypted, then we need to wrap the file input stream // in a cipher input stream. if (backupInfo.isEncrypted()) { try { inputStream = DirectoryServer.getCryptoManager() .getCipherInputStream(inputStream); } catch (CryptoManagerException e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_GET_CIPHER.get( backupFile.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Now wrap the resulting input stream in a zip stream so that we can read // its contents. We don't need to worry about whether to use compression or // not because it will be handled automatically. ZipInputStream zipStream = new ZipInputStream(inputStream); // Read through the archive file an entry at a time. For each entry, update // the digest or MAC if necessary. byte[] buffer = new byte[8192]; while (true) { ZipEntry zipEntry; try { zipEntry = zipStream.getNextEntry(); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_GET_ZIP_ENTRY.get( backupID, backupFile.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } if (zipEntry == null) { break; } // Get the filename for the zip entry and update the digest or MAC as // necessary. String fileName = zipEntry.getName(); if (digest != null) { digest.update(getBytes(fileName)); } if (mac != null) { mac.update(getBytes(fileName)); } // If we're doing the restore, then create the output stream to write the // file. File tasksFile = getFileForPath(taskBackingFile); String baseDirPath = tasksFile.getParent(); OutputStream outputStream = null; if (!verifyOnly) { String filePath = baseDirPath + File.separator + fileName; try { outputStream = new FileOutputStream(filePath); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_CREATE_FILE.get( backupID, filePath, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } // Read the contents of the file and update the digest or MAC as // necessary. try { while (true) { int bytesRead = zipStream.read(buffer); if (bytesRead < 0) { // We've reached the end of the entry. break; } // Update the digest or MAC if appropriate. if (digest != null) { digest.update(buffer, 0, bytesRead); } if (mac != null) { mac.update(buffer, 0, bytesRead); } // Write the data to the output stream if appropriate. if (outputStream != null) { outputStream.write(buffer, 0, bytesRead); } } // We're at the end of the file so close the output stream if we're // writing it. if (outputStream != null) { outputStream.close(); } } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_CANNOT_PROCESS_ARCHIVE_FILE.get( backupID, fileName, stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Close the zip stream since we don't need it anymore. try { zipStream.close(); } catch (Exception e) { LocalizableMessage message = ERR_TASKS_RESTORE_ERROR_ON_ZIP_STREAM_CLOSE.get( backupID, backupFile.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // At this point, we should be done with the contents of the ZIP file and // the restore should be complete. If we were generating a digest or MAC, // then make sure it checks out. if (digest != null) { byte[] calculatedHash = digest.digest(); if (Arrays.equals(calculatedHash, unsignedHash)) { logger.info(NOTE_TASKS_RESTORE_UNSIGNED_HASH_VALID); } else { LocalizableMessage message = ERR_TASKS_RESTORE_UNSIGNED_HASH_INVALID.get(backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } } if (mac != null) { byte[] calculatedSignature = mac.doFinal(); if (Arrays.equals(calculatedSignature, signedHash)) { logger.info(NOTE_TASKS_RESTORE_SIGNED_HASH_VALID); } else { LocalizableMessage message = ERR_TASKS_RESTORE_SIGNED_HASH_INVALID.get(backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } } // If we are just verifying the archive, then we're done. if (verifyOnly) { logger.info(NOTE_TASKS_RESTORE_VERIFY_SUCCESSFUL, backupID, backupPath); return; } // If we've gotten here, then the archive was restored successfully. logger.info(NOTE_TASKS_RESTORE_SUCCESSFUL, backupID, backupPath); } /** {@inheritDoc} */ @Override public boolean isConfigurationAcceptable(TaskBackendCfg config, List unacceptableReasons, ServerContext serverContext) { return isConfigAcceptable(config, unacceptableReasons, null); } /** {@inheritDoc} */ @Override public boolean isConfigurationChangeAcceptable(TaskBackendCfg configEntry, List unacceptableReasons) { return isConfigAcceptable(configEntry, unacceptableReasons, taskBackingFile); } /** * Indicates whether the provided configuration is acceptable for this task * backend. * * @param config The configuration for which to make the * determination. * @param unacceptableReasons A list into which the unacceptable reasons * should be placed. * @param taskBackingFile The currently-configured task backing file, or * {@code null} if it should not be taken into * account. * * @return {@code true} if the configuration is acceptable, or {@code false} * if not. */ private static boolean isConfigAcceptable(TaskBackendCfg config, List unacceptableReasons, String taskBackingFile) { boolean configIsAcceptable = true; try { String tmpBackingFile = config.getTaskBackingFile(); if (taskBackingFile == null || !taskBackingFile.equals(tmpBackingFile)) { File f = getFileForPath(tmpBackingFile); if (f.exists()) { // This is only a problem if it's different from the active one. if (taskBackingFile != null) { unacceptableReasons.add( ERR_TASKBE_BACKING_FILE_EXISTS.get(tmpBackingFile)); configIsAcceptable = false; } } else { File p = f.getParentFile(); if (p == null) { unacceptableReasons.add(ERR_TASKBE_INVALID_BACKING_FILE_PATH.get( tmpBackingFile)); configIsAcceptable = false; } else if (! p.exists()) { unacceptableReasons.add(ERR_TASKBE_BACKING_FILE_MISSING_PARENT.get( p.getPath(), tmpBackingFile)); configIsAcceptable = false; } else if (! p.isDirectory()) { unacceptableReasons.add( ERR_TASKBE_BACKING_FILE_PARENT_NOT_DIRECTORY.get( p.getPath(), tmpBackingFile)); configIsAcceptable = false; } } } } catch (Exception e) { logger.traceException(e); unacceptableReasons.add(ERR_TASKBE_ERROR_GETTING_BACKING_FILE.get( getExceptionMessage(e))); configIsAcceptable = false; } return configIsAcceptable; } /** {@inheritDoc} */ @Override public ConfigChangeResult applyConfigurationChange(TaskBackendCfg configEntry) { final ConfigChangeResult ccr = new ConfigChangeResult(); String tmpBackingFile = taskBackingFile; try { { tmpBackingFile = configEntry.getTaskBackingFile(); if (! taskBackingFile.equals(tmpBackingFile)) { File f = getFileForPath(tmpBackingFile); if (f.exists()) { ccr.addMessage(ERR_TASKBE_BACKING_FILE_EXISTS.get(tmpBackingFile)); ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); } else { File p = f.getParentFile(); if (p == null) { ccr.addMessage(ERR_TASKBE_INVALID_BACKING_FILE_PATH.get(tmpBackingFile)); ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); } else if (! p.exists()) { ccr.addMessage(ERR_TASKBE_BACKING_FILE_MISSING_PARENT.get(p, tmpBackingFile)); ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); } else if (! p.isDirectory()) { ccr.addMessage(ERR_TASKBE_BACKING_FILE_PARENT_NOT_DIRECTORY.get(p, tmpBackingFile)); ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); } } } } } catch (Exception e) { logger.traceException(e); ccr.addMessage(ERR_TASKBE_ERROR_GETTING_BACKING_FILE.get(getExceptionMessage(e))); ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); } long tmpRetentionTime = configEntry.getTaskRetentionTime(); if (ccr.getResultCode() == ResultCode.SUCCESS) { // Everything looks OK, so apply the changes. if (retentionTime != tmpRetentionTime) { retentionTime = tmpRetentionTime; ccr.addMessage(INFO_TASKBE_UPDATED_RETENTION_TIME.get(retentionTime)); } if (! taskBackingFile.equals(tmpBackingFile)) { taskBackingFile = tmpBackingFile; taskScheduler.writeState(); ccr.addMessage(INFO_TASKBE_UPDATED_BACKING_FILE.get(taskBackingFile)); } } String tmpNotificationAddress = configEntry.getNotificationSenderAddress(); if (tmpNotificationAddress == null) { try { tmpNotificationAddress = "opendj-task-notification@" + InetAddress.getLocalHost().getCanonicalHostName(); } catch (Exception e) { tmpNotificationAddress = "opendj-task-notification@opendj.org"; } } notificationSenderAddress = tmpNotificationAddress; currentConfig = configEntry; return ccr; } /** * Retrieves the DN of the configuration entry for this task backend. * * @return The DN of the configuration entry for this task backend. */ public DN getConfigEntryDN() { return configEntryDN; } /** * Retrieves the path to the backing file that will hold the scheduled and * recurring task definitions. * * @return The path to the backing file that will hold the scheduled and * recurring task definitions. */ public String getTaskBackingFile() { File f = getFileForPath(taskBackingFile); return f.getPath(); } /** * Retrieves the sender address that should be used for e-mail notifications * of task completion. * * @return The sender address that should be used for e-mail notifications of * task completion. */ public String getNotificationSenderAddress() { return notificationSenderAddress; } /** * Retrieves the length of time in seconds that information for a task should * be retained after processing on it has completed. * * @return The length of time in seconds that information for a task should * be retained after processing on it has completed. */ public long getRetentionTime() { return retentionTime; } /** * Retrieves the DN of the entry that is the root for all task information in * the Directory Server. * * @return The DN of the entry that is the root for all task information in * the Directory Server. */ public DN getTaskRootDN() { return taskRootDN; } /** * Retrieves the DN of the entry that is the immediate parent for all * recurring task information in the Directory Server. * * @return The DN of the entry that is the immediate parent for all recurring * task information in the Directory Server. */ public DN getRecurringTasksParentDN() { return recurringTaskParentDN; } /** * Retrieves the DN of the entry that is the immediate parent for all * scheduled task information in the Directory Server. * * @return The DN of the entry that is the immediate parent for all scheduled * task information in the Directory Server. */ public DN getScheduledTasksParentDN() { return scheduledTaskParentDN; } /** * Retrieves the scheduled task for the entry with the provided DN. * * @param taskEntryDN The DN of the entry for the task to retrieve. * * @return The requested task, or {@code null} if there is no task with the * specified entry DN. */ public Task getScheduledTask(DN taskEntryDN) { return taskScheduler.getScheduledTask(taskEntryDN); } /** * Retrieves the recurring task for the entry with the provided DN. * * @param taskEntryDN The DN of the entry for the recurring task to * retrieve. * * @return The requested recurring task, or {@code null} if there is no task * with the specified entry DN. */ public RecurringTask getRecurringTask(DN taskEntryDN) { return taskScheduler.getRecurringTask(taskEntryDN); } /** {@inheritDoc} */ @Override public void preloadEntryCache() throws UnsupportedOperationException { throw new UnsupportedOperationException("Operation not supported."); } }