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}