001/* 002 * Copyright 2008-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2008-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) 2008-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.ldap.sdk.examples; 037 038 039 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.InputStream; 043import java.io.IOException; 044import java.io.OutputStream; 045import java.util.ArrayList; 046import java.util.Iterator; 047import java.util.TreeMap; 048import java.util.LinkedHashMap; 049import java.util.List; 050import java.util.concurrent.atomic.AtomicLong; 051import java.util.zip.GZIPInputStream; 052 053import com.unboundid.ldap.sdk.Entry; 054import com.unboundid.ldap.sdk.LDAPConnection; 055import com.unboundid.ldap.sdk.LDAPException; 056import com.unboundid.ldap.sdk.ResultCode; 057import com.unboundid.ldap.sdk.Version; 058import com.unboundid.ldap.sdk.schema.Schema; 059import com.unboundid.ldap.sdk.schema.EntryValidator; 060import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils; 061import com.unboundid.ldif.DuplicateValueBehavior; 062import com.unboundid.ldif.LDIFException; 063import com.unboundid.ldif.LDIFReader; 064import com.unboundid.ldif.LDIFReaderEntryTranslator; 065import com.unboundid.ldif.LDIFWriter; 066import com.unboundid.util.Debug; 067import com.unboundid.util.LDAPCommandLineTool; 068import com.unboundid.util.NotNull; 069import com.unboundid.util.Nullable; 070import com.unboundid.util.StaticUtils; 071import com.unboundid.util.ThreadSafety; 072import com.unboundid.util.ThreadSafetyLevel; 073import com.unboundid.util.args.ArgumentException; 074import com.unboundid.util.args.ArgumentParser; 075import com.unboundid.util.args.BooleanArgument; 076import com.unboundid.util.args.FileArgument; 077import com.unboundid.util.args.IntegerArgument; 078import com.unboundid.util.args.StringArgument; 079 080 081 082/** 083 * This class provides a simple tool that can be used to validate that the 084 * contents of an LDIF file are valid. This includes ensuring that the contents 085 * can be parsed as valid LDIF, and it can also ensure that the LDIF content 086 * conforms to the server schema. It will obtain the schema by connecting to 087 * the server and retrieving the default schema (i.e., the schema which governs 088 * the root DSE). By default, a thorough set of validation will be performed, 089 * but it is possible to disable certain types of validation. 090 * <BR><BR> 091 * Some of the APIs demonstrated by this example include: 092 * <UL> 093 * <LI>Argument Parsing (from the {@code com.unboundid.util.args} 094 * package)</LI> 095 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util} 096 * package)</LI> 097 * <LI>LDIF Processing (from the {@code com.unboundid.ldif} package)</LI> 098 * <LI>Schema Parsing (from the {@code com.unboundid.ldap.sdk.schema} 099 * package)</LI> 100 * </UL> 101 * <BR><BR> 102 * Supported arguments include those allowed by the {@link LDAPCommandLineTool} 103 * class (to obtain the information to use to connect to the server to read the 104 * schema), as well as the following additional arguments: 105 * <UL> 106 * <LI>"--schemaDirectory {path}" -- specifies the path to a directory 107 * containing files with schema definitions. If this argument is 108 * provided, then no attempt will be made to communicate with a directory 109 * server.</LI> 110 * <LI>"-f {path}" or "--ldifFile {path}" -- specifies the path to the LDIF 111 * file to be validated.</LI> 112 * <LI>"-c" or "--isCompressed" -- indicates that the LDIF file is 113 * compressed.</LI> 114 * <LI>"-R {path}" or "--rejectFile {path}" -- specifies the path to the file 115 * to be written with information about all entries that failed 116 * validation.</LI> 117 * <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of 118 * concurrent threads to use when processing the LDIF. If this is not 119 * provided, then a default of one thread will be used.</LI> 120 * <LI>"--ignoreUndefinedObjectClasses" -- indicates that the validation 121 * process should ignore validation failures due to entries that contain 122 * object classes not defined in the server schema.</LI> 123 * <LI>"--ignoreUndefinedAttributes" -- indicates that the validation process 124 * should ignore validation failures due to entries that contain 125 * attributes not defined in the server schema.</LI> 126 * <LI>"--ignoreMalformedDNs" -- indicates that the validation process should 127 * ignore validation failures due to entries with malformed DNs.</LI> 128 * <LI>"--ignoreMissingRDNValues" -- indicates that the validation process 129 * should ignore validation failures due to entries that contain an RDN 130 * attribute value that is not present in the set of entry 131 * attributes.</LI> 132 * <LI>"--ignoreStructuralObjectClasses" -- indicates that the validation 133 * process should ignore validation failures due to entries that either do 134 * not have a structural object class or that have multiple structural 135 * object classes.</LI> 136 * <LI>"--ignoreProhibitedObjectClasses" -- indicates that the validation 137 * process should ignore validation failures due to entries containing 138 * auxiliary classes that are not allowed by a DIT content rule, or 139 * abstract classes that are not subclassed by an auxiliary or structural 140 * class contained in the entry.</LI> 141 * <LI>"--ignoreProhibitedAttributes" -- indicates that the validation process 142 * should ignore validation failures due to entries including attributes 143 * that are not allowed or are explicitly prohibited by a DIT content 144 * rule.</LI> 145 * <LI>"--ignoreMissingAttributes" -- indicates that the validation process 146 * should ignore validation failures due to entries missing required 147 * attributes.</LI> 148 * <LI>"--ignoreSingleValuedAttributes" -- indicates that the validation 149 * process should ignore validation failures due to single-valued 150 * attributes containing multiple values.</LI> 151 * <LI>"--ignoreAttributeSyntax" -- indicates that the validation process 152 * should ignore validation failures due to attribute values which violate 153 * the associated attribute syntax.</LI> 154 * <LI>"--ignoreSyntaxViolationsForAttribute" -- indicates that the validation 155 * process should ignore validation failures due to attribute values which 156 * violate the associated attribute syntax, but only for the specified 157 * attribute types.</LI> 158 * <LI>"--ignoreNameForms" -- indicates that the validation process should 159 * ignore validation failures due to name form violations (in which the 160 * entry's RDN does not comply with the associated name form).</LI> 161 * </UL> 162 */ 163@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 164public final class ValidateLDIF 165 extends LDAPCommandLineTool 166 implements LDIFReaderEntryTranslator 167{ 168 /** 169 * The end-of-line character for this platform. 170 */ 171 @NotNull private static final String EOL = 172 StaticUtils.getSystemProperty("line.separator", "\n"); 173 174 175 176 // The arguments used by this program. 177 @Nullable private BooleanArgument ignoreDuplicateValues; 178 @Nullable private BooleanArgument ignoreUndefinedObjectClasses; 179 @Nullable private BooleanArgument ignoreUndefinedAttributes; 180 @Nullable private BooleanArgument ignoreMalformedDNs; 181 @Nullable private BooleanArgument ignoreMissingRDNValues; 182 @Nullable private BooleanArgument ignoreMissingSuperiorObjectClasses; 183 @Nullable private BooleanArgument ignoreStructuralObjectClasses; 184 @Nullable private BooleanArgument ignoreProhibitedObjectClasses; 185 @Nullable private BooleanArgument ignoreProhibitedAttributes; 186 @Nullable private BooleanArgument ignoreMissingAttributes; 187 @Nullable private BooleanArgument ignoreSingleValuedAttributes; 188 @Nullable private BooleanArgument ignoreAttributeSyntax; 189 @Nullable private BooleanArgument ignoreNameForms; 190 @Nullable private BooleanArgument isCompressed; 191 @Nullable private FileArgument schemaDirectory; 192 @Nullable private FileArgument ldifFile; 193 @Nullable private FileArgument rejectFile; 194 @Nullable private FileArgument encryptionPassphraseFile; 195 @Nullable private IntegerArgument numThreads; 196 @Nullable private StringArgument ignoreSyntaxViolationsForAttribute; 197 198 // The counter used to keep track of the number of entries processed. 199 @NotNull private final AtomicLong entriesProcessed = new AtomicLong(0L); 200 201 // The counter used to keep track of the number of entries that could not be 202 // parsed as valid entries. 203 @NotNull private final AtomicLong malformedEntries = new AtomicLong(0L); 204 205 // The entry validator that will be used to validate the entries. 206 @Nullable private EntryValidator entryValidator; 207 208 // The LDIF writer that will be used to write rejected entries. 209 @Nullable private LDIFWriter rejectWriter; 210 211 212 213 /** 214 * Parse the provided command line arguments and make the appropriate set of 215 * changes. 216 * 217 * @param args The command line arguments provided to this program. 218 */ 219 public static void main(@NotNull final String[] args) 220 { 221 final ResultCode resultCode = main(args, System.out, System.err); 222 if (resultCode != ResultCode.SUCCESS) 223 { 224 System.exit(resultCode.intValue()); 225 } 226 } 227 228 229 230 /** 231 * Parse the provided command line arguments and make the appropriate set of 232 * changes. 233 * 234 * @param args The command line arguments provided to this program. 235 * @param outStream The output stream to which standard out should be 236 * written. It may be {@code null} if output should be 237 * suppressed. 238 * @param errStream The output stream to which standard error should be 239 * written. It may be {@code null} if error messages 240 * should be suppressed. 241 * 242 * @return A result code indicating whether the processing was successful. 243 */ 244 @NotNull() 245 public static ResultCode main(@NotNull final String[] args, 246 @Nullable final OutputStream outStream, 247 @Nullable final OutputStream errStream) 248 { 249 final ValidateLDIF validateLDIF = new ValidateLDIF(outStream, errStream); 250 return validateLDIF.runTool(args); 251 } 252 253 254 255 /** 256 * Creates a new instance of this tool. 257 * 258 * @param outStream The output stream to which standard out should be 259 * written. It may be {@code null} if output should be 260 * suppressed. 261 * @param errStream The output stream to which standard error should be 262 * written. It may be {@code null} if error messages 263 * should be suppressed. 264 */ 265 public ValidateLDIF(@Nullable final OutputStream outStream, 266 @Nullable final OutputStream errStream) 267 { 268 super(outStream, errStream); 269 } 270 271 272 273 /** 274 * Retrieves the name for this tool. 275 * 276 * @return The name for this tool. 277 */ 278 @Override() 279 @NotNull() 280 public String getToolName() 281 { 282 return "validate-ldif"; 283 } 284 285 286 287 /** 288 * Retrieves the description for this tool. 289 * 290 * @return The description for this tool. 291 */ 292 @Override() 293 @NotNull() 294 public String getToolDescription() 295 { 296 return "Validate the contents of an LDIF file " + 297 "against the server schema."; 298 } 299 300 301 302 /** 303 * Retrieves the version string for this tool. 304 * 305 * @return The version string for this tool. 306 */ 307 @Override() 308 @NotNull() 309 public String getToolVersion() 310 { 311 return Version.NUMERIC_VERSION_STRING; 312 } 313 314 315 316 /** 317 * Indicates whether this tool should provide support for an interactive mode, 318 * in which the tool offers a mode in which the arguments can be provided in 319 * a text-driven menu rather than requiring them to be given on the command 320 * line. If interactive mode is supported, it may be invoked using the 321 * "--interactive" argument. Alternately, if interactive mode is supported 322 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 323 * interactive mode may be invoked by simply launching the tool without any 324 * arguments. 325 * 326 * @return {@code true} if this tool supports interactive mode, or 327 * {@code false} if not. 328 */ 329 @Override() 330 public boolean supportsInteractiveMode() 331 { 332 return true; 333 } 334 335 336 337 /** 338 * Indicates whether this tool defaults to launching in interactive mode if 339 * the tool is invoked without any command-line arguments. This will only be 340 * used if {@link #supportsInteractiveMode()} returns {@code true}. 341 * 342 * @return {@code true} if this tool defaults to using interactive mode if 343 * launched without any command-line arguments, or {@code false} if 344 * not. 345 */ 346 @Override() 347 public boolean defaultsToInteractiveMode() 348 { 349 return true; 350 } 351 352 353 354 /** 355 * Indicates whether this tool should provide arguments for redirecting output 356 * to a file. If this method returns {@code true}, then the tool will offer 357 * an "--outputFile" argument that will specify the path to a file to which 358 * all standard output and standard error content will be written, and it will 359 * also offer a "--teeToStandardOut" argument that can only be used if the 360 * "--outputFile" argument is present and will cause all output to be written 361 * to both the specified output file and to standard output. 362 * 363 * @return {@code true} if this tool should provide arguments for redirecting 364 * output to a file, or {@code false} if not. 365 */ 366 @Override() 367 protected boolean supportsOutputFile() 368 { 369 return true; 370 } 371 372 373 374 /** 375 * Indicates whether this tool should default to interactively prompting for 376 * the bind password if a password is required but no argument was provided 377 * to indicate how to get the password. 378 * 379 * @return {@code true} if this tool should default to interactively 380 * prompting for the bind password, or {@code false} if not. 381 */ 382 @Override() 383 protected boolean defaultToPromptForBindPassword() 384 { 385 return true; 386 } 387 388 389 390 /** 391 * Indicates whether this tool supports the use of a properties file for 392 * specifying default values for arguments that aren't specified on the 393 * command line. 394 * 395 * @return {@code true} if this tool supports the use of a properties file 396 * for specifying default values for arguments that aren't specified 397 * on the command line, or {@code false} if not. 398 */ 399 @Override() 400 public boolean supportsPropertiesFile() 401 { 402 return true; 403 } 404 405 406 407 /** 408 * Indicates whether this tool supports the ability to generate a debug log 409 * file. If this method returns {@code true}, then the tool will expose 410 * additional arguments that can control debug logging. 411 * 412 * @return {@code true} if this tool supports the ability to generate a debug 413 * log file, or {@code false} if not. 414 */ 415 @Override() 416 protected boolean supportsDebugLogging() 417 { 418 return true; 419 } 420 421 422 423 /** 424 * Indicates whether the LDAP-specific arguments should include alternate 425 * versions of all long identifiers that consist of multiple words so that 426 * they are available in both camelCase and dash-separated versions. 427 * 428 * @return {@code true} if this tool should provide multiple versions of 429 * long identifiers for LDAP-specific arguments, or {@code false} if 430 * not. 431 */ 432 @Override() 433 protected boolean includeAlternateLongIdentifiers() 434 { 435 return true; 436 } 437 438 439 440 /** 441 * Indicates whether this tool should provide a command-line argument that 442 * allows for low-level SSL debugging. If this returns {@code true}, then an 443 * "--enableSSLDebugging}" argument will be added that sets the 444 * "javax.net.debug" system property to "all" before attempting any 445 * communication. 446 * 447 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 448 * argument, or {@code false} if not. 449 */ 450 @Override() 451 protected boolean supportsSSLDebugging() 452 { 453 return true; 454 } 455 456 457 458 /** 459 * Adds the arguments used by this program that aren't already provided by the 460 * generic {@code LDAPCommandLineTool} framework. 461 * 462 * @param parser The argument parser to which the arguments should be added. 463 * 464 * @throws ArgumentException If a problem occurs while adding the arguments. 465 */ 466 @Override() 467 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 468 throws ArgumentException 469 { 470 String description = "The path to the LDIF file to process. The tool " + 471 "will automatically attempt to detect whether the file is " + 472 "encrypted or compressed."; 473 ldifFile = new FileArgument('f', "ldifFile", true, 1, "{path}", description, 474 true, true, true, false); 475 ldifFile.addLongIdentifier("ldif-file", true); 476 parser.addArgument(ldifFile); 477 478 479 // Add an argument that makes it possible to read a compressed LDIF file. 480 // Note that this argument is no longer needed for dealing with compressed 481 // files, since the tool will automatically detect whether a file is 482 // compressed. However, the argument is still provided for the purpose of 483 // backward compatibility. 484 description = "Indicates that the specified LDIF file is compressed " + 485 "using gzip compression."; 486 isCompressed = new BooleanArgument('c', "isCompressed", description); 487 isCompressed.addLongIdentifier("is-compressed", true); 488 isCompressed.setHidden(true); 489 parser.addArgument(isCompressed); 490 491 492 // Add an argument that indicates that the tool should read the encryption 493 // passphrase from a file. 494 description = "Indicates that the specified LDIF file is encrypted and " + 495 "that the encryption passphrase is contained in the specified " + 496 "file. If the LDIF data is encrypted and this argument is not " + 497 "provided, then the tool will interactively prompt for the " + 498 "encryption passphrase."; 499 encryptionPassphraseFile = new FileArgument(null, 500 "encryptionPassphraseFile", false, 1, null, description, true, true, 501 true, false); 502 encryptionPassphraseFile.addLongIdentifier("encryption-passphrase-file", 503 true); 504 encryptionPassphraseFile.addLongIdentifier("encryptionPasswordFile", true); 505 encryptionPassphraseFile.addLongIdentifier("encryption-password-file", 506 true); 507 parser.addArgument(encryptionPassphraseFile); 508 509 510 description = "The path to the file to which rejected entries should be " + 511 "written."; 512 rejectFile = new FileArgument('R', "rejectFile", false, 1, "{path}", 513 description, false, true, true, false); 514 rejectFile.addLongIdentifier("reject-file", true); 515 parser.addArgument(rejectFile); 516 517 description = "The path to a directory containing one or more LDIF files " + 518 "with the schema information to use. If this is provided, " + 519 "then no LDAP communication will be performed."; 520 schemaDirectory = new FileArgument(null, "schemaDirectory", false, 1, 521 "{path}", description, true, true, false, true); 522 schemaDirectory.addLongIdentifier("schema-directory", true); 523 parser.addArgument(schemaDirectory); 524 525 description = "The number of threads to use when processing the LDIF file."; 526 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", 527 description, 1, Integer.MAX_VALUE, 1); 528 numThreads.addLongIdentifier("num-threads", true); 529 parser.addArgument(numThreads); 530 531 description = "Ignore validation failures due to entries containing " + 532 "duplicate values for the same attribute."; 533 ignoreDuplicateValues = 534 new BooleanArgument(null, "ignoreDuplicateValues", description); 535 ignoreDuplicateValues.setArgumentGroupName( 536 "Validation Strictness Arguments"); 537 ignoreDuplicateValues.addLongIdentifier("ignore-duplicate-values", true); 538 parser.addArgument(ignoreDuplicateValues); 539 540 description = "Ignore validation failures due to object classes not " + 541 "defined in the schema."; 542 ignoreUndefinedObjectClasses = 543 new BooleanArgument(null, "ignoreUndefinedObjectClasses", description); 544 ignoreUndefinedObjectClasses.setArgumentGroupName( 545 "Validation Strictness Arguments"); 546 ignoreUndefinedObjectClasses.addLongIdentifier( 547 "ignore-undefined-object-classes", true); 548 parser.addArgument(ignoreUndefinedObjectClasses); 549 550 description = "Ignore validation failures due to attributes not defined " + 551 "in the schema."; 552 ignoreUndefinedAttributes = 553 new BooleanArgument(null, "ignoreUndefinedAttributes", description); 554 ignoreUndefinedAttributes.setArgumentGroupName( 555 "Validation Strictness Arguments"); 556 ignoreUndefinedAttributes.addLongIdentifier("ignore-undefined-attributes", 557 true); 558 parser.addArgument(ignoreUndefinedAttributes); 559 560 description = "Ignore validation failures due to entries with malformed " + 561 "DNs."; 562 ignoreMalformedDNs = 563 new BooleanArgument(null, "ignoreMalformedDNs", description); 564 ignoreMalformedDNs.setArgumentGroupName("Validation Strictness Arguments"); 565 ignoreMalformedDNs.addLongIdentifier("ignore-malformed-dns", true); 566 parser.addArgument(ignoreMalformedDNs); 567 568 description = "Ignore validation failures due to entries with RDN " + 569 "attribute values that are missing from the set of entry " + 570 "attributes."; 571 ignoreMissingRDNValues = 572 new BooleanArgument(null, "ignoreMissingRDNValues", description); 573 ignoreMissingRDNValues.setArgumentGroupName( 574 "Validation Strictness Arguments"); 575 ignoreMissingRDNValues.addLongIdentifier("ignore-missing-rdn-values", true); 576 parser.addArgument(ignoreMissingRDNValues); 577 578 description = "Ignore validation failures due to entries without exactly " + 579 "structural object class."; 580 ignoreStructuralObjectClasses = 581 new BooleanArgument(null, "ignoreStructuralObjectClasses", 582 description); 583 ignoreStructuralObjectClasses.setArgumentGroupName( 584 "Validation Strictness Arguments"); 585 ignoreStructuralObjectClasses.addLongIdentifier( 586 "ignore-structural-object-classes", true); 587 parser.addArgument(ignoreStructuralObjectClasses); 588 589 description = "Ignore validation failures due to entries with object " + 590 "classes that are not allowed."; 591 ignoreProhibitedObjectClasses = 592 new BooleanArgument(null, "ignoreProhibitedObjectClasses", 593 description); 594 ignoreProhibitedObjectClasses.setArgumentGroupName( 595 "Validation Strictness Arguments"); 596 ignoreProhibitedObjectClasses.addLongIdentifier( 597 "ignore-prohibited-object-classes", true); 598 parser.addArgument(ignoreProhibitedObjectClasses); 599 600 description = "Ignore validation failures due to entries that are " + 601 "one or more superior object classes."; 602 ignoreMissingSuperiorObjectClasses = 603 new BooleanArgument(null, "ignoreMissingSuperiorObjectClasses", 604 description); 605 ignoreMissingSuperiorObjectClasses.setArgumentGroupName( 606 "Validation Strictness Arguments"); 607 ignoreMissingSuperiorObjectClasses.addLongIdentifier( 608 "ignore-missing-superior-object-classes", true); 609 parser.addArgument(ignoreMissingSuperiorObjectClasses); 610 611 description = "Ignore validation failures due to entries with attributes " + 612 "that are not allowed."; 613 ignoreProhibitedAttributes = 614 new BooleanArgument(null, "ignoreProhibitedAttributes", description); 615 ignoreProhibitedAttributes.setArgumentGroupName( 616 "Validation Strictness Arguments"); 617 ignoreProhibitedAttributes.addLongIdentifier( 618 "ignore-prohibited-attributes", true); 619 parser.addArgument(ignoreProhibitedAttributes); 620 621 description = "Ignore validation failures due to entries missing " + 622 "required attributes."; 623 ignoreMissingAttributes = 624 new BooleanArgument(null, "ignoreMissingAttributes", description); 625 ignoreMissingAttributes.setArgumentGroupName( 626 "Validation Strictness Arguments"); 627 ignoreMissingAttributes.addLongIdentifier("ignore-missing-attributes", 628 true); 629 parser.addArgument(ignoreMissingAttributes); 630 631 description = "Ignore validation failures due to entries with multiple " + 632 "values for single-valued attributes."; 633 ignoreSingleValuedAttributes = 634 new BooleanArgument(null, "ignoreSingleValuedAttributes", description); 635 ignoreSingleValuedAttributes.setArgumentGroupName( 636 "Validation Strictness Arguments"); 637 ignoreSingleValuedAttributes.addLongIdentifier( 638 "ignore-single-valued-attributes", true); 639 parser.addArgument(ignoreSingleValuedAttributes); 640 641 description = "Ignore validation failures due to entries with attribute " + 642 "values that violate their associated syntax. If this is " + 643 "provided, then no attribute syntax violations will be " + 644 "flagged. If this is not provided, then all attribute " + 645 "syntax violations will be flagged except for violations " + 646 "in those attributes excluded by the " + 647 "--ignoreSyntaxViolationsForAttribute argument."; 648 ignoreAttributeSyntax = 649 new BooleanArgument(null, "ignoreAttributeSyntax", description); 650 ignoreAttributeSyntax.setArgumentGroupName( 651 "Validation Strictness Arguments"); 652 ignoreAttributeSyntax.addLongIdentifier("ignore-attribute-syntax", true); 653 parser.addArgument(ignoreAttributeSyntax); 654 655 description = "The name or OID of an attribute for which to ignore " + 656 "validation failures due to violations of the associated " + 657 "attribute syntax. This argument can only be used if the " + 658 "--ignoreAttributeSyntax argument is not provided."; 659 ignoreSyntaxViolationsForAttribute = new StringArgument(null, 660 "ignoreSyntaxViolationsForAttribute", false, 0, "{attr}", description); 661 ignoreSyntaxViolationsForAttribute.setArgumentGroupName( 662 "Validation Strictness Arguments"); 663 ignoreSyntaxViolationsForAttribute.addLongIdentifier( 664 "ignore-syntax-violations-for-attribute", true); 665 parser.addArgument(ignoreSyntaxViolationsForAttribute); 666 667 description = "Ignore validation failures due to entries with RDNs " + 668 "that violate the associated name form definition."; 669 ignoreNameForms = new BooleanArgument(null, "ignoreNameForms", description); 670 ignoreNameForms.setArgumentGroupName("Validation Strictness Arguments"); 671 ignoreNameForms.addLongIdentifier("ignore-name-forms", true); 672 parser.addArgument(ignoreNameForms); 673 674 675 // The ignoreAttributeSyntax and ignoreAttributeSyntaxForAttribute arguments 676 // cannot be used together. 677 parser.addExclusiveArgumentSet(ignoreAttributeSyntax, 678 ignoreSyntaxViolationsForAttribute); 679 } 680 681 682 683 /** 684 * Performs the actual processing for this tool. In this case, it gets a 685 * connection to the directory server and uses it to retrieve the server 686 * schema. It then reads the LDIF file and validates each entry accordingly. 687 * 688 * @return The result code for the processing that was performed. 689 */ 690 @Override() 691 @NotNull() 692 public ResultCode doToolProcessing() 693 { 694 // Get the connection to the directory server and use it to read the schema. 695 final Schema schema; 696 if (schemaDirectory.isPresent()) 697 { 698 final File schemaDir = schemaDirectory.getValue(); 699 700 try 701 { 702 final TreeMap<String,File> fileMap = new TreeMap<>(); 703 for (final File f : schemaDir.listFiles()) 704 { 705 final String name = f.getName(); 706 if (f.isFile() && name.endsWith(".ldif")) 707 { 708 fileMap.put(name, f); 709 } 710 } 711 712 if (fileMap.isEmpty()) 713 { 714 err("No LDIF files found in directory " + 715 schemaDir.getAbsolutePath()); 716 return ResultCode.PARAM_ERROR; 717 } 718 719 final ArrayList<File> fileList = new ArrayList<>(fileMap.values()); 720 schema = Schema.getSchema(fileList); 721 } 722 catch (final Exception e) 723 { 724 Debug.debugException(e); 725 err("Unable to read schema from files in directory " + 726 schemaDir.getAbsolutePath() + ": " + 727 StaticUtils.getExceptionMessage(e)); 728 return ResultCode.LOCAL_ERROR; 729 } 730 } 731 else 732 { 733 try 734 { 735 final LDAPConnection connection = getConnection(); 736 schema = connection.getSchema(); 737 connection.close(); 738 } 739 catch (final LDAPException le) 740 { 741 Debug.debugException(le); 742 err("Unable to connect to the directory server and read the schema: ", 743 le.getMessage()); 744 return le.getResultCode(); 745 } 746 } 747 748 749 // Get the encryption passphrase, if it was provided. 750 String encryptionPassphrase = null; 751 if (encryptionPassphraseFile.isPresent()) 752 { 753 try 754 { 755 encryptionPassphrase = ToolUtils.readEncryptionPassphraseFromFile( 756 encryptionPassphraseFile.getValue()); 757 } 758 catch (final LDAPException e) 759 { 760 Debug.debugException(e); 761 err(e.getMessage()); 762 return e.getResultCode(); 763 } 764 } 765 766 767 // Create the entry validator and initialize its configuration. 768 entryValidator = new EntryValidator(schema); 769 entryValidator.setCheckAttributeSyntax(!ignoreAttributeSyntax.isPresent()); 770 entryValidator.setCheckMalformedDNs(!ignoreMalformedDNs.isPresent()); 771 entryValidator.setCheckEntryMissingRDNValues( 772 !ignoreMissingRDNValues.isPresent()); 773 entryValidator.setCheckMissingAttributes( 774 !ignoreMissingAttributes.isPresent()); 775 entryValidator.setCheckNameForms(!ignoreNameForms.isPresent()); 776 entryValidator.setCheckProhibitedAttributes( 777 !ignoreProhibitedAttributes.isPresent()); 778 entryValidator.setCheckProhibitedObjectClasses( 779 !ignoreProhibitedObjectClasses.isPresent()); 780 entryValidator.setCheckMissingSuperiorObjectClasses( 781 !ignoreMissingSuperiorObjectClasses.isPresent()); 782 entryValidator.setCheckSingleValuedAttributes( 783 !ignoreSingleValuedAttributes.isPresent()); 784 entryValidator.setCheckStructuralObjectClasses( 785 !ignoreStructuralObjectClasses.isPresent()); 786 entryValidator.setCheckUndefinedAttributes( 787 !ignoreUndefinedAttributes.isPresent()); 788 entryValidator.setCheckUndefinedObjectClasses( 789 !ignoreUndefinedObjectClasses.isPresent()); 790 791 if (ignoreSyntaxViolationsForAttribute.isPresent()) 792 { 793 entryValidator.setIgnoreSyntaxViolationAttributeTypes( 794 ignoreSyntaxViolationsForAttribute.getValues()); 795 } 796 797 798 // Create an LDIF reader that can be used to read through the LDIF file. 799 final LDIFReader ldifReader; 800 rejectWriter = null; 801 try 802 { 803 InputStream inputStream = new FileInputStream(ldifFile.getValue()); 804 805 inputStream = ToolUtils.getPossiblyPassphraseEncryptedInputStream( 806 inputStream, encryptionPassphrase, false, 807 "LDIF file '" + ldifFile.getValue().getPath() + 808 "' is encrypted. Please enter the encryption passphrase:", 809 "ERROR: The provided passphrase was incorrect.", 810 getOut(), getErr()).getFirst(); 811 812 if (isCompressed.isPresent()) 813 { 814 inputStream = new GZIPInputStream(inputStream); 815 } 816 else 817 { 818 inputStream = 819 ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 820 } 821 822 ldifReader = new LDIFReader(inputStream, numThreads.getValue(), this); 823 } 824 catch (final Exception e) 825 { 826 Debug.debugException(e); 827 err("Unable to open the LDIF reader: ", 828 StaticUtils.getExceptionMessage(e)); 829 return ResultCode.LOCAL_ERROR; 830 } 831 832 ldifReader.setSchema(schema); 833 if (ignoreDuplicateValues.isPresent()) 834 { 835 ldifReader.setDuplicateValueBehavior(DuplicateValueBehavior.STRIP); 836 } 837 else 838 { 839 ldifReader.setDuplicateValueBehavior(DuplicateValueBehavior.REJECT); 840 } 841 842 try 843 { 844 // Create an LDIF writer that can be used to write information about 845 // rejected entries. 846 try 847 { 848 if (rejectFile.isPresent()) 849 { 850 rejectWriter = new LDIFWriter(rejectFile.getValue()); 851 } 852 } 853 catch (final Exception e) 854 { 855 Debug.debugException(e); 856 err("Unable to create the reject writer: ", 857 StaticUtils.getExceptionMessage(e)); 858 return ResultCode.LOCAL_ERROR; 859 } 860 861 ResultCode resultCode = ResultCode.SUCCESS; 862 while (true) 863 { 864 try 865 { 866 final Entry e = ldifReader.readEntry(); 867 if (e == null) 868 { 869 // Because we're performing parallel processing and returning null 870 // from the translate method, LDIFReader.readEntry() should never 871 // return a non-null value. However, it can throw an LDIFException 872 // if it encounters an invalid entry, or an IOException if there's 873 // a problem reading from the file, so we should still iterate 874 // through all of the entries to catch and report on those problems. 875 break; 876 } 877 } 878 catch (final LDIFException le) 879 { 880 Debug.debugException(le); 881 malformedEntries.incrementAndGet(); 882 883 if (resultCode == ResultCode.SUCCESS) 884 { 885 resultCode = ResultCode.DECODING_ERROR; 886 } 887 888 if (rejectWriter != null) 889 { 890 try 891 { 892 rejectWriter.writeComment( 893 "Unable to parse an entry read from LDIF:", false, false); 894 if (le.mayContinueReading()) 895 { 896 rejectWriter.writeComment( 897 StaticUtils.getExceptionMessage(le), false, true); 898 } 899 else 900 { 901 rejectWriter.writeComment( 902 StaticUtils.getExceptionMessage(le), false, 903 false); 904 rejectWriter.writeComment("Unable to continue LDIF processing.", 905 false, true); 906 err("Aborting LDIF processing: ", 907 StaticUtils.getExceptionMessage(le)); 908 return ResultCode.LOCAL_ERROR; 909 } 910 } 911 catch (final IOException ioe) 912 { 913 Debug.debugException(ioe); 914 err("Unable to write to the reject file:", 915 StaticUtils.getExceptionMessage(ioe)); 916 err("LDIF parse failure that triggered the rejection: ", 917 StaticUtils.getExceptionMessage(le)); 918 return ResultCode.LOCAL_ERROR; 919 } 920 } 921 } 922 catch (final IOException ioe) 923 { 924 Debug.debugException(ioe); 925 926 if (rejectWriter != null) 927 { 928 try 929 { 930 rejectWriter.writeComment("I/O error reading from LDIF:", false, 931 false); 932 rejectWriter.writeComment(StaticUtils.getExceptionMessage(ioe), 933 false, true); 934 return ResultCode.LOCAL_ERROR; 935 } 936 catch (final Exception ex) 937 { 938 Debug.debugException(ex); 939 err("I/O error reading from LDIF:", 940 StaticUtils.getExceptionMessage(ioe)); 941 return ResultCode.LOCAL_ERROR; 942 } 943 } 944 } 945 } 946 947 if (malformedEntries.get() > 0) 948 { 949 out(malformedEntries.get() + " entries were malformed and could not " + 950 "be read from the LDIF file."); 951 } 952 953 if (entryValidator.getInvalidEntries() > 0) 954 { 955 if (resultCode == ResultCode.SUCCESS) 956 { 957 resultCode = ResultCode.OBJECT_CLASS_VIOLATION; 958 } 959 960 for (final String s : entryValidator.getInvalidEntrySummary(true)) 961 { 962 out(s); 963 } 964 } 965 else 966 { 967 if (malformedEntries.get() == 0) 968 { 969 out("No errors were encountered."); 970 } 971 } 972 973 return resultCode; 974 } 975 finally 976 { 977 try 978 { 979 ldifReader.close(); 980 } 981 catch (final Exception e) 982 { 983 Debug.debugException(e); 984 } 985 986 try 987 { 988 if (rejectWriter != null) 989 { 990 rejectWriter.close(); 991 } 992 } 993 catch (final Exception e) 994 { 995 Debug.debugException(e); 996 } 997 } 998 } 999 1000 1001 1002 /** 1003 * Examines the provided entry to determine whether it conforms to the 1004 * server schema. 1005 * 1006 * @param entry The entry to be examined. 1007 * @param firstLineNumber The line number of the LDIF source on which the 1008 * provided entry begins. 1009 * 1010 * @return The updated entry. This method will always return {@code null} 1011 * because all of the real processing needed for the entry is 1012 * performed in this method and the entry isn't needed any more 1013 * after this method is done. 1014 */ 1015 @Override() 1016 @Nullable() 1017 public Entry translate(@NotNull final Entry entry, final long firstLineNumber) 1018 { 1019 final ArrayList<String> invalidReasons = new ArrayList<>(5); 1020 if (! entryValidator.entryIsValid(entry, invalidReasons)) 1021 { 1022 if (rejectWriter != null) 1023 { 1024 synchronized (this) 1025 { 1026 try 1027 { 1028 rejectWriter.writeEntry(entry, listToString(invalidReasons)); 1029 } 1030 catch (final IOException ioe) 1031 { 1032 Debug.debugException(ioe); 1033 } 1034 } 1035 } 1036 } 1037 1038 final long numEntries = entriesProcessed.incrementAndGet(); 1039 if ((numEntries % 1000L) == 0L) 1040 { 1041 out("Processed ", numEntries, " entries."); 1042 } 1043 1044 return null; 1045 } 1046 1047 1048 1049 /** 1050 * Converts the provided list of strings into a single string. It will 1051 * contain line breaks after all but the last element. 1052 * 1053 * @param l The list of strings to convert to a single string. 1054 * 1055 * @return The string from the provided list, or {@code null} if the provided 1056 * list is empty or {@code null}. 1057 */ 1058 @Nullable() 1059 private static String listToString(@Nullable final List<String> l) 1060 { 1061 if ((l == null) || (l.isEmpty())) 1062 { 1063 return null; 1064 } 1065 1066 final StringBuilder buffer = new StringBuilder(); 1067 final Iterator<String> iterator = l.iterator(); 1068 while (iterator.hasNext()) 1069 { 1070 buffer.append(iterator.next()); 1071 if (iterator.hasNext()) 1072 { 1073 buffer.append(EOL); 1074 } 1075 } 1076 1077 return buffer.toString(); 1078 } 1079 1080 1081 1082 /** 1083 * {@inheritDoc} 1084 */ 1085 @Override() 1086 @NotNull() 1087 public LinkedHashMap<String[],String> getExampleUsages() 1088 { 1089 final LinkedHashMap<String[],String> examples = 1090 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 1091 1092 String[] args = 1093 { 1094 "--hostname", "server.example.com", 1095 "--port", "389", 1096 "--ldifFile", "data.ldif", 1097 "--rejectFile", "rejects.ldif", 1098 "--numThreads", "4" 1099 }; 1100 String description = 1101 "Validate the contents of the 'data.ldif' file using the schema " + 1102 "defined in the specified directory server using four concurrent " + 1103 "threads. All types of validation will be performed, and " + 1104 "information about any errors will be written to the 'rejects.ldif' " + 1105 "file."; 1106 examples.put(args, description); 1107 1108 1109 args = new String[] 1110 { 1111 "--schemaDirectory", "/ds/config/schema", 1112 "--ldifFile", "data.ldif", 1113 "--rejectFile", "rejects.ldif", 1114 "--ignoreStructuralObjectClasses", 1115 "--ignoreAttributeSyntax" 1116 }; 1117 description = 1118 "Validate the contents of the 'data.ldif' file using the schema " + 1119 "defined in LDIF files contained in the /ds/config/schema directory " + 1120 "using a single thread. Any errors resulting from entries that do " + 1121 "not have exactly one structural object class or from values which " + 1122 "violate the syntax for their associated attribute types will be " + 1123 "ignored. Information about any other failures will be written to " + 1124 "the 'rejects.ldif' file."; 1125 examples.put(args, description); 1126 1127 return examples; 1128 } 1129 1130 1131 1132 /** 1133 * @return EntryValidator 1134 * 1135 * Returns the EntryValidator 1136 */ 1137 @Nullable() 1138 public EntryValidator getEntryValidator() 1139 { 1140 return entryValidator; 1141 } 1142}