001/*
002 * Copyright 2016-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.transformations;
022
023
024
025import java.io.ByteArrayInputStream;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileOutputStream;
029import java.io.InputStream;
030import java.io.OutputStream;
031import java.util.ArrayList;
032import java.util.Iterator;
033import java.util.LinkedHashMap;
034import java.util.List;
035import java.util.TreeMap;
036import java.util.concurrent.atomic.AtomicLong;
037import java.util.zip.GZIPInputStream;
038import java.util.zip.GZIPOutputStream;
039
040import com.unboundid.ldap.sdk.Attribute;
041import com.unboundid.ldap.sdk.DN;
042import com.unboundid.ldap.sdk.Entry;
043import com.unboundid.ldap.sdk.LDAPException;
044import com.unboundid.ldap.sdk.ResultCode;
045import com.unboundid.ldap.sdk.Version;
046import com.unboundid.ldap.sdk.schema.Schema;
047import com.unboundid.ldif.AggregateLDIFReaderChangeRecordTranslator;
048import com.unboundid.ldif.AggregateLDIFReaderEntryTranslator;
049import com.unboundid.ldif.LDIFException;
050import com.unboundid.ldif.LDIFReader;
051import com.unboundid.ldif.LDIFReaderChangeRecordTranslator;
052import com.unboundid.ldif.LDIFReaderEntryTranslator;
053import com.unboundid.ldif.LDIFRecord;
054import com.unboundid.util.AggregateInputStream;
055import com.unboundid.util.ByteStringBuffer;
056import com.unboundid.util.CommandLineTool;
057import com.unboundid.util.Debug;
058import com.unboundid.util.StaticUtils;
059import com.unboundid.util.ThreadSafety;
060import com.unboundid.util.ThreadSafetyLevel;
061import com.unboundid.util.args.ArgumentException;
062import com.unboundid.util.args.ArgumentParser;
063import com.unboundid.util.args.BooleanArgument;
064import com.unboundid.util.args.DNArgument;
065import com.unboundid.util.args.FileArgument;
066import com.unboundid.util.args.FilterArgument;
067import com.unboundid.util.args.IntegerArgument;
068import com.unboundid.util.args.ScopeArgument;
069import com.unboundid.util.args.StringArgument;
070
071import static com.unboundid.ldap.sdk.transformations.TransformationMessages.*;
072
073
074
075/**
076 * This class provides a command-line tool that can be used to apply a number of
077 * transformations to an LDIF file.  The transformations that can be applied
078 * include:
079 * <UL>
080 *   <LI>
081 *     It can scramble the values of a specified set of attributes in a manner
082 *     that attempts to preserve the syntax and consistently scrambles the same
083 *     value to the same representation.
084 *   </LI>
085 *   <LI>
086 *     It can strip a specified set of attributes out of entries.
087 *   </LI>
088 *   <LI>
089 *     It can redact the values of a specified set of attributes, to indicate
090 *     that the values are there but providing no information about what their
091 *     values are.
092 *   </LI>
093 *   <LI>
094 *     It can replace the values of a specified attribute with a given set of
095 *     values.
096 *   </LI>
097 *   <LI>
098 *     It can add an attribute with a given set of values to any entry that does
099 *     not contain that attribute.
100 *   </LI>
101 *   <LI>
102 *     It can replace the values of a specified attribute with a value that
103 *     contains a sequentially-incrementing counter.
104 *   </LI>
105 *   <LI>
106 *     It can strip entries matching a given base DN, scope, and filter out of
107 *     the LDIF file.
108 *   </LI>
109 *   <LI>
110 *     It can perform DN mapping, so that entries that exist below one base DN
111 *     are moved below a different base DN.
112 *   </LI>
113 *   <LI>
114 *     It can perform attribute mapping, to replace uses of one attribute name
115 *     with another.
116 *   </LI>
117 * </UL>
118 */
119@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
120public final class TransformLDIF
121       extends CommandLineTool
122       implements LDIFReaderEntryTranslator
123{
124  /**
125   * The maximum length of any message to write to standard output or standard
126   * error.
127   */
128  private static final int MAX_OUTPUT_LINE_LENGTH =
129       StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
130
131
132
133  // The arguments for use by this program.
134  private BooleanArgument addToExistingValues = null;
135  private BooleanArgument appendToTargetLDIF = null;
136  private BooleanArgument compressTarget = null;
137  private BooleanArgument excludeNonMatchingEntries = null;
138  private BooleanArgument flattenAddOmittedRDNAttributesToEntry = null;
139  private BooleanArgument flattenAddOmittedRDNAttributesToRDN = null;
140  private BooleanArgument hideRedactedValueCount = null;
141  private BooleanArgument processDNs = null;
142  private BooleanArgument sourceCompressed = null;
143  private BooleanArgument sourceContainsChangeRecords = null;
144  private BooleanArgument sourceFromStandardInput = null;
145  private BooleanArgument targetToStandardOutput = null;
146  private DNArgument addAttributeBaseDN = null;
147  private DNArgument excludeEntryBaseDN = null;
148  private DNArgument flattenBaseDN = null;
149  private DNArgument moveSubtreeFrom = null;
150  private DNArgument moveSubtreeTo = null;
151  private FileArgument schemaPath = null;
152  private FileArgument sourceLDIF = null;
153  private FileArgument targetLDIF = null;
154  private FilterArgument addAttributeFilter = null;
155  private FilterArgument excludeEntryFilter = null;
156  private FilterArgument flattenExcludeFilter = null;
157  private IntegerArgument initialSequentialValue = null;
158  private IntegerArgument numThreads = null;
159  private IntegerArgument randomSeed = null;
160  private IntegerArgument sequentialValueIncrement = null;
161  private IntegerArgument wrapColumn = null;
162  private ScopeArgument addAttributeScope = null;
163  private ScopeArgument excludeEntryScope = null;
164  private StringArgument addAttributeName = null;
165  private StringArgument addAttributeValue = null;
166  private StringArgument excludeAttribute = null;
167  private StringArgument redactAttribute = null;
168  private StringArgument renameAttributeFrom = null;
169  private StringArgument renameAttributeTo = null;
170  private StringArgument replaceValuesAttribute = null;
171  private StringArgument replacementValue = null;
172  private StringArgument scrambleAttribute = null;
173  private StringArgument scrambleJSONField = null;
174  private StringArgument sequentialAttribute = null;
175  private StringArgument textAfterSequentialValue = null;
176  private StringArgument textBeforeSequentialValue = null;
177
178  // A set of thread-local byte stream buffers that will be used to construct
179  // the LDIF representations of records.
180  private final ThreadLocal<ByteStringBuffer> byteStringBuffers =
181       new ThreadLocal<ByteStringBuffer>();
182
183
184
185  /**
186   * Invokes this tool with the provided set of arguments.
187   *
188   * @param  args  The command-line arguments provided to this program.
189   */
190  public static void main(final String... args)
191  {
192    final ResultCode resultCode = main(System.out, System.err, args);
193    if (resultCode != ResultCode.SUCCESS)
194    {
195      System.exit(resultCode.intValue());
196    }
197  }
198
199
200
201  /**
202   * Invokes this tool with the provided set of arguments.
203   *
204   * @param  out   The output stream to use for standard output.  It may be
205   *               {@code null} if standard output should be suppressed.
206   * @param  err   The output stream to use for standard error.  It may be
207   *               {@code null} if standard error should be suppressed.
208   * @param  args  The command-line arguments provided to this program.
209   *
210   * @return  A result code indicating whether processing completed
211   *          successfully.
212   */
213  public static ResultCode main(final OutputStream out, final OutputStream err,
214                                final String... args)
215  {
216    final TransformLDIF tool = new TransformLDIF(out, err);
217    return tool.runTool(args);
218  }
219
220
221
222  /**
223   * Creates a new instance of this tool with the provided information.
224   *
225   * @param  out  The output stream to use for standard output.  It may be
226   *              {@code null} if standard output should be suppressed.
227   * @param  err  The output stream to use for standard error.  It may be
228   *              {@code null} if standard error should be suppressed.
229   */
230  public TransformLDIF(final OutputStream out, final OutputStream err)
231  {
232    super(out, err);
233  }
234
235
236
237  /**
238   * {@inheritDoc}
239   */
240  @Override()
241  public String getToolName()
242  {
243    return "transform-ldif";
244  }
245
246
247
248  /**
249   * {@inheritDoc}
250   */
251  @Override()
252  public String getToolDescription()
253  {
254    return INFO_TRANSFORM_LDIF_TOOL_DESCRIPTION.get();
255  }
256
257
258
259  /**
260   * {@inheritDoc}
261   */
262  @Override()
263  public String getToolVersion()
264  {
265    return Version.NUMERIC_VERSION_STRING;
266  }
267
268
269
270  /**
271   * {@inheritDoc}
272   */
273  @Override()
274  public boolean supportsInteractiveMode()
275  {
276    return true;
277  }
278
279
280
281  /**
282   * {@inheritDoc}
283   */
284  @Override()
285  public boolean defaultsToInteractiveMode()
286  {
287    return true;
288  }
289
290
291
292  /**
293   * {@inheritDoc}
294   */
295  @Override()
296  public boolean supportsPropertiesFile()
297  {
298    return true;
299  }
300
301
302
303  /**
304   * {@inheritDoc}
305   */
306  @Override()
307  public void addToolArguments(final ArgumentParser parser)
308         throws ArgumentException
309  {
310    // Add arguments pertaining to the source and target LDIF files.
311    sourceLDIF = new FileArgument('l', "sourceLDIF", false, 0, null,
312         INFO_TRANSFORM_LDIF_ARG_DESC_SOURCE_LDIF.get(), true, true, true,
313         false);
314    sourceLDIF.addLongIdentifier("inputLDIF", true);
315    sourceLDIF.addLongIdentifier("source-ldif", true);
316    sourceLDIF.addLongIdentifier("input-ldif", true);
317    sourceLDIF.setArgumentGroupName(INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
318    parser.addArgument(sourceLDIF);
319
320    sourceFromStandardInput = new BooleanArgument(null,
321         "sourceFromStandardInput", 1,
322         INFO_TRANSFORM_LDIF_ARG_DESC_SOURCE_STD_IN.get());
323    sourceFromStandardInput.addLongIdentifier("source-from-standard-input",
324         true);
325    sourceFromStandardInput.setArgumentGroupName(
326         INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
327    parser.addArgument(sourceFromStandardInput);
328    parser.addRequiredArgumentSet(sourceLDIF, sourceFromStandardInput);
329    parser.addExclusiveArgumentSet(sourceLDIF, sourceFromStandardInput);
330
331    targetLDIF = new FileArgument('o', "targetLDIF", false, 1, null,
332         INFO_TRANSFORM_LDIF_ARG_DESC_TARGET_LDIF.get(), false, true, true,
333         false);
334    targetLDIF.addLongIdentifier("outputLDIF", true);
335    targetLDIF.addLongIdentifier("target-ldif", true);
336    targetLDIF.addLongIdentifier("output-ldif", true);
337    targetLDIF.setArgumentGroupName(INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
338    parser.addArgument(targetLDIF);
339
340    targetToStandardOutput = new BooleanArgument(null, "targetToStandardOutput",
341         1, INFO_TRANSFORM_LDIF_ARG_DESC_TARGET_STD_OUT.get());
342    targetToStandardOutput.addLongIdentifier("target-to-standard-output", true);
343    targetToStandardOutput.setArgumentGroupName(
344         INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
345    parser.addArgument(targetToStandardOutput);
346    parser.addExclusiveArgumentSet(targetLDIF, targetToStandardOutput);
347
348    sourceContainsChangeRecords = new BooleanArgument(null,
349         "sourceContainsChangeRecords",
350         INFO_TRANSFORM_LDIF_ARG_DESC_SOURCE_CONTAINS_CHANGE_RECORDS.get());
351    sourceContainsChangeRecords.addLongIdentifier(
352         "source-contains-change-records", true);
353    sourceContainsChangeRecords.setArgumentGroupName(
354         INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
355    parser.addArgument(sourceContainsChangeRecords);
356
357    appendToTargetLDIF = new BooleanArgument(null, "appendToTargetLDIF",
358         INFO_TRANSFORM_LDIF_ARG_DESC_APPEND_TO_TARGET.get());
359    appendToTargetLDIF.addLongIdentifier("append-to-target-ldif", true);
360    appendToTargetLDIF.setArgumentGroupName(
361         INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
362    parser.addArgument(appendToTargetLDIF);
363    parser.addExclusiveArgumentSet(targetToStandardOutput, appendToTargetLDIF);
364
365    wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null,
366         INFO_TRANSFORM_LDIF_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE);
367    wrapColumn.addLongIdentifier("wrap-column", true);
368    wrapColumn.setArgumentGroupName(INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
369    parser.addArgument(wrapColumn);
370
371    sourceCompressed = new BooleanArgument('C', "sourceCompressed",
372         INFO_TRANSFORM_LDIF_ARG_DESC_SOURCE_COMPRESSED.get());
373    sourceCompressed.addLongIdentifier("inputCompressed", true);
374    sourceCompressed.addLongIdentifier("source-compressed", true);
375    sourceCompressed.addLongIdentifier("input-compressed", true);
376    sourceCompressed.setArgumentGroupName(
377         INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
378    parser.addArgument(sourceCompressed);
379
380    compressTarget = new BooleanArgument('c', "compressTarget",
381         INFO_TRANSFORM_LDIF_ARG_DESC_COMPRESS_TARGET.get());
382    compressTarget.addLongIdentifier("compressOutput", true);
383    compressTarget.addLongIdentifier("compress", true);
384    compressTarget.addLongIdentifier("compress-target", true);
385    compressTarget.addLongIdentifier("compress-output", true);
386    compressTarget.setArgumentGroupName(
387         INFO_TRANSFORM_LDIF_ARG_GROUP_LDIF.get());
388    parser.addArgument(compressTarget);
389
390
391    // Add arguments pertaining to attribute scrambling.
392    scrambleAttribute = new StringArgument('a', "scrambleAttribute", false, 0,
393         INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
394         INFO_TRANSFORM_LDIF_ARG_DESC_SCRAMBLE_ATTR.get());
395    scrambleAttribute.addLongIdentifier("attributeName", true);
396    scrambleAttribute.addLongIdentifier("scramble-attribute", true);
397    scrambleAttribute.addLongIdentifier("attribute-name", true);
398    scrambleAttribute.setArgumentGroupName(
399         INFO_TRANSFORM_LDIF_ARG_GROUP_SCRAMBLE.get());
400    parser.addArgument(scrambleAttribute);
401
402    scrambleJSONField = new StringArgument(null, "scrambleJSONField", false, 0,
403         INFO_TRANSFORM_LDIF_PLACEHOLDER_FIELD_NAME.get(),
404         INFO_TRANSFORM_LDIF_ARG_DESC_SCRAMBLE_JSON_FIELD.get(
405              scrambleAttribute.getIdentifierString()));
406    scrambleJSONField.addLongIdentifier("scramble-json-field", true);
407    scrambleJSONField.setArgumentGroupName(
408         INFO_TRANSFORM_LDIF_ARG_GROUP_SCRAMBLE.get());
409    parser.addArgument(scrambleJSONField);
410    parser.addDependentArgumentSet(scrambleJSONField, scrambleAttribute);
411
412    randomSeed = new IntegerArgument('s', "randomSeed", false, 1, null,
413         INFO_TRANSFORM_LDIF_ARG_DESC_RANDOM_SEED.get());
414    randomSeed.addLongIdentifier("random-seed", true);
415    randomSeed.setArgumentGroupName(
416         INFO_TRANSFORM_LDIF_ARG_GROUP_SCRAMBLE.get());
417    parser.addArgument(randomSeed);
418
419
420    // Add arguments pertaining to replacing attribute values with a generated
421    // value using a sequential counter.
422    sequentialAttribute = new StringArgument('S', "sequentialAttribute",
423         false, 0, INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
424         INFO_TRANSFORM_LDIF_ARG_DESC_SEQUENTIAL_ATTR.get(
425              sourceContainsChangeRecords.getIdentifierString()));
426    sequentialAttribute.addLongIdentifier("sequentialAttributeName", true);
427    sequentialAttribute.addLongIdentifier("sequential-attribute", true);
428    sequentialAttribute.addLongIdentifier("sequential-attribute-name", true);
429    sequentialAttribute.setArgumentGroupName(
430         INFO_TRANSFORM_LDIF_ARG_GROUP_SEQUENTIAL.get());
431    parser.addArgument(sequentialAttribute);
432    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
433         sequentialAttribute);
434
435    initialSequentialValue = new IntegerArgument('i', "initialSequentialValue",
436         false, 1, null,
437         INFO_TRANSFORM_LDIF_ARG_DESC_INITIAL_SEQUENTIAL_VALUE.get(
438              sequentialAttribute.getIdentifierString()));
439    initialSequentialValue.addLongIdentifier("initial-sequential-value", true);
440    initialSequentialValue.setArgumentGroupName(
441         INFO_TRANSFORM_LDIF_ARG_GROUP_SEQUENTIAL.get());
442    parser.addArgument(initialSequentialValue);
443    parser.addDependentArgumentSet(initialSequentialValue, sequentialAttribute);
444
445    sequentialValueIncrement = new IntegerArgument(null,
446         "sequentialValueIncrement", false, 1, null,
447         INFO_TRANSFORM_LDIF_ARG_DESC_SEQUENTIAL_INCREMENT.get(
448              sequentialAttribute.getIdentifierString()));
449    sequentialValueIncrement.addLongIdentifier("sequential-value-increment",
450         true);
451    sequentialValueIncrement.setArgumentGroupName(
452         INFO_TRANSFORM_LDIF_ARG_GROUP_SEQUENTIAL.get());
453    parser.addArgument(sequentialValueIncrement);
454    parser.addDependentArgumentSet(sequentialValueIncrement,
455         sequentialAttribute);
456
457    textBeforeSequentialValue = new StringArgument(null,
458         "textBeforeSequentialValue", false, 1, null,
459         INFO_TRANSFORM_LDIF_ARG_DESC_SEQUENTIAL_TEXT_BEFORE.get(
460              sequentialAttribute.getIdentifierString()));
461    textBeforeSequentialValue.addLongIdentifier("text-before-sequential-value",
462         true);
463    textBeforeSequentialValue.setArgumentGroupName(
464         INFO_TRANSFORM_LDIF_ARG_GROUP_SEQUENTIAL.get());
465    parser.addArgument(textBeforeSequentialValue);
466    parser.addDependentArgumentSet(textBeforeSequentialValue,
467         sequentialAttribute);
468
469    textAfterSequentialValue = new StringArgument(null,
470         "textAfterSequentialValue", false, 1, null,
471         INFO_TRANSFORM_LDIF_ARG_DESC_SEQUENTIAL_TEXT_AFTER.get(
472              sequentialAttribute.getIdentifierString()));
473    textAfterSequentialValue.addLongIdentifier("text-after-sequential-value",
474         true);
475    textAfterSequentialValue.setArgumentGroupName(
476         INFO_TRANSFORM_LDIF_ARG_GROUP_SEQUENTIAL.get());
477    parser.addArgument(textAfterSequentialValue);
478    parser.addDependentArgumentSet(textAfterSequentialValue,
479         sequentialAttribute);
480
481
482    // Add arguments pertaining to attribute value replacement.
483    replaceValuesAttribute = new StringArgument(null, "replaceValuesAttribute",
484         false, 1, INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
485         INFO_TRANSFORM_LDIF_ARG_DESC_REPLACE_VALUES_ATTR.get(
486              sourceContainsChangeRecords.getIdentifierString()));
487    replaceValuesAttribute.addLongIdentifier("replace-values-attribute", true);
488    replaceValuesAttribute.setArgumentGroupName(
489         INFO_TRANSFORM_LDIF_ARG_GROUP_REPLACE_VALUES.get());
490    parser.addArgument(replaceValuesAttribute);
491    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
492         replaceValuesAttribute);
493
494    replacementValue = new StringArgument(null, "replacementValue", false, 0,
495         null,
496         INFO_TRANSFORM_LDIF_ARG_DESC_REPLACEMENT_VALUE.get(
497              replaceValuesAttribute.getIdentifierString()));
498    replacementValue.addLongIdentifier("replacement-value", true);
499    replacementValue.setArgumentGroupName(
500         INFO_TRANSFORM_LDIF_ARG_GROUP_REPLACE_VALUES.get());
501    parser.addArgument(replacementValue);
502    parser.addDependentArgumentSet(replaceValuesAttribute, replacementValue);
503    parser.addDependentArgumentSet(replacementValue, replaceValuesAttribute);
504
505
506    // Add arguments pertaining to adding missing attributes.
507    addAttributeName = new StringArgument(null, "addAttributeName", false, 1,
508         INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
509         INFO_TRANSFORM_LDIF_ARG_DESC_ADD_ATTR.get(
510              "--addAttributeValue",
511              sourceContainsChangeRecords.getIdentifierString()));
512    addAttributeName.addLongIdentifier("add-attribute-name", true);
513    addAttributeName.setArgumentGroupName(
514         INFO_TRANSFORM_LDIF_ARG_GROUP_ADD_ATTR.get());
515    parser.addArgument(addAttributeName);
516    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
517         addAttributeName);
518
519    addAttributeValue = new StringArgument(null, "addAttributeValue", false, 0,
520         null,
521         INFO_TRANSFORM_LDIF_ARG_DESC_ADD_VALUE.get(
522              addAttributeName.getIdentifierString()));
523    addAttributeValue.addLongIdentifier("add-attribute-value", true);
524    addAttributeValue.setArgumentGroupName(
525         INFO_TRANSFORM_LDIF_ARG_GROUP_ADD_ATTR.get());
526    parser.addArgument(addAttributeValue);
527    parser.addDependentArgumentSet(addAttributeName, addAttributeValue);
528    parser.addDependentArgumentSet(addAttributeValue, addAttributeName);
529
530    addToExistingValues = new BooleanArgument(null, "addToExistingValues",
531         INFO_TRANSFORM_LDIF_ARG_DESC_ADD_MERGE_VALUES.get(
532              addAttributeName.getIdentifierString(),
533              addAttributeValue.getIdentifierString()));
534    addToExistingValues.addLongIdentifier("add-to-existing-values", true);
535    addToExistingValues.setArgumentGroupName(
536         INFO_TRANSFORM_LDIF_ARG_GROUP_ADD_ATTR.get());
537    parser.addArgument(addToExistingValues);
538    parser.addDependentArgumentSet(addToExistingValues, addAttributeName);
539
540    addAttributeBaseDN = new DNArgument(null, "addAttributeBaseDN", false, 1,
541         null,
542         INFO_TRANSFORM_LDIF_ARG_DESC_ADD_BASE_DN.get(
543              addAttributeName.getIdentifierString()));
544    addAttributeBaseDN.addLongIdentifier("add-attribute-base-dn", true);
545    addAttributeBaseDN.setArgumentGroupName(
546         INFO_TRANSFORM_LDIF_ARG_GROUP_ADD_ATTR.get());
547    parser.addArgument(addAttributeBaseDN);
548    parser.addDependentArgumentSet(addAttributeBaseDN, addAttributeName);
549
550    addAttributeScope = new ScopeArgument(null, "addAttributeScope", false,
551         null,
552         INFO_TRANSFORM_LDIF_ARG_DESC_ADD_SCOPE.get(
553              addAttributeBaseDN.getIdentifierString(),
554              addAttributeName.getIdentifierString()));
555    addAttributeScope.addLongIdentifier("add-attribute-scope", true);
556    addAttributeScope.setArgumentGroupName(
557         INFO_TRANSFORM_LDIF_ARG_GROUP_ADD_ATTR.get());
558    parser.addArgument(addAttributeScope);
559    parser.addDependentArgumentSet(addAttributeScope, addAttributeName);
560
561    addAttributeFilter = new FilterArgument(null, "addAttributeFilter", false,
562         1, null,
563         INFO_TRANSFORM_LDIF_ARG_DESC_ADD_FILTER.get(
564              addAttributeName.getIdentifierString()));
565    addAttributeFilter.addLongIdentifier("add-attribute-filter", true);
566    addAttributeFilter.setArgumentGroupName(
567         INFO_TRANSFORM_LDIF_ARG_GROUP_ADD_ATTR.get());
568    parser.addArgument(addAttributeFilter);
569    parser.addDependentArgumentSet(addAttributeFilter, addAttributeName);
570
571
572    // Add arguments pertaining to renaming attributes.
573    renameAttributeFrom = new StringArgument(null, "renameAttributeFrom",
574         false, 0, INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
575         INFO_TRANSFORM_LDIF_ARG_DESC_RENAME_FROM.get());
576    renameAttributeFrom.addLongIdentifier("rename-attribute-from", true);
577    renameAttributeFrom.setArgumentGroupName(
578         INFO_TRANSFORM_LDIF_ARG_GROUP_RENAME.get());
579    parser.addArgument(renameAttributeFrom);
580
581    renameAttributeTo = new StringArgument(null, "renameAttributeTo",
582         false, 0, INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
583         INFO_TRANSFORM_LDIF_ARG_DESC_RENAME_TO.get(
584              renameAttributeFrom.getIdentifierString()));
585    renameAttributeTo.addLongIdentifier("rename-attribute-to", true);
586    renameAttributeTo.setArgumentGroupName(
587         INFO_TRANSFORM_LDIF_ARG_GROUP_RENAME.get());
588    parser.addArgument(renameAttributeTo);
589    parser.addDependentArgumentSet(renameAttributeFrom, renameAttributeTo);
590    parser.addDependentArgumentSet(renameAttributeTo, renameAttributeFrom);
591
592
593    // Add arguments pertaining to flattening subtrees.
594    flattenBaseDN = new DNArgument(null, "flattenBaseDN", false, 1, null,
595         INFO_TRANSFORM_LDIF_ARG_DESC_FLATTEN_BASE_DN.get());
596    flattenBaseDN.addLongIdentifier("flatten-base-dn", true);
597    flattenBaseDN.setArgumentGroupName(
598         INFO_TRANSFORM_LDIF_ARG_GROUP_FLATTEN.get());
599    parser.addArgument(flattenBaseDN);
600    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
601         flattenBaseDN);
602
603    flattenAddOmittedRDNAttributesToEntry = new BooleanArgument(null,
604         "flattenAddOmittedRDNAttributesToEntry", 1,
605         INFO_TRANSFORM_LDIF_ARG_DESC_FLATTEN_ADD_OMITTED_TO_ENTRY.get());
606    flattenAddOmittedRDNAttributesToEntry.addLongIdentifier(
607         "flatten-add-omitted-rdn-attributes-to-entry", true);
608    flattenAddOmittedRDNAttributesToEntry.setArgumentGroupName(
609         INFO_TRANSFORM_LDIF_ARG_GROUP_FLATTEN.get());
610    parser.addArgument(flattenAddOmittedRDNAttributesToEntry);
611    parser.addDependentArgumentSet(flattenAddOmittedRDNAttributesToEntry,
612         flattenBaseDN);
613
614    flattenAddOmittedRDNAttributesToRDN = new BooleanArgument(null,
615         "flattenAddOmittedRDNAttributesToRDN", 1,
616         INFO_TRANSFORM_LDIF_ARG_DESC_FLATTEN_ADD_OMITTED_TO_RDN.get());
617    flattenAddOmittedRDNAttributesToRDN.addLongIdentifier(
618         "flatten-add-omitted-rdn-attributes-to-rdn", true);
619    flattenAddOmittedRDNAttributesToRDN.setArgumentGroupName(
620         INFO_TRANSFORM_LDIF_ARG_GROUP_FLATTEN.get());
621    parser.addArgument(flattenAddOmittedRDNAttributesToRDN);
622    parser.addDependentArgumentSet(flattenAddOmittedRDNAttributesToRDN,
623         flattenBaseDN);
624
625    flattenExcludeFilter = new FilterArgument(null, "flattenExcludeFilter",
626         false, 1, null,
627         INFO_TRANSFORM_LDIF_ARG_DESC_FLATTEN_EXCLUDE_FILTER.get());
628    flattenExcludeFilter.addLongIdentifier("flatten-exclude-filter", true);
629    flattenExcludeFilter.setArgumentGroupName(
630         INFO_TRANSFORM_LDIF_ARG_GROUP_FLATTEN.get());
631    parser.addArgument(flattenExcludeFilter);
632    parser.addDependentArgumentSet(flattenExcludeFilter, flattenBaseDN);
633
634
635    // Add arguments pertaining to moving subtrees.
636    moveSubtreeFrom = new DNArgument(null, "moveSubtreeFrom", false, 0, null,
637         INFO_TRANSFORM_LDIF_ARG_DESC_MOVE_SUBTREE_FROM.get());
638    moveSubtreeFrom.addLongIdentifier("move-subtree-from", true);
639    moveSubtreeFrom.setArgumentGroupName(
640         INFO_TRANSFORM_LDIF_ARG_GROUP_MOVE.get());
641    parser.addArgument(moveSubtreeFrom);
642
643    moveSubtreeTo = new DNArgument(null, "moveSubtreeTo", false, 0, null,
644         INFO_TRANSFORM_LDIF_ARG_DESC_MOVE_SUBTREE_TO.get(
645              moveSubtreeFrom.getIdentifierString()));
646    moveSubtreeTo.addLongIdentifier("move-subtree-to", true);
647    moveSubtreeTo.setArgumentGroupName(
648         INFO_TRANSFORM_LDIF_ARG_GROUP_MOVE.get());
649    parser.addArgument(moveSubtreeTo);
650    parser.addDependentArgumentSet(moveSubtreeFrom, moveSubtreeTo);
651    parser.addDependentArgumentSet(moveSubtreeTo, moveSubtreeFrom);
652
653
654    // Add arguments pertaining to redacting attribute values.
655    redactAttribute = new StringArgument(null, "redactAttribute", false, 0,
656         INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
657         INFO_TRANSFORM_LDIF_ARG_DESC_REDACT_ATTR.get());
658    redactAttribute.addLongIdentifier("redact-attribute", true);
659    redactAttribute.setArgumentGroupName(
660         INFO_TRANSFORM_LDIF_ARG_GROUP_REDACT.get());
661    parser.addArgument(redactAttribute);
662
663    hideRedactedValueCount = new BooleanArgument(null, "hideRedactedValueCount",
664         INFO_TRANSFORM_LDIF_ARG_DESC_HIDE_REDACTED_COUNT.get());
665    hideRedactedValueCount.addLongIdentifier("hide-redacted-value-count",
666         true);
667    hideRedactedValueCount.setArgumentGroupName(
668         INFO_TRANSFORM_LDIF_ARG_GROUP_REDACT.get());
669    parser.addArgument(hideRedactedValueCount);
670    parser.addDependentArgumentSet(hideRedactedValueCount, redactAttribute);
671
672
673    // Add arguments pertaining to excluding attributes and entries.
674    excludeAttribute = new StringArgument(null, "excludeAttribute", false, 0,
675         INFO_TRANSFORM_LDIF_PLACEHOLDER_ATTR_NAME.get(),
676         INFO_TRANSFORM_LDIF_ARG_DESC_EXCLUDE_ATTR.get());
677    excludeAttribute.addLongIdentifier("suppressAttribute", true);
678    excludeAttribute.addLongIdentifier("exclude-attribute", true);
679    excludeAttribute.addLongIdentifier("suppress-attribute", true);
680    excludeAttribute.setArgumentGroupName(
681         INFO_TRANSFORM_LDIF_ARG_GROUP_EXCLUDE.get());
682    parser.addArgument(excludeAttribute);
683
684    excludeEntryBaseDN = new DNArgument(null, "excludeEntryBaseDN", false, 1,
685         null,
686         INFO_TRANSFORM_LDIF_ARG_DESC_EXCLUDE_ENTRY_BASE_DN.get(
687              sourceContainsChangeRecords.getIdentifierString()));
688    excludeEntryBaseDN.addLongIdentifier("suppressEntryBaseDN", true);
689    excludeEntryBaseDN.addLongIdentifier("exclude-entry-base-dn", true);
690    excludeEntryBaseDN.addLongIdentifier("suppress-entry-base-dn", true);
691    excludeEntryBaseDN.setArgumentGroupName(
692         INFO_TRANSFORM_LDIF_ARG_GROUP_EXCLUDE.get());
693    parser.addArgument(excludeEntryBaseDN);
694    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
695         excludeEntryBaseDN);
696
697    excludeEntryScope = new ScopeArgument(null, "excludeEntryScope", false,
698         null,
699         INFO_TRANSFORM_LDIF_ARG_DESC_EXCLUDE_ENTRY_SCOPE.get(
700              sourceContainsChangeRecords.getIdentifierString()));
701    excludeEntryScope.addLongIdentifier("suppressEntryScope", true);
702    excludeEntryScope.addLongIdentifier("exclude-entry-scope", true);
703    excludeEntryScope.addLongIdentifier("suppress-entry-scope", true);
704    excludeEntryScope.setArgumentGroupName(
705         INFO_TRANSFORM_LDIF_ARG_GROUP_EXCLUDE.get());
706    parser.addArgument(excludeEntryScope);
707    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
708         excludeEntryScope);
709
710    excludeEntryFilter = new FilterArgument(null, "excludeEntryFilter", false,
711         1, null,
712         INFO_TRANSFORM_LDIF_ARG_DESC_EXCLUDE_ENTRY_FILTER.get(
713              sourceContainsChangeRecords.getIdentifierString()));
714    excludeEntryFilter.addLongIdentifier("suppressEntryFilter", true);
715    excludeEntryFilter.addLongIdentifier("exclude-entry-filter", true);
716    excludeEntryFilter.addLongIdentifier("suppress-entry-filter", true);
717    excludeEntryFilter.setArgumentGroupName(
718         INFO_TRANSFORM_LDIF_ARG_GROUP_EXCLUDE.get());
719    parser.addArgument(excludeEntryFilter);
720    parser.addExclusiveArgumentSet(sourceContainsChangeRecords,
721         excludeEntryFilter);
722
723    excludeNonMatchingEntries = new BooleanArgument(null,
724         "excludeNonMatchingEntries",
725         INFO_TRANSFORM_LDIF_ARG_DESC_EXCLUDE_NON_MATCHING.get());
726    excludeNonMatchingEntries.addLongIdentifier("exclude-non-matching-entries",
727         true);
728    excludeNonMatchingEntries.setArgumentGroupName(
729         INFO_TRANSFORM_LDIF_ARG_GROUP_EXCLUDE.get());
730    parser.addArgument(excludeNonMatchingEntries);
731    parser.addDependentArgumentSet(excludeNonMatchingEntries,
732         excludeEntryBaseDN, excludeEntryScope, excludeEntryFilter);
733
734
735    // Add the remaining arguments.
736    schemaPath = new FileArgument(null, "schemaPath", false, 0, null,
737         INFO_TRANSFORM_LDIF_ARG_DESC_SCHEMA_PATH.get(),
738         true, true, false, false);
739    schemaPath.addLongIdentifier("schemaFile", true);
740    schemaPath.addLongIdentifier("schemaDirectory", true);
741    schemaPath.addLongIdentifier("schema-path", true);
742    schemaPath.addLongIdentifier("schema-file", true);
743    schemaPath.addLongIdentifier("schema-directory", true);
744    parser.addArgument(schemaPath);
745
746    numThreads = new IntegerArgument('t', "numThreads", false, 1, null,
747         INFO_TRANSFORM_LDIF_ARG_DESC_NUM_THREADS.get(), 1, Integer.MAX_VALUE,
748         1);
749    numThreads.addLongIdentifier("num-threads", true);
750    parser.addArgument(numThreads);
751
752    processDNs = new BooleanArgument('d', "processDNs",
753         INFO_TRANSFORM_LDIF_ARG_DESC_PROCESS_DNS.get());
754    processDNs.addLongIdentifier("process-dns", true);
755    parser.addArgument(processDNs);
756
757
758    // Ensure that at least one kind of transformation was requested.
759    parser.addRequiredArgumentSet(scrambleAttribute, sequentialAttribute,
760         replaceValuesAttribute, addAttributeName, renameAttributeFrom,
761         flattenBaseDN, moveSubtreeFrom, redactAttribute, excludeAttribute,
762         excludeEntryBaseDN, excludeEntryScope, excludeEntryFilter);
763  }
764
765
766
767  /**
768   * {@inheritDoc}
769   */
770  @Override()
771  public void doExtendedArgumentValidation()
772         throws ArgumentException
773  {
774    // Ideally, exactly one of the targetLDIF and targetToStandardOutput
775    // arguments should always be provided.  But in order to preserve backward
776    // compatibility with a legacy scramble-ldif tool, we will allow both to be
777    // omitted if either --scrambleAttribute or --sequentialArgument is
778    // provided.  In that case, the path of the output file will be the path of
779    // the first input file with ".scrambled" appended to it.
780    if (! (targetLDIF.isPresent() || targetToStandardOutput.isPresent()))
781    {
782      if (! (scrambleAttribute.isPresent() || sequentialAttribute.isPresent()))
783      {
784        throw new ArgumentException(ERR_TRANSFORM_LDIF_MISSING_TARGET_ARG.get(
785             targetLDIF.getIdentifierString(),
786             targetToStandardOutput.getIdentifierString()));
787      }
788    }
789
790
791    // Make sure that the --renameAttributeFrom and --renameAttributeTo
792    // arguments were provided an equal number of times.
793    final int renameFromOccurrences = renameAttributeFrom.getNumOccurrences();
794    final int renameToOccurrences = renameAttributeTo.getNumOccurrences();
795    if (renameFromOccurrences != renameToOccurrences)
796    {
797      throw new ArgumentException(
798           ERR_TRANSFORM_LDIF_ARG_COUNT_MISMATCH.get(
799                renameAttributeFrom.getIdentifierString(),
800                renameAttributeTo.getIdentifierString()));
801    }
802
803
804    // Make sure that the --moveSubtreeFrom and --moveSubtreeTo arguments were
805    // provided an equal number of times.
806    final int moveFromOccurrences = moveSubtreeFrom.getNumOccurrences();
807    final int moveToOccurrences = moveSubtreeTo.getNumOccurrences();
808    if (moveFromOccurrences != moveToOccurrences)
809    {
810      throw new ArgumentException(
811           ERR_TRANSFORM_LDIF_ARG_COUNT_MISMATCH.get(
812                moveSubtreeFrom.getIdentifierString(),
813                moveSubtreeTo.getIdentifierString()));
814    }
815  }
816
817
818
819  /**
820   * {@inheritDoc}
821   */
822  @Override()
823  public ResultCode doToolProcessing()
824  {
825    final Schema schema;
826    try
827    {
828      schema = getSchema();
829    }
830    catch (final LDAPException le)
831    {
832      wrapErr(0, MAX_OUTPUT_LINE_LENGTH, le.getMessage());
833      return le.getResultCode();
834    }
835
836
837    // Create the translators to use to apply the transformations.
838    final ArrayList<LDIFReaderEntryTranslator> entryTranslators =
839         new ArrayList<LDIFReaderEntryTranslator>(10);
840    final ArrayList<LDIFReaderChangeRecordTranslator> changeRecordTranslators =
841         new ArrayList<LDIFReaderChangeRecordTranslator>(10);
842
843    final AtomicLong excludedEntryCount = new AtomicLong(0L);
844    createTranslators(entryTranslators, changeRecordTranslators,
845         schema, excludedEntryCount);
846
847    final AggregateLDIFReaderEntryTranslator entryTranslator =
848         new AggregateLDIFReaderEntryTranslator(entryTranslators);
849    final AggregateLDIFReaderChangeRecordTranslator changeRecordTranslator =
850         new AggregateLDIFReaderChangeRecordTranslator(changeRecordTranslators);
851
852
853    // Determine the path to the target file to be written.
854    final File targetFile;
855    if (targetLDIF.isPresent())
856    {
857      targetFile = targetLDIF.getValue();
858    }
859    else if (targetToStandardOutput.isPresent())
860    {
861      targetFile = null;
862    }
863    else
864    {
865      targetFile =
866           new File(sourceLDIF.getValue().getAbsolutePath() + ".scrambled");
867    }
868
869
870    // Create the LDIF reader.
871    final LDIFReader ldifReader;
872    try
873    {
874      InputStream inputStream;
875      if (sourceLDIF.isPresent())
876      {
877        final List<File> sourceFiles = sourceLDIF.getValues();
878        final ArrayList<InputStream> fileInputStreams =
879             new ArrayList<InputStream>(2*sourceFiles.size());
880        for (final File f : sourceFiles)
881        {
882          if (! fileInputStreams.isEmpty())
883          {
884            // Go ahead and ensure that there are at least new end-of-line
885            // markers between each file.  Otherwise, it's possible for entries
886            // to run together.
887            final byte[] doubleEOL = new byte[StaticUtils.EOL_BYTES.length * 2];
888            System.arraycopy(StaticUtils.EOL_BYTES, 0, doubleEOL, 0,
889                 StaticUtils.EOL_BYTES.length);
890            System.arraycopy(StaticUtils.EOL_BYTES, 0, doubleEOL,
891                 StaticUtils.EOL_BYTES.length, StaticUtils.EOL_BYTES.length);
892            fileInputStreams.add(new ByteArrayInputStream(doubleEOL));
893          }
894          fileInputStreams.add(new FileInputStream(f));
895        }
896
897        if (fileInputStreams.size() == 1)
898        {
899          inputStream = fileInputStreams.get(0);
900        }
901        else
902        {
903          inputStream = new AggregateInputStream(fileInputStreams);
904        }
905      }
906      else
907      {
908        inputStream = System.in;
909      }
910
911      if (sourceCompressed.isPresent())
912      {
913        inputStream = new GZIPInputStream(inputStream);
914      }
915
916      ldifReader = new LDIFReader(inputStream, numThreads.getValue(),
917           entryTranslator, changeRecordTranslator);
918      if (schema != null)
919      {
920        ldifReader.setSchema(schema);
921      }
922    }
923    catch (final Exception e)
924    {
925      Debug.debugException(e);
926      wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
927           ERR_TRANSFORM_LDIF_ERROR_CREATING_LDIF_READER.get(
928                StaticUtils.getExceptionMessage(e)));
929      return ResultCode.LOCAL_ERROR;
930    }
931
932
933    ResultCode resultCode = ResultCode.SUCCESS;
934    OutputStream outputStream = null;
935processingBlock:
936    try
937    {
938      // Create the output stream to use to write the transformed data.
939      try
940      {
941        if (targetFile == null)
942        {
943          outputStream = getOut();
944        }
945        else
946        {
947          outputStream =
948               new FileOutputStream(targetFile, appendToTargetLDIF.isPresent());
949        }
950
951        if (compressTarget.isPresent())
952        {
953          outputStream = new GZIPOutputStream(outputStream);
954        }
955      }
956      catch (final Exception e)
957      {
958        Debug.debugException(e);
959        wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
960             ERR_TRANSFORM_LDIF_ERROR_CREATING_OUTPUT_STREAM.get(
961                  targetFile.getAbsolutePath(),
962                  StaticUtils.getExceptionMessage(e)));
963        resultCode = ResultCode.LOCAL_ERROR;
964        break processingBlock;
965      }
966
967
968      // Read the source data one record at a time.  The transformations will
969      // automatically be applied by the LDIF reader's translators, and even if
970      // there are multiple reader threads, we're guaranteed to get the results
971      // in the right order.
972      long entriesWritten = 0L;
973      while (true)
974      {
975        final LDIFRecord ldifRecord;
976        try
977        {
978          ldifRecord = ldifReader.readLDIFRecord();
979        }
980        catch (final LDIFException le)
981        {
982          Debug.debugException(le);
983          if (le.mayContinueReading())
984          {
985            wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
986                 ERR_TRANSFORM_LDIF_RECOVERABLE_MALFORMED_RECORD.get(
987                      StaticUtils.getExceptionMessage(le)));
988            if (resultCode == ResultCode.SUCCESS)
989            {
990              resultCode = ResultCode.PARAM_ERROR;
991            }
992            continue;
993          }
994          else
995          {
996            wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
997                 ERR_TRANSFORM_LDIF_UNRECOVERABLE_MALFORMED_RECORD.get(
998                      StaticUtils.getExceptionMessage(le)));
999            if (resultCode == ResultCode.SUCCESS)
1000            {
1001              resultCode = ResultCode.PARAM_ERROR;
1002            }
1003            break processingBlock;
1004          }
1005        }
1006        catch (final Exception e)
1007        {
1008          Debug.debugException(e);
1009          wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
1010               ERR_TRANSFORM_LDIF_UNEXPECTED_READ_ERROR.get(
1011                    StaticUtils.getExceptionMessage(e)));
1012          resultCode = ResultCode.LOCAL_ERROR;
1013          break processingBlock;
1014        }
1015
1016
1017        // If the LDIF record is null, then we've run out of records so we're
1018        // done.
1019        if (ldifRecord == null)
1020        {
1021          break;
1022        }
1023
1024
1025        // Write the record to the output stream.
1026        try
1027        {
1028          if (ldifRecord instanceof PreEncodedLDIFEntry)
1029          {
1030            outputStream.write(
1031                 ((PreEncodedLDIFEntry) ldifRecord).getLDIFBytes());
1032          }
1033          else
1034          {
1035            final ByteStringBuffer buffer = getBuffer();
1036            if (wrapColumn.isPresent())
1037            {
1038              ldifRecord.toLDIF(buffer, wrapColumn.getValue());
1039            }
1040            else
1041            {
1042              ldifRecord.toLDIF(buffer, 0);
1043            }
1044            buffer.append(StaticUtils.EOL_BYTES);
1045            buffer.write(outputStream);
1046          }
1047        }
1048        catch (final Exception e)
1049        {
1050          Debug.debugException(e);
1051          wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
1052               ERR_TRANSFORM_LDIF_WRITE_ERROR.get(targetFile.getAbsolutePath(),
1053                    StaticUtils.getExceptionMessage(e)));
1054          resultCode = ResultCode.LOCAL_ERROR;
1055          break processingBlock;
1056        }
1057
1058
1059        // If we've written a multiple of 1000 entries, print a progress
1060        // message.
1061        entriesWritten++;
1062        if ((! targetToStandardOutput.isPresent()) &&
1063            ((entriesWritten % 1000L) == 0))
1064        {
1065          final long numExcluded = excludedEntryCount.get();
1066          if (numExcluded > 0L)
1067          {
1068            wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
1069                 INFO_TRANSFORM_LDIF_WROTE_ENTRIES_WITH_EXCLUDED.get(
1070                      entriesWritten, numExcluded));
1071          }
1072          else
1073          {
1074            wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
1075                 INFO_TRANSFORM_LDIF_WROTE_ENTRIES_NONE_EXCLUDED.get(
1076                      entriesWritten));
1077          }
1078        }
1079      }
1080
1081
1082      if (! targetToStandardOutput.isPresent())
1083      {
1084        final long numExcluded = excludedEntryCount.get();
1085        if (numExcluded > 0L)
1086        {
1087          wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
1088               INFO_TRANSFORM_LDIF_COMPLETE_WITH_EXCLUDED.get(entriesWritten,
1089                    numExcluded));
1090        }
1091        else
1092        {
1093          wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
1094               INFO_TRANSFORM_LDIF_COMPLETE_NONE_EXCLUDED.get(entriesWritten));
1095        }
1096      }
1097    }
1098    finally
1099    {
1100      if (outputStream != null)
1101      {
1102        try
1103        {
1104          outputStream.close();
1105        }
1106        catch (final Exception e)
1107        {
1108          Debug.debugException(e);
1109          wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
1110               ERR_TRANSFORM_LDIF_ERROR_CLOSING_OUTPUT_STREAM.get(
1111                    targetFile.getAbsolutePath(),
1112                    StaticUtils.getExceptionMessage(e)));
1113          if (resultCode == ResultCode.SUCCESS)
1114          {
1115            resultCode = ResultCode.LOCAL_ERROR;
1116          }
1117        }
1118      }
1119
1120      try
1121      {
1122        ldifReader.close();
1123      }
1124      catch (final Exception e)
1125      {
1126        Debug.debugException(e);
1127        // We can ignore this.
1128      }
1129    }
1130
1131
1132    return resultCode;
1133  }
1134
1135
1136
1137  /**
1138   * Retrieves the schema that should be used for processing.
1139   *
1140   * @return  The schema that was created.
1141   *
1142   * @throws  LDAPException  If a problem is encountered while retrieving the
1143   *                         schema.
1144   */
1145  private Schema getSchema()
1146          throws LDAPException
1147  {
1148    // If any schema paths were specified, then load the schema only from those
1149    // paths.
1150    if (schemaPath.isPresent())
1151    {
1152      final ArrayList<File> schemaFiles = new ArrayList<File>(10);
1153      for (final File path : schemaPath.getValues())
1154      {
1155        if (path.isFile())
1156        {
1157          schemaFiles.add(path);
1158        }
1159        else
1160        {
1161          final TreeMap<String,File> fileMap = new TreeMap<String,File>();
1162          for (final File schemaDirFile : path.listFiles())
1163          {
1164            final String name = schemaDirFile.getName();
1165            if (schemaDirFile.isFile() && name.toLowerCase().endsWith(".ldif"))
1166            {
1167              fileMap.put(name, schemaDirFile);
1168            }
1169          }
1170          schemaFiles.addAll(fileMap.values());
1171        }
1172      }
1173
1174      if (schemaFiles.isEmpty())
1175      {
1176        throw new LDAPException(ResultCode.PARAM_ERROR,
1177             ERR_TRANSFORM_LDIF_NO_SCHEMA_FILES.get(
1178                  schemaPath.getIdentifierString()));
1179      }
1180      else
1181      {
1182        try
1183        {
1184          return Schema.getSchema(schemaFiles);
1185        }
1186        catch (final Exception e)
1187        {
1188          Debug.debugException(e);
1189          throw new LDAPException(ResultCode.LOCAL_ERROR,
1190               ERR_TRANSFORM_LDIF_ERROR_LOADING_SCHEMA.get(
1191                    StaticUtils.getExceptionMessage(e)));
1192        }
1193      }
1194    }
1195    else
1196    {
1197      // If the INSTANCE_ROOT environment variable is set and it refers to a
1198      // directory that has a config/schema subdirectory that has one or more
1199      // schema files in it, then read the schema from that directory.
1200      try
1201      {
1202        final String instanceRootStr = System.getenv("INSTANCE_ROOT");
1203        if (instanceRootStr != null)
1204        {
1205          final File instanceRoot = new File(instanceRootStr);
1206          final File configDir = new File(instanceRoot, "config");
1207          final File schemaDir = new File(configDir, "schema");
1208          if (schemaDir.exists())
1209          {
1210            final TreeMap<String,File> fileMap = new TreeMap<String,File>();
1211            for (final File schemaDirFile : schemaDir.listFiles())
1212            {
1213              final String name = schemaDirFile.getName();
1214              if (schemaDirFile.isFile() &&
1215                  name.toLowerCase().endsWith(".ldif"))
1216              {
1217                fileMap.put(name, schemaDirFile);
1218              }
1219            }
1220
1221            if (! fileMap.isEmpty())
1222            {
1223              return Schema.getSchema(new ArrayList<File>(fileMap.values()));
1224            }
1225          }
1226        }
1227      }
1228      catch (final Exception e)
1229      {
1230        Debug.debugException(e);
1231      }
1232    }
1233
1234
1235    // If we've gotten here, then just return null and the tool will try to use
1236    // the default standard schema.
1237    return null;
1238  }
1239
1240
1241
1242  /**
1243   * Creates the entry and change record translators that will be used to
1244   * perform the transformations.
1245   *
1246   * @param  entryTranslators         A list to which all created entry
1247   *                                  translators should be written.
1248   * @param  changeRecordTranslators  A list to which all created change record
1249   *                                  translators should be written.
1250   * @param  schema                   The schema to use when processing.
1251   * @param  excludedEntryCount       A counter used to keep track of the number
1252   *                                  of entries that have been excluded from
1253   *                                  the result set.
1254   */
1255  private void createTranslators(
1256       final List<LDIFReaderEntryTranslator> entryTranslators,
1257       final List<LDIFReaderChangeRecordTranslator> changeRecordTranslators,
1258       final Schema schema, final AtomicLong excludedEntryCount)
1259  {
1260    if (scrambleAttribute.isPresent())
1261    {
1262      final Long seed;
1263      if (randomSeed.isPresent())
1264      {
1265        seed = randomSeed.getValue().longValue();
1266      }
1267      else
1268      {
1269        seed = null;
1270      }
1271
1272      final ScrambleAttributeTransformation t =
1273           new ScrambleAttributeTransformation(schema, seed,
1274                processDNs.isPresent(), scrambleAttribute.getValues(),
1275                scrambleJSONField.getValues());
1276      entryTranslators.add(t);
1277      changeRecordTranslators.add(t);
1278    }
1279
1280    if (sequentialAttribute.isPresent())
1281    {
1282      final long initialValue;
1283      if (initialSequentialValue.isPresent())
1284      {
1285        initialValue = initialSequentialValue.getValue().longValue();
1286      }
1287      else
1288      {
1289        initialValue = 0L;
1290      }
1291
1292      final long incrementAmount;
1293      if (sequentialValueIncrement.isPresent())
1294      {
1295        incrementAmount = sequentialValueIncrement.getValue().longValue();
1296      }
1297      else
1298      {
1299        incrementAmount = 1L;
1300      }
1301
1302      for (final String attrName : sequentialAttribute.getValues())
1303      {
1304
1305
1306        final ReplaceWithCounterTransformation t =
1307             new ReplaceWithCounterTransformation(schema, attrName,
1308                  initialValue, incrementAmount,
1309                  textBeforeSequentialValue.getValue(),
1310                  textAfterSequentialValue.getValue(), processDNs.isPresent());
1311        entryTranslators.add(t);
1312      }
1313    }
1314
1315    if (replaceValuesAttribute.isPresent())
1316    {
1317      final ReplaceAttributeTransformation t =
1318           new ReplaceAttributeTransformation(schema,
1319                replaceValuesAttribute.getValue(),
1320                replacementValue.getValues());
1321      entryTranslators.add(t);
1322    }
1323
1324    if (addAttributeName.isPresent())
1325    {
1326      final AddAttributeTransformation t = new AddAttributeTransformation(
1327           schema, addAttributeBaseDN.getValue(), addAttributeScope.getValue(),
1328           addAttributeFilter.getValue(),
1329           new Attribute(addAttributeName.getValue(), schema,
1330                addAttributeValue.getValues()),
1331           (! addToExistingValues.isPresent()));
1332      entryTranslators.add(t);
1333    }
1334
1335    if (renameAttributeFrom.isPresent())
1336    {
1337      final Iterator<String> renameFromIterator =
1338           renameAttributeFrom.getValues().iterator();
1339      final Iterator<String> renameToIterator =
1340           renameAttributeTo.getValues().iterator();
1341      while (renameFromIterator.hasNext())
1342      {
1343        final RenameAttributeTransformation t =
1344             new RenameAttributeTransformation(schema,
1345                  renameFromIterator.next(), renameToIterator.next(),
1346                  processDNs.isPresent());
1347        entryTranslators.add(t);
1348        changeRecordTranslators.add(t);
1349      }
1350    }
1351
1352    if (flattenBaseDN.isPresent())
1353    {
1354      final FlattenSubtreeTransformation t = new FlattenSubtreeTransformation(
1355           schema, flattenBaseDN.getValue(),
1356           flattenAddOmittedRDNAttributesToEntry.isPresent(),
1357           flattenAddOmittedRDNAttributesToRDN.isPresent(),
1358           flattenExcludeFilter.getValue());
1359      entryTranslators.add(t);
1360    }
1361
1362    if (moveSubtreeFrom.isPresent())
1363    {
1364      final Iterator<DN> moveFromIterator =
1365           moveSubtreeFrom.getValues().iterator();
1366      final Iterator<DN> moveToIterator = moveSubtreeTo.getValues().iterator();
1367      while (moveFromIterator.hasNext())
1368      {
1369        final MoveSubtreeTransformation t =
1370             new MoveSubtreeTransformation(moveFromIterator.next(),
1371                  moveToIterator.next());
1372        entryTranslators.add(t);
1373        changeRecordTranslators.add(t);
1374      }
1375    }
1376
1377    if (redactAttribute.isPresent())
1378    {
1379      final RedactAttributeTransformation t = new RedactAttributeTransformation(
1380           schema, processDNs.isPresent(),
1381           (! hideRedactedValueCount.isPresent()), redactAttribute.getValues());
1382      entryTranslators.add(t);
1383      changeRecordTranslators.add(t);
1384    }
1385
1386    if (excludeAttribute.isPresent())
1387    {
1388      final ExcludeAttributeTransformation t =
1389           new ExcludeAttributeTransformation(schema,
1390                excludeAttribute.getValues());
1391      entryTranslators.add(t);
1392      changeRecordTranslators.add(t);
1393    }
1394
1395    if (excludeEntryBaseDN.isPresent() || excludeEntryScope.isPresent() ||
1396        excludeEntryFilter.isPresent())
1397    {
1398      final ExcludeEntryTransformation t = new ExcludeEntryTransformation(
1399           schema, excludeEntryBaseDN.getValue(), excludeEntryScope.getValue(),
1400           excludeEntryFilter.getValue(),
1401           (! excludeNonMatchingEntries.isPresent()), excludedEntryCount);
1402      entryTranslators.add(t);
1403    }
1404
1405    entryTranslators.add(this);
1406  }
1407
1408
1409
1410  /**
1411   * {@inheritDoc}
1412   */
1413  @Override()
1414  public LinkedHashMap<String[],String> getExampleUsages()
1415  {
1416    final LinkedHashMap<String[],String> examples =
1417         new LinkedHashMap<String[],String>(4);
1418
1419    examples.put(
1420         new String[]
1421         {
1422           "--sourceLDIF", "input.ldif",
1423           "--targetLDIF", "scrambled.ldif",
1424           "--scrambleAttribute", "givenName",
1425           "--scrambleAttribute", "sn",
1426           "--scrambleAttribute", "cn",
1427           "--numThreads", "10",
1428           "--schemaPath", "/ds/config/schema",
1429           "--processDNs"
1430         },
1431         INFO_TRANSFORM_LDIF_EXAMPLE_SCRAMBLE.get());
1432
1433    examples.put(
1434         new String[]
1435         {
1436           "--sourceLDIF", "input.ldif",
1437           "--targetLDIF", "sequential.ldif",
1438           "--sequentialAttribute", "uid",
1439           "--initialSequentialValue", "1",
1440           "--sequentialValueIncrement", "1",
1441           "--textBeforeSequentialValue", "user.",
1442           "--numThreads", "10",
1443           "--schemaPath", "/ds/config/schema",
1444           "--processDNs"
1445         },
1446         INFO_TRANSFORM_LDIF_EXAMPLE_SEQUENTIAL.get());
1447
1448    examples.put(
1449         new String[]
1450         {
1451           "--sourceLDIF", "input.ldif",
1452           "--targetLDIF", "added-organization.ldif",
1453           "--addAttributeName", "o",
1454           "--addAttributeValue", "Example Corp.",
1455           "--addAttributeFilter", "(objectClass=person)",
1456           "--numThreads", "10",
1457           "--schemaPath", "/ds/config/schema"
1458         },
1459         INFO_TRANSFORM_LDIF_EXAMPLE_ADD.get());
1460
1461    examples.put(
1462         new String[]
1463         {
1464           "--sourceLDIF", "input.ldif",
1465           "--targetLDIF", "rebased.ldif",
1466           "--moveSubtreeFrom", "o=example.com",
1467           "--moveSubtreeTo", "dc=example,dc=com",
1468           "--numThreads", "10",
1469           "--schemaPath", "/ds/config/schema"
1470         },
1471         INFO_TRANSFORM_LDIF_EXAMPLE_REBASE.get());
1472
1473    return examples;
1474  }
1475
1476
1477
1478  /**
1479   * {@inheritDoc}
1480   */
1481  @Override()
1482  public Entry translate(final Entry original, final long firstLineNumber)
1483         throws LDIFException
1484  {
1485    final ByteStringBuffer buffer = getBuffer();
1486    if (wrapColumn.isPresent())
1487    {
1488      original.toLDIF(buffer, wrapColumn.getValue());
1489    }
1490    else
1491    {
1492      original.toLDIF(buffer, 0);
1493    }
1494    buffer.append(StaticUtils.EOL_BYTES);
1495
1496    return new PreEncodedLDIFEntry(original, buffer.toByteArray());
1497  }
1498
1499
1500
1501  /**
1502   * Retrieves a byte string buffer that can be used to perform LDIF encoding.
1503   *
1504   * @return  A byte string buffer that can be used to perform LDIF encoding.
1505   */
1506  private ByteStringBuffer getBuffer()
1507  {
1508    ByteStringBuffer buffer = byteStringBuffers.get();
1509    if (buffer == null)
1510    {
1511      buffer = new ByteStringBuffer();
1512      byteStringBuffers.set(buffer);
1513    }
1514    else
1515    {
1516      buffer.clear();
1517    }
1518
1519    return buffer;
1520  }
1521}