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 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.tasks;
018
019import static org.opends.messages.TaskMessages.*;
020import static org.opends.messages.ToolMessages.*;
021import static org.opends.server.config.ConfigConstants.*;
022import static org.opends.server.core.DirectoryServer.*;
023import static org.opends.server.util.StaticUtils.*;
024
025import java.io.File;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031
032import org.forgerock.i18n.LocalizableMessage;
033import org.forgerock.i18n.slf4j.LocalizedLogger;
034import org.forgerock.opendj.ldap.DN;
035import org.forgerock.opendj.ldap.ResultCode;
036import org.forgerock.opendj.ldap.schema.AttributeType;
037import org.opends.messages.Severity;
038import org.opends.messages.TaskMessages;
039import org.opends.server.api.Backend;
040import org.opends.server.api.Backend.BackendOperation;
041import org.opends.server.api.ClientConnection;
042import org.opends.server.backends.task.Task;
043import org.opends.server.backends.task.TaskState;
044import org.opends.server.core.DirectoryServer;
045import org.opends.server.core.LockFileManager;
046import org.opends.server.types.Attribute;
047import org.opends.server.types.DirectoryException;
048import org.opends.server.types.Entry;
049import org.opends.server.types.ExistingFileBehavior;
050import org.opends.server.types.LDIFExportConfig;
051import org.opends.server.types.Operation;
052import org.opends.server.types.Privilege;
053import org.opends.server.types.SearchFilter;
054
055/**
056 * This class provides an implementation of a Directory Server task that can
057 * be used to export the contents of a Directory Server backend to an LDIF file.
058 */
059public class ExportTask extends Task
060{
061  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
062
063  /** Stores mapping between configuration attribute name and its label. */
064  private static final Map<String, LocalizableMessage> argDisplayMap = new HashMap<>();
065  static {
066    argDisplayMap.put(ATTR_TASK_EXPORT_LDIF_FILE, INFO_EXPORT_ARG_LDIF_FILE.get());
067    argDisplayMap.put(ATTR_TASK_EXPORT_BACKEND_ID, INFO_EXPORT_ARG_BACKEND_ID.get());
068    argDisplayMap.put(ATTR_TASK_EXPORT_APPEND_TO_LDIF, INFO_EXPORT_ARG_APPEND_TO_LDIF.get());
069    argDisplayMap.put(ATTR_TASK_EXPORT_COMPRESS_LDIF, INFO_EXPORT_ARG_COMPRESS_LDIF.get());
070    argDisplayMap.put(ATTR_TASK_EXPORT_ENCRYPT_LDIF, INFO_EXPORT_ARG_ENCRYPT_LDIF.get());
071    argDisplayMap.put(ATTR_TASK_EXPORT_SIGN_HASH, INFO_EXPORT_ARG_SIGN_HASH.get());
072    argDisplayMap.put(ATTR_TASK_EXPORT_INCLUDE_ATTRIBUTE, INFO_EXPORT_ARG_INCL_ATTR.get());
073    argDisplayMap.put(ATTR_TASK_EXPORT_EXCLUDE_ATTRIBUTE, INFO_EXPORT_ARG_EXCL_ATTR.get());
074    argDisplayMap.put(ATTR_TASK_EXPORT_INCLUDE_FILTER, INFO_EXPORT_ARG_INCL_FILTER.get());
075    argDisplayMap.put(ATTR_TASK_EXPORT_EXCLUDE_FILTER, INFO_EXPORT_ARG_EXCL_FILTER.get());
076    argDisplayMap.put(ATTR_TASK_EXPORT_INCLUDE_BRANCH, INFO_EXPORT_ARG_INCL_BRANCH.get());
077    argDisplayMap.put(ATTR_TASK_EXPORT_EXCLUDE_BRANCH, INFO_EXPORT_ARG_EXCL_BRANCH.get());
078    argDisplayMap.put(ATTR_TASK_EXPORT_WRAP_COLUMN, INFO_EXPORT_ARG_WRAP_COLUMN.get());
079  }
080
081  private String  ldifFile;
082  private String  backendID;
083  private int     wrapColumn;
084  private boolean appendToLDIF;
085  private boolean compressLDIF;
086  private boolean encryptLDIF;
087  private boolean signHash;
088  private boolean includeOperationalAttributes;
089  private ArrayList<String> includeAttributeStrings;
090  private ArrayList<String> excludeAttributeStrings;
091  private ArrayList<String> includeFilterStrings;
092  private ArrayList<String> excludeFilterStrings;
093  private ArrayList<String> includeBranchStrings;
094  private ArrayList<String> excludeBranchStrings;
095
096  private LDIFExportConfig exportConfig;
097
098  @Override
099  public LocalizableMessage getDisplayName() {
100    return INFO_TASK_EXPORT_NAME.get();
101  }
102
103  @Override
104  public LocalizableMessage getAttributeDisplayName(String name) {
105    return argDisplayMap.get(name);
106  }
107
108  @Override
109  public void initializeTask() throws DirectoryException
110  {
111    // If the client connection is available, then make sure the associated
112    // client has the LDIF_EXPORT privilege.
113    Operation operation = getOperation();
114    if (operation != null)
115    {
116      ClientConnection clientConnection = operation.getClientConnection();
117      if (! clientConnection.hasPrivilege(Privilege.LDIF_EXPORT, operation))
118      {
119        LocalizableMessage message = ERR_TASK_LDIFEXPORT_INSUFFICIENT_PRIVILEGES.get();
120        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
121                                     message);
122      }
123    }
124
125    Entry taskEntry = getTaskEntry();
126    AttributeType typeWrapColumn = getSchema().getAttributeType(ATTR_TASK_EXPORT_WRAP_COLUMN);
127
128    ldifFile = toString(taskEntry, ATTR_TASK_EXPORT_LDIF_FILE);
129    File f = new File (ldifFile);
130    if (! f.isAbsolute())
131    {
132      f = new File(DirectoryServer.getInstanceRoot(), ldifFile);
133      try
134      {
135        ldifFile = f.getCanonicalPath();
136      }
137      catch (Exception ex)
138      {
139        ldifFile = f.getAbsolutePath();
140      }
141    }
142
143    backendID = toString(taskEntry, ATTR_TASK_EXPORT_BACKEND_ID);
144    appendToLDIF = toBoolean(taskEntry, false, ATTR_TASK_EXPORT_APPEND_TO_LDIF);
145    compressLDIF = toBoolean(taskEntry, false, ATTR_TASK_EXPORT_COMPRESS_LDIF);
146    encryptLDIF = toBoolean(taskEntry, false, ATTR_TASK_EXPORT_ENCRYPT_LDIF);
147    signHash = toBoolean(taskEntry, false, ATTR_TASK_EXPORT_SIGN_HASH);
148    includeAttributeStrings = toListOfString(taskEntry, ATTR_TASK_EXPORT_INCLUDE_ATTRIBUTE);
149    excludeAttributeStrings = toListOfString(taskEntry, ATTR_TASK_EXPORT_EXCLUDE_ATTRIBUTE);
150    includeFilterStrings = toListOfString(taskEntry, ATTR_TASK_EXPORT_INCLUDE_FILTER);
151    excludeFilterStrings = toListOfString(taskEntry, ATTR_TASK_EXPORT_EXCLUDE_FILTER);
152    includeBranchStrings = toListOfString(taskEntry, ATTR_TASK_EXPORT_INCLUDE_BRANCH);
153    excludeBranchStrings = toListOfString(taskEntry, ATTR_TASK_EXPORT_EXCLUDE_BRANCH);
154
155    List<Attribute> attrList = taskEntry.getAttribute(typeWrapColumn);
156    wrapColumn = TaskUtils.getSingleValueInteger(attrList, 0);
157
158    includeOperationalAttributes = toBoolean(taskEntry, true, ATTR_TASK_EXPORT_INCLUDE_OPERATIONAL_ATTRIBUTES);
159  }
160
161  private boolean toBoolean(Entry entry, boolean defaultValue, String attrName)
162  {
163    final AttributeType attrType = getSchema().getAttributeType(attrName);
164    final List<Attribute> attrs = entry.getAttribute(attrType);
165    return TaskUtils.getBoolean(attrs, defaultValue);
166  }
167
168  private ArrayList<String> toListOfString(Entry entry, String attrName)
169  {
170    final AttributeType attrType = getSchema().getAttributeType(attrName);
171    final List<Attribute> attrs = entry.getAttribute(attrType);
172    return TaskUtils.getMultiValueString(attrs);
173  }
174
175  private String toString(Entry entry, String attrName)
176  {
177    final AttributeType attrType = getSchema().getAttributeType(attrName);
178    final List<Attribute> attrs = entry.getAttribute(attrType);
179    return TaskUtils.getSingleValueString(attrs);
180  }
181
182  @Override
183  public void interruptTask(TaskState interruptState, LocalizableMessage interruptReason)
184  {
185    if (TaskState.STOPPED_BY_ADMINISTRATOR.equals(interruptState) &&
186            exportConfig != null)
187    {
188      addLogMessage(Severity.INFORMATION, TaskMessages.INFO_TASK_STOPPED_BY_ADMIN.get(
189      interruptReason));
190      setTaskInterruptState(interruptState);
191      exportConfig.cancel();
192    }
193  }
194
195  @Override
196  public boolean isInterruptable() {
197    return true;
198  }
199
200  @Override
201  protected TaskState runTask()
202  {
203    // See if there were any user-defined sets of include/exclude attributes or
204    // filters.  If so, then process them.
205    HashSet<AttributeType> excludeAttributes = toAttributeTypes(excludeAttributeStrings);
206    HashSet<AttributeType> includeAttributes = toAttributeTypes(includeAttributeStrings);
207
208    ArrayList<SearchFilter> excludeFilters;
209    if (excludeFilterStrings == null)
210    {
211      excludeFilters = null;
212    }
213    else
214    {
215      excludeFilters = new ArrayList<>();
216      for (String filterString : excludeFilterStrings)
217      {
218        try
219        {
220          excludeFilters.add(SearchFilter.createFilterFromString(filterString));
221        }
222        catch (DirectoryException de)
223        {
224          logger.error(ERR_LDIFEXPORT_CANNOT_PARSE_EXCLUDE_FILTER, filterString, de.getMessageObject());
225          return TaskState.STOPPED_BY_ERROR;
226        }
227        catch (Exception e)
228        {
229          logger.error(ERR_LDIFEXPORT_CANNOT_PARSE_EXCLUDE_FILTER, filterString, getExceptionMessage(e));
230          return TaskState.STOPPED_BY_ERROR;
231        }
232      }
233    }
234
235    ArrayList<SearchFilter> includeFilters;
236    if (includeFilterStrings == null)
237    {
238      includeFilters = null;
239    }
240    else
241    {
242      includeFilters = new ArrayList<>();
243      for (String filterString : includeFilterStrings)
244      {
245        try
246        {
247          includeFilters.add(SearchFilter.createFilterFromString(filterString));
248        }
249        catch (DirectoryException de)
250        {
251          logger.error(ERR_LDIFEXPORT_CANNOT_PARSE_INCLUDE_FILTER, filterString, de.getMessageObject());
252          return TaskState.STOPPED_BY_ERROR;
253        }
254        catch (Exception e)
255        {
256          logger.error(ERR_LDIFEXPORT_CANNOT_PARSE_INCLUDE_FILTER, filterString, getExceptionMessage(e));
257          return TaskState.STOPPED_BY_ERROR;
258        }
259      }
260    }
261
262    // Get the backend into which the LDIF should be imported.
263
264    Backend<?> backend = DirectoryServer.getBackend(backendID);
265
266    if (backend == null)
267    {
268      logger.error(ERR_LDIFEXPORT_NO_BACKENDS_FOR_ID, backendID);
269      return TaskState.STOPPED_BY_ERROR;
270    }
271    else if (!backend.supports(BackendOperation.LDIF_EXPORT))
272    {
273      logger.error(ERR_LDIFEXPORT_CANNOT_EXPORT_BACKEND, backendID);
274      return TaskState.STOPPED_BY_ERROR;
275    }
276
277    ArrayList<DN> defaultIncludeBranches = new ArrayList<>(backend.getBaseDNs());
278
279    ArrayList<DN> excludeBranches = new ArrayList<>();
280    if (excludeBranchStrings != null)
281    {
282      for (String s : excludeBranchStrings)
283      {
284        DN excludeBranch;
285        try
286        {
287          excludeBranch = DN.valueOf(s);
288        }
289        catch (Exception e)
290        {
291          logger.error(ERR_LDIFEXPORT_CANNOT_DECODE_EXCLUDE_BASE, s, getExceptionMessage(e));
292          return TaskState.STOPPED_BY_ERROR;
293        }
294
295        if (! excludeBranches.contains(excludeBranch))
296        {
297          excludeBranches.add(excludeBranch);
298        }
299      }
300    }
301
302    ArrayList<DN> includeBranches;
303    if (!includeBranchStrings.isEmpty())
304    {
305      includeBranches = new ArrayList<>();
306      for (String s : includeBranchStrings)
307      {
308        DN includeBranch;
309        try
310        {
311          includeBranch = DN.valueOf(s);
312        }
313        catch (Exception e)
314        {
315          logger.error(ERR_LDIFIMPORT_CANNOT_DECODE_INCLUDE_BASE, s, getExceptionMessage(e));
316          return TaskState.STOPPED_BY_ERROR;
317        }
318
319        if (! Backend.handlesEntry(includeBranch, defaultIncludeBranches,
320                                   excludeBranches))
321        {
322          logger.error(ERR_LDIFEXPORT_INVALID_INCLUDE_BASE, s, backendID);
323          return TaskState.STOPPED_BY_ERROR;
324        }
325
326        includeBranches.add(includeBranch);
327      }
328    }
329    else
330    {
331      includeBranches = defaultIncludeBranches;
332    }
333
334    // Create the LDIF export configuration to use when reading the LDIF.
335    ExistingFileBehavior existingBehavior;
336    if (appendToLDIF)
337    {
338      existingBehavior = ExistingFileBehavior.APPEND;
339    }
340    else
341    {
342      existingBehavior = ExistingFileBehavior.OVERWRITE;
343    }
344
345    exportConfig = new LDIFExportConfig(ldifFile, existingBehavior);
346    exportConfig.setCompressData(compressLDIF);
347    exportConfig.setEncryptData(encryptLDIF);
348    exportConfig.setExcludeAttributes(excludeAttributes);
349    exportConfig.setExcludeBranches(excludeBranches);
350    exportConfig.setExcludeFilters(excludeFilters);
351    exportConfig.setIncludeAttributes(includeAttributes);
352    exportConfig.setIncludeBranches(includeBranches);
353    exportConfig.setIncludeFilters(includeFilters);
354    exportConfig.setSignHash(signHash);
355    exportConfig.setWrapColumn(wrapColumn);
356    exportConfig.setIncludeOperationalAttributes(includeOperationalAttributes);
357
358    // FIXME -- Should this be conditional?
359    exportConfig.setInvokeExportPlugins(true);
360
361    // Get the set of base DNs for the backend as an array.
362    DN[] baseDNs = new DN[defaultIncludeBranches.size()];
363    defaultIncludeBranches.toArray(baseDNs);
364
365    // From here we must make sure we close the export config.
366    try
367    {
368      // Acquire a shared lock for the backend.
369      try
370      {
371        String lockFile = LockFileManager.getBackendLockFileName(backend);
372        StringBuilder failureReason = new StringBuilder();
373        if (! LockFileManager.acquireSharedLock(lockFile, failureReason))
374        {
375          logger.error(ERR_LDIFEXPORT_CANNOT_LOCK_BACKEND, backend.getBackendID(), failureReason);
376          return TaskState.STOPPED_BY_ERROR;
377        }
378      }
379      catch (Exception e)
380      {
381        logger.error(ERR_LDIFEXPORT_CANNOT_LOCK_BACKEND, backend.getBackendID(), getExceptionMessage(e));
382        return TaskState.STOPPED_BY_ERROR;
383      }
384
385      // From here we must make sure we release the shared backend lock.
386      try
387      {
388        // Launch the export.
389        try
390        {
391          DirectoryServer.notifyExportBeginning(backend, exportConfig);
392          addLogMessage(Severity.INFORMATION, INFO_LDIFEXPORT_PATH_TO_LDIF_FILE.get(ldifFile));
393          backend.exportLDIF(exportConfig);
394          DirectoryServer.notifyExportEnded(backend, exportConfig, true);
395        }
396        catch (DirectoryException de)
397        {
398          DirectoryServer.notifyExportEnded(backend, exportConfig, false);
399          logger.error(ERR_LDIFEXPORT_ERROR_DURING_EXPORT, de.getMessageObject());
400          return TaskState.STOPPED_BY_ERROR;
401        }
402        catch (Exception e)
403        {
404          DirectoryServer.notifyExportEnded(backend, exportConfig, false);
405          logger.error(ERR_LDIFEXPORT_ERROR_DURING_EXPORT, getExceptionMessage(e));
406          return TaskState.STOPPED_BY_ERROR;
407        }
408      }
409      finally
410      {
411        // Release the shared lock on the backend.
412        try
413        {
414          String lockFile = LockFileManager.getBackendLockFileName(backend);
415          StringBuilder failureReason = new StringBuilder();
416          if (! LockFileManager.releaseLock(lockFile, failureReason))
417          {
418            logger.warn(WARN_LDIFEXPORT_CANNOT_UNLOCK_BACKEND, backend.getBackendID(), failureReason);
419            return TaskState.COMPLETED_WITH_ERRORS;
420          }
421        }
422        catch (Exception e)
423        {
424          logger.warn(WARN_LDIFEXPORT_CANNOT_UNLOCK_BACKEND, backend.getBackendID(), getExceptionMessage(e));
425          return TaskState.COMPLETED_WITH_ERRORS;
426        }
427      }
428    }
429    finally
430    {
431      // Clean up after the export by closing the export config.
432      exportConfig.close();
433    }
434
435    // If the operation was cancelled delete the export file since
436    // if will be incomplete.
437    if (exportConfig.isCancelled())
438    {
439      File f = new File(ldifFile);
440      if (f.exists())
441      {
442        f.delete();
443      }
444    }
445
446    // If we got here the task either completed successfully or was interrupted
447    return getFinalTaskState();
448  }
449
450  private HashSet<AttributeType> toAttributeTypes(ArrayList<String> attributeStrings)
451  {
452    if (attributeStrings == null)
453    {
454      return null;
455    }
456    HashSet<AttributeType> attributes = new HashSet<>();
457    for (String attrName : attributeStrings)
458    {
459      attributes.add(DirectoryServer.getSchema().getAttributeType(attrName));
460    }
461    return attributes;
462  }
463}