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