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}