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.File;
041import java.io.OutputStream;
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.Collections;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.TreeMap;
048
049import com.unboundid.ldap.sdk.InternalSDKHelper;
050import com.unboundid.ldap.sdk.LDAPException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.ldap.sdk.Version;
053import com.unboundid.ldap.sdk.schema.Schema;
054import com.unboundid.util.ColumnFormatter;
055import com.unboundid.util.CommandLineTool;
056import com.unboundid.util.Debug;
057import com.unboundid.util.FormattableColumn;
058import com.unboundid.util.HorizontalAlignment;
059import com.unboundid.util.NotNull;
060import com.unboundid.util.Nullable;
061import com.unboundid.util.OIDRegistry;
062import com.unboundid.util.OIDRegistryItem;
063import com.unboundid.util.OutputFormat;
064import com.unboundid.util.StaticUtils;
065import com.unboundid.util.ThreadSafety;
066import com.unboundid.util.ThreadSafetyLevel;
067import com.unboundid.util.args.ArgumentException;
068import com.unboundid.util.args.ArgumentParser;
069import com.unboundid.util.args.BooleanArgument;
070import com.unboundid.util.args.FileArgument;
071import com.unboundid.util.args.StringArgument;
072
073import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
074
075
076
077/**
078 * This class provides a command-line tool that can be used to search the OID
079 * registry to retrieve information an item with a specified OID or name.
080 * <BR>
081 * <BLOCKQUOTE>
082 *   <B>NOTE:</B>  This class, and other classes within the
083 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
084 *   supported for use against Ping Identity, UnboundID, and
085 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
086 *   for proprietary functionality or for external specifications that are not
087 *   considered stable or mature enough to be guaranteed to work in an
088 *   interoperable way with other types of LDAP servers.
089 * </BLOCKQUOTE>
090 */
091@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
092public final class OIDLookup
093       extends CommandLineTool
094{
095  /**
096   * The column at which long lines of output should be wrapped.
097   */
098  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
099
100
101
102  /**
103   * The output format value that indicates that output should be generated as
104   * comma-separated values.
105   */
106  @NotNull private static final String OUTPUT_FORMAT_CSV = "csv";
107
108
109
110  /**
111   * The output format value that indicates that output should be generated as
112   * JSON objects.
113   */
114  @NotNull private static final String OUTPUT_FORMAT_JSON = "json";
115
116
117
118  /**
119   * The output format value that indicates that output should be generated as
120   * multi-line text.
121   */
122  @NotNull private static final String OUTPUT_FORMAT_MULTI_LINE = "multi-line";
123
124
125
126  /**
127   * The output format value that indicates that output should be generated as
128   * tab-delimited text.
129   */
130  @NotNull private static final String OUTPUT_FORMAT_TAB_DELIMITED =
131       "tab-delimited";
132
133
134
135  // The argument parser used by this tool.
136  @Nullable private ArgumentParser parser;
137
138  // The argument used to indicate that only exact matches should be returned.
139  @Nullable private BooleanArgument exactMatchArg;
140
141  // The argument used to request terse output.
142  @Nullable private BooleanArgument terseArg;
143
144  // The argument used to specify the path to an LDAP schema to use to augment
145  // the default OID registry.
146  @Nullable private FileArgument schemaPathArg;
147
148  // The argument used to specify the output format.
149  @Nullable private StringArgument outputFormatArg;
150
151
152
153  /**
154   * Invokes this tool with the provided set of command-line arguments.
155   *
156   * @param  args  The set of command-line arguments provided to this program.
157   */
158  public static void main(@NotNull final String... args)
159  {
160    final ResultCode resultCode = main(System.out, System.err, args);
161    if (resultCode != ResultCode.SUCCESS)
162    {
163      System.exit(resultCode.intValue());
164    }
165  }
166
167
168
169  /**
170   * Invokes this tool with the provided set of command-line arguments.
171   *
172   * @param  out   The output stream to use for standard output.  It may be
173   *               {@code null} if standard output should be suppressed.
174   * @param  err   The output stream to use for standard error.  It may be
175   *               {@code null} if standard error should be suppressed.
176   * @param  args  The set of command-line arguments provided to this program.
177   *
178   * @return  A result code that indicates whether processing was successful.
179   */
180  @NotNull()
181  public static ResultCode main(@Nullable final OutputStream out,
182                                @Nullable final OutputStream err,
183                                @NotNull final String... args)
184  {
185    final OIDLookup tool = new OIDLookup(out, err);
186    return tool.runTool(args);
187  }
188
189
190
191  /**
192   * Creates an instance of this tool with the provided standard output and
193   * error streams.
194   *
195   * @param  out  The output stream to use for standard output.  It may be
196   *              {@code null} if standard output should be suppressed.
197   * @param  err  The output stream to use for standard error.  It may be
198   *              {@code null} if standard error should be suppressed.
199   */
200  public OIDLookup(@Nullable final OutputStream out,
201                   @Nullable final OutputStream err)
202  {
203    super(out, err);
204
205    parser = null;
206    exactMatchArg = null;
207    terseArg = null;
208    schemaPathArg = null;
209    outputFormatArg = null;
210  }
211
212
213
214  /**
215   * {@inheritDoc}
216   */
217  @Override()
218  @NotNull()
219  public String getToolName()
220  {
221    return "oid-lookup";
222  }
223
224
225
226  /**
227   * {@inheritDoc}
228   */
229  @Override()
230  @NotNull()
231  public String getToolDescription()
232  {
233    return INFO_OID_LOOKUP_TOOL_DESC_1.get();
234  }
235
236
237
238  /**
239   * {@inheritDoc}
240   */
241  @Override()
242  @NotNull()
243  public List<String> getAdditionalDescriptionParagraphs()
244  {
245    return Collections.singletonList(INFO_OID_LOOKUP_TOOL_DESC_2.get());
246  }
247
248
249
250  /**
251   * {@inheritDoc}
252   */
253  @Override()
254  @NotNull()
255  public String getToolVersion()
256  {
257    return Version.NUMERIC_VERSION_STRING;
258  }
259
260
261
262  /**
263   * {@inheritDoc}
264   */
265  @Override()
266  public int getMinTrailingArguments()
267  {
268    return 0;
269  }
270
271
272
273  /**
274   * {@inheritDoc}
275   */
276  @Override()
277  public int getMaxTrailingArguments()
278  {
279    return 1;
280  }
281
282
283
284  /**
285   * {@inheritDoc}
286   */
287  @Override()
288  @NotNull()
289  public String getTrailingArgumentsPlaceholder()
290  {
291    return INFO_OID_LOOKUP_TRAILING_ARG_PLACEHOLDER.get();
292  }
293
294
295
296  /**
297   * {@inheritDoc}
298   */
299  @Override()
300  public boolean supportsInteractiveMode()
301  {
302    return true;
303  }
304
305
306
307  /**
308   * {@inheritDoc}
309   */
310  @Override()
311  public boolean defaultsToInteractiveMode()
312  {
313    return false;
314  }
315
316
317
318  /**
319   * {@inheritDoc}
320   */
321  @Override()
322  public boolean supportsPropertiesFile()
323  {
324    return true;
325  }
326
327
328
329  /**
330   * {@inheritDoc}
331   */
332  @Override()
333  protected boolean supportsOutputFile()
334  {
335    return true;
336  }
337
338
339
340  /**
341   * {@inheritDoc}
342   */
343  @Override()
344  protected boolean logToolInvocationByDefault()
345  {
346    return false;
347  }
348
349
350
351  /**
352   * {@inheritDoc}
353   */
354  @Override()
355  public void addToolArguments(@NotNull final ArgumentParser parser)
356         throws ArgumentException
357  {
358    this.parser = parser;
359
360    schemaPathArg = new FileArgument(null, "schema-path", false, 0, null,
361         INFO_OID_LOOKUP_ARG_DESC_SCHEMA_PATH.get(), true, true, false, false);
362    schemaPathArg.addLongIdentifier("schemaPath", true);
363    schemaPathArg.addLongIdentifier("schema-file", true);
364    schemaPathArg.addLongIdentifier("schemaFile", true);
365    schemaPathArg.addLongIdentifier("schema-directory", true);
366    schemaPathArg.addLongIdentifier("schemaDirectory", true);
367    schemaPathArg.addLongIdentifier("schema-dir", true);
368    schemaPathArg.addLongIdentifier("schemaDir", true);
369    schemaPathArg.addLongIdentifier("schema", true);
370    parser.addArgument(schemaPathArg);
371
372
373    outputFormatArg = new StringArgument(null, "output-format", false, 1,
374         INFO_OID_LOOKUP_ARG_PLACEHOLDER_OUTPUT_FORMAT.get(),
375         INFO_OID_LOOKUP_ARG_DESC_OUTPUT_FORMAT.get(),
376         StaticUtils.setOf(
377              OUTPUT_FORMAT_CSV,
378              OUTPUT_FORMAT_JSON,
379              OUTPUT_FORMAT_MULTI_LINE,
380              OUTPUT_FORMAT_TAB_DELIMITED),
381         OUTPUT_FORMAT_MULTI_LINE);
382    outputFormatArg.addLongIdentifier("outputFormat", true);
383    outputFormatArg.addLongIdentifier("format", true);
384    parser.addArgument(outputFormatArg);
385
386
387    exactMatchArg = new BooleanArgument(null, "exact-match", 1,
388         INFO_OID_LOOKUP_ARG_DESC_EXACT_MATCH.get());
389    exactMatchArg.addLongIdentifier("exactMatch", true);
390    exactMatchArg.addLongIdentifier("exact", true);
391    parser.addArgument(exactMatchArg);
392
393
394    terseArg = new BooleanArgument(null, "terse", 1,
395         INFO_OID_LOOKUP_ARG_DESC_TERSE.get());
396    parser.addArgument(terseArg);
397  }
398
399
400
401  /**
402   * {@inheritDoc}
403   */
404  @Override()
405  @NotNull()
406  public ResultCode doToolProcessing()
407  {
408    // Get a reference to the default OID registry.
409    OIDRegistry oidRegistry = OIDRegistry.getDefault();
410
411
412    // If any schema paths were provided, then read the schema(s) and use that
413    // to augment the default OID registry.  If not, and if this tool is running
414    // from a Ping Identity Directory Server installation, then see if we can
415    // use its default schema.
416    List<File> schemaPaths = Collections.emptyList();
417    if ((schemaPathArg != null) && (schemaPathArg.isPresent()))
418    {
419      schemaPaths = schemaPathArg.getValues();
420    }
421    else
422    {
423      try
424      {
425        final File instanceRoot = InternalSDKHelper.getPingIdentityServerRoot();
426        if (instanceRoot != null)
427        {
428          final File instanceRootSchemaDir =
429               StaticUtils.constructPath(instanceRoot, "config", "schema");
430          if (new File(instanceRootSchemaDir, "00-core.ldif").exists())
431          {
432            schemaPaths = Collections.singletonList(instanceRootSchemaDir);
433          }
434        }
435      }
436      catch (final Throwable t)
437      {
438        // This is fine.  We're just not running with access to a Ping Identity
439        // Directory Server.
440      }
441    }
442
443    if (! schemaPaths.isEmpty())
444    {
445      try
446      {
447        oidRegistry = augmentOIDRegistry(oidRegistry, schemaPaths);
448      }
449      catch (final LDAPException e)
450      {
451        Debug.debugException(e);
452        wrapErr(0, WRAP_COLUMN, e.getMessage());
453        return e.getResultCode();
454      }
455    }
456
457
458    // See if there is a search string.  If so, then identify the appropriate
459    // set of matching OID registry items.  Otherwise, just grab everything in
460    // the OID registry.
461    final Collection<OIDRegistryItem> matchingItems;
462    if ((parser != null) && (! parser.getTrailingArguments().isEmpty()))
463    {
464      matchingItems = new ArrayList<>();
465      final String lowerSearchString =
466           StaticUtils.toLowerCase(parser.getTrailingArguments().get(0));
467      for (final OIDRegistryItem item : oidRegistry.getItems().values())
468      {
469        if (itemMatchesSearchString(item, lowerSearchString,
470             exactMatchArg.isPresent()))
471        {
472          matchingItems.add(item);
473        }
474      }
475    }
476    else
477    {
478      matchingItems = oidRegistry.getItems().values();
479    }
480
481
482    // If there weren't any matches, then display a message if appropriate.
483    boolean json = false;
484    ColumnFormatter columnFormatter = null;
485    if ((outputFormatArg != null) && outputFormatArg.isPresent())
486    {
487      final String outputFormat = outputFormatArg.getValue();
488      if (outputFormat != null)
489      {
490        if (outputFormat.equalsIgnoreCase(OUTPUT_FORMAT_CSV))
491        {
492          columnFormatter = new ColumnFormatter(false, null,
493               OutputFormat.CSV, null,
494               new FormattableColumn(1, HorizontalAlignment.LEFT, "OID"),
495               new FormattableColumn(1, HorizontalAlignment.LEFT, "Name"),
496               new FormattableColumn(1, HorizontalAlignment.LEFT, "Type"),
497               new FormattableColumn(1, HorizontalAlignment.LEFT, "Origin"),
498               new FormattableColumn(1, HorizontalAlignment.LEFT, "URL"));
499        }
500        else if (outputFormat.equalsIgnoreCase(OUTPUT_FORMAT_JSON))
501        {
502          json = true;
503        }
504        else if (outputFormat.equalsIgnoreCase(OUTPUT_FORMAT_TAB_DELIMITED))
505        {
506          columnFormatter = new ColumnFormatter(false, null,
507               OutputFormat.TAB_DELIMITED_TEXT, null,
508               new FormattableColumn(1, HorizontalAlignment.LEFT, "OID"),
509               new FormattableColumn(1, HorizontalAlignment.LEFT, "Name"),
510               new FormattableColumn(1, HorizontalAlignment.LEFT, "Type"),
511               new FormattableColumn(1, HorizontalAlignment.LEFT, "Origin"),
512               new FormattableColumn(1, HorizontalAlignment.LEFT, "URL"));
513        }
514      }
515    }
516
517
518    final int numMatches = matchingItems.size();
519    switch (numMatches)
520    {
521      case 0:
522        wrapComment(WARN_OID_LOOKUP_NO_MATCHES.get());
523        return ResultCode.NO_RESULTS_RETURNED;
524
525      case 1:
526        wrapComment(INFO_OID_LOOKUP_ONE_MATCH.get());
527        break;
528
529      default:
530        wrapComment(INFO_OID_LOOKUP_MULTIPLE_MATCHES.get(numMatches));
531        break;
532    }
533
534
535    if (columnFormatter != null)
536    {
537      for (final String line : columnFormatter.getHeaderLines(false))
538      {
539        out(line);
540      }
541    }
542
543
544    for (final OIDRegistryItem item : matchingItems)
545    {
546      if (json)
547      {
548        out(item.asJSONObject().toSingleLineString());
549      }
550      else if (columnFormatter != null)
551      {
552        out(columnFormatter.formatRow(item.getOID(), item.getName(),
553             item.getType(), item.getOrigin(), item.getURL()));
554      }
555      else
556      {
557        out();
558        out(INFO_OID_LOOKUP_OUTPUT_LINE_OID.get(item.getOID()));
559        out(INFO_OID_LOOKUP_OUTPUT_LINE_NAME.get(item.getName()));
560        out(INFO_OID_LOOKUP_OUTPUT_LINE_TYPE.get(item.getType()));
561
562        final String origin = item.getOrigin();
563        if (origin != null)
564        {
565          out(INFO_OID_LOOKUP_OUTPUT_LINE_ORIGIN.get(origin));
566        }
567
568        final String url = item.getURL();
569        if (url != null)
570        {
571          out(INFO_OID_LOOKUP_OUTPUT_LINE_URL.get(url));
572        }
573      }
574    }
575
576    return ResultCode.SUCCESS;
577  }
578
579
580
581  /**
582   * Retrieves a copy of the provided OID registry that has been augmented with
583   * information read from a specified set of schema files.
584   *
585   * @param  registry     The OID registry to be augmented.
586   * @param  schemaPaths  The paths containing the schema information to use to
587   *                      augment the default registry.
588   *
589   * @return  The augmented OID registry.
590   *
591   * @throws  LDAPException  If a problem occurs while trying to read and parse
592   *                         the schema.
593   */
594  @NotNull()
595  private static OIDRegistry augmentOIDRegistry(
596               @NotNull final OIDRegistry registry,
597               @NotNull final List<File> schemaPaths)
598          throws LDAPException
599  {
600    OIDRegistry oidRegistry = registry;
601    for (final File schemaPath : schemaPaths)
602    {
603      if (schemaPath.isFile())
604      {
605        oidRegistry = augmentOIDRegistry(oidRegistry, schemaPath);
606      }
607      else if (schemaPath.isDirectory())
608      {
609        final File[] files = schemaPath.listFiles();
610        if (files != null)
611        {
612          final TreeMap<String,File> fileMap = new TreeMap<>();
613          for (final File f : files)
614          {
615            fileMap.put(f.getName(), f);
616          }
617
618          for (final File f : fileMap.values())
619          {
620            oidRegistry = augmentOIDRegistry(oidRegistry, f);
621          }
622        }
623      }
624    }
625
626    return oidRegistry;
627  }
628
629
630
631  /**
632   * Retrieves a copy of the provided OID registry that has been augmented with
633   * information read from the specified schema file.
634   *
635   * @param  registry    The OID registry to be augmented.
636   * @param  schemaFile  The file from which to read the schema elements.
637   *
638   * @return  The augmented OID registry.
639   *
640   * @throws  LDAPException  If a problem occurs while trying to read and parse
641   *                         the schema.
642 */
643  @NotNull()
644  private static OIDRegistry augmentOIDRegistry(
645               @NotNull final OIDRegistry registry,
646               @NotNull final File schemaFile)
647          throws LDAPException
648  {
649    final Schema schema;
650    try
651    {
652      schema = Schema.getSchema(schemaFile);
653    }
654    catch (final Exception e)
655    {
656      Debug.debugException(e);
657      throw new LDAPException(ResultCode.LOCAL_ERROR,
658           ERR_OID_LOOKUP_CANNOT_GET_SCHEMA_FROM_FILE.get(
659                schemaFile.getAbsolutePath(),
660                StaticUtils.getExceptionMessage(e)),
661           e);
662    }
663
664    if (schema == null)
665    {
666      return registry;
667    }
668    else
669    {
670      return registry.withSchema(schema);
671    }
672  }
673
674
675
676  /**
677   * Determines whether the provided OID registry item matches the given search
678   * string.
679   *
680   * @param  item               The item for which to make the determination.
681   * @param  lowerSearchString  The search string to match against the item.  It
682   *                            must have already been converted to lowercase.
683   * @param  exactMatch         Indicates whether to use exact matching (if
684   *                            {@code true}) or substring matching (if
685   *                            {@code false}).
686   *
687   * @return  {@code true} if the provided item matches the given search string,
688   *          or {@code false} if not.
689   */
690  private static boolean itemMatchesSearchString(
691               @NotNull final OIDRegistryItem item,
692               @NotNull final String lowerSearchString,
693               final boolean exactMatch)
694  {
695    return (matches(item.getOID(), lowerSearchString, exactMatch) ||
696         matches(item.getName(), lowerSearchString, exactMatch) ||
697         matches(item.getType(), lowerSearchString, exactMatch) ||
698         matches(item.getOrigin(), lowerSearchString, exactMatch) ||
699         matches(item.getURL(), lowerSearchString, exactMatch));
700  }
701
702
703
704  /**
705   * Indicates whether the provided item matches the given search string.
706   *
707   * @param  itemString         A string from the registry item being
708   *                            considered.  It may be {@code null}, and it may
709   *                            be mixed-case.
710   * @param  lowerSearchString  The search string to match against the item.  It
711   *                            must have already been converted to lowercase.
712   * @param  exactMatch         Indicates whether to use exact matching (if
713   *                            {@code true}) or substring matching (if
714   *                            {@code false}).
715   *
716   * @return  {@code true} if the provided item string matches the given search
717   *          string, or {@code false} if not.
718   */
719  private static boolean matches(@Nullable final String itemString,
720                                 @NotNull final String lowerSearchString,
721                                 final boolean exactMatch)
722  {
723    if (itemString == null)
724    {
725      return false;
726    }
727
728    final String lowerItemString = StaticUtils.toLowerCase(itemString);
729    if (exactMatch)
730    {
731      return lowerItemString.equals(lowerSearchString);
732    }
733    else
734    {
735      return lowerItemString.contains(lowerSearchString);
736    }
737  }
738
739
740
741  /**
742   * Writes a wrapped version of the provided message with each line preceded
743   * by "# " to indicate that it is a comment.  No output will be written if the
744   * tool is running in terse mode.
745   *
746   * @param  message  The message to write as a comment.
747   */
748  private void wrapComment(@NotNull final String message)
749  {
750    final boolean terse = ((terseArg != null) && terseArg.isPresent());
751    if (! terse)
752    {
753      for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
754      {
755        out("# ", line);
756      }
757    }
758  }
759
760
761
762  /**
763   * {@inheritDoc}
764   */
765  @Override()
766  @NotNull()
767  public LinkedHashMap<String[],String> getExampleUsages()
768  {
769    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
770
771    examples.put(
772         StaticUtils.NO_STRINGS,
773         INFO_OID_LOOKUP_EXAMPLE_1.get());
774
775    examples.put(
776         new String[]
777         {
778           "--output-format", "json",
779           "2.5.4.3"
780         },
781         INFO_OID_LOOKUP_EXAMPLE_2.get());
782
783    return examples;
784  }
785}