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}