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.unboundidds.tools;
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.IOException;
031import java.io.OutputStream;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.LinkedHashMap;
035import java.util.LinkedHashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039import java.util.TreeMap;
040import java.util.concurrent.atomic.AtomicLong;
041import java.util.zip.GZIPInputStream;
042import java.util.zip.GZIPOutputStream;
043
044import com.unboundid.ldap.sdk.Filter;
045import com.unboundid.ldap.sdk.LDAPException;
046import com.unboundid.ldap.sdk.ResultCode;
047import com.unboundid.ldap.sdk.Version;
048import com.unboundid.ldap.sdk.schema.Schema;
049import com.unboundid.ldif.LDIFException;
050import com.unboundid.ldif.LDIFReader;
051import com.unboundid.util.ByteStringBuffer;
052import com.unboundid.util.CommandLineTool;
053import com.unboundid.util.AggregateInputStream;
054import com.unboundid.util.Debug;
055import com.unboundid.util.StaticUtils;
056import com.unboundid.util.ThreadSafety;
057import com.unboundid.util.ThreadSafetyLevel;
058import com.unboundid.util.args.ArgumentException;
059import com.unboundid.util.args.ArgumentParser;
060import com.unboundid.util.args.BooleanArgument;
061import com.unboundid.util.args.DNArgument;
062import com.unboundid.util.args.FileArgument;
063import com.unboundid.util.args.FilterArgument;
064import com.unboundid.util.args.IntegerArgument;
065import com.unboundid.util.args.SubCommand;
066import com.unboundid.util.args.StringArgument;
067
068import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
069
070
071
072/**
073 * This class provides a command-line tool that can be used to split an LDIF
074 * file below a specified base DN.  This can be used to help initialize an
075 * entry-balancing deployment for use with the Directory Proxy Server.
076 * <BR>
077 * <BLOCKQUOTE>
078 *   <B>NOTE:</B>  This class, and other classes within the
079 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
080 *   supported for use against Ping Identity, UnboundID, and Alcatel-Lucent 8661
081 *   server products.  These classes provide support for proprietary
082 *   functionality or for external specifications that are not considered stable
083 *   or mature enough to be guaranteed to work in an interoperable way with
084 *   other types of LDAP servers.
085 * </BLOCKQUOTE>
086 * <BR>
087 * It supports a number of algorithms for determining how to split the data,
088 * including:
089 * <UL>
090 *   <LI>
091 *     split-using-hash-on-rdn -- The tool will compute a digest of the DN
092 *     component that is immediately below the split base DN, and will use a
093 *     modulus to select a backend set for a given entry.  Since the split is
094 *     based purely on computation involving the DN, the there is no need for
095 *     caching to ensure that children are placed in the same sets as their
096 *     parent, which allows it to run effectively with a small memory footprint.
097 *   </LI>
098 *   <LI>
099 *     split-using-hash-on-attribute -- The tool will compute a digest of the
100 *     value(s) of a specified attribute, and will use a modulus to select a
101 *     backend set for a given entry.  This hash will only be computed for
102 *     entries immediately below the split base DN, and a cache will be used to
103 *     ensure that entries more than one level below the split base DN are
104 *     placed in the same backend set as their parent.
105 *   </LI>
106 *   <LI>
107 *     split-using-fewest-entries -- When examining an entry immediately below
108 *     the split base DN, the tool will place that entry in the set that has the
109 *     fewest entries.  For flat DITs in which entries only exist one level
110 *     below the split base DN, this will effectively ensure a round-robin
111 *     distribution.  But for cases in which there are branches of varying sizes
112 *     below the split base DN, this can help ensure that entries are more
113 *     evenly distributed across backend sets.  A cache will be used to ensure
114 *     that entries more than one level below the split base DN are placed in
115 *     the same backend set as their parent.
116 *   </LI>
117 *   <LI>
118 *     split-using-filter -- When examining an entry immediately below the split
119 *     base DN, a series of filters will be evaluated against that entry, which
120 *     each filter associated with a specific backend set.  If an entry doesn't
121 *     match any of the provided filters, an RDN hash can be used to select the
122 *     set.  A cache will be used to ensure that entries more than one level
123 *     below the split base DN are placed in the same backend set as their
124 *     parent.
125 *   </LI>
126 * </UL>
127 */
128@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
129public final class SplitLDIF
130     extends CommandLineTool
131{
132  /**
133   * The maximum length of any message to write to standard output or standard
134   * error.
135   */
136  private static final int MAX_OUTPUT_LINE_LENGTH =
137       StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
138
139
140
141  // The global arguments used by this tool.
142  private BooleanArgument addEntriesOutsideSplitBaseDNToAllSets = null;
143  private BooleanArgument addEntriesOutsideSplitBaseDNToDedicatedSet = null;
144  private BooleanArgument compressTarget = null;
145  private BooleanArgument sourceCompressed = null;
146  private DNArgument splitBaseDN = null;
147  private FileArgument schemaPath = null;
148  private FileArgument sourceLDIF = null;
149  private FileArgument targetLDIFBasePath = null;
150  private IntegerArgument numThreads = null;
151
152  // The arguments used to split using a hash of the RDN.
153  private IntegerArgument splitUsingHashOnRDNNumSets = null;
154  private SubCommand splitUsingHashOnRDN = null;
155
156  // The arguments used to split using a hash on a specified attribute.
157  private BooleanArgument splitUsingHashOnAttributeAssumeFlatDIT = null;
158  private BooleanArgument splitUsingHashOnAttributeUseAllValues = null;
159  private IntegerArgument splitUsingHashOnAttributeNumSets = null;
160  private StringArgument splitUsingHashOnAttributeAttributeName = null;
161  private SubCommand splitUsingHashOnAttribute = null;
162
163  // The arguments used to choose the set with the fewest entries.
164  private BooleanArgument splitUsingFewestEntriesAssumeFlatDIT = null;
165  private IntegerArgument splitUsingFewestEntriesNumSets = null;
166  private SubCommand splitUsingFewestEntries = null;
167
168  // The arguments used to choose the set using a provided set of filters.
169  private BooleanArgument splitUsingFilterAssumeFlatDIT = null;
170  private FilterArgument splitUsingFilterFilter = null;
171  private SubCommand splitUsingFilter = null;
172
173
174
175  /**
176   * Runs the tool with the provided set of command-line arguments.
177   *
178   * @param  args  The command-line arguments provided to this tool.
179   */
180  public static void main(final String... args)
181  {
182    final ResultCode resultCode = main(System.out, System.err, args);
183    if (resultCode != ResultCode.SUCCESS)
184    {
185      System.exit(resultCode.intValue());
186    }
187  }
188
189
190
191  /**
192   * Runs the tool with the provided set of command-line arguments.
193   *
194   * @param  out   The output stream used for standard output.  It may be
195   *               {@code null} if standard output should be suppressed.
196   * @param  err   The output stream used for standard error.  It may be
197   *               {@code null} if standard error should be suppressed.
198   * @param  args  The command-line arguments provided to this tool.
199   *
200   * @return  A result code with information about the processing performed.
201   *          Any result code other than {@link ResultCode#SUCCESS} indicates
202   *          that an error occurred.
203   */
204  public static ResultCode main(final OutputStream out, final OutputStream err,
205                                final String... args)
206  {
207    final SplitLDIF tool = new SplitLDIF(out, err);
208    return tool.runTool(args);
209  }
210
211
212
213  /**
214   * Creates a new instance of this tool with the provided information.
215   *
216   * @param  out  The output stream used for standard output.  It may be
217   *              {@code null} if standard output should be suppressed.
218   * @param  err  The output stream used for standard error.  It may be
219   *              {@code null} if standard error should be suppressed.
220   */
221  public SplitLDIF(final OutputStream out, final OutputStream err)
222  {
223    super(out, err);
224  }
225
226
227
228  /**
229   * {@inheritDoc}
230   */
231  @Override()
232  public String getToolName()
233  {
234    return "split-ldif";
235  }
236
237
238
239  /**
240   * {@inheritDoc}
241   */
242  @Override()
243  public String getToolDescription()
244  {
245    return INFO_SPLIT_LDIF_TOOL_DESCRIPTION.get();
246  }
247
248
249
250  /**
251   * {@inheritDoc}
252   */
253  @Override()
254  public String getToolVersion()
255  {
256    return Version.NUMERIC_VERSION_STRING;
257  }
258
259
260
261  /**
262   * {@inheritDoc}
263   */
264  @Override()
265  public boolean supportsInteractiveMode()
266  {
267    return true;
268  }
269
270
271
272  /**
273   * {@inheritDoc}
274   */
275  @Override()
276  public boolean defaultsToInteractiveMode()
277  {
278    return true;
279  }
280
281
282
283  /**
284   * {@inheritDoc}
285   */
286  @Override()
287  public boolean supportsPropertiesFile()
288  {
289    return true;
290  }
291
292
293
294  /**
295   * {@inheritDoc}
296   */
297  @Override()
298  public void addToolArguments(final ArgumentParser parser)
299         throws ArgumentException
300  {
301    // Add the global arguments.
302    sourceLDIF = new FileArgument('l', "sourceLDIF", true, 0, null,
303         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_SOURCE_LDIF.get(), true, false, true,
304         false);
305    sourceLDIF.addLongIdentifier("inputLDIF", true);
306    sourceLDIF.addLongIdentifier("source-ldif", true);
307    sourceLDIF.addLongIdentifier("input-ldif", true);
308    parser.addArgument(sourceLDIF);
309
310    sourceCompressed = new BooleanArgument('C', "sourceCompressed",
311         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_SOURCE_COMPRESSED.get());
312    sourceCompressed.addLongIdentifier("inputCompressed", true);
313    sourceCompressed.addLongIdentifier("source-compressed", true);
314    sourceCompressed.addLongIdentifier("input-compressed", true);
315    parser.addArgument(sourceCompressed);
316
317    targetLDIFBasePath = new FileArgument('o', "targetLDIFBasePath", false, 1,
318         null, INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_TARGET_LDIF_BASE.get(), false,
319         true, true, false);
320    targetLDIFBasePath.addLongIdentifier("outputLDIFBasePath", true);
321    targetLDIFBasePath.addLongIdentifier("target-ldif-base-path", true);
322    targetLDIFBasePath.addLongIdentifier("output-ldif-base-path", true);
323    parser.addArgument(targetLDIFBasePath);
324
325    compressTarget = new BooleanArgument('c', "compressTarget",
326         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_COMPRESS_TARGET.get());
327    compressTarget.addLongIdentifier("compressOutput", true);
328    compressTarget.addLongIdentifier("compress", true);
329    compressTarget.addLongIdentifier("compress-target", true);
330    compressTarget.addLongIdentifier("compress-output", true);
331    parser.addArgument(compressTarget);
332
333    splitBaseDN = new DNArgument('b', "splitBaseDN", true, 1, null,
334         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_SPLIT_BASE_DN.get());
335    splitBaseDN.addLongIdentifier("baseDN", true);
336    splitBaseDN.addLongIdentifier("split-base-dn", true);
337    splitBaseDN.addLongIdentifier("base-dn", true);
338    parser.addArgument(splitBaseDN);
339
340    addEntriesOutsideSplitBaseDNToAllSets = new BooleanArgument(null,
341         "addEntriesOutsideSplitBaseDNToAllSets", 1,
342         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_OUTSIDE_TO_ALL_SETS.get());
343    addEntriesOutsideSplitBaseDNToAllSets.addLongIdentifier(
344         "add-entries-outside-split-base-dn-to-all-sets", true);
345    parser.addArgument(addEntriesOutsideSplitBaseDNToAllSets);
346
347    addEntriesOutsideSplitBaseDNToDedicatedSet = new BooleanArgument(null,
348         "addEntriesOutsideSplitBaseDNToDedicatedSet", 1,
349         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_OUTSIDE_TO_DEDICATED_SET.get());
350    addEntriesOutsideSplitBaseDNToDedicatedSet.addLongIdentifier(
351         "add-entries-outside-split-base-dn-to-dedicated-set", true);
352    parser.addArgument(addEntriesOutsideSplitBaseDNToDedicatedSet);
353
354    schemaPath = new FileArgument(null, "schemaPath", false, 0, null,
355         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_SCHEMA_PATH.get(), true, false, false,
356         false);
357    schemaPath.addLongIdentifier("schemaFile", true);
358    schemaPath.addLongIdentifier("schemaDirectory", true);
359    schemaPath.addLongIdentifier("schema-path", true);
360    schemaPath.addLongIdentifier("schema-file", true);
361    schemaPath.addLongIdentifier("schema-directory", true);
362    parser.addArgument(schemaPath);
363
364    numThreads = new IntegerArgument('t', "numThreads", false, 1, null,
365         INFO_SPLIT_LDIF_GLOBAL_ARG_DESC_NUM_THREADS.get(), 1,
366         Integer.MAX_VALUE, 1);
367    numThreads.addLongIdentifier("num-threads", true);
368    parser.addArgument(numThreads);
369
370
371    // Add the subcommand used to split entries using a hash on the RDN.
372    final ArgumentParser splitUsingHashOnRDNParser = new ArgumentParser(
373         "split-using-hash-on-rdn", INFO_SPLIT_LDIF_SC_HASH_ON_RDN_DESC.get());
374
375    splitUsingHashOnRDNNumSets = new IntegerArgument(null, "numSets", true, 1,
376         null, INFO_SPLIT_LDIF_SC_HASH_ON_RDN_ARG_DESC_NUM_SETS.get(), 2,
377         Integer.MAX_VALUE);
378    splitUsingHashOnRDNNumSets.addLongIdentifier("num-sets", true);
379    splitUsingHashOnRDNParser.addArgument(splitUsingHashOnRDNNumSets);
380
381    final LinkedHashMap<String[],String> splitUsingHashOnRDNExamples =
382         new LinkedHashMap<String[],String>(1);
383    splitUsingHashOnRDNExamples.put(
384         new String[]
385         {
386           "split-using-hash-on-rdn",
387           "--sourceLDIF", "whole.ldif",
388           "--targetLDIFBasePath", "split.ldif",
389           "--splitBaseDN", "ou=People,dc=example,dc=com",
390           "--numSets", "4",
391           "--schemaPath", "config/schema",
392           "--addEntriesOutsideSplitBaseDNToAllSets"
393         },
394         INFO_SPLIT_LDIF_SC_HASH_ON_RDN_EXAMPLE.get());
395
396    splitUsingHashOnRDN = new SubCommand("split-using-hash-on-rdn",
397         INFO_SPLIT_LDIF_SC_HASH_ON_RDN_DESC.get(), splitUsingHashOnRDNParser,
398         splitUsingHashOnRDNExamples);
399    splitUsingHashOnRDN.addName("hash-on-rdn", true);
400
401    parser.addSubCommand(splitUsingHashOnRDN);
402
403
404    // Add the subcommand used to split entries using a hash on a specified
405    // attribute.
406    final ArgumentParser splitUsingHashOnAttributeParser = new ArgumentParser(
407         "split-using-hash-on-attribute",
408         INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_DESC.get());
409
410    splitUsingHashOnAttributeAttributeName = new StringArgument(null,
411         "attributeName", true, 1, "{attr}",
412         INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_ARG_DESC_ATTR_NAME.get());
413    splitUsingHashOnAttributeAttributeName.addLongIdentifier("attribute-name",
414         true);
415    splitUsingHashOnAttributeParser.addArgument(
416         splitUsingHashOnAttributeAttributeName);
417
418    splitUsingHashOnAttributeNumSets = new IntegerArgument(null, "numSets",
419         true, 1, null, INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_ARG_DESC_NUM_SETS.get(),
420         2, Integer.MAX_VALUE);
421    splitUsingHashOnAttributeNumSets.addLongIdentifier("num-sets", true);
422    splitUsingHashOnAttributeParser.addArgument(
423         splitUsingHashOnAttributeNumSets);
424
425    splitUsingHashOnAttributeUseAllValues = new BooleanArgument(null,
426         "useAllValues", 1,
427         INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_ARG_DESC_ALL_VALUES.get());
428    splitUsingHashOnAttributeUseAllValues.addLongIdentifier("use-all-values",
429         true);
430    splitUsingHashOnAttributeParser.addArgument(
431         splitUsingHashOnAttributeUseAllValues);
432
433    splitUsingHashOnAttributeAssumeFlatDIT = new BooleanArgument(null,
434         "assumeFlatDIT", 1,
435         INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_ARG_DESC_ASSUME_FLAT_DIT.get());
436    splitUsingHashOnAttributeAssumeFlatDIT.addLongIdentifier("assume-flat-dit",
437         true);
438    splitUsingHashOnAttributeParser.addArgument(
439         splitUsingHashOnAttributeAssumeFlatDIT);
440
441    final LinkedHashMap<String[],String> splitUsingHashOnAttributeExamples =
442         new LinkedHashMap<String[],String>(1);
443    splitUsingHashOnAttributeExamples.put(
444         new String[]
445         {
446           "split-using-hash-on-attribute",
447           "--sourceLDIF", "whole.ldif",
448           "--targetLDIFBasePath", "split.ldif",
449           "--splitBaseDN", "ou=People,dc=example,dc=com",
450           "--attributeName", "uid",
451           "--numSets", "4",
452           "--schemaPath", "config/schema",
453           "--addEntriesOutsideSplitBaseDNToAllSets"
454         },
455         INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_EXAMPLE.get());
456
457    splitUsingHashOnAttribute = new SubCommand("split-using-hash-on-attribute",
458         INFO_SPLIT_LDIF_SC_HASH_ON_ATTR_DESC.get(),
459         splitUsingHashOnAttributeParser, splitUsingHashOnAttributeExamples);
460    splitUsingHashOnAttribute.addName("hash-on-attribute", true);
461
462    parser.addSubCommand(splitUsingHashOnAttribute);
463
464
465    // Add the subcommand used to split entries by selecting the set with the
466    // fewest entries.
467    final ArgumentParser splitUsingFewestEntriesParser = new ArgumentParser(
468         "split-using-fewest-entries",
469         INFO_SPLIT_LDIF_SC_FEWEST_ENTRIES_DESC.get());
470
471    splitUsingFewestEntriesNumSets = new IntegerArgument(null, "numSets",
472         true, 1, null,
473         INFO_SPLIT_LDIF_SC_FEWEST_ENTRIES_ARG_DESC_NUM_SETS.get(),
474         2, Integer.MAX_VALUE);
475    splitUsingFewestEntriesNumSets.addLongIdentifier("num-sets", true);
476    splitUsingFewestEntriesParser.addArgument(splitUsingFewestEntriesNumSets);
477
478    splitUsingFewestEntriesAssumeFlatDIT = new BooleanArgument(null,
479         "assumeFlatDIT", 1,
480         INFO_SPLIT_LDIF_SC_FEWEST_ENTRIES_ARG_DESC_ASSUME_FLAT_DIT.get());
481    splitUsingFewestEntriesAssumeFlatDIT.addLongIdentifier("assume-flat-dit",
482         true);
483    splitUsingFewestEntriesParser.addArgument(
484         splitUsingFewestEntriesAssumeFlatDIT);
485
486    final LinkedHashMap<String[],String> splitUsingFewestEntriesExamples =
487         new LinkedHashMap<String[],String>(1);
488    splitUsingFewestEntriesExamples.put(
489         new String[]
490         {
491           "split-using-fewest-entries",
492           "--sourceLDIF", "whole.ldif",
493           "--targetLDIFBasePath", "split.ldif",
494           "--splitBaseDN", "ou=People,dc=example,dc=com",
495           "--numSets", "4",
496           "--schemaPath", "config/schema",
497           "--addEntriesOutsideSplitBaseDNToAllSets"
498         },
499         INFO_SPLIT_LDIF_SC_FEWEST_ENTRIES_EXAMPLE.get());
500
501    splitUsingFewestEntries = new SubCommand("split-using-fewest-entries",
502         INFO_SPLIT_LDIF_SC_FEWEST_ENTRIES_DESC.get(),
503         splitUsingFewestEntriesParser, splitUsingFewestEntriesExamples);
504    splitUsingFewestEntries.addName("fewest-entries", true);
505
506    parser.addSubCommand(splitUsingFewestEntries);
507
508
509    // Add the subcommand used to split entries by selecting the set based on a
510    // filter.
511    final ArgumentParser splitUsingFilterParser = new ArgumentParser(
512         "split-using-filter", INFO_SPLIT_LDIF_SC_FILTER_DESC.get());
513
514    splitUsingFilterFilter = new FilterArgument(null, "filter", true, 0, null,
515         INFO_SPLIT_LDIF_SC_FILTER_ARG_DESC_FILTER.get());
516    splitUsingFilterParser.addArgument(splitUsingFilterFilter);
517
518    splitUsingFilterAssumeFlatDIT = new BooleanArgument(null, "assumeFlatDIT",
519         1, INFO_SPLIT_LDIF_SC_FILTER_ARG_DESC_ASSUME_FLAT_DIT.get());
520    splitUsingFilterAssumeFlatDIT.addLongIdentifier("assume-flat-dit", true);
521    splitUsingFilterParser.addArgument(splitUsingFilterAssumeFlatDIT);
522
523    final LinkedHashMap<String[],String> splitUsingFilterExamples =
524         new LinkedHashMap<String[],String>(1);
525    splitUsingFilterExamples.put(
526         new String[]
527         {
528           "split-using-filter",
529           "--sourceLDIF", "whole.ldif",
530           "--targetLDIFBasePath", "split.ldif",
531           "--splitBaseDN", "ou=People,dc=example,dc=com",
532           "--filter", "(timeZone=Eastern)",
533           "--filter", "(timeZone=Central)",
534           "--filter", "(timeZone=Mountain)",
535           "--filter", "(timeZone=Pacific)",
536           "--schemaPath", "config/schema",
537           "--addEntriesOutsideSplitBaseDNToAllSets"
538         },
539         INFO_SPLIT_LDIF_SC_FILTER_EXAMPLE.get());
540
541    splitUsingFilter = new SubCommand("split-using-filter",
542         INFO_SPLIT_LDIF_SC_FILTER_DESC.get(),
543         splitUsingFilterParser, splitUsingFilterExamples);
544    splitUsingFilter.addName("filter", true);
545
546    parser.addSubCommand(splitUsingFilter);
547  }
548
549
550
551  /**
552   * {@inheritDoc}
553   */
554  @Override()
555  public void doExtendedArgumentValidation()
556         throws ArgumentException
557  {
558    // If multiple sourceLDIF values were provided, then a target LDIF base path
559    // must have been given.
560    final List<File> sourceLDIFValues = sourceLDIF.getValues();
561    if (sourceLDIFValues.size() > 1)
562    {
563      if (! targetLDIFBasePath.isPresent())
564      {
565        throw new ArgumentException(ERR_SPLIT_LDIF_NO_TARGET_BASE_PATH.get(
566             sourceLDIF.getIdentifierString(),
567             targetLDIFBasePath.getIdentifierString()));
568      }
569    }
570
571
572    // If the split-using-filter subcommand was provided, then at least two
573    // filters must have been provided, and none of the filters can be logically
574    // equivalent to any of the others.
575    if (splitUsingFilter.isPresent())
576    {
577      final List<Filter> filterList = splitUsingFilterFilter.getValues();
578      final Set<Filter> filterSet =
579           new LinkedHashSet<Filter>(filterList.size());
580      for (final Filter f : filterList)
581      {
582        if (filterSet.contains(f))
583        {
584          throw new ArgumentException(ERR_SPLIT_LDIF_NON_UNIQUE_FILTER.get(
585               splitUsingFilterFilter.getIdentifierString(), f.toString()));
586        }
587        else
588        {
589          filterSet.add(f);
590        }
591      }
592
593      if (filterSet.size() < 2)
594      {
595        throw new ArgumentException(ERR_SPLIT_LDIF_NOT_ENOUGH_FILTERS.get(
596             splitUsingFilter.getPrimaryName(),
597             splitUsingFilterFilter.getIdentifierString()));
598      }
599    }
600  }
601
602
603
604  /**
605   * {@inheritDoc}
606   */
607  @Override()
608  public ResultCode doToolProcessing()
609  {
610    // Get the schema to use during processing.
611    final Schema schema;
612    try
613    {
614      schema = getSchema();
615    }
616    catch (final LDAPException le)
617    {
618      wrapErr(0, MAX_OUTPUT_LINE_LENGTH, le.getMessage());
619      return le.getResultCode();
620    }
621
622
623    // Figure out which subcommand was selected, and create the appropriate
624    // translator to use to perform the processing.
625    final SplitLDIFTranslator translator;
626    if (splitUsingHashOnRDN.isPresent())
627    {
628      translator = new SplitLDIFRDNHashTranslator(splitBaseDN.getValue(),
629           splitUsingHashOnRDNNumSets.getValue(),
630           addEntriesOutsideSplitBaseDNToAllSets.isPresent(),
631           addEntriesOutsideSplitBaseDNToDedicatedSet.isPresent());
632    }
633    else if (splitUsingHashOnAttribute.isPresent())
634    {
635      translator = new SplitLDIFAttributeHashTranslator(splitBaseDN.getValue(),
636           splitUsingHashOnAttributeNumSets.getValue(),
637           splitUsingHashOnAttributeAttributeName.getValue(),
638           splitUsingHashOnAttributeUseAllValues.isPresent(),
639           splitUsingHashOnAttributeAssumeFlatDIT.isPresent(),
640           addEntriesOutsideSplitBaseDNToAllSets.isPresent(),
641           addEntriesOutsideSplitBaseDNToDedicatedSet.isPresent());
642    }
643    else if (splitUsingFewestEntries.isPresent())
644    {
645      translator = new SplitLDIFFewestEntriesTranslator(splitBaseDN.getValue(),
646           splitUsingFewestEntriesNumSets.getValue(),
647           splitUsingFewestEntriesAssumeFlatDIT.isPresent(),
648           addEntriesOutsideSplitBaseDNToAllSets.isPresent(),
649           addEntriesOutsideSplitBaseDNToDedicatedSet.isPresent());
650    }
651    else if (splitUsingFilter.isPresent())
652    {
653      final List<Filter> filterList = splitUsingFilterFilter.getValues();
654      final LinkedHashSet<Filter> filterSet =
655           new LinkedHashSet<Filter>(filterList.size());
656      for (final Filter f : filterList)
657      {
658        filterSet.add(f);
659      }
660
661      translator = new SplitLDIFFilterTranslator(splitBaseDN.getValue(),
662           schema, filterSet, splitUsingFilterAssumeFlatDIT.isPresent(),
663           addEntriesOutsideSplitBaseDNToAllSets.isPresent(),
664           addEntriesOutsideSplitBaseDNToDedicatedSet.isPresent());
665    }
666    else
667    {
668      // This should never happen.
669      wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
670           ERR_SPLIT_LDIF_CANNOT_DETERMINE_SPLIT_ALGORITHM.get(
671                splitUsingHashOnRDN.getPrimaryName() + ", " +
672                splitUsingHashOnAttribute.getPrimaryName() + ", " +
673                splitUsingFewestEntries.getPrimaryName() + ", " +
674                splitUsingFilter.getPrimaryName()));
675      return ResultCode.PARAM_ERROR;
676    }
677
678
679    // Create the LDIF reader.
680    final LDIFReader ldifReader;
681    try
682    {
683      InputStream inputStream;
684      if (sourceLDIF.isPresent())
685      {
686        final List<File> sourceFiles = sourceLDIF.getValues();
687        final ArrayList<InputStream> fileInputStreams =
688             new ArrayList<InputStream>(2*sourceFiles.size());
689        for (final File f : sourceFiles)
690        {
691          if (! fileInputStreams.isEmpty())
692          {
693            // Go ahead and ensure that there are at least new end-of-line
694            // markers between each file.  Otherwise, it's possible for entries
695            // to run together.
696            final byte[] doubleEOL = new byte[StaticUtils.EOL_BYTES.length * 2];
697            System.arraycopy(StaticUtils.EOL_BYTES, 0, doubleEOL, 0,
698                 StaticUtils.EOL_BYTES.length);
699            System.arraycopy(StaticUtils.EOL_BYTES, 0, doubleEOL,
700                 StaticUtils.EOL_BYTES.length, StaticUtils.EOL_BYTES.length);
701            fileInputStreams.add(new ByteArrayInputStream(doubleEOL));
702          }
703          fileInputStreams.add(new FileInputStream(f));
704        }
705
706        if (fileInputStreams.size() == 1)
707        {
708          inputStream = fileInputStreams.get(0);
709        }
710        else
711        {
712          inputStream = new AggregateInputStream(fileInputStreams);
713        }
714      }
715      else
716      {
717        inputStream = System.in;
718      }
719
720      if (sourceCompressed.isPresent())
721      {
722        inputStream = new GZIPInputStream(inputStream);
723      }
724
725      ldifReader = new LDIFReader(inputStream, numThreads.getValue(),
726           translator);
727      if (schema != null)
728      {
729        ldifReader.setSchema(schema);
730      }
731    }
732    catch (final Exception e)
733    {
734      Debug.debugException(e);
735      wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
736           ERR_SPLIT_LDIF_ERROR_CREATING_LDIF_READER.get(
737                StaticUtils.getExceptionMessage(e)));
738      return ResultCode.LOCAL_ERROR;
739    }
740
741
742    // Iterate through and process all of the entries.
743    ResultCode resultCode = ResultCode.SUCCESS;
744    final LinkedHashMap<String,OutputStream> outputStreams =
745         new LinkedHashMap<String,OutputStream>(10);
746    try
747    {
748      final AtomicLong entriesRead = new AtomicLong(0L);
749      final AtomicLong entriesExcluded = new AtomicLong(0L);
750      final TreeMap<String,AtomicLong> fileCounts =
751           new TreeMap<String,AtomicLong>();
752
753readLoop:
754      while (true)
755      {
756        final SplitLDIFEntry entry;
757        try
758        {
759          entry = (SplitLDIFEntry) ldifReader.readEntry();
760        }
761        catch (final LDIFException le)
762        {
763          Debug.debugException(le);
764          resultCode = ResultCode.LOCAL_ERROR;
765
766          final File f = getOutputFile(SplitLDIFEntry.SET_NAME_ERRORS);
767          OutputStream s = outputStreams.get(SplitLDIFEntry.SET_NAME_ERRORS);
768          if (s == null)
769          {
770            try
771            {
772              s = new FileOutputStream(f);
773              if (compressTarget.isPresent())
774              {
775                s = new GZIPOutputStream(s);
776              }
777
778              outputStreams.put(SplitLDIFEntry.SET_NAME_ERRORS, s);
779              fileCounts.put(SplitLDIFEntry.SET_NAME_ERRORS,
780                   new AtomicLong(0L));
781            }
782            catch (final Exception e)
783            {
784              Debug.debugException(e);
785              resultCode = ResultCode.LOCAL_ERROR;
786              wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
787                   ERR_SPLIT_LDIF_CANNOT_OPEN_OUTPUT_FILE.get(
788                        f.getAbsolutePath(),
789                        StaticUtils.getExceptionMessage(e)));
790              break readLoop;
791            }
792          }
793
794          final ByteStringBuffer buffer = new ByteStringBuffer();
795          buffer.append("# ");
796          buffer.append(le.getMessage());
797          buffer.append(StaticUtils.EOL_BYTES);
798
799          final List<String> dataLines = le.getDataLines();
800          if (dataLines != null)
801          {
802            for (final String dataLine : dataLines)
803            {
804              buffer.append(dataLine);
805              buffer.append(StaticUtils.EOL_BYTES);
806            }
807          }
808
809          buffer.append(StaticUtils.EOL_BYTES);
810
811          try
812          {
813            s.write(buffer.toByteArray());
814          }
815          catch (final Exception e)
816          {
817              Debug.debugException(e);
818              resultCode = ResultCode.LOCAL_ERROR;
819              wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
820                   ERR_SPLIT_LDIF_ERROR_WRITING_ERROR_TO_FILE.get(
821                        le.getMessage(), f.getAbsolutePath(),
822                        StaticUtils.getExceptionMessage(e)));
823              break readLoop;
824          }
825
826          if (le.mayContinueReading())
827          {
828            wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
829                 ERR_SPLIT_LDIF_INVALID_LDIF_RECORD_RECOVERABLE.get(
830                      StaticUtils.getExceptionMessage(le)));
831            continue;
832          }
833          else
834          {
835            wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
836                 ERR_SPLIT_LDIF_INVALID_LDIF_RECORD_UNRECOVERABLE.get(
837                      StaticUtils.getExceptionMessage(le)));
838            break;
839          }
840        }
841        catch (final IOException ioe)
842        {
843          Debug.debugException(ioe);
844          resultCode = ResultCode.LOCAL_ERROR;
845          wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
846               ERR_SPLIT_LDIF_IO_READ_ERROR.get(
847                    StaticUtils.getExceptionMessage(ioe)));
848          break;
849        }
850        catch (final Exception e)
851        {
852          Debug.debugException(e);
853          resultCode = ResultCode.LOCAL_ERROR;
854          wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
855               ERR_SPLIT_LDIF_UNEXPECTED_READ_ERROR.get(
856                    StaticUtils.getExceptionMessage(e)));
857          break;
858        }
859
860        if (entry == null)
861        {
862          break;
863        }
864
865        final long readCount = entriesRead.incrementAndGet();
866        if ((readCount % 1000L) == 0)
867        {
868          // Even though we aren't done with this entry yet, we'll go ahead and
869          // log a progress message now because it's easier to do that now than
870          // to ensure that it's handled properly through all possible error
871          // conditions that need to be handled below.
872          wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
873               INFO_SPLIT_LDIF_PROGRESS.get(readCount));
874        }
875
876
877        // Get the set(s) to which the entry should be written.  If this is
878        // null (which could be the case as a result of a race condition when
879        // using multiple threads where processing for a child completes before
880        // processing for its parent, or as a result of a case in which a
881        // child is included without or before its parent), then try to see if
882        // we can get the sets by passing the entry through the translator.
883        Set<String> sets = entry.getSets();
884        byte[] ldifBytes = entry.getLDIFBytes();
885        if (sets == null)
886        {
887          try
888          {
889            sets = translator.translate(entry, 0L).getSets();
890          }
891          catch (final Exception e)
892          {
893            Debug.debugException(e);
894          }
895
896          if (sets == null)
897          {
898            final SplitLDIFEntry errorEntry =  translator.createEntry(entry,
899                 ERR_SPLIT_LDIF_ENTRY_WITHOUT_PARENT.get(
900                      entry.getDN(), splitBaseDN.getStringValue()),
901                 Collections.singleton(SplitLDIFEntry.SET_NAME_ERRORS));
902            ldifBytes = errorEntry.getLDIFBytes();
903            sets = errorEntry.getSets();
904          }
905        }
906
907
908        // If the entry shouldn't be written into any sets, then we don't need
909        // to do anything else.
910        if (sets.isEmpty())
911        {
912          entriesExcluded.incrementAndGet();
913          continue;
914        }
915
916
917        // Write the entry into each of the target sets, creating the output
918        // files if necessary.
919        for (final String set : sets)
920        {
921          if (set.equals(SplitLDIFEntry.SET_NAME_ERRORS))
922          {
923            // This indicates that an error was encountered during processing,
924            // so we'll update the result code to reflect that.
925            resultCode = ResultCode.LOCAL_ERROR;
926          }
927
928          final File f = getOutputFile(set);
929          OutputStream s = outputStreams.get(set);
930          if (s == null)
931          {
932            try
933            {
934              s = new FileOutputStream(f);
935              if (compressTarget.isPresent())
936              {
937                s = new GZIPOutputStream(s);
938              }
939
940              outputStreams.put(set, s);
941              fileCounts.put(set, new AtomicLong(0L));
942            }
943            catch (final Exception e)
944            {
945              Debug.debugException(e);
946              resultCode = ResultCode.LOCAL_ERROR;
947              wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
948                   ERR_SPLIT_LDIF_CANNOT_OPEN_OUTPUT_FILE.get(
949                        f.getAbsolutePath(),
950                        StaticUtils.getExceptionMessage(e)));
951              break readLoop;
952            }
953          }
954
955          try
956          {
957            s.write(ldifBytes);
958          }
959          catch (final Exception e)
960          {
961              Debug.debugException(e);
962              resultCode = ResultCode.LOCAL_ERROR;
963              wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
964                   ERR_SPLIT_LDIF_ERROR_WRITING_TO_FILE.get(
965                        entry.getDN(), f.getAbsolutePath(),
966                        StaticUtils.getExceptionMessage(e)));
967              break readLoop;
968          }
969
970          fileCounts.get(set).incrementAndGet();
971        }
972      }
973
974
975      // Processing is complete.  Summarize the processing that was performed.
976      final long finalReadCount = entriesRead.get();
977      if (finalReadCount > 1000L)
978      {
979        out();
980      }
981
982      wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
983           INFO_SPLIT_LDIF_PROCESSING_COMPLETE.get(finalReadCount));
984
985      final long excludedCount = entriesExcluded.get();
986      if (excludedCount > 0L)
987      {
988        wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
989             INFO_SPLIT_LDIF_EXCLUDED_COUNT.get(excludedCount));
990      }
991
992      for (final Map.Entry<String,AtomicLong> e : fileCounts.entrySet())
993      {
994        final File f = getOutputFile(e.getKey());
995        wrapOut(0, MAX_OUTPUT_LINE_LENGTH,
996             INFO_SPLIT_LDIF_COUNT_TO_FILE.get(e.getValue().get(),
997                  f.getName()));
998      }
999    }
1000    finally
1001    {
1002      try
1003      {
1004        ldifReader.close();
1005      }
1006      catch (final Exception e)
1007      {
1008        Debug.debugException(e);
1009      }
1010
1011      for (final Map.Entry<String,OutputStream> e : outputStreams.entrySet())
1012      {
1013        try
1014        {
1015          e.getValue().close();
1016        }
1017        catch (final Exception ex)
1018        {
1019          Debug.debugException(ex);
1020          resultCode = ResultCode.LOCAL_ERROR;
1021          wrapErr(0, MAX_OUTPUT_LINE_LENGTH,
1022               ERR_SPLIT_LDIF_ERROR_CLOSING_FILE.get(
1023                    getOutputFile(e.getKey()),
1024                    StaticUtils.getExceptionMessage(ex)));
1025        }
1026      }
1027    }
1028
1029    return resultCode;
1030  }
1031
1032
1033
1034  /**
1035   * Retrieves the schema that should be used for processing.
1036   *
1037   * @return  The schema that was created.
1038   *
1039   * @throws  LDAPException  If a problem is encountered while retrieving the
1040   *                         schema.
1041   */
1042  private Schema getSchema()
1043          throws LDAPException
1044  {
1045    // If any schema paths were specified, then load the schema only from those
1046    // paths.
1047    if (schemaPath.isPresent())
1048    {
1049      final ArrayList<File> schemaFiles = new ArrayList<File>(10);
1050      for (final File path : schemaPath.getValues())
1051      {
1052        if (path.isFile())
1053        {
1054          schemaFiles.add(path);
1055        }
1056        else
1057        {
1058          final TreeMap<String,File> fileMap = new TreeMap<String,File>();
1059          for (final File schemaDirFile : path.listFiles())
1060          {
1061            final String name = schemaDirFile.getName();
1062            if (schemaDirFile.isFile() && name.toLowerCase().endsWith(".ldif"))
1063            {
1064              fileMap.put(name, schemaDirFile);
1065            }
1066          }
1067          schemaFiles.addAll(fileMap.values());
1068        }
1069      }
1070
1071      if (schemaFiles.isEmpty())
1072      {
1073        throw new LDAPException(ResultCode.PARAM_ERROR,
1074             ERR_SPLIT_LDIF_NO_SCHEMA_FILES.get(
1075                  schemaPath.getIdentifierString()));
1076      }
1077      else
1078      {
1079        try
1080        {
1081          return Schema.getSchema(schemaFiles);
1082        }
1083        catch (final Exception e)
1084        {
1085          Debug.debugException(e);
1086          throw new LDAPException(ResultCode.LOCAL_ERROR,
1087               ERR_SPLIT_LDIF_ERROR_LOADING_SCHEMA.get(
1088                    StaticUtils.getExceptionMessage(e)));
1089        }
1090      }
1091    }
1092    else
1093    {
1094      // If the INSTANCE_ROOT environment variable is set and it refers to a
1095      // directory that has a config/schema subdirectory that has one or more
1096      // schema files in it, then read the schema from that directory.
1097      try
1098      {
1099        final String instanceRootStr = System.getenv("INSTANCE_ROOT");
1100        if (instanceRootStr != null)
1101        {
1102          final File instanceRoot = new File(instanceRootStr);
1103          final File configDir = new File(instanceRoot, "config");
1104          final File schemaDir = new File(configDir, "schema");
1105          if (schemaDir.exists())
1106          {
1107            final TreeMap<String,File> fileMap = new TreeMap<String,File>();
1108            for (final File schemaDirFile : schemaDir.listFiles())
1109            {
1110              final String name = schemaDirFile.getName();
1111              if (schemaDirFile.isFile() &&
1112                  name.toLowerCase().endsWith(".ldif"))
1113              {
1114                fileMap.put(name, schemaDirFile);
1115              }
1116            }
1117
1118            if (! fileMap.isEmpty())
1119            {
1120              return Schema.getSchema(new ArrayList<File>(fileMap.values()));
1121            }
1122          }
1123        }
1124      }
1125      catch (final Exception e)
1126      {
1127        Debug.debugException(e);
1128      }
1129    }
1130
1131
1132    // If we've gotten here, then just return null and the tool will try to use
1133    // the default standard schema.
1134    return null;
1135  }
1136
1137
1138
1139  /**
1140   * Retrieves a file object that refers to an output file with the provided
1141   * extension.
1142   *
1143   * @param  extension  The extension to use for the file.
1144   *
1145   * @return  A file object that refers to an output file with the provided
1146   *          extension.
1147   */
1148  private File getOutputFile(final String extension)
1149  {
1150    final File baseFile;
1151    if (targetLDIFBasePath.isPresent())
1152    {
1153      baseFile = targetLDIFBasePath.getValue();
1154    }
1155    else
1156    {
1157      baseFile = sourceLDIF.getValue();
1158    }
1159
1160    return new File(baseFile.getAbsolutePath() + extension);
1161  }
1162
1163
1164
1165  /**
1166   * {@inheritDoc}
1167   */
1168  @Override()
1169  public LinkedHashMap<String[],String> getExampleUsages()
1170  {
1171    final LinkedHashMap<String[],String> exampleMap =
1172         new LinkedHashMap<String[],String>(4);
1173
1174    for (final Map.Entry<String[],String> e :
1175         splitUsingHashOnRDN.getExampleUsages().entrySet())
1176    {
1177      exampleMap.put(e.getKey(), e.getValue());
1178    }
1179
1180    for (final Map.Entry<String[],String> e :
1181         splitUsingHashOnAttribute.getExampleUsages().entrySet())
1182    {
1183      exampleMap.put(e.getKey(), e.getValue());
1184    }
1185
1186    for (final Map.Entry<String[],String> e :
1187         splitUsingFewestEntries.getExampleUsages().entrySet())
1188    {
1189      exampleMap.put(e.getKey(), e.getValue());
1190    }
1191
1192    for (final Map.Entry<String[],String> e :
1193         splitUsingFilter.getExampleUsages().entrySet())
1194    {
1195      exampleMap.put(e.getKey(), e.getValue());
1196    }
1197
1198    return exampleMap;
1199  }
1200}