001/* 002 * Copyright 2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2019 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk.examples; 022 023 024 025import java.io.OutputStream; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.LinkedHashMap; 029import java.util.List; 030 031import com.unboundid.ldap.sdk.Filter; 032import com.unboundid.ldap.sdk.LDAPException; 033import com.unboundid.ldap.sdk.ResultCode; 034import com.unboundid.ldap.sdk.Version; 035import com.unboundid.util.CommandLineTool; 036import com.unboundid.util.Debug; 037import com.unboundid.util.StaticUtils; 038import com.unboundid.util.ThreadSafety; 039import com.unboundid.util.ThreadSafetyLevel; 040import com.unboundid.util.args.ArgumentException; 041import com.unboundid.util.args.ArgumentParser; 042import com.unboundid.util.args.BooleanArgument; 043import com.unboundid.util.args.IntegerArgument; 044 045 046 047/** 048 * This class provides a command-line tool that can be used to display a 049 * complex LDAP search filter in a multi-line form that makes it easier to 050 * visualize its hierarchy. It will also attempt to simply the filter if 051 * possible (using the {@link Filter#simplifyFilter} method) to remove 052 * unnecessary complexity. 053 */ 054@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 055public final class IndentLDAPFilter 056 extends CommandLineTool 057{ 058 /** 059 * The column at which to wrap long lines. 060 */ 061 private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 062 063 064 065 /** 066 * The name of the argument used to specify the number of additional spaces 067 * to indent each level of hierarchy. 068 */ 069 private static final String ARG_INDENT_SPACES = "indent-spaces"; 070 071 072 073 /** 074 * The name of the argument used to indicate that the tool should not attempt 075 * to simplify the provided filter. 076 */ 077 private static final String ARG_DO_NOT_SIMPLIFY = "do-not-simplify"; 078 079 080 081 // The argument parser for this tool. 082 private ArgumentParser parser; 083 084 085 086 /** 087 * Runs this tool with the provided set of command-line arguments. 088 * 089 * @param args The command line arguments provided to this program. 090 */ 091 public static void main(final String... args) 092 { 093 final ResultCode resultCode = main(System.out, System.err, args); 094 if (resultCode != ResultCode.SUCCESS) 095 { 096 System.exit(resultCode.intValue()); 097 } 098 } 099 100 101 102 /** 103 * Runs this tool with the provided set of command-line arguments. 104 * 105 * @param out The output stream to which standard out should be written. 106 * It may be {@code null} if standard output should be 107 * suppressed. 108 * @param err The output stream to which standard error should be written. 109 * It may be {@code null} if standard error should be 110 * suppressed. 111 * @param args The command line arguments provided to this program. 112 * 113 * @return A result code that indicates whether processing was successful. 114 * Any result code other than {@link ResultCode#SUCCESS} should be 115 * considered an error. 116 */ 117 public static ResultCode main(final OutputStream out, 118 final OutputStream err, 119 final String... args) 120 { 121 final IndentLDAPFilter indentLDAPFilter = new IndentLDAPFilter(out, err); 122 return indentLDAPFilter.runTool(args); 123 } 124 125 126 127 /** 128 * Creates a new instance of this command-line tool with the provided output 129 * and error streams. 130 * 131 * @param out The output stream to which standard out should be written. It 132 * may be {@code null} if standard output should be 133 * suppressed. 134 * @param err The output stream to which standard error should be written. 135 * It may be {@code null} if standard error should be suppressed. 136 */ 137 public IndentLDAPFilter(final OutputStream out, final OutputStream err) 138 { 139 super(out, err); 140 141 parser = null; 142 } 143 144 145 146 /** 147 * Retrieves the name of this tool. It should be the name of the command used 148 * to invoke this tool. 149 * 150 * @return The name for this tool. 151 */ 152 @Override() 153 public String getToolName() 154 { 155 return "indent-ldap-filter"; 156 } 157 158 159 160 /** 161 * Retrieves a human-readable description for this tool. If the description 162 * should include multiple paragraphs, then this method should return the text 163 * for the first paragraph, and the 164 * {@link #getAdditionalDescriptionParagraphs()} method should be used to 165 * return the text for the subsequent paragraphs. 166 * 167 * @return A human-readable description for this tool. 168 */ 169 @Override() 170 public String getToolDescription() 171 { 172 return "Parses a provided LDAP filter string and displays it a " + 173 "multi-line form that makes it easier to understand its hierarchy " + 174 "and embedded components. If possible, it may also be able to " + 175 "simplify the provided filter in certain ways (for example, by " + 176 "removing unnecessary levels of hierarchy, like an AND embedded in " + 177 "an AND)."; 178 } 179 180 181 182 /** 183 * Retrieves a version string for this tool, if available. 184 * 185 * @return A version string for this tool, or {@code null} if none is 186 * available. 187 */ 188 @Override() 189 public String getToolVersion() 190 { 191 return Version.NUMERIC_VERSION_STRING; 192 } 193 194 195 196 /** 197 * Retrieves the minimum number of unnamed trailing arguments that must be 198 * provided for this tool. If a tool requires the use of trailing arguments, 199 * then it must override this method and the {@link #getMaxTrailingArguments} 200 * arguments to return nonzero values, and it must also override the 201 * {@link #getTrailingArgumentsPlaceholder} method to return a 202 * non-{@code null} value. 203 * 204 * @return The minimum number of unnamed trailing arguments that may be 205 * provided for this tool. A value of zero indicates that the tool 206 * may be invoked without any trailing arguments. 207 */ 208 @Override() 209 public int getMinTrailingArguments() 210 { 211 return 1; 212 } 213 214 215 216 /** 217 * Retrieves the maximum number of unnamed trailing arguments that may be 218 * provided for this tool. If a tool supports trailing arguments, then it 219 * must override this method to return a nonzero value, and must also override 220 * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to 221 * return a non-{@code null} value. 222 * 223 * @return The maximum number of unnamed trailing arguments that may be 224 * provided for this tool. A value of zero indicates that trailing 225 * arguments are not allowed. A negative value indicates that there 226 * should be no limit on the number of trailing arguments. 227 */ 228 @Override() 229 public int getMaxTrailingArguments() 230 { 231 return 1; 232 } 233 234 235 236 /** 237 * Retrieves a placeholder string that should be used for trailing arguments 238 * in the usage information for this tool. 239 * 240 * @return A placeholder string that should be used for trailing arguments in 241 * the usage information for this tool, or {@code null} if trailing 242 * arguments are not supported. 243 */ 244 @Override() 245 public String getTrailingArgumentsPlaceholder() 246 { 247 return "{filter}"; 248 } 249 250 251 252 /** 253 * Indicates whether this tool should provide support for an interactive mode, 254 * in which the tool offers a mode in which the arguments can be provided in 255 * a text-driven menu rather than requiring them to be given on the command 256 * line. If interactive mode is supported, it may be invoked using the 257 * "--interactive" argument. Alternately, if interactive mode is supported 258 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 259 * interactive mode may be invoked by simply launching the tool without any 260 * arguments. 261 * 262 * @return {@code true} if this tool supports interactive mode, or 263 * {@code false} if not. 264 */ 265 @Override() 266 public boolean supportsInteractiveMode() 267 { 268 return true; 269 } 270 271 272 273 /** 274 * Indicates whether this tool defaults to launching in interactive mode if 275 * the tool is invoked without any command-line arguments. This will only be 276 * used if {@link #supportsInteractiveMode()} returns {@code true}. 277 * 278 * @return {@code true} if this tool defaults to using interactive mode if 279 * launched without any command-line arguments, or {@code false} if 280 * not. 281 */ 282 @Override() 283 public boolean defaultsToInteractiveMode() 284 { 285 return true; 286 } 287 288 289 290 /** 291 * Indicates whether this tool supports the use of a properties file for 292 * specifying default values for arguments that aren't specified on the 293 * command line. 294 * 295 * @return {@code true} if this tool supports the use of a properties file 296 * for specifying default values for arguments that aren't specified 297 * on the command line, or {@code false} if not. 298 */ 299 @Override() 300 public boolean supportsPropertiesFile() 301 { 302 return true; 303 } 304 305 306 307 /** 308 * Indicates whether this tool should provide arguments for redirecting output 309 * to a file. If this method returns {@code true}, then the tool will offer 310 * an "--outputFile" argument that will specify the path to a file to which 311 * all standard output and standard error content will be written, and it will 312 * also offer a "--teeToStandardOut" argument that can only be used if the 313 * "--outputFile" argument is present and will cause all output to be written 314 * to both the specified output file and to standard output. 315 * 316 * @return {@code true} if this tool should provide arguments for redirecting 317 * output to a file, or {@code false} if not. 318 */ 319 @Override() 320 protected boolean supportsOutputFile() 321 { 322 return true; 323 } 324 325 326 327 /** 328 * Adds the command-line arguments supported for use with this tool to the 329 * provided argument parser. The tool may need to retain references to the 330 * arguments (and/or the argument parser, if trailing arguments are allowed) 331 * to it in order to obtain their values for use in later processing. 332 * 333 * @param parser The argument parser to which the arguments are to be added. 334 * 335 * @throws ArgumentException If a problem occurs while adding any of the 336 * tool-specific arguments to the provided 337 * argument parser. 338 */ 339 @Override() 340 public void addToolArguments(final ArgumentParser parser) 341 throws ArgumentException 342 { 343 this.parser = parser; 344 345 final IntegerArgument indentColumnsArg = new IntegerArgument(null, 346 ARG_INDENT_SPACES, false, 1, "{numSpaces}", 347 "Specifies the number of spaces that should be used to indent each " + 348 "additional level of filter hierarchy. A value of zero " + 349 "indicates that the hierarchy should be displayed without any " + 350 "additional indenting. If this argument is not provided, a " + 351 "default indent of two spaces will be used.", 352 0, Integer.MAX_VALUE, 2); 353 indentColumnsArg.addLongIdentifier("indentSpaces", true); 354 indentColumnsArg.addLongIdentifier("indent-columns", true); 355 indentColumnsArg.addLongIdentifier("indentColumns", true); 356 indentColumnsArg.addLongIdentifier("indent", true); 357 parser.addArgument(indentColumnsArg); 358 359 final BooleanArgument doNotSimplifyArg = new BooleanArgument(null, 360 ARG_DO_NOT_SIMPLIFY, 1, 361 "Indicates that the tool should not make any attempt to simplify " + 362 "the provided filter. If this argument is not provided, then " + 363 "the tool will try to simplify the provided filter (for " + 364 "example, by removing unnecessary levels of hierarchy, like an " + 365 "AND embedded in an AND)."); 366 doNotSimplifyArg.addLongIdentifier("doNotSimplify", true); 367 doNotSimplifyArg.addLongIdentifier("do-not-simplify-filter", true); 368 doNotSimplifyArg.addLongIdentifier("doNotSimplifyFilter", true); 369 doNotSimplifyArg.addLongIdentifier("dont-simplify", true); 370 doNotSimplifyArg.addLongIdentifier("dontSimplify", true); 371 doNotSimplifyArg.addLongIdentifier("dont-simplify-filter", true); 372 doNotSimplifyArg.addLongIdentifier("dontSimplifyFilter", true); 373 parser.addArgument(doNotSimplifyArg); 374 } 375 376 377 378 /** 379 * Performs the core set of processing for this tool. 380 * 381 * @return A result code that indicates whether the processing completed 382 * successfully. 383 */ 384 @Override() 385 public ResultCode doToolProcessing() 386 { 387 // Make sure that we can parse the filter string. 388 final Filter filter; 389 try 390 { 391 filter = Filter.create(parser.getTrailingArguments().get(0)); 392 } 393 catch (final LDAPException e) 394 { 395 Debug.debugException(e); 396 wrapErr(0, WRAP_COLUMN, 397 "ERROR: Unable to parse the provided filter string: " + 398 StaticUtils.getExceptionMessage(e)); 399 return e.getResultCode(); 400 } 401 402 403 // Construct the base indent string. 404 final int indentSpaces = 405 parser.getIntegerArgument(ARG_INDENT_SPACES).getValue(); 406 final char[] indentChars = new char[indentSpaces]; 407 Arrays.fill(indentChars, ' '); 408 final String indentString = new String(indentChars); 409 410 411 // Display an indented representation of the provided filter. 412 final List<String> indentedFilterLines = new ArrayList<>(10); 413 indentLDAPFilter(filter, "", indentString, indentedFilterLines); 414 for (final String line : indentedFilterLines) 415 { 416 out(line); 417 } 418 419 420 // See if we can simplify the provided filter. 421 if (! parser.getBooleanArgument(ARG_DO_NOT_SIMPLIFY).isPresent()) 422 { 423 out(); 424 final Filter simplifiedFilter = Filter.simplifyFilter(filter, false); 425 if (simplifiedFilter.equals(filter)) 426 { 427 wrapOut(0, WRAP_COLUMN, "The provided filter cannot be simplified."); 428 } 429 else 430 { 431 wrapOut(0, WRAP_COLUMN, "The provided filter can be simplified to:"); 432 out(); 433 out(" ", simplifiedFilter.toString()); 434 out(); 435 wrapOut(0, WRAP_COLUMN, 436 "An indented representation of the simplified filter:"); 437 out(); 438 439 indentedFilterLines.clear(); 440 indentLDAPFilter(simplifiedFilter, "", indentString, 441 indentedFilterLines); 442 for (final String line : indentedFilterLines) 443 { 444 out(line); 445 } 446 } 447 } 448 449 return ResultCode.SUCCESS; 450 } 451 452 453 454 /** 455 * Generates an indented representation of the provided filter. 456 * 457 * @param filter The filter to be indented. It must not be 458 * {@code null}. 459 * @param currentIndentString A string that represents the current indent 460 * that should be added before each line of the 461 * filter. It may be empty, but must not be 462 * {@code null}. 463 * @param indentSpaces A string that represents the number of 464 * additional spaces that each subsequent level 465 * of the hierarchy should be indented. It may 466 * be empty, but must not be {@code null}. 467 * @param indentedFilterLines A list to which the lines that comprise the 468 * indented filter should be added. It must not 469 * be {@code null}, and must be updatable. 470 */ 471 public static void indentLDAPFilter(final Filter filter, 472 final String currentIndentString, 473 final String indentSpaces, 474 final List<String> indentedFilterLines) 475 { 476 switch (filter.getFilterType()) 477 { 478 case Filter.FILTER_TYPE_AND: 479 final Filter[] andComponents = filter.getComponents(); 480 if (andComponents.length == 0) 481 { 482 indentedFilterLines.add(currentIndentString + "(&)"); 483 } 484 else 485 { 486 indentedFilterLines.add(currentIndentString + "(&"); 487 488 final String andComponentIndent = 489 currentIndentString + " &" + indentSpaces; 490 for (final Filter andComponent : andComponents) 491 { 492 indentLDAPFilter(andComponent, andComponentIndent, indentSpaces, 493 indentedFilterLines); 494 } 495 indentedFilterLines.add(currentIndentString + " &)"); 496 } 497 break; 498 499 500 case Filter.FILTER_TYPE_OR: 501 final Filter[] orComponents = filter.getComponents(); 502 if (orComponents.length == 0) 503 { 504 indentedFilterLines.add(currentIndentString + "(|)"); 505 } 506 else 507 { 508 indentedFilterLines.add(currentIndentString + "(|"); 509 510 final String orComponentIndent = 511 currentIndentString + " |" + indentSpaces; 512 for (final Filter orComponent : orComponents) 513 { 514 indentLDAPFilter(orComponent, orComponentIndent, indentSpaces, 515 indentedFilterLines); 516 } 517 indentedFilterLines.add(currentIndentString + " |)"); 518 } 519 break; 520 521 522 case Filter.FILTER_TYPE_NOT: 523 indentedFilterLines.add(currentIndentString + "(!"); 524 indentLDAPFilter(filter.getNOTComponent(), 525 currentIndentString + " !" + indentSpaces, indentSpaces, 526 indentedFilterLines); 527 indentedFilterLines.add(currentIndentString + " !)"); 528 break; 529 530 531 default: 532 indentedFilterLines.add(currentIndentString + filter.toString()); 533 break; 534 } 535 } 536 537 538 539 /** 540 * Retrieves a set of information that may be used to generate example usage 541 * information. Each element in the returned map should consist of a map 542 * between an example set of arguments and a string that describes the 543 * behavior of the tool when invoked with that set of arguments. 544 * 545 * @return A set of information that may be used to generate example usage 546 * information. It may be {@code null} or empty if no example usage 547 * information is available. 548 */ 549 @Override() 550 public LinkedHashMap<String[],String> getExampleUsages() 551 { 552 final LinkedHashMap<String[],String> examples = 553 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 554 555 examples.put( 556 new String[] 557 { 558 "(|(givenName=jdoe)(|(sn=jdoe)(|(cn=jdoe)(|(uid=jdoe)(mail=jdoe)))))" 559 }, 560 "Displays an indented representation of the provided filter, as " + 561 "well as a simplified version of that filter."); 562 563 return examples; 564 } 565}