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