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