001/* 002 * Copyright 2019-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2019-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) 2019-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.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 * Adds the command-line arguments supported for use with this tool to the 352 * provided argument parser. The tool may need to retain references to the 353 * arguments (and/or the argument parser, if trailing arguments are allowed) 354 * to it in order to obtain their values for use in later processing. 355 * 356 * @param parser The argument parser to which the arguments are to be added. 357 * 358 * @throws ArgumentException If a problem occurs while adding any of the 359 * tool-specific arguments to the provided 360 * argument parser. 361 */ 362 @Override() 363 public void addToolArguments(@NotNull final ArgumentParser parser) 364 throws ArgumentException 365 { 366 this.parser = parser; 367 368 final IntegerArgument indentColumnsArg = new IntegerArgument(null, 369 ARG_INDENT_SPACES, false, 1, "{numSpaces}", 370 "Specifies the number of spaces that should be used to indent each " + 371 "additional level of filter hierarchy. A value of zero " + 372 "indicates that the hierarchy should be displayed without any " + 373 "additional indenting. If this argument is not provided, a " + 374 "default indent of two spaces will be used.", 375 0, Integer.MAX_VALUE, 2); 376 indentColumnsArg.addLongIdentifier("indentSpaces", true); 377 indentColumnsArg.addLongIdentifier("indent-columns", true); 378 indentColumnsArg.addLongIdentifier("indentColumns", true); 379 indentColumnsArg.addLongIdentifier("indent", true); 380 parser.addArgument(indentColumnsArg); 381 382 final BooleanArgument doNotSimplifyArg = new BooleanArgument(null, 383 ARG_DO_NOT_SIMPLIFY, 1, 384 "Indicates that the tool should not make any attempt to simplify " + 385 "the provided filter. If this argument is not provided, then " + 386 "the tool will try to simplify the provided filter (for " + 387 "example, by removing unnecessary levels of hierarchy, like an " + 388 "AND embedded in an AND)."); 389 doNotSimplifyArg.addLongIdentifier("doNotSimplify", true); 390 doNotSimplifyArg.addLongIdentifier("do-not-simplify-filter", true); 391 doNotSimplifyArg.addLongIdentifier("doNotSimplifyFilter", true); 392 doNotSimplifyArg.addLongIdentifier("dont-simplify", true); 393 doNotSimplifyArg.addLongIdentifier("dontSimplify", true); 394 doNotSimplifyArg.addLongIdentifier("dont-simplify-filter", true); 395 doNotSimplifyArg.addLongIdentifier("dontSimplifyFilter", true); 396 parser.addArgument(doNotSimplifyArg); 397 } 398 399 400 401 /** 402 * Performs the core set of processing for this tool. 403 * 404 * @return A result code that indicates whether the processing completed 405 * successfully. 406 */ 407 @Override() 408 @NotNull() 409 public ResultCode doToolProcessing() 410 { 411 // Make sure that we can parse the filter string. 412 final Filter filter; 413 try 414 { 415 filter = Filter.create(parser.getTrailingArguments().get(0)); 416 } 417 catch (final LDAPException e) 418 { 419 Debug.debugException(e); 420 wrapErr(0, WRAP_COLUMN, 421 "ERROR: Unable to parse the provided filter string: " + 422 StaticUtils.getExceptionMessage(e)); 423 return e.getResultCode(); 424 } 425 426 427 // Construct the base indent string. 428 final int indentSpaces = 429 parser.getIntegerArgument(ARG_INDENT_SPACES).getValue(); 430 final char[] indentChars = new char[indentSpaces]; 431 Arrays.fill(indentChars, ' '); 432 final String indentString = new String(indentChars); 433 434 435 // Display an indented representation of the provided filter. 436 final List<String> indentedFilterLines = new ArrayList<>(10); 437 indentLDAPFilter(filter, "", indentString, indentedFilterLines); 438 for (final String line : indentedFilterLines) 439 { 440 out(line); 441 } 442 443 444 // See if we can simplify the provided filter. 445 if (! parser.getBooleanArgument(ARG_DO_NOT_SIMPLIFY).isPresent()) 446 { 447 out(); 448 final Filter simplifiedFilter = Filter.simplifyFilter(filter, false); 449 if (simplifiedFilter.equals(filter)) 450 { 451 wrapOut(0, WRAP_COLUMN, "The provided filter cannot be simplified."); 452 } 453 else 454 { 455 wrapOut(0, WRAP_COLUMN, "The provided filter can be simplified to:"); 456 out(); 457 out(" ", simplifiedFilter.toString()); 458 out(); 459 wrapOut(0, WRAP_COLUMN, 460 "An indented representation of the simplified filter:"); 461 out(); 462 463 indentedFilterLines.clear(); 464 indentLDAPFilter(simplifiedFilter, "", indentString, 465 indentedFilterLines); 466 for (final String line : indentedFilterLines) 467 { 468 out(line); 469 } 470 } 471 } 472 473 return ResultCode.SUCCESS; 474 } 475 476 477 478 /** 479 * Generates an indented representation of the provided filter. 480 * 481 * @param filter The filter to be indented. It must not be 482 * {@code null}. 483 * @param currentIndentString A string that represents the current indent 484 * that should be added before each line of the 485 * filter. It may be empty, but must not be 486 * {@code null}. 487 * @param indentSpaces A string that represents the number of 488 * additional spaces that each subsequent level 489 * of the hierarchy should be indented. It may 490 * be empty, but must not be {@code null}. 491 * @param indentedFilterLines A list to which the lines that comprise the 492 * indented filter should be added. It must not 493 * be {@code null}, and must be updatable. 494 */ 495 public static void indentLDAPFilter(@NotNull final Filter filter, 496 @NotNull final String currentIndentString, 497 @NotNull final String indentSpaces, 498 @NotNull final List<String> indentedFilterLines) 499 { 500 switch (filter.getFilterType()) 501 { 502 case Filter.FILTER_TYPE_AND: 503 final Filter[] andComponents = filter.getComponents(); 504 if (andComponents.length == 0) 505 { 506 indentedFilterLines.add(currentIndentString + "(&)"); 507 } 508 else 509 { 510 indentedFilterLines.add(currentIndentString + "(&"); 511 512 final String andComponentIndent = 513 currentIndentString + " &" + indentSpaces; 514 for (final Filter andComponent : andComponents) 515 { 516 indentLDAPFilter(andComponent, andComponentIndent, indentSpaces, 517 indentedFilterLines); 518 } 519 indentedFilterLines.add(currentIndentString + " &)"); 520 } 521 break; 522 523 524 case Filter.FILTER_TYPE_OR: 525 final Filter[] orComponents = filter.getComponents(); 526 if (orComponents.length == 0) 527 { 528 indentedFilterLines.add(currentIndentString + "(|)"); 529 } 530 else 531 { 532 indentedFilterLines.add(currentIndentString + "(|"); 533 534 final String orComponentIndent = 535 currentIndentString + " |" + indentSpaces; 536 for (final Filter orComponent : orComponents) 537 { 538 indentLDAPFilter(orComponent, orComponentIndent, indentSpaces, 539 indentedFilterLines); 540 } 541 indentedFilterLines.add(currentIndentString + " |)"); 542 } 543 break; 544 545 546 case Filter.FILTER_TYPE_NOT: 547 indentedFilterLines.add(currentIndentString + "(!"); 548 indentLDAPFilter(filter.getNOTComponent(), 549 currentIndentString + " !" + indentSpaces, indentSpaces, 550 indentedFilterLines); 551 indentedFilterLines.add(currentIndentString + " !)"); 552 break; 553 554 555 default: 556 indentedFilterLines.add(currentIndentString + filter.toString()); 557 break; 558 } 559 } 560 561 562 563 /** 564 * Retrieves a set of information that may be used to generate example usage 565 * information. Each element in the returned map should consist of a map 566 * between an example set of arguments and a string that describes the 567 * behavior of the tool when invoked with that set of arguments. 568 * 569 * @return A set of information that may be used to generate example usage 570 * information. It may be {@code null} or empty if no example usage 571 * information is available. 572 */ 573 @Override() 574 @NotNull() 575 public LinkedHashMap<String[],String> getExampleUsages() 576 { 577 final LinkedHashMap<String[],String> examples = 578 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 579 580 examples.put( 581 new String[] 582 { 583 "(|(givenName=jdoe)(|(sn=jdoe)(|(cn=jdoe)(|(uid=jdoe)(mail=jdoe)))))" 584 }, 585 "Displays an indented representation of the provided filter, as " + 586 "well as a simplified version of that filter."); 587 588 return examples; 589 } 590}