001/* 002 * Copyright 2020-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2020-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldif; 037 038 039 040import java.io.BufferedReader; 041import java.io.File; 042import java.io.FileInputStream; 043import java.io.FileOutputStream; 044import java.io.IOException; 045import java.io.InputStream; 046import java.io.InputStreamReader; 047import java.io.OutputStream; 048import java.util.ArrayList; 049import java.util.Arrays; 050import java.util.Collections; 051import java.util.HashSet; 052import java.util.Iterator; 053import java.util.LinkedHashMap; 054import java.util.LinkedHashSet; 055import java.util.List; 056import java.util.Map; 057import java.util.Set; 058import java.util.TreeMap; 059import java.util.concurrent.atomic.AtomicReference; 060import java.util.zip.GZIPOutputStream; 061 062import com.unboundid.ldap.listener.SearchEntryParer; 063import com.unboundid.ldap.sdk.DN; 064import com.unboundid.ldap.sdk.Entry; 065import com.unboundid.ldap.sdk.Filter; 066import com.unboundid.ldap.sdk.InternalSDKHelper; 067import com.unboundid.ldap.sdk.LDAPException; 068import com.unboundid.ldap.sdk.LDAPURL; 069import com.unboundid.ldap.sdk.ResultCode; 070import com.unboundid.ldap.sdk.SearchResultEntry; 071import com.unboundid.ldap.sdk.SearchScope; 072import com.unboundid.ldap.sdk.Version; 073import com.unboundid.ldap.sdk.schema.EntryValidator; 074import com.unboundid.ldap.sdk.schema.Schema; 075import com.unboundid.ldap.sdk.unboundidds.tools.ColumnBasedLDAPResultWriter; 076import com.unboundid.ldap.sdk.unboundidds.tools.DNsOnlyLDAPResultWriter; 077import com.unboundid.ldap.sdk.unboundidds.tools.JSONLDAPResultWriter; 078import com.unboundid.ldap.sdk.unboundidds.tools.LDAPResultWriter; 079import com.unboundid.ldap.sdk.unboundidds.tools.LDIFLDAPResultWriter; 080import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils; 081import com.unboundid.ldap.sdk.unboundidds.tools.ValuesOnlyLDAPResultWriter; 082import com.unboundid.util.CommandLineTool; 083import com.unboundid.util.Debug; 084import com.unboundid.util.NotNull; 085import com.unboundid.util.Nullable; 086import com.unboundid.util.ObjectPair; 087import com.unboundid.util.OutputFormat; 088import com.unboundid.util.PassphraseEncryptedOutputStream; 089import com.unboundid.util.StaticUtils; 090import com.unboundid.util.ThreadSafety; 091import com.unboundid.util.ThreadSafetyLevel; 092import com.unboundid.util.args.ArgumentException; 093import com.unboundid.util.args.ArgumentParser; 094import com.unboundid.util.args.BooleanArgument; 095import com.unboundid.util.args.DNArgument; 096import com.unboundid.util.args.FileArgument; 097import com.unboundid.util.args.IntegerArgument; 098import com.unboundid.util.args.ScopeArgument; 099import com.unboundid.util.args.StringArgument; 100 101import static com.unboundid.ldif.LDIFMessages.*; 102 103 104 105/** 106 * This class provides a command-line tool that can be used to search for 107 * entries matching a given set of criteria in an LDIF file. 108 */ 109@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 110public final class LDIFSearch 111 extends CommandLineTool 112{ 113 /** 114 * The server root directory for the Ping Identity Directory Server (or 115 * related Ping Identity server product) that contains this tool, if 116 * applicable. 117 */ 118 @Nullable private static final File PING_SERVER_ROOT = 119 InternalSDKHelper.getPingIdentityServerRoot(); 120 121 122 123 /** 124 * Indicates whether the tool is running as part of a Ping Identity Directory 125 * Server (or related Ping Identity Server Product) installation. 126 */ 127 private static final boolean PING_SERVER_AVAILABLE = 128 (PING_SERVER_ROOT != null); 129 130 131 132 /** 133 * The column at which to wrap long lines. 134 */ 135 private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 136 137 138 139 // The argument parser for this tool. 140 @Nullable private volatile ArgumentParser parser; 141 142 // The completion message for this tool. 143 @NotNull private final AtomicReference<String> completionMessage; 144 145 // Indicates whether the LDIF encryption passphrase file has been read. 146 private volatile boolean ldifEncryptionPassphraseFileRead; 147 148 // Encryption passphrases used thus far. 149 @NotNull private final List<char[]> inputEncryptionPassphrases; 150 151 // The list of LDAP URLs to use when processing searches, mapped to the 152 // corresponding search entry parers. 153 @NotNull private final List<LDAPURL> searchURLs; 154 155 // The LDAP result writer for this tool. 156 @NotNull private volatile LDAPResultWriter resultWriter; 157 158 // The command-line arguments supported by this tool. 159 @Nullable private BooleanArgument checkSchema; 160 @Nullable private BooleanArgument compressOutput; 161 @Nullable private BooleanArgument doNotWrap; 162 @Nullable private BooleanArgument encryptOutput; 163 @Nullable private BooleanArgument isCompressed; 164 @Nullable private BooleanArgument overwriteExistingOutputFile; 165 @Nullable private BooleanArgument separateOutputFilePerSearch; 166 @Nullable private BooleanArgument stripTrailingSpaces; 167 @Nullable private DNArgument baseDN; 168 @Nullable private FileArgument filterFile; 169 @Nullable private FileArgument ldapURLFile; 170 @Nullable private FileArgument ldifEncryptionPassphraseFile; 171 @Nullable private FileArgument ldifFile; 172 @Nullable private FileArgument outputFile; 173 @Nullable private FileArgument outputEncryptionPassphraseFile; 174 @Nullable private FileArgument schemaPath; 175 @Nullable private IntegerArgument sizeLimit; 176 @Nullable private IntegerArgument timeLimitSeconds; 177 @Nullable private IntegerArgument wrapColumn; 178 @Nullable private ScopeArgument scope; 179 @Nullable private StringArgument outputFormat = null; 180 181 182 183 /** 184 * Invokes this tool with the provided set of command-line arguments. 185 * 186 * @param args The set of arguments provided to this tool. It may be 187 * empty but must not be {@code null}. 188 */ 189 public static void main(@NotNull final String... args) 190 { 191 final ResultCode resultCode = main(System.out, System.err, args); 192 if (resultCode != ResultCode.SUCCESS) 193 { 194 System.exit(resultCode.intValue()); 195 } 196 } 197 198 199 200 /** 201 * Invokes this tool with the provided set of command-line arguments, using 202 * the given output and error streams. 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 set of arguments provided to this tool. It may be 209 * empty but must not be {@code null}. 210 * 211 * @return A result code indicating the status of processing. Any result 212 * code other than {@link ResultCode#SUCCESS} should be considered 213 * an error. 214 */ 215 @NotNull() 216 public static ResultCode main(@Nullable final OutputStream out, 217 @Nullable final OutputStream err, 218 @NotNull final String... args) 219 { 220 final LDIFSearch tool = new LDIFSearch(out, err); 221 return tool.runTool(args); 222 } 223 224 225 226 /** 227 * Creates a new instance of this tool with the provided output and error 228 * streams. 229 * 230 * @param out The output stream to use for standard output. It may be 231 * {@code null} if standard output should be suppressed. 232 * @param err The output stream to use for standard error. It may be 233 * {@code null} if standard error should be suppressed. 234 */ 235 public LDIFSearch(@Nullable final OutputStream out, 236 @Nullable final OutputStream err) 237 { 238 super(out, err); 239 240 resultWriter = new LDIFLDAPResultWriter(getOut(), WRAP_COLUMN); 241 242 parser = null; 243 completionMessage = new AtomicReference<>(); 244 inputEncryptionPassphrases = new ArrayList<>(5); 245 searchURLs = new ArrayList<>(); 246 ldifEncryptionPassphraseFileRead = false; 247 248 checkSchema = null; 249 compressOutput = null; 250 doNotWrap = null; 251 encryptOutput = null; 252 isCompressed = null; 253 overwriteExistingOutputFile = null; 254 separateOutputFilePerSearch = null; 255 stripTrailingSpaces = null; 256 baseDN = null; 257 filterFile = null; 258 ldapURLFile = null; 259 ldifEncryptionPassphraseFile = null; 260 ldifFile = null; 261 outputFile = null; 262 outputFormat = null; 263 outputEncryptionPassphraseFile = null; 264 schemaPath = null; 265 sizeLimit = null; 266 timeLimitSeconds = null; 267 wrapColumn = null; 268 scope = null; 269 } 270 271 272 273 /** 274 * {@inheritDoc} 275 */ 276 @Override() 277 @NotNull() 278 public String getToolName() 279 { 280 return "ldifsearch"; 281 } 282 283 284 285 /** 286 * {@inheritDoc} 287 */ 288 @Override() 289 @NotNull() 290 public String getToolDescription() 291 { 292 return INFO_LDIFSEARCH_TOOL_DESCRIPTION.get(); 293 } 294 295 296 297 /** 298 * {@inheritDoc} 299 */ 300 @Override() 301 @NotNull() 302 public String getToolVersion() 303 { 304 return Version.NUMERIC_VERSION_STRING; 305 } 306 307 308 309 /** 310 * {@inheritDoc} 311 */ 312 @Override() 313 public int getMinTrailingArguments() 314 { 315 return 0; 316 } 317 318 319 320 /** 321 * {@inheritDoc} 322 */ 323 @Override() 324 public int getMaxTrailingArguments() 325 { 326 return -1; 327 } 328 329 330 331 /** 332 * {@inheritDoc} 333 */ 334 @Override() 335 @NotNull() 336 public String getTrailingArgumentsPlaceholder() 337 { 338 return INFO_LDIFSEARCH_TRAILING_ARGS_PLACEHOLDER.get(); 339 } 340 341 342 343 /** 344 * {@inheritDoc} 345 */ 346 @Override() 347 public boolean supportsInteractiveMode() 348 { 349 return true; 350 } 351 352 353 354 /** 355 * {@inheritDoc} 356 */ 357 @Override() 358 public boolean defaultsToInteractiveMode() 359 { 360 return true; 361 } 362 363 364 365 /** 366 * {@inheritDoc} 367 */ 368 @Override() 369 public boolean supportsPropertiesFile() 370 { 371 return true; 372 } 373 374 375 376 /** 377 * {@inheritDoc} 378 */ 379 @Override() 380 protected boolean supportsDebugLogging() 381 { 382 return true; 383 } 384 385 386 387 /** 388 * {@inheritDoc} 389 */ 390 @Override() 391 @Nullable() 392 protected String getToolCompletionMessage() 393 { 394 return completionMessage.get(); 395 } 396 397 398 399 /** 400 * {@inheritDoc} 401 */ 402 @Override() 403 public void addToolArguments(@NotNull final ArgumentParser parser) 404 throws ArgumentException 405 { 406 this.parser = parser; 407 408 409 ldifFile = new FileArgument('l', "ldifFile", true, 0, null, 410 INFO_LDIFSEARCH_ARG_DESC_LDIF_FILE.get(), true, true, true, false); 411 ldifFile.addLongIdentifier("ldif-file", true); 412 ldifFile.addLongIdentifier("inputFile", true); 413 ldifFile.addLongIdentifier("input-file", true); 414 ldifFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 415 parser.addArgument(ldifFile); 416 417 418 final String ldifPWDesc; 419 if (PING_SERVER_AVAILABLE) 420 { 421 ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_PING_SERVER.get(); 422 } 423 else 424 { 425 ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_STANDALONE.get(); 426 } 427 ldifEncryptionPassphraseFile = new FileArgument(null, 428 "ldifEncryptionPassphraseFile", false, 1, null, ldifPWDesc, true, 429 true, true, false); 430 ldifEncryptionPassphraseFile.addLongIdentifier( 431 "ldif-encryption-passphrase-file", true); 432 ldifEncryptionPassphraseFile.addLongIdentifier("ldifPassphraseFile", true); 433 ldifEncryptionPassphraseFile.addLongIdentifier("ldif-passphrase-file", 434 true); 435 ldifEncryptionPassphraseFile.addLongIdentifier("ldifEncryptionPasswordFile", 436 true); 437 ldifEncryptionPassphraseFile.addLongIdentifier( 438 "ldif-encryption-password-file", true); 439 ldifEncryptionPassphraseFile.addLongIdentifier("ldifPasswordFile", true); 440 ldifEncryptionPassphraseFile.addLongIdentifier("ldif-password-file", true); 441 ldifEncryptionPassphraseFile.addLongIdentifier( 442 "inputEncryptionPassphraseFile", true); 443 ldifEncryptionPassphraseFile.addLongIdentifier( 444 "input-encryption-passphrase-file", true); 445 ldifEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile", true); 446 ldifEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file", 447 true); 448 ldifEncryptionPassphraseFile.addLongIdentifier( 449 "inputEncryptionPasswordFile", true); 450 ldifEncryptionPassphraseFile.addLongIdentifier( 451 "input-encryption-password-file", true); 452 ldifEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true); 453 ldifEncryptionPassphraseFile.addLongIdentifier("input-password-file", true); 454 ldifEncryptionPassphraseFile.setArgumentGroupName( 455 INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 456 parser.addArgument(ldifEncryptionPassphraseFile); 457 458 459 stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1, 460 INFO_LDIFSEARCH_ARG_DESC_STRIP_TRAILING_SPACES.get()); 461 stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true); 462 stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true); 463 stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true); 464 stripTrailingSpaces.setArgumentGroupName( 465 INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 466 parser.addArgument(stripTrailingSpaces); 467 468 469 final String schemaPathDesc; 470 if (PING_SERVER_AVAILABLE) 471 { 472 schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_PING_SERVER.get(); 473 } 474 else 475 { 476 schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_STANDALONE.get(); 477 } 478 schemaPath = new FileArgument(null, "schemaPath", false, 0, null, 479 schemaPathDesc, true, true, false, false); 480 schemaPath.addLongIdentifier("schema-path", true); 481 schemaPath.addLongIdentifier("schemaFile", true); 482 schemaPath.addLongIdentifier("schema-file", true); 483 schemaPath.addLongIdentifier("schemaDirectory", true); 484 schemaPath.addLongIdentifier("schema-directory", true); 485 schemaPath.addLongIdentifier("schema", true); 486 schemaPath.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 487 parser.addArgument(schemaPath); 488 489 490 checkSchema = new BooleanArgument(null, "checkSchema", 1, 491 INFO_LDIFSEARCH_ARG_DESC_CHECK_SCHEMA.get()); 492 checkSchema.addLongIdentifier("check-schema", true); 493 checkSchema.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 494 parser.addArgument(checkSchema); 495 496 497 isCompressed = new BooleanArgument(null, "isCompressed", 1, 498 INFO_LDIFSEARCH_ARG_DESC_IS_COMPRESSED.get()); 499 isCompressed.addLongIdentifier("is-compressed", true); 500 isCompressed.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 501 isCompressed.setHidden(true); 502 parser.addArgument(isCompressed); 503 504 505 outputFile = new FileArgument('o', "outputFile", false, 1, null, 506 INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FILE.get(), false, true, true, false); 507 outputFile.addLongIdentifier("output-file", true); 508 outputFile.addLongIdentifier("outputLDIF", true); 509 outputFile.addLongIdentifier("output-ldif", true); 510 outputFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 511 parser.addArgument(outputFile); 512 513 514 separateOutputFilePerSearch = new BooleanArgument(null, 515 "separateOutputFilePerSearch", 1, 516 INFO_LDIFSEARCH_ARG_DESC_SEPARATE_OUTPUT_FILES.get()); 517 separateOutputFilePerSearch.addLongIdentifier( 518 "separate-output-file-per-search", true); 519 separateOutputFilePerSearch.addLongIdentifier("separateOutputFiles", true); 520 separateOutputFilePerSearch.addLongIdentifier("separate-output-files", 521 true); 522 separateOutputFilePerSearch.setArgumentGroupName( 523 INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 524 parser.addArgument(separateOutputFilePerSearch); 525 526 527 compressOutput = new BooleanArgument(null, "compressOutput", 1, 528 INFO_LDIFSEARCH_ARG_DESC_COMPRESS_OUTPUT.get()); 529 compressOutput.addLongIdentifier("compress-output", true); 530 compressOutput.addLongIdentifier("compressLDIF", true); 531 compressOutput.addLongIdentifier("compress-ldif", true); 532 compressOutput.addLongIdentifier("compress", true); 533 compressOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 534 parser.addArgument(compressOutput); 535 536 537 encryptOutput = new BooleanArgument(null, "encryptOutput", 1, 538 INFO_LDIFSEARCH_ARG_DESC_ENCRYPT_OUTPUT.get()); 539 encryptOutput.addLongIdentifier("encrypt-output", true); 540 encryptOutput.addLongIdentifier("encryptLDIF", true); 541 encryptOutput.addLongIdentifier("encrypt-ldif", true); 542 encryptOutput.addLongIdentifier("encrypt", true); 543 encryptOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 544 parser.addArgument(encryptOutput); 545 546 547 outputEncryptionPassphraseFile = new FileArgument(null, 548 "outputEncryptionPassphraseFile", false, 1, null, 549 INFO_LDIFSEARCH_ARG_DESC_OUTPUT_PW_FILE.get(), true, true, true, 550 false); 551 outputEncryptionPassphraseFile.addLongIdentifier( 552 "output-encryption-passphrase-file", true); 553 outputEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile", 554 true); 555 outputEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file", 556 true); 557 outputEncryptionPassphraseFile.addLongIdentifier( 558 "outputEncryptionPasswordFile", true); 559 outputEncryptionPassphraseFile.addLongIdentifier( 560 "output-encryption-password-file", true); 561 outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile", 562 true); 563 outputEncryptionPassphraseFile.addLongIdentifier("output-password-file", 564 true); 565 outputEncryptionPassphraseFile.addLongIdentifier( 566 "outputEncryptionPasswordFile", true); 567 outputEncryptionPassphraseFile.addLongIdentifier( 568 "output-encryption-password-file", true); 569 outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile", 570 true); 571 outputEncryptionPassphraseFile.addLongIdentifier("output-password-file", 572 true); 573 outputEncryptionPassphraseFile.setArgumentGroupName( 574 INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 575 parser.addArgument(outputEncryptionPassphraseFile); 576 577 578 overwriteExistingOutputFile = new BooleanArgument('O', 579 "overwriteExistingOutputFile", 1, 580 INFO_LDIFSEARCH_ARG_DESC_OVERWRITE_EXISTING.get()); 581 overwriteExistingOutputFile.addLongIdentifier( 582 "overwrite-existing-output-file", true); 583 overwriteExistingOutputFile.addLongIdentifier( 584 "overwriteExistingOutputFiles", true); 585 overwriteExistingOutputFile.addLongIdentifier( 586 "overwrite-existing-output-files", true); 587 overwriteExistingOutputFile.addLongIdentifier("overwriteExistingOutput", 588 true); 589 overwriteExistingOutputFile.addLongIdentifier("overwrite-existing-output", 590 true); 591 overwriteExistingOutputFile.addLongIdentifier("overwriteExisting", true); 592 overwriteExistingOutputFile.addLongIdentifier("overwrite-existing", true); 593 overwriteExistingOutputFile.addLongIdentifier("overwrite", true); 594 overwriteExistingOutputFile.setArgumentGroupName( 595 INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 596 parser.addArgument(overwriteExistingOutputFile); 597 598 599 final Set<String> outputFormatAllowedValues = StaticUtils.setOf("ldif", 600 "json", "csv", "multi-valued-csv", "tab-delimited", 601 "multi-valued-tab-delimited", "dns-only", "values-only"); 602 outputFormat = new StringArgument(null, "outputFormat", false, 1, 603 "{ldif|json|csv|multi-valued-csv|tab-delimited|" + 604 "multi-valued-tab-delimited|dns-only|values-only}", 605 INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FORMAT.get(), 606 outputFormatAllowedValues, "ldif"); 607 outputFormat.addLongIdentifier("output-format", true); 608 outputFormat.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 609 parser.addArgument(outputFormat); 610 611 612 wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null, 613 INFO_LDIFSEARCH_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE); 614 wrapColumn.addLongIdentifier("wrap-column", true); 615 wrapColumn.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 616 parser.addArgument(wrapColumn); 617 618 619 doNotWrap = new BooleanArgument('T', "doNotWrap", 1, 620 INFO_LDIFSEARCH_ARG_DESC_DO_NOT_WRAP.get()); 621 doNotWrap.addLongIdentifier("do-not-wrap", true); 622 doNotWrap.addLongIdentifier("dontWrap", true); 623 doNotWrap.addLongIdentifier("dont-wrap", true); 624 doNotWrap.addLongIdentifier("noWrap", true); 625 doNotWrap.addLongIdentifier("no-wrap", true); 626 doNotWrap.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 627 parser.addArgument(doNotWrap); 628 629 630 baseDN = new DNArgument('b', "baseDN", false, 1, null, 631 INFO_LDIFSEARCH_ARG_DESC_BASE_DN.get()); 632 baseDN.addLongIdentifier("base-dn", true); 633 baseDN.addLongIdentifier("searchBaseDN", true); 634 baseDN.addLongIdentifier("search-base-dn", true); 635 baseDN.addLongIdentifier("searchBase", true); 636 baseDN.addLongIdentifier("search-base", true); 637 baseDN.addLongIdentifier("base", true); 638 baseDN.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 639 parser.addArgument(baseDN); 640 641 642 scope = new ScopeArgument('s', "scope", false, null, 643 INFO_LDIFSEARCH_ARG_DESC_SCOPE.get()); 644 scope.addLongIdentifier("searchScope", true); 645 scope.addLongIdentifier("search-scope", true); 646 scope.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 647 parser.addArgument(scope); 648 649 650 filterFile = new FileArgument('f', "filterFile", false, 0, null, 651 INFO_LDIFSEARCH_ARG_DESC_FILTER_FILE.get(), true, true, true, false); 652 filterFile.addLongIdentifier("filter-file", true); 653 filterFile.addLongIdentifier("filtersFile", true); 654 filterFile.addLongIdentifier("filters-file", true); 655 filterFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 656 parser.addArgument(filterFile); 657 658 659 ldapURLFile = new FileArgument(null, "ldapURLFile", false, 0, null, 660 INFO_LDIFSEARCH_ARG_DESC_LDAP_URL_FILE.get(), true, true, true, false); 661 ldapURLFile.addLongIdentifier("ldap-url-file", true); 662 ldapURLFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 663 parser.addArgument(ldapURLFile); 664 665 666 sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, null, 667 INFO_LDIFSEARCH_ARG_DESC_SIZE_LIMIT.get(), 0, Integer.MAX_VALUE, 0); 668 sizeLimit.addLongIdentifier("size-limit", true); 669 sizeLimit.addLongIdentifier("searchSizeLimit", true); 670 sizeLimit.addLongIdentifier("search-size-limit", true); 671 sizeLimit.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 672 sizeLimit.setHidden(true); 673 parser.addArgument(sizeLimit); 674 675 676 timeLimitSeconds = new IntegerArgument('t', "timeLimitSeconds", false, 1, 677 null, INFO_LDIFSEARCH_ARG_DESC_TIME_LIMIT_SECONDS.get(), 0, 678 Integer.MAX_VALUE, 0); 679 timeLimitSeconds.addLongIdentifier("time-limit-seconds", true); 680 timeLimitSeconds.addLongIdentifier("timeLimit", true); 681 timeLimitSeconds.setArgumentGroupName( 682 INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 683 timeLimitSeconds.setHidden(true); 684 parser.addArgument(timeLimitSeconds); 685 686 687 parser.addDependentArgumentSet(separateOutputFilePerSearch, outputFile); 688 parser.addDependentArgumentSet(compressOutput, outputFile); 689 parser.addDependentArgumentSet(encryptOutput, outputFile); 690 parser.addDependentArgumentSet(overwriteExistingOutputFile, outputFile); 691 parser.addDependentArgumentSet(outputEncryptionPassphraseFile, 692 encryptOutput); 693 694 parser.addExclusiveArgumentSet(wrapColumn, doNotWrap); 695 parser.addExclusiveArgumentSet(baseDN, ldapURLFile); 696 parser.addExclusiveArgumentSet(scope, ldapURLFile); 697 parser.addExclusiveArgumentSet(filterFile, ldapURLFile); 698 parser.addExclusiveArgumentSet(outputFormat, separateOutputFilePerSearch); 699 } 700 701 702 703 /** 704 * {@inheritDoc} 705 */ 706 @Override() 707 public void doExtendedArgumentValidation() 708 throws ArgumentException 709 { 710 // If the output file exists and either compressOutput or encryptOutput is 711 // present, then the overwrite argument must also be present. 712 final File outFile = outputFile.getValue(); 713 if ((outFile != null) && outFile.exists() && 714 (compressOutput.isPresent() || encryptOutput.isPresent()) && 715 (! overwriteExistingOutputFile.isPresent())) 716 { 717 throw new ArgumentException( 718 ERR_LDIFSEARCH_APPEND_WITH_COMPRESSION_OR_ENCRYPTION.get( 719 compressOutput.getIdentifierString(), 720 encryptOutput.getIdentifierString(), 721 overwriteExistingOutputFile.getIdentifierString())); 722 } 723 724 725 // Create the set of LDAP URLs to use when issuing the searches. 726 final List<String> trailingArgs = parser.getTrailingArguments(); 727 final List<String> requestedAttributes = new ArrayList<>(); 728 if (filterFile.isPresent()) 729 { 730 // If there are trailing arguments, then make sure the first one is not a 731 // valid filter. 732 if (! trailingArgs.isEmpty()) 733 { 734 try 735 { 736 Filter.create(trailingArgs.get(0)); 737 throw new ArgumentException( 738 ERR_LDIFSEARCH_FILTER_FILE_WITH_TRAILING_FILTER.get()); 739 } 740 catch (final LDAPException e) 741 { 742 // This was expected. 743 } 744 } 745 746 requestedAttributes.addAll(trailingArgs); 747 readFilterFile(); 748 } 749 else if (ldapURLFile.isPresent()) 750 { 751 // Make sure there aren't any trailing arguments. 752 if (! trailingArgs.isEmpty()) 753 { 754 throw new ArgumentException( 755 ERR_LDIFSEARCH_LDAP_URL_FILE_WITH_TRAILING_ARGS.get()); 756 } 757 758 readLDAPURLFile(); 759 760 761 // If there are multiple LDAP URLs, and if they should not be sent to 762 // separate output files, then they must all have the same set of 763 // requested attributes. 764 if ((searchURLs.size() > 1) && 765 (! separateOutputFilePerSearch.isPresent())) 766 { 767 final Iterator<LDAPURL> iterator = searchURLs.iterator(); 768 final Set<String> requestedAttrs = 769 new HashSet<>(Arrays.asList(iterator.next().getAttributes())); 770 while (iterator.hasNext()) 771 { 772 final Set<String> attrSet = new HashSet<>(Arrays.asList( 773 iterator.next().getAttributes())); 774 if (! requestedAttrs.equals(attrSet)) 775 { 776 throw new ArgumentException( 777 ERR_LDIFSEARCH_DIFFERENT_URL_ATTRS_IN_SAME_FILE.get( 778 ldapURLFile.getIdentifierString(), 779 separateOutputFilePerSearch.getIdentifierString())); 780 } 781 } 782 } 783 } 784 else 785 { 786 // Make sure there is at least one trailing argument, and that it's a 787 // valid filter. If there are any others, then they must be the 788 // requested arguments. 789 if (trailingArgs.isEmpty()) 790 { 791 throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTER.get()); 792 } 793 794 795 final Filter filter; 796 try 797 { 798 final List<String> trailingArgList = new ArrayList<>(trailingArgs); 799 final Iterator<String> trailingArgIterator = trailingArgList.iterator(); 800 filter = Filter.create(trailingArgIterator.next()); 801 802 while (trailingArgIterator.hasNext()) 803 { 804 requestedAttributes.add(trailingArgIterator.next()); 805 } 806 } 807 catch (final LDAPException e) 808 { 809 Debug.debugException(e); 810 throw new ArgumentException( 811 ERR_LDIFSEARCH_FIRST_TRAILING_ARG_NOT_FILTER.get( 812 trailingArgs.get(0)), 813 e); 814 } 815 816 817 DN dn = baseDN.getValue(); 818 if (dn == null) 819 { 820 dn = DN.NULL_DN; 821 } 822 823 SearchScope searchScope = scope.getValue(); 824 if (searchScope == null) 825 { 826 searchScope = SearchScope.SUB; 827 } 828 829 try 830 { 831 searchURLs.add(new LDAPURL("ldap", null, null, dn, 832 requestedAttributes.toArray(StaticUtils.NO_STRINGS), 833 searchScope, filter)); 834 } 835 catch (final LDAPException e) 836 { 837 Debug.debugException(e); 838 // This should never happen. 839 throw new ArgumentException(StaticUtils.getExceptionMessage(e), e); 840 } 841 } 842 843 844 // Create the result writer. 845 final String outputFormatStr = 846 StaticUtils.toLowerCase(outputFormat.getValue()); 847 if (outputFormatStr.equals("json")) 848 { 849 resultWriter = new JSONLDAPResultWriter(getOut()); 850 } 851 else if (outputFormatStr.equals("csv") || 852 outputFormatStr.equals("multi-valued-csv") || 853 outputFormatStr.equals("tab-delimited") || 854 outputFormatStr.equals("multi-valued-tab-delimited")) 855 { 856 // These output formats cannot be used with the --ldapURLFile argument. 857 if (ldapURLFile.isPresent()) 858 { 859 throw new ArgumentException( 860 ERR_LDIFSEARCH_OUTPUT_FORMAT_NOT_SUPPORTED_WITH_URLS.get( 861 outputFormat.getValue(), ldapURLFile.getIdentifierString())); 862 } 863 864 865 // These output formats require a set of requested attributes. 866 if (requestedAttributes.isEmpty()) 867 { 868 throw new ArgumentException( 869 ERR_LDIFSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTRS.get( 870 outputFormat.getValue())); 871 } 872 873 final OutputFormat format; 874 final boolean includeAllValues; 875 switch (outputFormatStr) 876 { 877 case "multi-valued-csv": 878 format = OutputFormat.CSV; 879 includeAllValues = true; 880 break; 881 case "tab-delimited": 882 format = OutputFormat.TAB_DELIMITED_TEXT; 883 includeAllValues = false; 884 break; 885 case "multi-valued-tab-delimited": 886 format = OutputFormat.TAB_DELIMITED_TEXT; 887 includeAllValues = true; 888 break; 889 case "csv": 890 default: 891 format = OutputFormat.CSV; 892 includeAllValues = false; 893 break; 894 } 895 896 897 resultWriter = new ColumnBasedLDAPResultWriter(getOut(), 898 format, requestedAttributes, WRAP_COLUMN, includeAllValues); 899 } 900 else if (outputFormatStr.equals("dns-only")) 901 { 902 resultWriter = new DNsOnlyLDAPResultWriter(getOut()); 903 } 904 else if (outputFormatStr.equals("values-only")) 905 { 906 resultWriter = new ValuesOnlyLDAPResultWriter(getOut()); 907 } 908 else 909 { 910 final int wc; 911 if (doNotWrap.isPresent()) 912 { 913 wc = Integer.MAX_VALUE; 914 } 915 else if (wrapColumn.isPresent()) 916 { 917 wc = wrapColumn.getValue(); 918 } 919 else 920 { 921 wc = WRAP_COLUMN; 922 } 923 924 resultWriter = new LDIFLDAPResultWriter(getOut(), wc); 925 } 926 } 927 928 929 930 /** 931 * Uses the contents of any specified filter files, along with the configured 932 * base DN, scope, and requested attributes, to populate the set of search 933 * URLs. 934 * 935 * @throws ArgumentException If a problem is encountered while constructing 936 * the search URLs. 937 */ 938 private void readFilterFile() 939 throws ArgumentException 940 { 941 DN dn = baseDN.getValue(); 942 if (dn == null) 943 { 944 dn = DN.NULL_DN; 945 } 946 947 SearchScope searchScope = scope.getValue(); 948 if (searchScope == null) 949 { 950 searchScope = SearchScope.SUB; 951 } 952 953 final String[] requestedAttributes = 954 parser.getTrailingArguments().toArray(StaticUtils.NO_STRINGS); 955 956 for (final File f : filterFile.getValues()) 957 { 958 final InputStream inputStream; 959 try 960 { 961 inputStream = openInputStream(f); 962 } 963 catch (final LDAPException e) 964 { 965 Debug.debugException(e); 966 throw new ArgumentException(e.getMessage(), e); 967 } 968 969 try (BufferedReader reader = 970 new BufferedReader(new InputStreamReader(inputStream))) 971 { 972 while (true) 973 { 974 final String line = reader.readLine(); 975 if (line == null) 976 { 977 break; 978 } 979 980 if (line.isEmpty() || line.startsWith("#")) 981 { 982 continue; 983 } 984 985 try 986 { 987 final Filter filter = Filter.create(line.trim()); 988 searchURLs.add(new LDAPURL("ldap", null, null, dn, 989 requestedAttributes, searchScope, filter)); 990 } 991 catch (final LDAPException e) 992 { 993 Debug.debugException(e); 994 throw new ArgumentException( 995 ERR_LDIFSEARCH_FILTER_FILE_INVALID_FILTER.get(line, 996 f.getAbsolutePath(), e.getMessage()), 997 e); 998 } 999 } 1000 } 1001 catch (final IOException e) 1002 { 1003 Debug.debugException(e); 1004 throw new ArgumentException( 1005 ERR_LDIFSEARCH_ERROR_READING_FILTER_FILE.get(f.getAbsolutePath(), 1006 StaticUtils.getExceptionMessage(e)), 1007 e); 1008 } 1009 finally 1010 { 1011 try 1012 { 1013 inputStream.close(); 1014 } 1015 catch (final Exception e) 1016 { 1017 Debug.debugException(e); 1018 } 1019 } 1020 1021 } 1022 1023 if (searchURLs.isEmpty()) 1024 { 1025 throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTERS_FROM_FILE.get( 1026 filterFile.getValues().get(0).getAbsolutePath())); 1027 } 1028 } 1029 1030 1031 1032 /** 1033 * Uses the contents of any specified LDAP URL files to populate the set of 1034 * search URLs. 1035 * 1036 * @throws ArgumentException If a problem is encountered while constructing 1037 * the search URLs. 1038 */ 1039 private void readLDAPURLFile() 1040 throws ArgumentException 1041 { 1042 for (final File f : ldapURLFile.getValues()) 1043 { 1044 final InputStream inputStream; 1045 try 1046 { 1047 inputStream = openInputStream(f); 1048 } 1049 catch (final LDAPException e) 1050 { 1051 Debug.debugException(e); 1052 throw new ArgumentException(e.getMessage(), e); 1053 } 1054 1055 try (BufferedReader reader = 1056 new BufferedReader(new InputStreamReader(inputStream))) 1057 { 1058 while (true) 1059 { 1060 final String line = reader.readLine(); 1061 if (line == null) 1062 { 1063 break; 1064 } 1065 1066 if (line.isEmpty() || line.startsWith("#")) 1067 { 1068 continue; 1069 } 1070 1071 try 1072 { 1073 searchURLs.add(new LDAPURL(line.trim())); 1074 } 1075 catch (final LDAPException e) 1076 { 1077 Debug.debugException(e); 1078 throw new ArgumentException( 1079 ERR_LDIFSEARCH_LDAP_URL_FILE_INVALID_URL.get(line, 1080 f.getAbsolutePath(), e.getMessage()), 1081 e); 1082 } 1083 } 1084 } 1085 catch (final IOException e) 1086 { 1087 Debug.debugException(e); 1088 throw new ArgumentException( 1089 ERR_LDIFSEARCH_ERROR_READING_LDAP_URL_FILE.get(f.getAbsolutePath(), 1090 StaticUtils.getExceptionMessage(e)), 1091 e); 1092 } 1093 finally 1094 { 1095 try 1096 { 1097 inputStream.close(); 1098 } 1099 catch (final Exception e) 1100 { 1101 Debug.debugException(e); 1102 } 1103 } 1104 } 1105 1106 if (searchURLs.isEmpty()) 1107 { 1108 throw new ArgumentException(ERR_LDIFSEARCH_NO_URLS_FROM_FILE.get( 1109 ldapURLFile.getValues().get(0).getAbsolutePath())); 1110 } 1111 } 1112 1113 1114 1115 /** 1116 * {@inheritDoc} 1117 */ 1118 @Override() 1119 @NotNull() 1120 public ResultCode doToolProcessing() 1121 { 1122 // Get the schema to use when performing LDIF processing. 1123 final Schema schema; 1124 try 1125 { 1126 if (schemaPath.isPresent()) 1127 { 1128 schema = getSchema(schemaPath.getValues()); 1129 } 1130 else if (PING_SERVER_AVAILABLE) 1131 { 1132 schema = getSchema(Collections.singletonList(StaticUtils.constructPath( 1133 PING_SERVER_ROOT, "config", "schema"))); 1134 } 1135 else 1136 { 1137 schema = Schema.getDefaultStandardSchema(); 1138 } 1139 } 1140 catch (final Exception e) 1141 { 1142 Debug.debugException(e); 1143 logCompletionMessage(true, 1144 ERR_LDIFSEARCH_CANNOT_GET_SCHEMA.get( 1145 StaticUtils.getExceptionMessage(e))); 1146 return ResultCode.LOCAL_ERROR; 1147 } 1148 1149 1150 // Create search entry parers for all of the search URLs. 1151 final Map<LDAPURL,SearchEntryParer> urlMap = new LinkedHashMap<>(); 1152 for (final LDAPURL url : searchURLs) 1153 { 1154 final SearchEntryParer parer = new SearchEntryParer( 1155 Arrays.asList(url.getAttributes()), schema); 1156 urlMap.put(url, parer); 1157 } 1158 1159 1160 // If we should check schema, then create the entry validator. 1161 final EntryValidator entryValidator; 1162 if (checkSchema.isPresent()) 1163 { 1164 entryValidator = new EntryValidator(schema); 1165 } 1166 else 1167 { 1168 entryValidator = null; 1169 } 1170 1171 1172 // Create the output files, if appropriate. 1173 OutputStream outputStream = null; 1174 SearchEntryParer singleParer = null; 1175 final Map<LDAPURL,LDIFSearchSeparateSearchDetails> separateWriters = 1176 new LinkedHashMap<>(); 1177 try 1178 { 1179 if (outputFile.isPresent()) 1180 { 1181 final int numURLs = searchURLs.size(); 1182 if (separateOutputFilePerSearch.isPresent() && (numURLs > 1)) 1183 { 1184 int i=1; 1185 for (final LDAPURL url : searchURLs) 1186 { 1187 final File f = new 1188 File(outputFile.getValue().getAbsolutePath() + '.' + i); 1189 final LDIFSearchSeparateSearchDetails details = 1190 new LDIFSearchSeparateSearchDetails(url, f, 1191 createLDIFWriter(f, url), schema); 1192 separateWriters.put(url, details); 1193 i++; 1194 } 1195 } 1196 else 1197 { 1198 try 1199 { 1200 outputStream = createOutputStream(outputFile.getValue()); 1201 resultWriter.updateOutputStream(outputStream); 1202 } 1203 catch (final Exception e) 1204 { 1205 Debug.debugException(e); 1206 throw new LDAPException(ResultCode.LOCAL_ERROR, 1207 ERR_LDIFSEARCH_CANNOT_WRITE_TO_FILE.get( 1208 outputFile.getValue().getAbsolutePath(), 1209 StaticUtils.getExceptionMessage(e)), 1210 e); 1211 } 1212 } 1213 } 1214 1215 1216 // If we're not using separate writers, then write any appropriate header 1217 // to the top of the output. 1218 if (separateWriters.isEmpty()) 1219 { 1220 resultWriter.writeHeader(); 1221 } 1222 1223 1224 // Iterate through the LDIF files and process the entries they contain. 1225 boolean errorEncountered = false; 1226 final List<LDAPURL> matchingURLs = new ArrayList<>(); 1227 final List<String> entryInvalidReasons = new ArrayList<>(); 1228 for (final File f : ldifFile.getValues()) 1229 { 1230 final LDIFReader ldifReader; 1231 try 1232 { 1233 ldifReader = new LDIFReader(openInputStream(f)); 1234 1235 if (stripTrailingSpaces.isPresent()) 1236 { 1237 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP); 1238 } 1239 else 1240 { 1241 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT); 1242 } 1243 } 1244 catch (final Exception e) 1245 { 1246 Debug.debugException(e); 1247 logCompletionMessage(true, 1248 ERR_LDIFSEARCH_CANNOT_OPEN_LDIF_FILE.get(f.getName(), 1249 StaticUtils.getExceptionMessage(e))); 1250 return ResultCode.LOCAL_ERROR; 1251 } 1252 1253 try 1254 { 1255 while (true) 1256 { 1257 final Entry entry; 1258 try 1259 { 1260 entry = ldifReader.readEntry(); 1261 } 1262 catch (final LDIFException e) 1263 { 1264 Debug.debugException(e); 1265 if (e.mayContinueReading()) 1266 { 1267 commentToErr(ERR_LDIFSEARCH_RECOVERABLE_READ_ERROR.get( 1268 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e))); 1269 errorEncountered = true; 1270 continue; 1271 } 1272 else 1273 { 1274 logCompletionMessage(true, 1275 ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get( 1276 f.getAbsolutePath(), 1277 StaticUtils.getExceptionMessage(e))); 1278 return ResultCode.LOCAL_ERROR; 1279 } 1280 } 1281 catch (final Exception e) 1282 { 1283 logCompletionMessage(true, 1284 ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get( 1285 f.getAbsolutePath(), 1286 StaticUtils.getExceptionMessage(e))); 1287 return ResultCode.LOCAL_ERROR; 1288 } 1289 1290 if (entry == null) 1291 { 1292 break; 1293 } 1294 1295 if (entryValidator != null) 1296 { 1297 entryInvalidReasons.clear(); 1298 if (! entryValidator.entryIsValid(entry, entryInvalidReasons)) 1299 { 1300 commentToErr(ERR_LDIFSEARCH_ENTRY_VIOLATES_SCHEMA.get( 1301 entry.getDN())); 1302 for (final String invalidReason : entryInvalidReasons) 1303 { 1304 commentToErr("- " + invalidReason); 1305 } 1306 1307 err(); 1308 errorEncountered = true; 1309 continue; 1310 } 1311 } 1312 1313 if (separateWriters.isEmpty()) 1314 { 1315 matchingURLs.clear(); 1316 for (final LDAPURL url : searchURLs) 1317 { 1318 if (urlMatchesEntry(url, entry)) 1319 { 1320 matchingURLs.add(url); 1321 } 1322 } 1323 1324 if (matchingURLs.isEmpty()) 1325 { 1326 continue; 1327 } 1328 1329 try 1330 { 1331 if (searchURLs.size() > 1) 1332 { 1333 resultWriter.writeComment( 1334 INFO_LDIFSEARCH_ENTRY_MATCHES_URLS.get(entry.getDN())); 1335 for (final LDAPURL url : matchingURLs) 1336 { 1337 resultWriter.writeComment(url.toString()); 1338 } 1339 } 1340 1341 if (singleParer == null) 1342 { 1343 singleParer = new SearchEntryParer( 1344 Arrays.asList(searchURLs.get(0).getAttributes()), 1345 schema); 1346 } 1347 1348 resultWriter.writeSearchResultEntry( 1349 new SearchResultEntry(singleParer.pareEntry(entry))); 1350 1351 if (! outputFile.isPresent()) 1352 { 1353 resultWriter.flush(); 1354 } 1355 } 1356 catch (final Exception e) 1357 { 1358 Debug.debugException(e); 1359 if (outputFile.isPresent()) 1360 { 1361 logCompletionMessage(true, 1362 ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(), 1363 outputFile.getValue().getAbsolutePath(), 1364 StaticUtils.getExceptionMessage(e))); 1365 } 1366 else 1367 { 1368 logCompletionMessage(true, 1369 ERR_LDIFSEARCH_WRITE_ERROR_NO_FILE.get(entry.getDN(), 1370 StaticUtils.getExceptionMessage(e))); 1371 } 1372 return ResultCode.LOCAL_ERROR; 1373 } 1374 } 1375 else 1376 { 1377 for (final LDIFSearchSeparateSearchDetails details : 1378 separateWriters.values()) 1379 { 1380 final LDAPURL url = details.getLDAPURL(); 1381 if (urlMatchesEntry(url, entry)) 1382 { 1383 try 1384 { 1385 final Entry paredEntry = 1386 details.getSearchEntryParer().pareEntry(entry); 1387 details.getLDIFWriter().writeEntry(paredEntry); 1388 } 1389 catch (final Exception ex) 1390 { 1391 Debug.debugException(ex); 1392 logCompletionMessage(true, 1393 ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(), 1394 details.getOutputFile().getAbsolutePath(), 1395 StaticUtils.getExceptionMessage(ex))); 1396 return ResultCode.LOCAL_ERROR; 1397 } 1398 } 1399 } 1400 } 1401 } 1402 } 1403 finally 1404 { 1405 try 1406 { 1407 ldifReader.close(); 1408 } 1409 catch (final Exception e) 1410 { 1411 Debug.debugException(e); 1412 } 1413 } 1414 } 1415 1416 if (errorEncountered) 1417 { 1418 logCompletionMessage(true, 1419 WARN_LDIFSEARCH_COMPLETED_WITH_ERRORS.get()); 1420 return ResultCode.PARAM_ERROR; 1421 } 1422 else 1423 { 1424 logCompletionMessage(false, 1425 INFO_LDIFSEARCH_COMPLETED_SUCCESSFULLY.get()); 1426 return ResultCode.SUCCESS; 1427 } 1428 } 1429 catch (final LDAPException e) 1430 { 1431 Debug.debugException(e); 1432 logCompletionMessage(true, e.getMessage()); 1433 return e.getResultCode(); 1434 } 1435 finally 1436 { 1437 try 1438 { 1439 resultWriter.flush(); 1440 if (outputStream != null) 1441 { 1442 outputStream.close(); 1443 } 1444 } 1445 catch (final Exception e) 1446 { 1447 Debug.debugException(e); 1448 } 1449 1450 for (final LDIFSearchSeparateSearchDetails details : 1451 separateWriters.values()) 1452 { 1453 try 1454 { 1455 details.getLDIFWriter().close(); 1456 } 1457 catch (final Exception e) 1458 { 1459 Debug.debugException(e); 1460 } 1461 } 1462 } 1463 } 1464 1465 1466 1467 /** 1468 * Retrieves the schema contained in the specified paths. 1469 * 1470 * @param paths The paths to use to access the schema. 1471 * 1472 * @return The schema read from the specified files. 1473 * 1474 * @throws Exception If a problem is encountered while loading the schema. 1475 */ 1476 @NotNull() 1477 private static Schema getSchema(@NotNull final List<File> paths) 1478 throws Exception 1479 { 1480 final Set<File> schemaFiles = new LinkedHashSet<>(); 1481 for (final File f : paths) 1482 { 1483 if (f.exists()) 1484 { 1485 if (f.isFile()) 1486 { 1487 schemaFiles.add(f); 1488 } 1489 else if (f.isDirectory()) 1490 { 1491 final TreeMap<String,File> sortedFiles = new TreeMap<>(); 1492 for (final File fileInDir : f.listFiles()) 1493 { 1494 if (fileInDir.isFile()) 1495 { 1496 sortedFiles.put(fileInDir.getName(), fileInDir); 1497 } 1498 } 1499 1500 schemaFiles.addAll(sortedFiles.values()); 1501 } 1502 } 1503 } 1504 1505 return Schema.getSchema(new ArrayList<>(schemaFiles)); 1506 } 1507 1508 1509 1510 /** 1511 * Opens the input stream to use to read from the specified file. 1512 * 1513 * @param f The file for which to open the input stream. It may optionally 1514 * be compressed and/or encrypted. 1515 * 1516 * @return The input stream that was created. 1517 * 1518 * @throws LDAPException If a problem is encountered while opening the file. 1519 */ 1520 @NotNull() 1521 private InputStream openInputStream(@NotNull final File f) 1522 throws LDAPException 1523 { 1524 if (ldifEncryptionPassphraseFile.isPresent() && 1525 (! ldifEncryptionPassphraseFileRead)) 1526 { 1527 readPassphraseFile(ldifEncryptionPassphraseFile.getValue()); 1528 ldifEncryptionPassphraseFileRead = true; 1529 } 1530 1531 1532 boolean closeStream = true; 1533 InputStream inputStream = null; 1534 try 1535 { 1536 inputStream = new FileInputStream(f); 1537 1538 final ObjectPair<InputStream,char[]> p = 1539 ToolUtils.getPossiblyPassphraseEncryptedInputStream( 1540 inputStream, inputEncryptionPassphrases, 1541 (! ldifEncryptionPassphraseFile.isPresent()), 1542 INFO_LDIFSEARCH_ENTER_ENCRYPTION_PW.get(f.getName()), 1543 ERR_LDIFSEARCH_WRONG_ENCRYPTION_PW.get(), getOut(), getErr()); 1544 inputStream = p.getFirst(); 1545 addPassphrase(p.getSecond()); 1546 1547 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 1548 closeStream = false; 1549 return inputStream; 1550 } 1551 catch (final Exception e) 1552 { 1553 Debug.debugException(e); 1554 throw new LDAPException(ResultCode.LOCAL_ERROR, 1555 ERR_LDIFSEARCH_ERROR_OPENING_INPUT_FILE.get(f.getAbsolutePath(), 1556 StaticUtils.getExceptionMessage(e)), 1557 e); 1558 } 1559 finally 1560 { 1561 if ((inputStream != null) && closeStream) 1562 { 1563 try 1564 { 1565 inputStream.close(); 1566 } 1567 catch (final Exception e) 1568 { 1569 Debug.debugException(e); 1570 } 1571 } 1572 } 1573 } 1574 1575 1576 1577 /** 1578 * Reads the contents of the specified passphrase file and adds it to the list 1579 * of passphrases. 1580 * 1581 * @param f The passphrase file to read. 1582 * 1583 * @throws LDAPException If a problem is encountered while trying to read 1584 * the passphrase from the provided file. 1585 */ 1586 private void readPassphraseFile(@NotNull final File f) 1587 throws LDAPException 1588 { 1589 try 1590 { 1591 addPassphrase(getPasswordFileReader().readPassword(f)); 1592 } 1593 catch (final Exception e) 1594 { 1595 Debug.debugException(e); 1596 throw new LDAPException(ResultCode.LOCAL_ERROR, 1597 ERR_LDIFSEARCH_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(), 1598 StaticUtils.getExceptionMessage(e)), 1599 e); 1600 } 1601 } 1602 1603 1604 1605 /** 1606 * Updates the list of encryption passphrases with the provided passphrase, if 1607 * it is not already present. 1608 * 1609 * @param passphrase The passphrase to be added. It may optionally be 1610 * {@code null} (in which case no action will be taken). 1611 */ 1612 private void addPassphrase(@Nullable final char[] passphrase) 1613 { 1614 if (passphrase == null) 1615 { 1616 return; 1617 } 1618 1619 for (final char[] existingPassphrase : inputEncryptionPassphrases) 1620 { 1621 if (Arrays.equals(existingPassphrase, passphrase)) 1622 { 1623 return; 1624 } 1625 } 1626 1627 inputEncryptionPassphrases.add(passphrase); 1628 } 1629 1630 1631 1632 /** 1633 * Creates an output stream that may be used to write to the specified file. 1634 * 1635 * @param f The file to be written. 1636 * 1637 * @return The output stream that was created. 1638 * 1639 * @throws LDAPException If a problem occurs while creating the output 1640 * stream. 1641 */ 1642 @NotNull() 1643 private OutputStream createOutputStream(@NotNull final File f) 1644 throws LDAPException 1645 { 1646 OutputStream outputStream = null; 1647 boolean closeOutputStream = true; 1648 try 1649 { 1650 try 1651 { 1652 1653 outputStream = new FileOutputStream(f, 1654 (! overwriteExistingOutputFile.isPresent())); 1655 } 1656 catch (final Exception e) 1657 { 1658 Debug.debugException(e); 1659 throw new LDAPException(ResultCode.LOCAL_ERROR, 1660 ERR_LDIFSEARCH_CANNOT_OPEN_OUTPUT_FILE.get(f.getAbsolutePath(), 1661 StaticUtils.getExceptionMessage(e)), 1662 e); 1663 } 1664 1665 if (encryptOutput.isPresent()) 1666 { 1667 try 1668 { 1669 final char[] passphrase; 1670 if (outputEncryptionPassphraseFile.isPresent()) 1671 { 1672 passphrase = getPasswordFileReader().readPassword( 1673 outputEncryptionPassphraseFile.getValue()); 1674 } 1675 else 1676 { 1677 passphrase = ToolUtils.promptForEncryptionPassphrase(false, true, 1678 INFO_LDIFSEARCH_PROMPT_OUTPUT_FILE_ENC_PW.get(), 1679 INFO_LDIFSEARCH_CONFIRM_OUTPUT_FILE_ENC_PW.get(), getOut(), 1680 getErr()).toCharArray(); 1681 } 1682 1683 outputStream = new PassphraseEncryptedOutputStream(passphrase, 1684 outputStream, null, true, true); 1685 } 1686 catch (final Exception e) 1687 { 1688 Debug.debugException(e); 1689 throw new LDAPException(ResultCode.LOCAL_ERROR, 1690 ERR_LDIFSEARCH_CANNOT_ENCRYPT_OUTPUT_FILE.get( 1691 StaticUtils.getExceptionMessage(e)), 1692 e); 1693 } 1694 } 1695 1696 if (compressOutput.isPresent()) 1697 { 1698 try 1699 { 1700 outputStream = new GZIPOutputStream(outputStream); 1701 } 1702 catch (final Exception e) 1703 { 1704 Debug.debugException(e); 1705 throw new LDAPException(ResultCode.LOCAL_ERROR, 1706 ERR_LDIFSEARCH_CANNOT_COMPRESS_OUTPUT_FILE.get( 1707 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)), 1708 e); 1709 } 1710 } 1711 1712 closeOutputStream = false; 1713 return outputStream; 1714 } 1715 finally 1716 { 1717 if (closeOutputStream && (outputStream != null)) 1718 { 1719 try 1720 { 1721 outputStream.close(); 1722 } 1723 catch (final Exception e) 1724 { 1725 Debug.debugException(e); 1726 } 1727 } 1728 } 1729 } 1730 1731 1732 1733 /** 1734 * Creates an LDIF writer to write to the specified file. 1735 * 1736 * @param f The file to be written. 1737 * @param ldapURL The LDAP URL with which the file will be associated. It 1738 * may be {@code null} if the file is shared across multiple 1739 * URLs. 1740 * 1741 * @return The LDIF writer that was created. 1742 * 1743 * @throws LDAPException If a problem occurs while creating the LDIF writer. 1744 */ 1745 @NotNull() 1746 private LDIFWriter createLDIFWriter(@NotNull final File f, 1747 @Nullable final LDAPURL ldapURL) 1748 throws LDAPException 1749 { 1750 boolean closeOutputStream = true; 1751 final OutputStream outputStream = createOutputStream(f); 1752 try 1753 { 1754 final LDIFWriter ldifWriter = new LDIFWriter(outputStream); 1755 if (doNotWrap.isPresent()) 1756 { 1757 ldifWriter.setWrapColumn(0); 1758 } 1759 else if (wrapColumn.isPresent()) 1760 { 1761 ldifWriter.setWrapColumn(wrapColumn.getValue()); 1762 } 1763 else 1764 { 1765 ldifWriter.setWrapColumn(WRAP_COLUMN); 1766 } 1767 1768 if (ldapURL != null) 1769 { 1770 try 1771 { 1772 ldifWriter.writeComment( 1773 INFO_LDIFSEARCH_ENTRIES_MATCHING_URL.get(ldapURL.toString()), 1774 false, true); 1775 } 1776 catch (final Exception e) 1777 { 1778 Debug.debugException(e); 1779 } 1780 } 1781 1782 closeOutputStream = false; 1783 return ldifWriter; 1784 } 1785 finally 1786 { 1787 if (closeOutputStream) 1788 { 1789 try 1790 { 1791 outputStream.close(); 1792 } 1793 catch (final Exception e) 1794 { 1795 Debug.debugException(e); 1796 } 1797 } 1798 } 1799 } 1800 1801 1802 1803 /** 1804 * Indicates whether the given entry matches the criteria in the provided LDAP 1805 * URL. 1806 * 1807 * @param url The URL for which to make the determination. 1808 * @param entry The entry for which to make the determination. 1809 * 1810 * @return {@code true} if the entry matches the criteria in the LDAP URL, or 1811 * {@code false} if not. 1812 */ 1813 private boolean urlMatchesEntry(@NotNull final LDAPURL url, 1814 @NotNull final Entry entry) 1815 { 1816 try 1817 { 1818 return (entry.matchesBaseAndScope(url.getBaseDN(), url.getScope()) && 1819 url.getFilter().matchesEntry(entry)); 1820 } 1821 catch (final Exception e) 1822 { 1823 Debug.debugException(e); 1824 return false; 1825 } 1826 } 1827 1828 1829 1830 /** 1831 * Writes a line-wrapped, commented version of the provided message to 1832 * standard output. 1833 * 1834 * @param message The message to be written. 1835 */ 1836 private void commentToOut(@NotNull final String message) 1837 { 1838 getOut().flush(); 1839 for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2))) 1840 { 1841 out("# " + line); 1842 } 1843 getOut().flush(); 1844 } 1845 1846 1847 1848 /** 1849 * Writes a line-wrapped, commented version of the provided message to 1850 * standard error. 1851 * 1852 * @param message The message to be written. 1853 */ 1854 private void commentToErr(@NotNull final String message) 1855 { 1856 getErr().flush(); 1857 for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2))) 1858 { 1859 err("# " + line); 1860 } 1861 getErr().flush(); 1862 } 1863 1864 1865 1866 /** 1867 * Writes the provided message and sets it as the completion message. 1868 * 1869 * @param isError Indicates whether the message should be written to 1870 * standard error rather than standard output. 1871 * @param message The message to be written. 1872 */ 1873 private void logCompletionMessage(final boolean isError, 1874 @NotNull final String message) 1875 { 1876 completionMessage.compareAndSet(null, message); 1877 1878 if (! outputFile.isPresent()) 1879 { 1880 resultWriter.writeComment(message); 1881 } 1882 } 1883 1884 1885 1886 /** 1887 * {@inheritDoc} 1888 */ 1889 @Override() 1890 @NotNull() 1891 public LinkedHashMap<String[],String> getExampleUsages() 1892 { 1893 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 1894 1895 examples.put( 1896 new String[] 1897 { 1898 "--ldifFile", "data.ldif", 1899 "(uid=jdoe)" 1900 }, 1901 INFO_LDIFSEARCH_EXAMPLE_1.get()); 1902 1903 examples.put( 1904 new String[] 1905 { 1906 "--ldifFile", "data.ldif", 1907 "--outputFile", "people.ldif", 1908 "--baseDN", "dc=example,dc=com", 1909 "--scope", "sub", 1910 "(objectClass=person)", 1911 "givenName", 1912 "sn", 1913 "cn", 1914 }, 1915 INFO_LDIFSEARCH_EXAMPLE_2.get()); 1916 1917 return examples; 1918 } 1919}