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