001/* 002 * Copyright 2020-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2023 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-2023 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 @Nullable() 381 protected String getToolCompletionMessage() 382 { 383 return completionMessage.get(); 384 } 385 386 387 388 /** 389 * {@inheritDoc} 390 */ 391 @Override() 392 public void addToolArguments(@NotNull final ArgumentParser parser) 393 throws ArgumentException 394 { 395 this.parser = parser; 396 397 398 ldifFile = new FileArgument('l', "ldifFile", true, 0, null, 399 INFO_LDIFSEARCH_ARG_DESC_LDIF_FILE.get(), true, true, true, false); 400 ldifFile.addLongIdentifier("ldif-file", true); 401 ldifFile.addLongIdentifier("inputFile", true); 402 ldifFile.addLongIdentifier("input-file", true); 403 ldifFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 404 parser.addArgument(ldifFile); 405 406 407 final String ldifPWDesc; 408 if (PING_SERVER_AVAILABLE) 409 { 410 ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_PING_SERVER.get(); 411 } 412 else 413 { 414 ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_STANDALONE.get(); 415 } 416 ldifEncryptionPassphraseFile = new FileArgument(null, 417 "ldifEncryptionPassphraseFile", false, 1, null, ldifPWDesc, true, 418 true, true, false); 419 ldifEncryptionPassphraseFile.addLongIdentifier( 420 "ldif-encryption-passphrase-file", true); 421 ldifEncryptionPassphraseFile.addLongIdentifier("ldifPassphraseFile", true); 422 ldifEncryptionPassphraseFile.addLongIdentifier("ldif-passphrase-file", 423 true); 424 ldifEncryptionPassphraseFile.addLongIdentifier("ldifEncryptionPasswordFile", 425 true); 426 ldifEncryptionPassphraseFile.addLongIdentifier( 427 "ldif-encryption-password-file", true); 428 ldifEncryptionPassphraseFile.addLongIdentifier("ldifPasswordFile", true); 429 ldifEncryptionPassphraseFile.addLongIdentifier("ldif-password-file", true); 430 ldifEncryptionPassphraseFile.addLongIdentifier( 431 "inputEncryptionPassphraseFile", true); 432 ldifEncryptionPassphraseFile.addLongIdentifier( 433 "input-encryption-passphrase-file", true); 434 ldifEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile", true); 435 ldifEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file", 436 true); 437 ldifEncryptionPassphraseFile.addLongIdentifier( 438 "inputEncryptionPasswordFile", true); 439 ldifEncryptionPassphraseFile.addLongIdentifier( 440 "input-encryption-password-file", true); 441 ldifEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true); 442 ldifEncryptionPassphraseFile.addLongIdentifier("input-password-file", true); 443 ldifEncryptionPassphraseFile.setArgumentGroupName( 444 INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 445 parser.addArgument(ldifEncryptionPassphraseFile); 446 447 448 stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1, 449 INFO_LDIFSEARCH_ARG_DESC_STRIP_TRAILING_SPACES.get()); 450 stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true); 451 stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true); 452 stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true); 453 stripTrailingSpaces.setArgumentGroupName( 454 INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 455 parser.addArgument(stripTrailingSpaces); 456 457 458 final String schemaPathDesc; 459 if (PING_SERVER_AVAILABLE) 460 { 461 schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_PING_SERVER.get(); 462 } 463 else 464 { 465 schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_STANDALONE.get(); 466 } 467 schemaPath = new FileArgument(null, "schemaPath", false, 0, null, 468 schemaPathDesc, true, true, false, false); 469 schemaPath.addLongIdentifier("schema-path", true); 470 schemaPath.addLongIdentifier("schemaFile", true); 471 schemaPath.addLongIdentifier("schema-file", true); 472 schemaPath.addLongIdentifier("schemaDirectory", true); 473 schemaPath.addLongIdentifier("schema-directory", true); 474 schemaPath.addLongIdentifier("schema", true); 475 schemaPath.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 476 parser.addArgument(schemaPath); 477 478 479 checkSchema = new BooleanArgument(null, "checkSchema", 1, 480 INFO_LDIFSEARCH_ARG_DESC_CHECK_SCHEMA.get()); 481 checkSchema.addLongIdentifier("check-schema", true); 482 checkSchema.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 483 parser.addArgument(checkSchema); 484 485 486 isCompressed = new BooleanArgument(null, "isCompressed", 1, 487 INFO_LDIFSEARCH_ARG_DESC_IS_COMPRESSED.get()); 488 isCompressed.addLongIdentifier("is-compressed", true); 489 isCompressed.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get()); 490 isCompressed.setHidden(true); 491 parser.addArgument(isCompressed); 492 493 494 outputFile = new FileArgument('o', "outputFile", false, 1, null, 495 INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FILE.get(), false, true, true, false); 496 outputFile.addLongIdentifier("output-file", true); 497 outputFile.addLongIdentifier("outputLDIF", true); 498 outputFile.addLongIdentifier("output-ldif", true); 499 outputFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 500 parser.addArgument(outputFile); 501 502 503 separateOutputFilePerSearch = new BooleanArgument(null, 504 "separateOutputFilePerSearch", 1, 505 INFO_LDIFSEARCH_ARG_DESC_SEPARATE_OUTPUT_FILES.get()); 506 separateOutputFilePerSearch.addLongIdentifier( 507 "separate-output-file-per-search", true); 508 separateOutputFilePerSearch.addLongIdentifier("separateOutputFiles", true); 509 separateOutputFilePerSearch.addLongIdentifier("separate-output-files", 510 true); 511 separateOutputFilePerSearch.setArgumentGroupName( 512 INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 513 parser.addArgument(separateOutputFilePerSearch); 514 515 516 compressOutput = new BooleanArgument(null, "compressOutput", 1, 517 INFO_LDIFSEARCH_ARG_DESC_COMPRESS_OUTPUT.get()); 518 compressOutput.addLongIdentifier("compress-output", true); 519 compressOutput.addLongIdentifier("compressLDIF", true); 520 compressOutput.addLongIdentifier("compress-ldif", true); 521 compressOutput.addLongIdentifier("compress", true); 522 compressOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 523 parser.addArgument(compressOutput); 524 525 526 encryptOutput = new BooleanArgument(null, "encryptOutput", 1, 527 INFO_LDIFSEARCH_ARG_DESC_ENCRYPT_OUTPUT.get()); 528 encryptOutput.addLongIdentifier("encrypt-output", true); 529 encryptOutput.addLongIdentifier("encryptLDIF", true); 530 encryptOutput.addLongIdentifier("encrypt-ldif", true); 531 encryptOutput.addLongIdentifier("encrypt", true); 532 encryptOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 533 parser.addArgument(encryptOutput); 534 535 536 outputEncryptionPassphraseFile = new FileArgument(null, 537 "outputEncryptionPassphraseFile", false, 1, null, 538 INFO_LDIFSEARCH_ARG_DESC_OUTPUT_PW_FILE.get(), true, true, true, 539 false); 540 outputEncryptionPassphraseFile.addLongIdentifier( 541 "output-encryption-passphrase-file", true); 542 outputEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile", 543 true); 544 outputEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file", 545 true); 546 outputEncryptionPassphraseFile.addLongIdentifier( 547 "outputEncryptionPasswordFile", true); 548 outputEncryptionPassphraseFile.addLongIdentifier( 549 "output-encryption-password-file", true); 550 outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile", 551 true); 552 outputEncryptionPassphraseFile.addLongIdentifier("output-password-file", 553 true); 554 outputEncryptionPassphraseFile.addLongIdentifier( 555 "outputEncryptionPasswordFile", true); 556 outputEncryptionPassphraseFile.addLongIdentifier( 557 "output-encryption-password-file", true); 558 outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile", 559 true); 560 outputEncryptionPassphraseFile.addLongIdentifier("output-password-file", 561 true); 562 outputEncryptionPassphraseFile.setArgumentGroupName( 563 INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 564 parser.addArgument(outputEncryptionPassphraseFile); 565 566 567 overwriteExistingOutputFile = new BooleanArgument('O', 568 "overwriteExistingOutputFile", 1, 569 INFO_LDIFSEARCH_ARG_DESC_OVERWRITE_EXISTING.get()); 570 overwriteExistingOutputFile.addLongIdentifier( 571 "overwrite-existing-output-file", true); 572 overwriteExistingOutputFile.addLongIdentifier( 573 "overwriteExistingOutputFiles", true); 574 overwriteExistingOutputFile.addLongIdentifier( 575 "overwrite-existing-output-files", true); 576 overwriteExistingOutputFile.addLongIdentifier("overwriteExistingOutput", 577 true); 578 overwriteExistingOutputFile.addLongIdentifier("overwrite-existing-output", 579 true); 580 overwriteExistingOutputFile.addLongIdentifier("overwriteExisting", true); 581 overwriteExistingOutputFile.addLongIdentifier("overwrite-existing", true); 582 overwriteExistingOutputFile.addLongIdentifier("overwrite", true); 583 overwriteExistingOutputFile.setArgumentGroupName( 584 INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 585 parser.addArgument(overwriteExistingOutputFile); 586 587 588 final Set<String> outputFormatAllowedValues = StaticUtils.setOf("ldif", 589 "json", "csv", "multi-valued-csv", "tab-delimited", 590 "multi-valued-tab-delimited", "dns-only", "values-only"); 591 outputFormat = new StringArgument(null, "outputFormat", false, 1, 592 "{ldif|json|csv|multi-valued-csv|tab-delimited|" + 593 "multi-valued-tab-delimited|dns-only|values-only}", 594 INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FORMAT.get(), 595 outputFormatAllowedValues, "ldif"); 596 outputFormat.addLongIdentifier("output-format", true); 597 outputFormat.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 598 parser.addArgument(outputFormat); 599 600 601 wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null, 602 INFO_LDIFSEARCH_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE); 603 wrapColumn.addLongIdentifier("wrap-column", true); 604 wrapColumn.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 605 parser.addArgument(wrapColumn); 606 607 608 doNotWrap = new BooleanArgument('T', "doNotWrap", 1, 609 INFO_LDIFSEARCH_ARG_DESC_DO_NOT_WRAP.get()); 610 doNotWrap.addLongIdentifier("do-not-wrap", true); 611 doNotWrap.addLongIdentifier("dontWrap", true); 612 doNotWrap.addLongIdentifier("dont-wrap", true); 613 doNotWrap.addLongIdentifier("noWrap", true); 614 doNotWrap.addLongIdentifier("no-wrap", true); 615 doNotWrap.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get()); 616 parser.addArgument(doNotWrap); 617 618 619 baseDN = new DNArgument('b', "baseDN", false, 1, null, 620 INFO_LDIFSEARCH_ARG_DESC_BASE_DN.get()); 621 baseDN.addLongIdentifier("base-dn", true); 622 baseDN.addLongIdentifier("searchBaseDN", true); 623 baseDN.addLongIdentifier("search-base-dn", true); 624 baseDN.addLongIdentifier("searchBase", true); 625 baseDN.addLongIdentifier("search-base", true); 626 baseDN.addLongIdentifier("base", true); 627 baseDN.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 628 parser.addArgument(baseDN); 629 630 631 scope = new ScopeArgument('s', "scope", false, null, 632 INFO_LDIFSEARCH_ARG_DESC_SCOPE.get()); 633 scope.addLongIdentifier("searchScope", true); 634 scope.addLongIdentifier("search-scope", true); 635 scope.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 636 parser.addArgument(scope); 637 638 639 filterFile = new FileArgument('f', "filterFile", false, 0, null, 640 INFO_LDIFSEARCH_ARG_DESC_FILTER_FILE.get(), true, true, true, false); 641 filterFile.addLongIdentifier("filter-file", true); 642 filterFile.addLongIdentifier("filtersFile", true); 643 filterFile.addLongIdentifier("filters-file", true); 644 filterFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 645 parser.addArgument(filterFile); 646 647 648 ldapURLFile = new FileArgument(null, "ldapURLFile", false, 0, null, 649 INFO_LDIFSEARCH_ARG_DESC_LDAP_URL_FILE.get(), true, true, true, false); 650 ldapURLFile.addLongIdentifier("ldap-url-file", true); 651 ldapURLFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 652 parser.addArgument(ldapURLFile); 653 654 655 sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, null, 656 INFO_LDIFSEARCH_ARG_DESC_SIZE_LIMIT.get(), 0, Integer.MAX_VALUE, 0); 657 sizeLimit.addLongIdentifier("size-limit", true); 658 sizeLimit.addLongIdentifier("searchSizeLimit", true); 659 sizeLimit.addLongIdentifier("search-size-limit", true); 660 sizeLimit.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 661 sizeLimit.setHidden(true); 662 parser.addArgument(sizeLimit); 663 664 665 timeLimitSeconds = new IntegerArgument('t', "timeLimitSeconds", false, 1, 666 null, INFO_LDIFSEARCH_ARG_DESC_TIME_LIMIT_SECONDS.get(), 0, 667 Integer.MAX_VALUE, 0); 668 timeLimitSeconds.addLongIdentifier("time-limit-seconds", true); 669 timeLimitSeconds.addLongIdentifier("timeLimit", true); 670 timeLimitSeconds.setArgumentGroupName( 671 INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get()); 672 timeLimitSeconds.setHidden(true); 673 parser.addArgument(timeLimitSeconds); 674 675 676 parser.addDependentArgumentSet(separateOutputFilePerSearch, outputFile); 677 parser.addDependentArgumentSet(compressOutput, outputFile); 678 parser.addDependentArgumentSet(encryptOutput, outputFile); 679 parser.addDependentArgumentSet(overwriteExistingOutputFile, outputFile); 680 parser.addDependentArgumentSet(outputEncryptionPassphraseFile, 681 encryptOutput); 682 683 parser.addExclusiveArgumentSet(wrapColumn, doNotWrap); 684 parser.addExclusiveArgumentSet(baseDN, ldapURLFile); 685 parser.addExclusiveArgumentSet(scope, ldapURLFile); 686 parser.addExclusiveArgumentSet(filterFile, ldapURLFile); 687 parser.addExclusiveArgumentSet(outputFormat, separateOutputFilePerSearch); 688 } 689 690 691 692 /** 693 * {@inheritDoc} 694 */ 695 @Override() 696 public void doExtendedArgumentValidation() 697 throws ArgumentException 698 { 699 // If the output file exists and either compressOutput or encryptOutput is 700 // present, then the overwrite argument must also be present. 701 final File outFile = outputFile.getValue(); 702 if ((outFile != null) && outFile.exists() && 703 (compressOutput.isPresent() || encryptOutput.isPresent()) && 704 (! overwriteExistingOutputFile.isPresent())) 705 { 706 throw new ArgumentException( 707 ERR_LDIFSEARCH_APPEND_WITH_COMPRESSION_OR_ENCRYPTION.get( 708 compressOutput.getIdentifierString(), 709 encryptOutput.getIdentifierString(), 710 overwriteExistingOutputFile.getIdentifierString())); 711 } 712 713 714 // Create the set of LDAP URLs to use when issuing the searches. 715 final List<String> trailingArgs = parser.getTrailingArguments(); 716 final List<String> requestedAttributes = new ArrayList<>(); 717 if (filterFile.isPresent()) 718 { 719 // If there are trailing arguments, then make sure the first one is not a 720 // valid filter. 721 if (! trailingArgs.isEmpty()) 722 { 723 try 724 { 725 Filter.create(trailingArgs.get(0)); 726 throw new ArgumentException( 727 ERR_LDIFSEARCH_FILTER_FILE_WITH_TRAILING_FILTER.get()); 728 } 729 catch (final LDAPException e) 730 { 731 // This was expected. 732 } 733 } 734 735 requestedAttributes.addAll(trailingArgs); 736 readFilterFile(); 737 } 738 else if (ldapURLFile.isPresent()) 739 { 740 // Make sure there aren't any trailing arguments. 741 if (! trailingArgs.isEmpty()) 742 { 743 throw new ArgumentException( 744 ERR_LDIFSEARCH_LDAP_URL_FILE_WITH_TRAILING_ARGS.get()); 745 } 746 747 readLDAPURLFile(); 748 749 750 // If there are multiple LDAP URLs, and if they should not be sent to 751 // separate output files, then they must all have the same set of 752 // requested attributes. 753 if ((searchURLs.size() > 1) && 754 (! separateOutputFilePerSearch.isPresent())) 755 { 756 final Iterator<LDAPURL> iterator = searchURLs.iterator(); 757 final Set<String> requestedAttrs = 758 new HashSet<>(Arrays.asList(iterator.next().getAttributes())); 759 while (iterator.hasNext()) 760 { 761 final Set<String> attrSet = new HashSet<>(Arrays.asList( 762 iterator.next().getAttributes())); 763 if (! requestedAttrs.equals(attrSet)) 764 { 765 throw new ArgumentException( 766 ERR_LDIFSEARCH_DIFFERENT_URL_ATTRS_IN_SAME_FILE.get( 767 ldapURLFile.getIdentifierString(), 768 separateOutputFilePerSearch.getIdentifierString())); 769 } 770 } 771 } 772 } 773 else 774 { 775 // Make sure there is at least one trailing argument, and that it's a 776 // valid filter. If there are any others, then they must be the 777 // requested arguments. 778 if (trailingArgs.isEmpty()) 779 { 780 throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTER.get()); 781 } 782 783 784 final Filter filter; 785 try 786 { 787 final List<String> trailingArgList = new ArrayList<>(trailingArgs); 788 final Iterator<String> trailingArgIterator = trailingArgList.iterator(); 789 filter = Filter.create(trailingArgIterator.next()); 790 791 while (trailingArgIterator.hasNext()) 792 { 793 requestedAttributes.add(trailingArgIterator.next()); 794 } 795 } 796 catch (final LDAPException e) 797 { 798 Debug.debugException(e); 799 throw new ArgumentException( 800 ERR_LDIFSEARCH_FIRST_TRAILING_ARG_NOT_FILTER.get( 801 trailingArgs.get(0)), 802 e); 803 } 804 805 806 DN dn = baseDN.getValue(); 807 if (dn == null) 808 { 809 dn = DN.NULL_DN; 810 } 811 812 SearchScope searchScope = scope.getValue(); 813 if (searchScope == null) 814 { 815 searchScope = SearchScope.SUB; 816 } 817 818 try 819 { 820 searchURLs.add(new LDAPURL("ldap", null, null, dn, 821 requestedAttributes.toArray(StaticUtils.NO_STRINGS), 822 searchScope, filter)); 823 } 824 catch (final LDAPException e) 825 { 826 Debug.debugException(e); 827 // This should never happen. 828 throw new ArgumentException(StaticUtils.getExceptionMessage(e), e); 829 } 830 } 831 832 833 // Create the result writer. 834 final String outputFormatStr = 835 StaticUtils.toLowerCase(outputFormat.getValue()); 836 if (outputFormatStr.equals("json")) 837 { 838 resultWriter = new JSONLDAPResultWriter(getOut()); 839 } 840 else if (outputFormatStr.equals("csv") || 841 outputFormatStr.equals("multi-valued-csv") || 842 outputFormatStr.equals("tab-delimited") || 843 outputFormatStr.equals("multi-valued-tab-delimited")) 844 { 845 // These output formats cannot be used with the --ldapURLFile argument. 846 if (ldapURLFile.isPresent()) 847 { 848 throw new ArgumentException( 849 ERR_LDIFSEARCH_OUTPUT_FORMAT_NOT_SUPPORTED_WITH_URLS.get( 850 outputFormat.getValue(), ldapURLFile.getIdentifierString())); 851 } 852 853 854 // These output formats require a set of requested attributes. 855 if (requestedAttributes.isEmpty()) 856 { 857 throw new ArgumentException( 858 ERR_LDIFSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTRS.get( 859 outputFormat.getValue())); 860 } 861 862 final OutputFormat format; 863 final boolean includeAllValues; 864 switch (outputFormatStr) 865 { 866 case "multi-valued-csv": 867 format = OutputFormat.CSV; 868 includeAllValues = true; 869 break; 870 case "tab-delimited": 871 format = OutputFormat.TAB_DELIMITED_TEXT; 872 includeAllValues = false; 873 break; 874 case "multi-valued-tab-delimited": 875 format = OutputFormat.TAB_DELIMITED_TEXT; 876 includeAllValues = true; 877 break; 878 case "csv": 879 default: 880 format = OutputFormat.CSV; 881 includeAllValues = false; 882 break; 883 } 884 885 886 resultWriter = new ColumnBasedLDAPResultWriter(getOut(), 887 format, requestedAttributes, WRAP_COLUMN, includeAllValues); 888 } 889 else if (outputFormatStr.equals("dns-only")) 890 { 891 resultWriter = new DNsOnlyLDAPResultWriter(getOut()); 892 } 893 else if (outputFormatStr.equals("values-only")) 894 { 895 resultWriter = new ValuesOnlyLDAPResultWriter(getOut()); 896 } 897 else 898 { 899 final int wc; 900 if (doNotWrap.isPresent()) 901 { 902 wc = Integer.MAX_VALUE; 903 } 904 else if (wrapColumn.isPresent()) 905 { 906 wc = wrapColumn.getValue(); 907 } 908 else 909 { 910 wc = WRAP_COLUMN; 911 } 912 913 resultWriter = new LDIFLDAPResultWriter(getOut(), wc); 914 } 915 } 916 917 918 919 /** 920 * Uses the contents of any specified filter files, along with the configured 921 * base DN, scope, and requested attributes, to populate the set of search 922 * URLs. 923 * 924 * @throws ArgumentException If a problem is encountered while constructing 925 * the search URLs. 926 */ 927 private void readFilterFile() 928 throws ArgumentException 929 { 930 DN dn = baseDN.getValue(); 931 if (dn == null) 932 { 933 dn = DN.NULL_DN; 934 } 935 936 SearchScope searchScope = scope.getValue(); 937 if (searchScope == null) 938 { 939 searchScope = SearchScope.SUB; 940 } 941 942 final String[] requestedAttributes = 943 parser.getTrailingArguments().toArray(StaticUtils.NO_STRINGS); 944 945 for (final File f : filterFile.getValues()) 946 { 947 final InputStream inputStream; 948 try 949 { 950 inputStream = openInputStream(f); 951 } 952 catch (final LDAPException e) 953 { 954 Debug.debugException(e); 955 throw new ArgumentException(e.getMessage(), e); 956 } 957 958 try (BufferedReader reader = 959 new BufferedReader(new InputStreamReader(inputStream))) 960 { 961 while (true) 962 { 963 final String line = reader.readLine(); 964 if (line == null) 965 { 966 break; 967 } 968 969 if (line.isEmpty() || line.startsWith("#")) 970 { 971 continue; 972 } 973 974 try 975 { 976 final Filter filter = Filter.create(line.trim()); 977 searchURLs.add(new LDAPURL("ldap", null, null, dn, 978 requestedAttributes, searchScope, filter)); 979 } 980 catch (final LDAPException e) 981 { 982 Debug.debugException(e); 983 throw new ArgumentException( 984 ERR_LDIFSEARCH_FILTER_FILE_INVALID_FILTER.get(line, 985 f.getAbsolutePath(), e.getMessage()), 986 e); 987 } 988 } 989 } 990 catch (final IOException e) 991 { 992 Debug.debugException(e); 993 throw new ArgumentException( 994 ERR_LDIFSEARCH_ERROR_READING_FILTER_FILE.get(f.getAbsolutePath(), 995 StaticUtils.getExceptionMessage(e)), 996 e); 997 } 998 finally 999 { 1000 try 1001 { 1002 inputStream.close(); 1003 } 1004 catch (final Exception e) 1005 { 1006 Debug.debugException(e); 1007 } 1008 } 1009 1010 } 1011 1012 if (searchURLs.isEmpty()) 1013 { 1014 throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTERS_FROM_FILE.get( 1015 filterFile.getValues().get(0).getAbsolutePath())); 1016 } 1017 } 1018 1019 1020 1021 /** 1022 * Uses the contents of any specified LDAP URL files to populate the set of 1023 * search URLs. 1024 * 1025 * @throws ArgumentException If a problem is encountered while constructing 1026 * the search URLs. 1027 */ 1028 private void readLDAPURLFile() 1029 throws ArgumentException 1030 { 1031 for (final File f : ldapURLFile.getValues()) 1032 { 1033 final InputStream inputStream; 1034 try 1035 { 1036 inputStream = openInputStream(f); 1037 } 1038 catch (final LDAPException e) 1039 { 1040 Debug.debugException(e); 1041 throw new ArgumentException(e.getMessage(), e); 1042 } 1043 1044 try (BufferedReader reader = 1045 new BufferedReader(new InputStreamReader(inputStream))) 1046 { 1047 while (true) 1048 { 1049 final String line = reader.readLine(); 1050 if (line == null) 1051 { 1052 break; 1053 } 1054 1055 if (line.isEmpty() || line.startsWith("#")) 1056 { 1057 continue; 1058 } 1059 1060 try 1061 { 1062 searchURLs.add(new LDAPURL(line.trim())); 1063 } 1064 catch (final LDAPException e) 1065 { 1066 Debug.debugException(e); 1067 throw new ArgumentException( 1068 ERR_LDIFSEARCH_LDAP_URL_FILE_INVALID_URL.get(line, 1069 f.getAbsolutePath(), e.getMessage()), 1070 e); 1071 } 1072 } 1073 } 1074 catch (final IOException e) 1075 { 1076 Debug.debugException(e); 1077 throw new ArgumentException( 1078 ERR_LDIFSEARCH_ERROR_READING_LDAP_URL_FILE.get(f.getAbsolutePath(), 1079 StaticUtils.getExceptionMessage(e)), 1080 e); 1081 } 1082 finally 1083 { 1084 try 1085 { 1086 inputStream.close(); 1087 } 1088 catch (final Exception e) 1089 { 1090 Debug.debugException(e); 1091 } 1092 } 1093 } 1094 1095 if (searchURLs.isEmpty()) 1096 { 1097 throw new ArgumentException(ERR_LDIFSEARCH_NO_URLS_FROM_FILE.get( 1098 ldapURLFile.getValues().get(0).getAbsolutePath())); 1099 } 1100 } 1101 1102 1103 1104 /** 1105 * {@inheritDoc} 1106 */ 1107 @Override() 1108 @NotNull() 1109 public ResultCode doToolProcessing() 1110 { 1111 // Get the schema to use when performing LDIF processing. 1112 final Schema schema; 1113 try 1114 { 1115 if (schemaPath.isPresent()) 1116 { 1117 schema = getSchema(schemaPath.getValues()); 1118 } 1119 else if (PING_SERVER_AVAILABLE) 1120 { 1121 schema = getSchema(Collections.singletonList(StaticUtils.constructPath( 1122 PING_SERVER_ROOT, "config", "schema"))); 1123 } 1124 else 1125 { 1126 schema = Schema.getDefaultStandardSchema(); 1127 } 1128 } 1129 catch (final Exception e) 1130 { 1131 Debug.debugException(e); 1132 logCompletionMessage(true, 1133 ERR_LDIFSEARCH_CANNOT_GET_SCHEMA.get( 1134 StaticUtils.getExceptionMessage(e))); 1135 return ResultCode.LOCAL_ERROR; 1136 } 1137 1138 1139 // Create search entry parers for all of the search URLs. 1140 final Map<LDAPURL,SearchEntryParer> urlMap = new LinkedHashMap<>(); 1141 for (final LDAPURL url : searchURLs) 1142 { 1143 final SearchEntryParer parer = new SearchEntryParer( 1144 Arrays.asList(url.getAttributes()), schema); 1145 urlMap.put(url, parer); 1146 } 1147 1148 1149 // If we should check schema, then create the entry validator. 1150 final EntryValidator entryValidator; 1151 if (checkSchema.isPresent()) 1152 { 1153 entryValidator = new EntryValidator(schema); 1154 } 1155 else 1156 { 1157 entryValidator = null; 1158 } 1159 1160 1161 // Create the output files, if appropriate. 1162 OutputStream outputStream = null; 1163 SearchEntryParer singleParer = null; 1164 final Map<LDAPURL,LDIFSearchSeparateSearchDetails> separateWriters = 1165 new LinkedHashMap<>(); 1166 try 1167 { 1168 if (outputFile.isPresent()) 1169 { 1170 final int numURLs = searchURLs.size(); 1171 if (separateOutputFilePerSearch.isPresent() && (numURLs > 1)) 1172 { 1173 int i=1; 1174 for (final LDAPURL url : searchURLs) 1175 { 1176 final File f = new 1177 File(outputFile.getValue().getAbsolutePath() + '.' + i); 1178 final LDIFSearchSeparateSearchDetails details = 1179 new LDIFSearchSeparateSearchDetails(url, f, 1180 createLDIFWriter(f, url), schema); 1181 separateWriters.put(url, details); 1182 i++; 1183 } 1184 } 1185 else 1186 { 1187 try 1188 { 1189 outputStream = createOutputStream(outputFile.getValue()); 1190 resultWriter.updateOutputStream(outputStream); 1191 } 1192 catch (final Exception e) 1193 { 1194 Debug.debugException(e); 1195 throw new LDAPException(ResultCode.LOCAL_ERROR, 1196 ERR_LDIFSEARCH_CANNOT_WRITE_TO_FILE.get( 1197 outputFile.getValue().getAbsolutePath(), 1198 StaticUtils.getExceptionMessage(e)), 1199 e); 1200 } 1201 } 1202 } 1203 1204 1205 // If we're not using separate writers, then write any appropriate header 1206 // to the top of the output. 1207 if (separateWriters.isEmpty()) 1208 { 1209 resultWriter.writeHeader(); 1210 } 1211 1212 1213 // Iterate through the LDIF files and process the entries they contain. 1214 boolean errorEncountered = false; 1215 final List<LDAPURL> matchingURLs = new ArrayList<>(); 1216 final List<String> entryInvalidReasons = new ArrayList<>(); 1217 for (final File f : ldifFile.getValues()) 1218 { 1219 final LDIFReader ldifReader; 1220 try 1221 { 1222 ldifReader = new LDIFReader(openInputStream(f)); 1223 1224 if (stripTrailingSpaces.isPresent()) 1225 { 1226 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP); 1227 } 1228 else 1229 { 1230 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT); 1231 } 1232 } 1233 catch (final Exception e) 1234 { 1235 Debug.debugException(e); 1236 logCompletionMessage(true, 1237 ERR_LDIFSEARCH_CANNOT_OPEN_LDIF_FILE.get(f.getName(), 1238 StaticUtils.getExceptionMessage(e))); 1239 return ResultCode.LOCAL_ERROR; 1240 } 1241 1242 try 1243 { 1244 while (true) 1245 { 1246 final Entry entry; 1247 try 1248 { 1249 entry = ldifReader.readEntry(); 1250 } 1251 catch (final LDIFException e) 1252 { 1253 Debug.debugException(e); 1254 if (e.mayContinueReading()) 1255 { 1256 commentToErr(ERR_LDIFSEARCH_RECOVERABLE_READ_ERROR.get( 1257 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e))); 1258 errorEncountered = true; 1259 continue; 1260 } 1261 else 1262 { 1263 logCompletionMessage(true, 1264 ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get( 1265 f.getAbsolutePath(), 1266 StaticUtils.getExceptionMessage(e))); 1267 return ResultCode.LOCAL_ERROR; 1268 } 1269 } 1270 catch (final Exception e) 1271 { 1272 logCompletionMessage(true, 1273 ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get( 1274 f.getAbsolutePath(), 1275 StaticUtils.getExceptionMessage(e))); 1276 return ResultCode.LOCAL_ERROR; 1277 } 1278 1279 if (entry == null) 1280 { 1281 break; 1282 } 1283 1284 if (entryValidator != null) 1285 { 1286 entryInvalidReasons.clear(); 1287 if (! entryValidator.entryIsValid(entry, entryInvalidReasons)) 1288 { 1289 commentToErr(ERR_LDIFSEARCH_ENTRY_VIOLATES_SCHEMA.get( 1290 entry.getDN())); 1291 for (final String invalidReason : entryInvalidReasons) 1292 { 1293 commentToErr("- " + invalidReason); 1294 } 1295 1296 err(); 1297 errorEncountered = true; 1298 continue; 1299 } 1300 } 1301 1302 if (separateWriters.isEmpty()) 1303 { 1304 matchingURLs.clear(); 1305 for (final LDAPURL url : searchURLs) 1306 { 1307 if (urlMatchesEntry(url, entry)) 1308 { 1309 matchingURLs.add(url); 1310 } 1311 } 1312 1313 if (matchingURLs.isEmpty()) 1314 { 1315 continue; 1316 } 1317 1318 try 1319 { 1320 if (searchURLs.size() > 1) 1321 { 1322 resultWriter.writeComment( 1323 INFO_LDIFSEARCH_ENTRY_MATCHES_URLS.get(entry.getDN())); 1324 for (final LDAPURL url : matchingURLs) 1325 { 1326 resultWriter.writeComment(url.toString()); 1327 } 1328 } 1329 1330 if (singleParer == null) 1331 { 1332 singleParer = new SearchEntryParer( 1333 Arrays.asList(searchURLs.get(0).getAttributes()), 1334 schema); 1335 } 1336 1337 resultWriter.writeSearchResultEntry( 1338 new SearchResultEntry(singleParer.pareEntry(entry))); 1339 1340 if (! outputFile.isPresent()) 1341 { 1342 resultWriter.flush(); 1343 } 1344 } 1345 catch (final Exception e) 1346 { 1347 Debug.debugException(e); 1348 if (outputFile.isPresent()) 1349 { 1350 logCompletionMessage(true, 1351 ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(), 1352 outputFile.getValue().getAbsolutePath(), 1353 StaticUtils.getExceptionMessage(e))); 1354 } 1355 else 1356 { 1357 logCompletionMessage(true, 1358 ERR_LDIFSEARCH_WRITE_ERROR_NO_FILE.get(entry.getDN(), 1359 StaticUtils.getExceptionMessage(e))); 1360 } 1361 return ResultCode.LOCAL_ERROR; 1362 } 1363 } 1364 else 1365 { 1366 for (final LDIFSearchSeparateSearchDetails details : 1367 separateWriters.values()) 1368 { 1369 final LDAPURL url = details.getLDAPURL(); 1370 if (urlMatchesEntry(url, entry)) 1371 { 1372 try 1373 { 1374 final Entry paredEntry = 1375 details.getSearchEntryParer().pareEntry(entry); 1376 details.getLDIFWriter().writeEntry(paredEntry); 1377 } 1378 catch (final Exception ex) 1379 { 1380 Debug.debugException(ex); 1381 logCompletionMessage(true, 1382 ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(), 1383 details.getOutputFile().getAbsolutePath(), 1384 StaticUtils.getExceptionMessage(ex))); 1385 return ResultCode.LOCAL_ERROR; 1386 } 1387 } 1388 } 1389 } 1390 } 1391 } 1392 finally 1393 { 1394 try 1395 { 1396 ldifReader.close(); 1397 } 1398 catch (final Exception e) 1399 { 1400 Debug.debugException(e); 1401 } 1402 } 1403 } 1404 1405 if (errorEncountered) 1406 { 1407 logCompletionMessage(true, 1408 WARN_LDIFSEARCH_COMPLETED_WITH_ERRORS.get()); 1409 return ResultCode.PARAM_ERROR; 1410 } 1411 else 1412 { 1413 logCompletionMessage(false, 1414 INFO_LDIFSEARCH_COMPLETED_SUCCESSFULLY.get()); 1415 return ResultCode.SUCCESS; 1416 } 1417 } 1418 catch (final LDAPException e) 1419 { 1420 Debug.debugException(e); 1421 logCompletionMessage(true, e.getMessage()); 1422 return e.getResultCode(); 1423 } 1424 finally 1425 { 1426 try 1427 { 1428 resultWriter.flush(); 1429 if (outputStream != null) 1430 { 1431 outputStream.close(); 1432 } 1433 } 1434 catch (final Exception e) 1435 { 1436 Debug.debugException(e); 1437 } 1438 1439 for (final LDIFSearchSeparateSearchDetails details : 1440 separateWriters.values()) 1441 { 1442 try 1443 { 1444 details.getLDIFWriter().close(); 1445 } 1446 catch (final Exception e) 1447 { 1448 Debug.debugException(e); 1449 } 1450 } 1451 } 1452 } 1453 1454 1455 1456 /** 1457 * Retrieves the schema contained in the specified paths. 1458 * 1459 * @param paths The paths to use to access the schema. 1460 * 1461 * @return The schema read from the specified files. 1462 * 1463 * @throws Exception If a problem is encountered while loading the schema. 1464 */ 1465 @NotNull() 1466 private static Schema getSchema(@NotNull final List<File> paths) 1467 throws Exception 1468 { 1469 final Set<File> schemaFiles = new LinkedHashSet<>(); 1470 for (final File f : paths) 1471 { 1472 if (f.exists()) 1473 { 1474 if (f.isFile()) 1475 { 1476 schemaFiles.add(f); 1477 } 1478 else if (f.isDirectory()) 1479 { 1480 final TreeMap<String,File> sortedFiles = new TreeMap<>(); 1481 for (final File fileInDir : f.listFiles()) 1482 { 1483 if (fileInDir.isFile()) 1484 { 1485 sortedFiles.put(fileInDir.getName(), fileInDir); 1486 } 1487 } 1488 1489 schemaFiles.addAll(sortedFiles.values()); 1490 } 1491 } 1492 } 1493 1494 return Schema.getSchema(new ArrayList<>(schemaFiles)); 1495 } 1496 1497 1498 1499 /** 1500 * Opens the input stream to use to read from the specified file. 1501 * 1502 * @param f The file for which to open the input stream. It may optionally 1503 * be compressed and/or encrypted. 1504 * 1505 * @return The input stream that was created. 1506 * 1507 * @throws LDAPException If a problem is encountered while opening the file. 1508 */ 1509 @NotNull() 1510 private InputStream openInputStream(@NotNull final File f) 1511 throws LDAPException 1512 { 1513 if (ldifEncryptionPassphraseFile.isPresent() && 1514 (! ldifEncryptionPassphraseFileRead)) 1515 { 1516 readPassphraseFile(ldifEncryptionPassphraseFile.getValue()); 1517 ldifEncryptionPassphraseFileRead = true; 1518 } 1519 1520 1521 boolean closeStream = true; 1522 InputStream inputStream = null; 1523 try 1524 { 1525 inputStream = new FileInputStream(f); 1526 1527 final ObjectPair<InputStream,char[]> p = 1528 ToolUtils.getPossiblyPassphraseEncryptedInputStream( 1529 inputStream, inputEncryptionPassphrases, 1530 (! ldifEncryptionPassphraseFile.isPresent()), 1531 INFO_LDIFSEARCH_ENTER_ENCRYPTION_PW.get(f.getName()), 1532 ERR_LDIFSEARCH_WRONG_ENCRYPTION_PW.get(), getOut(), getErr()); 1533 inputStream = p.getFirst(); 1534 addPassphrase(p.getSecond()); 1535 1536 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 1537 closeStream = false; 1538 return inputStream; 1539 } 1540 catch (final Exception e) 1541 { 1542 Debug.debugException(e); 1543 throw new LDAPException(ResultCode.LOCAL_ERROR, 1544 ERR_LDIFSEARCH_ERROR_OPENING_INPUT_FILE.get(f.getAbsolutePath(), 1545 StaticUtils.getExceptionMessage(e)), 1546 e); 1547 } 1548 finally 1549 { 1550 if ((inputStream != null) && closeStream) 1551 { 1552 try 1553 { 1554 inputStream.close(); 1555 } 1556 catch (final Exception e) 1557 { 1558 Debug.debugException(e); 1559 } 1560 } 1561 } 1562 } 1563 1564 1565 1566 /** 1567 * Reads the contents of the specified passphrase file and adds it to the list 1568 * of passphrases. 1569 * 1570 * @param f The passphrase file to read. 1571 * 1572 * @throws LDAPException If a problem is encountered while trying to read 1573 * the passphrase from the provided file. 1574 */ 1575 private void readPassphraseFile(@NotNull final File f) 1576 throws LDAPException 1577 { 1578 try 1579 { 1580 addPassphrase(getPasswordFileReader().readPassword(f)); 1581 } 1582 catch (final Exception e) 1583 { 1584 Debug.debugException(e); 1585 throw new LDAPException(ResultCode.LOCAL_ERROR, 1586 ERR_LDIFSEARCH_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(), 1587 StaticUtils.getExceptionMessage(e)), 1588 e); 1589 } 1590 } 1591 1592 1593 1594 /** 1595 * Updates the list of encryption passphrases with the provided passphrase, if 1596 * it is not already present. 1597 * 1598 * @param passphrase The passphrase to be added. It may optionally be 1599 * {@code null} (in which case no action will be taken). 1600 */ 1601 private void addPassphrase(@Nullable final char[] passphrase) 1602 { 1603 if (passphrase == null) 1604 { 1605 return; 1606 } 1607 1608 for (final char[] existingPassphrase : inputEncryptionPassphrases) 1609 { 1610 if (Arrays.equals(existingPassphrase, passphrase)) 1611 { 1612 return; 1613 } 1614 } 1615 1616 inputEncryptionPassphrases.add(passphrase); 1617 } 1618 1619 1620 1621 /** 1622 * Creates an output stream that may be used to write to the specified file. 1623 * 1624 * @param f The file to be written. 1625 * 1626 * @return The output stream that was created. 1627 * 1628 * @throws LDAPException If a problem occurs while creating the output 1629 * stream. 1630 */ 1631 @NotNull() 1632 private OutputStream createOutputStream(@NotNull final File f) 1633 throws LDAPException 1634 { 1635 OutputStream outputStream = null; 1636 boolean closeOutputStream = true; 1637 try 1638 { 1639 try 1640 { 1641 1642 outputStream = new FileOutputStream(f, 1643 (! overwriteExistingOutputFile.isPresent())); 1644 } 1645 catch (final Exception e) 1646 { 1647 Debug.debugException(e); 1648 throw new LDAPException(ResultCode.LOCAL_ERROR, 1649 ERR_LDIFSEARCH_CANNOT_OPEN_OUTPUT_FILE.get(f.getAbsolutePath(), 1650 StaticUtils.getExceptionMessage(e)), 1651 e); 1652 } 1653 1654 if (encryptOutput.isPresent()) 1655 { 1656 try 1657 { 1658 final char[] passphrase; 1659 if (outputEncryptionPassphraseFile.isPresent()) 1660 { 1661 passphrase = getPasswordFileReader().readPassword( 1662 outputEncryptionPassphraseFile.getValue()); 1663 } 1664 else 1665 { 1666 passphrase = ToolUtils.promptForEncryptionPassphrase(false, true, 1667 INFO_LDIFSEARCH_PROMPT_OUTPUT_FILE_ENC_PW.get(), 1668 INFO_LDIFSEARCH_CONFIRM_OUTPUT_FILE_ENC_PW.get(), getOut(), 1669 getErr()).toCharArray(); 1670 } 1671 1672 outputStream = new PassphraseEncryptedOutputStream(passphrase, 1673 outputStream, null, true, true); 1674 } 1675 catch (final Exception e) 1676 { 1677 Debug.debugException(e); 1678 throw new LDAPException(ResultCode.LOCAL_ERROR, 1679 ERR_LDIFSEARCH_CANNOT_ENCRYPT_OUTPUT_FILE.get( 1680 StaticUtils.getExceptionMessage(e)), 1681 e); 1682 } 1683 } 1684 1685 if (compressOutput.isPresent()) 1686 { 1687 try 1688 { 1689 outputStream = new GZIPOutputStream(outputStream); 1690 } 1691 catch (final Exception e) 1692 { 1693 Debug.debugException(e); 1694 throw new LDAPException(ResultCode.LOCAL_ERROR, 1695 ERR_LDIFSEARCH_CANNOT_COMPRESS_OUTPUT_FILE.get( 1696 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)), 1697 e); 1698 } 1699 } 1700 1701 closeOutputStream = false; 1702 return outputStream; 1703 } 1704 finally 1705 { 1706 if (closeOutputStream && (outputStream != null)) 1707 { 1708 try 1709 { 1710 outputStream.close(); 1711 } 1712 catch (final Exception e) 1713 { 1714 Debug.debugException(e); 1715 } 1716 } 1717 } 1718 } 1719 1720 1721 1722 /** 1723 * Creates an LDIF writer to write to the specified file. 1724 * 1725 * @param f The file to be written. 1726 * @param ldapURL The LDAP URL with which the file will be associated. It 1727 * may be {@code null} if the file is shared across multiple 1728 * URLs. 1729 * 1730 * @return The LDIF writer that was created. 1731 * 1732 * @throws LDAPException If a problem occurs while creating the LDIF writer. 1733 */ 1734 @NotNull() 1735 private LDIFWriter createLDIFWriter(@NotNull final File f, 1736 @Nullable final LDAPURL ldapURL) 1737 throws LDAPException 1738 { 1739 boolean closeOutputStream = true; 1740 final OutputStream outputStream = createOutputStream(f); 1741 try 1742 { 1743 final LDIFWriter ldifWriter = new LDIFWriter(outputStream); 1744 if (doNotWrap.isPresent()) 1745 { 1746 ldifWriter.setWrapColumn(0); 1747 } 1748 else if (wrapColumn.isPresent()) 1749 { 1750 ldifWriter.setWrapColumn(wrapColumn.getValue()); 1751 } 1752 else 1753 { 1754 ldifWriter.setWrapColumn(WRAP_COLUMN); 1755 } 1756 1757 if (ldapURL != null) 1758 { 1759 try 1760 { 1761 ldifWriter.writeComment( 1762 INFO_LDIFSEARCH_ENTRIES_MATCHING_URL.get(ldapURL.toString()), 1763 false, true); 1764 } 1765 catch (final Exception e) 1766 { 1767 Debug.debugException(e); 1768 } 1769 } 1770 1771 closeOutputStream = false; 1772 return ldifWriter; 1773 } 1774 finally 1775 { 1776 if (closeOutputStream) 1777 { 1778 try 1779 { 1780 outputStream.close(); 1781 } 1782 catch (final Exception e) 1783 { 1784 Debug.debugException(e); 1785 } 1786 } 1787 } 1788 } 1789 1790 1791 1792 /** 1793 * Indicates whether the given entry matches the criteria in the provided LDAP 1794 * URL. 1795 * 1796 * @param url The URL for which to make the determination. 1797 * @param entry The entry for which to make the determination. 1798 * 1799 * @return {@code true} if the entry matches the criteria in the LDAP URL, or 1800 * {@code false} if not. 1801 */ 1802 private boolean urlMatchesEntry(@NotNull final LDAPURL url, 1803 @NotNull final Entry entry) 1804 { 1805 try 1806 { 1807 return (entry.matchesBaseAndScope(url.getBaseDN(), url.getScope()) && 1808 url.getFilter().matchesEntry(entry)); 1809 } 1810 catch (final Exception e) 1811 { 1812 Debug.debugException(e); 1813 return false; 1814 } 1815 } 1816 1817 1818 1819 /** 1820 * Writes a line-wrapped, commented version of the provided message to 1821 * standard output. 1822 * 1823 * @param message The message to be written. 1824 */ 1825 private void commentToOut(@NotNull final String message) 1826 { 1827 getOut().flush(); 1828 for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2))) 1829 { 1830 out("# " + line); 1831 } 1832 getOut().flush(); 1833 } 1834 1835 1836 1837 /** 1838 * Writes a line-wrapped, commented version of the provided message to 1839 * standard error. 1840 * 1841 * @param message The message to be written. 1842 */ 1843 private void commentToErr(@NotNull final String message) 1844 { 1845 getErr().flush(); 1846 for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2))) 1847 { 1848 err("# " + line); 1849 } 1850 getErr().flush(); 1851 } 1852 1853 1854 1855 /** 1856 * Writes the provided message and sets it as the completion message. 1857 * 1858 * @param isError Indicates whether the message should be written to 1859 * standard error rather than standard output. 1860 * @param message The message to be written. 1861 */ 1862 private void logCompletionMessage(final boolean isError, 1863 @NotNull final String message) 1864 { 1865 completionMessage.compareAndSet(null, message); 1866 1867 if (! outputFile.isPresent()) 1868 { 1869 resultWriter.writeComment(message); 1870 } 1871 } 1872 1873 1874 1875 /** 1876 * {@inheritDoc} 1877 */ 1878 @Override() 1879 @NotNull() 1880 public LinkedHashMap<String[],String> getExampleUsages() 1881 { 1882 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 1883 1884 examples.put( 1885 new String[] 1886 { 1887 "--ldifFile", "data.ldif", 1888 "(uid=jdoe)" 1889 }, 1890 INFO_LDIFSEARCH_EXAMPLE_1.get()); 1891 1892 examples.put( 1893 new String[] 1894 { 1895 "--ldifFile", "data.ldif", 1896 "--outputFile", "people.ldif", 1897 "--baseDN", "dc=example,dc=com", 1898 "--scope", "sub", 1899 "(objectClass=person)", 1900 "givenName", 1901 "sn", 1902 "cn", 1903 }, 1904 INFO_LDIFSEARCH_EXAMPLE_2.get()); 1905 1906 return examples; 1907 } 1908}