001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-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) 2020-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.unboundidds.tools;
037
038
039
040import java.io.OutputStream;
041import java.util.Arrays;
042import java.util.Collection;
043import java.util.LinkedHashMap;
044import java.util.List;
045import java.util.Map;
046import java.util.TreeMap;
047
048import com.unboundid.ldap.sdk.ResultCode;
049import com.unboundid.ldap.sdk.Version;
050import com.unboundid.util.ColumnFormatter;
051import com.unboundid.util.CommandLineTool;
052import com.unboundid.util.FormattableColumn;
053import com.unboundid.util.HorizontalAlignment;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.Nullable;
056import com.unboundid.util.OutputFormat;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060import com.unboundid.util.args.ArgumentException;
061import com.unboundid.util.args.ArgumentParser;
062import com.unboundid.util.args.BooleanArgument;
063import com.unboundid.util.args.IntegerArgument;
064import com.unboundid.util.args.StringArgument;
065import com.unboundid.util.json.JSONField;
066import com.unboundid.util.json.JSONObject;
067
068import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
069
070
071
072/**
073 * This class provides a command-line tool that can be used to list LDAP result
074 * code names and their numeric values, or to search for result codes that match
075 * a given name or value.
076 * <BR>
077 * <BLOCKQUOTE>
078 *   <B>NOTE:</B>  This class, and other classes within the
079 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
080 *   supported for use against Ping Identity, UnboundID, and
081 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
082 *   for proprietary functionality or for external specifications that are not
083 *   considered stable or mature enough to be guaranteed to work in an
084 *   interoperable way with other types of LDAP servers.
085 * </BLOCKQUOTE>
086 */
087@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
088public final class LDAPResultCode
089       extends CommandLineTool
090{
091  /**
092   * The column at which to wrap long lines.
093   */
094  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 2;
095
096
097
098  /**
099   * The name of the JSON field that will hold the integer value for the result
100   * code.
101   */
102  @NotNull private static final String JSON_FIELD_INT_VALUE = "int-value";
103
104
105
106  /**
107   * The name of the JSON field that will hold the name for the result code.
108   */
109  @NotNull private static final String JSON_FIELD_NAME = "name";
110
111
112
113  /**
114   * The output format value that indicates that output should be generated as
115   * comma-separated values.
116   */
117  @NotNull private static final String OUTPUT_FORMAT_CSV = "csv";
118
119
120
121  /**
122   * The output format value that indicates that output should be generated as
123   * JSON objects.
124   */
125  @NotNull private static final String OUTPUT_FORMAT_JSON = "json";
126
127
128
129  /**
130   * The output format value that indicates that output should be generated as
131   * tab-delimited text.
132   */
133  @NotNull private static final String OUTPUT_FORMAT_TAB_DELIMITED =
134       "tab-delimited";
135
136
137
138  /**
139   * The output format value that indicates that output should be generated as
140   * a table.
141   */
142  @NotNull private static final String OUTPUT_FORMAT_TABLE = "table";
143
144
145
146  // The argument used to indicate that the tool should list result codes in
147  // alphabetic order rather than numeric order.
148  @Nullable private BooleanArgument alphabeticOrderArg;
149
150  // The argument used to indicate that the tool should list all defined result
151  // codes.
152  @Nullable private BooleanArgument listArg;
153
154  // The argument used to indicate that the tool should generate tab-delimited
155  // text output rather than as a table.
156  @Nullable private BooleanArgument scriptFriendlyArg;
157
158  // The argument used to indicate that the tool should search for the result
159  // code with the specified integer value.
160  @Nullable private IntegerArgument intValueArg;
161
162  // The argument used to specify the output format for the tool.
163  @Nullable private StringArgument outputFormatArg;
164
165  // The argument used to indicate that the tool should search for result codes
166  // that have the specified string in their name
167  @Nullable private StringArgument searchArg;
168
169
170
171  /**
172   * Runs this tool with the provided set of command-line arguments.
173   *
174   * @param  args  The command-line arguments provided to this program.  It may
175   *               be empty, but must not be {@code null}.
176   */
177  public static void main(@NotNull final String... args)
178  {
179    final ResultCode resultCode = main(System.out, System.err, args);
180    if (resultCode != ResultCode.SUCCESS)
181    {
182      System.exit(resultCode.intValue());
183    }
184  }
185
186
187
188  /**
189   * Runs this tool with the provided set of command-line arguments.
190   *
191   * @param  out   The output stream to use for standard output.  It may be
192   *               {@code null} if standard output should be suppressed.
193   * @param  err   The output stream to use for standard error.  It may be
194   *               {@code null} if standard error should be suppressed.
195   * @param  args  The command-line arguments provided to this program.  It may
196   *               be empty, but must not be {@code null}.
197   *
198   * @return  A result code that indicates the result of tool processing.
199   */
200  @NotNull()
201  public static ResultCode main(@Nullable final OutputStream out,
202                                @Nullable final OutputStream err,
203                                @NotNull final String... args)
204  {
205    final LDAPResultCode tool = new LDAPResultCode(out, err);
206    return tool.runTool(args);
207  }
208
209
210
211  /**
212   * Creates a new instance of this tool with the provided output and error
213   * streams.
214   *
215   * @param  out  The output stream to use for standard output.  It may be
216   *              {@code null} if standard output should be suppressed.
217   * @param  err  The output stream to use for standard error.  It may be
218   *              {@code null} if standard error should be suppressed.
219   */
220  public LDAPResultCode(@Nullable final OutputStream out,
221                        @Nullable final OutputStream err)
222  {
223    super(out, err);
224
225    alphabeticOrderArg = null;
226    listArg = null;
227    scriptFriendlyArg = null;
228    intValueArg = null;
229    outputFormatArg = null;
230    searchArg = null;
231  }
232
233
234
235  /**
236   * {@inheritDoc}
237   */
238  @Override()
239  @NotNull()
240  public String getToolName()
241  {
242    return "ldap-result-code";
243  }
244
245
246
247  /**
248   * {@inheritDoc}
249   */
250  @Override()
251  @NotNull()
252  public String getToolDescription()
253  {
254    return INFO_LDAP_RC_TOOL_DESC_1.get();
255  }
256
257
258
259  /**
260   * {@inheritDoc}
261   */
262  @Override()
263  @NotNull()
264  public List<String> getAdditionalDescriptionParagraphs()
265  {
266    return Arrays.asList(
267         INFO_LDAP_RC_TOOL_DESC_2.get(),
268         INFO_LDAP_RC_TOOL_DESC_3.get());
269  }
270
271
272
273  /**
274   * {@inheritDoc}
275   */
276  @Override()
277  @NotNull()
278  public String getToolVersion()
279  {
280    return Version.NUMERIC_VERSION_STRING;
281  }
282
283
284
285  /**
286   * {@inheritDoc}
287   */
288  @Override()
289  public boolean supportsInteractiveMode()
290  {
291    return true;
292  }
293
294
295
296  /**
297   * {@inheritDoc}
298   */
299  @Override()
300  public boolean defaultsToInteractiveMode()
301  {
302    return false;
303  }
304
305
306
307  /**
308   * {@inheritDoc}
309   */
310  @Override()
311  public boolean supportsPropertiesFile()
312  {
313    return true;
314  }
315
316
317
318  /**
319   * {@inheritDoc}
320   */
321  @Override()
322  protected boolean supportsOutputFile()
323  {
324    return true;
325  }
326
327
328
329  /**
330   * {@inheritDoc}
331   */
332  @Override()
333  protected boolean logToolInvocationByDefault()
334  {
335    return false;
336  }
337
338
339
340  /**
341   * {@inheritDoc}
342   */
343  @Override()
344  public void addToolArguments(@NotNull final ArgumentParser parser)
345         throws ArgumentException
346  {
347    listArg = new BooleanArgument('l', "list", 1,
348         INFO_LDAP_RC_ARG_DESC_LIST.get());
349    parser.addArgument(listArg);
350
351    intValueArg = new IntegerArgument('i', "int-value", false, 1,
352         INFO_LDAP_RC_ARG_PLACEHOLDER_INT_VALUE.get(),
353         INFO_LDAP_RC_ARG_DESC_INT_VALUE.get());
354    intValueArg.addLongIdentifier("intValue", true);
355    parser.addArgument(intValueArg);
356
357    searchArg = new StringArgument('S', "search", false, 1,
358         INFO_LDAP_RC_ARG_PLACEHOLDER_SEARCH_STRING.get(),
359         INFO_LDAP_RC_ARG_DESC_SEARCH.get());
360    parser.addArgument(searchArg);
361
362    alphabeticOrderArg = new BooleanArgument('a', "alphabetic-order", 1,
363         INFO_LDAP_RC_ARG_DESC_ALPHABETIC.get());
364    alphabeticOrderArg.addLongIdentifier("alphabeticOrder", true);
365    alphabeticOrderArg.addLongIdentifier("alphabetical-order", true);
366    alphabeticOrderArg.addLongIdentifier("alphabeticalOrder", true);
367    alphabeticOrderArg.addLongIdentifier("alphabetic", true);
368    alphabeticOrderArg.addLongIdentifier("alphabetical", true);
369    parser.addArgument(alphabeticOrderArg);
370
371    outputFormatArg = new StringArgument(null, "output-format", false, 1,
372         INFO_LDAP_RC_ARG_PLACEHOLDER_OUTPUT_FORMAT.get(),
373         INFO_LDAP_RC_ARG_DESC_OUTPUT_FORMAT.get(),
374         StaticUtils.setOf(
375              OUTPUT_FORMAT_CSV,
376              OUTPUT_FORMAT_JSON,
377              OUTPUT_FORMAT_TAB_DELIMITED,
378              OUTPUT_FORMAT_TABLE));
379    outputFormatArg.addLongIdentifier("outputFormat", true);
380    outputFormatArg.addLongIdentifier("format", true);
381    parser.addArgument(outputFormatArg);
382
383    scriptFriendlyArg = new BooleanArgument(null, "script-friendly", 1,
384         INFO_LDAP_RC_ARG_DESC_SCRIPT_FRIENDLY.get());
385    scriptFriendlyArg.addLongIdentifier("scriptFriendly", true);
386    scriptFriendlyArg.setHidden(true);
387    parser.addArgument(scriptFriendlyArg);
388
389    parser.addExclusiveArgumentSet(listArg, intValueArg, searchArg);
390    parser.addExclusiveArgumentSet(outputFormatArg, scriptFriendlyArg);
391  }
392
393
394
395  /**
396   * {@inheritDoc}
397   */
398  @Override()
399  @NotNull()
400  public ResultCode doToolProcessing()
401  {
402    // Get all result codes that should be included in the output.
403    final Map<Integer,ResultCode> resultCodesByIntValue = new TreeMap<>();
404    final Map<String,ResultCode> resultCodesByName = new TreeMap<>();
405    if ((intValueArg != null) && intValueArg.isPresent())
406    {
407      final int intValue = intValueArg.getValue();
408      final ResultCode rc = ResultCode.valueOf(intValue, null, false);
409      if (rc != null)
410      {
411        resultCodesByIntValue.put(intValue, rc);
412        resultCodesByName.put(StaticUtils.toLowerCase(rc.getName()), rc);
413      }
414    }
415    else
416    {
417      final String searchString;
418      if ((searchArg != null) && searchArg.isPresent())
419      {
420        searchString = StaticUtils.toLowerCase(searchArg.getValue());
421      }
422      else
423      {
424        searchString = null;
425      }
426
427      for (final ResultCode rc : ResultCode.values())
428      {
429        final String name = rc.getName();
430        final String lowerName = StaticUtils.toLowerCase(name);
431        if (searchString != null)
432        {
433          if (! lowerName.contains(searchString))
434          {
435            continue;
436          }
437        }
438
439        resultCodesByIntValue.put(rc.intValue(), rc);
440        resultCodesByName.put(lowerName, rc);
441      }
442    }
443
444
445    // If there weren't any matching result codes, then inform the user and
446    // exit with an error.
447    if (resultCodesByIntValue.isEmpty())
448    {
449      wrapErr(0, WRAP_COLUMN, ERR_LDAP_RC_NO_RESULTS.get());
450      return ResultCode.NO_RESULTS_RETURNED;
451    }
452
453
454    // Iterate through the matching result codes and figure out how many
455    // characters are in the longest name and
456    final String nameLabel = INFO_LDAP_RC_NAME_LABEL.get();
457    final String intValueLabel = INFO_LDAP_RC_INT_VALUE_LABEL.get();
458    int numCharsInLongestName = nameLabel.length();
459    int numCharsInLongestIntValue = intValueLabel.length();
460    for (final Map.Entry<Integer,ResultCode> e :
461         resultCodesByIntValue.entrySet())
462    {
463      final String intValueString = String.valueOf(e.getKey());
464      numCharsInLongestIntValue =
465           Math.max(numCharsInLongestIntValue, intValueString.length());
466
467      final String name = e.getValue().getName();
468      numCharsInLongestName = Math.max(numCharsInLongestName, name.length());
469    }
470
471
472    // Construct the column formatter that will be used to generate the output.
473    final boolean json;
474    final OutputFormat outputFormat;
475    final boolean scriptFriendly =
476         ((scriptFriendlyArg != null) && scriptFriendlyArg.isPresent());
477    if (scriptFriendly)
478    {
479      json = false;
480      outputFormat = OutputFormat.TAB_DELIMITED_TEXT;
481    }
482    else if ((outputFormatArg != null) && outputFormatArg.isPresent())
483    {
484      final String outputFormatValue =
485           StaticUtils.toLowerCase(outputFormatArg.getValue());
486      if (outputFormatValue.equals(OUTPUT_FORMAT_CSV))
487      {
488        json = false;
489        outputFormat = OutputFormat.CSV;
490      }
491      else if (outputFormatValue.equals(OUTPUT_FORMAT_JSON))
492      {
493        json = true;
494        outputFormat = null;
495      }
496      else if (outputFormatValue.equals(OUTPUT_FORMAT_TAB_DELIMITED))
497      {
498        json = false;
499        outputFormat = OutputFormat.TAB_DELIMITED_TEXT;
500      }
501      else
502      {
503        json = false;
504        outputFormat = OutputFormat.COLUMNS;
505      }
506    }
507    else
508    {
509      json = false;
510      outputFormat = OutputFormat.COLUMNS;
511    }
512
513    final ColumnFormatter formatter;
514    if (json)
515    {
516      formatter = null;
517    }
518    else
519    {
520      formatter = new ColumnFormatter(false, null, outputFormat, " | ",
521           new FormattableColumn(numCharsInLongestName,
522                HorizontalAlignment.LEFT, nameLabel),
523           new FormattableColumn(numCharsInLongestIntValue,
524                HorizontalAlignment.LEFT, intValueLabel));
525    }
526
527
528    // Display the table header, if appropriate.
529    if ((formatter != null) && (outputFormat == OutputFormat.COLUMNS))
530    {
531      for (final String line : formatter.getHeaderLines(true))
532      {
533        out(line);
534      }
535    }
536
537
538    // Display the main output.
539    final Collection<ResultCode> resultCodes;
540    if ((alphabeticOrderArg != null) && alphabeticOrderArg.isPresent())
541    {
542      resultCodes = resultCodesByName.values();
543    }
544    else
545    {
546      resultCodes = resultCodesByIntValue.values();
547    }
548
549    for (final ResultCode rc : resultCodes)
550    {
551      if (formatter == null)
552      {
553        final JSONObject jsonObject = new JSONObject(
554             new JSONField(JSON_FIELD_NAME, rc.getName()),
555             new JSONField(JSON_FIELD_INT_VALUE, rc.intValue()));
556        out(jsonObject.toSingleLineString());
557      }
558      else
559      {
560        out(formatter.formatRow(rc.getName(), rc.intValue()));
561      }
562    }
563
564    return ResultCode.SUCCESS;
565  }
566
567
568
569  /**
570   * {@inheritDoc}
571   */
572  @Override()
573  @NotNull()
574  public LinkedHashMap<String[],String> getExampleUsages()
575  {
576    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
577
578    examples.put(
579         StaticUtils.NO_STRINGS,
580         INFO_LDAP_RC_EXAMPLE_1.get());
581
582    examples.put(
583         new String[]
584         {
585           "--int-value", "49",
586           "--output-format", "json"
587         },
588         INFO_LDAP_RC_EXAMPLE_2.get());
589
590    examples.put(
591         new String[]
592         {
593           "--search", "attribute"
594         },
595         INFO_LDAP_RC_EXAMPLE_3.get());
596
597    return examples;
598  }
599}