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}