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 supportsDebugLogging()
345  {
346    return true;
347  }
348
349
350
351  /**
352   * {@inheritDoc}
353   */
354  @Override()
355  protected boolean logToolInvocationByDefault()
356  {
357    return false;
358  }
359
360
361
362  /**
363   * {@inheritDoc}
364   */
365  @Override()
366  public void addToolArguments(@NotNull final ArgumentParser parser)
367         throws ArgumentException
368  {
369    this.parser = parser;
370
371    schemaPathArg = new FileArgument(null, "schema-path", false, 0, null,
372         INFO_OID_LOOKUP_ARG_DESC_SCHEMA_PATH.get(), true, true, false, false);
373    schemaPathArg.addLongIdentifier("schemaPath", true);
374    schemaPathArg.addLongIdentifier("schema-file", true);
375    schemaPathArg.addLongIdentifier("schemaFile", true);
376    schemaPathArg.addLongIdentifier("schema-directory", true);
377    schemaPathArg.addLongIdentifier("schemaDirectory", true);
378    schemaPathArg.addLongIdentifier("schema-dir", true);
379    schemaPathArg.addLongIdentifier("schemaDir", true);
380    schemaPathArg.addLongIdentifier("schema", true);
381    parser.addArgument(schemaPathArg);
382
383
384    outputFormatArg = new StringArgument(null, "output-format", false, 1,
385         INFO_OID_LOOKUP_ARG_PLACEHOLDER_OUTPUT_FORMAT.get(),
386         INFO_OID_LOOKUP_ARG_DESC_OUTPUT_FORMAT.get(),
387         StaticUtils.setOf(
388              OUTPUT_FORMAT_CSV,
389              OUTPUT_FORMAT_JSON,
390              OUTPUT_FORMAT_MULTI_LINE,
391              OUTPUT_FORMAT_TAB_DELIMITED),
392         OUTPUT_FORMAT_MULTI_LINE);
393    outputFormatArg.addLongIdentifier("outputFormat", true);
394    outputFormatArg.addLongIdentifier("format", true);
395    parser.addArgument(outputFormatArg);
396
397
398    exactMatchArg = new BooleanArgument(null, "exact-match", 1,
399         INFO_OID_LOOKUP_ARG_DESC_EXACT_MATCH.get());
400    exactMatchArg.addLongIdentifier("exactMatch", true);
401    exactMatchArg.addLongIdentifier("exact", true);
402    parser.addArgument(exactMatchArg);
403
404
405    terseArg = new BooleanArgument(null, "terse", 1,
406         INFO_OID_LOOKUP_ARG_DESC_TERSE.get());
407    parser.addArgument(terseArg);
408  }
409
410
411
412  /**
413   * {@inheritDoc}
414   */
415  @Override()
416  @NotNull()
417  public ResultCode doToolProcessing()
418  {
419    // Get a reference to the default OID registry.
420    OIDRegistry oidRegistry = OIDRegistry.getDefault();
421
422
423    // If any schema paths were provided, then read the schema(s) and use that
424    // to augment the default OID registry.  If not, and if this tool is running
425    // from a Ping Identity Directory Server installation, then see if we can
426    // use its default schema.
427    List<File> schemaPaths = Collections.emptyList();
428    if ((schemaPathArg != null) && (schemaPathArg.isPresent()))
429    {
430      schemaPaths = schemaPathArg.getValues();
431    }
432    else
433    {
434      try
435      {
436        final File instanceRoot = InternalSDKHelper.getPingIdentityServerRoot();
437        if (instanceRoot != null)
438        {
439          final File instanceRootSchemaDir =
440               StaticUtils.constructPath(instanceRoot, "config", "schema");
441          if (new File(instanceRootSchemaDir, "00-core.ldif").exists())
442          {
443            schemaPaths = Collections.singletonList(instanceRootSchemaDir);
444          }
445        }
446      }
447      catch (final Throwable t)
448      {
449        // This is fine.  We're just not running with access to a Ping Identity
450        // Directory Server.
451      }
452    }
453
454    if (! schemaPaths.isEmpty())
455    {
456      try
457      {
458        oidRegistry = augmentOIDRegistry(oidRegistry, schemaPaths);
459      }
460      catch (final LDAPException e)
461      {
462        Debug.debugException(e);
463        wrapErr(0, WRAP_COLUMN, e.getMessage());
464        return e.getResultCode();
465      }
466    }
467
468
469    // See if there is a search string.  If so, then identify the appropriate
470    // set of matching OID registry items.  Otherwise, just grab everything in
471    // the OID registry.
472    final Collection<OIDRegistryItem> matchingItems;
473    if ((parser != null) && (! parser.getTrailingArguments().isEmpty()))
474    {
475      matchingItems = new ArrayList<>();
476      final String lowerSearchString =
477           StaticUtils.toLowerCase(parser.getTrailingArguments().get(0));
478      for (final OIDRegistryItem item : oidRegistry.getItems().values())
479      {
480        if (itemMatchesSearchString(item, lowerSearchString,
481             exactMatchArg.isPresent()))
482        {
483          matchingItems.add(item);
484        }
485      }
486    }
487    else
488    {
489      matchingItems = oidRegistry.getItems().values();
490    }
491
492
493    // If there weren't any matches, then display a message if appropriate.
494    boolean json = false;
495    ColumnFormatter columnFormatter = null;
496    if ((outputFormatArg != null) && outputFormatArg.isPresent())
497    {
498      final String outputFormat = outputFormatArg.getValue();
499      if (outputFormat != null)
500      {
501        if (outputFormat.equalsIgnoreCase(OUTPUT_FORMAT_CSV))
502        {
503          columnFormatter = new ColumnFormatter(false, null,
504               OutputFormat.CSV, null,
505               new FormattableColumn(1, HorizontalAlignment.LEFT, "OID"),
506               new FormattableColumn(1, HorizontalAlignment.LEFT, "Name"),
507               new FormattableColumn(1, HorizontalAlignment.LEFT, "Type"),
508               new FormattableColumn(1, HorizontalAlignment.LEFT, "Origin"),
509               new FormattableColumn(1, HorizontalAlignment.LEFT, "URL"));
510        }
511        else if (outputFormat.equalsIgnoreCase(OUTPUT_FORMAT_JSON))
512        {
513          json = true;
514        }
515        else if (outputFormat.equalsIgnoreCase(OUTPUT_FORMAT_TAB_DELIMITED))
516        {
517          columnFormatter = new ColumnFormatter(false, null,
518               OutputFormat.TAB_DELIMITED_TEXT, null,
519               new FormattableColumn(1, HorizontalAlignment.LEFT, "OID"),
520               new FormattableColumn(1, HorizontalAlignment.LEFT, "Name"),
521               new FormattableColumn(1, HorizontalAlignment.LEFT, "Type"),
522               new FormattableColumn(1, HorizontalAlignment.LEFT, "Origin"),
523               new FormattableColumn(1, HorizontalAlignment.LEFT, "URL"));
524        }
525      }
526    }
527
528
529    final int numMatches = matchingItems.size();
530    switch (numMatches)
531    {
532      case 0:
533        wrapComment(WARN_OID_LOOKUP_NO_MATCHES.get());
534        return ResultCode.NO_RESULTS_RETURNED;
535
536      case 1:
537        wrapComment(INFO_OID_LOOKUP_ONE_MATCH.get());
538        break;
539
540      default:
541        wrapComment(INFO_OID_LOOKUP_MULTIPLE_MATCHES.get(numMatches));
542        break;
543    }
544
545
546    if (columnFormatter != null)
547    {
548      for (final String line : columnFormatter.getHeaderLines(false))
549      {
550        out(line);
551      }
552    }
553
554
555    for (final OIDRegistryItem item : matchingItems)
556    {
557      if (json)
558      {
559        out(item.asJSONObject().toSingleLineString());
560      }
561      else if (columnFormatter != null)
562      {
563        out(columnFormatter.formatRow(item.getOID(), item.getName(),
564             item.getType(), item.getOrigin(), item.getURL()));
565      }
566      else
567      {
568        out();
569        out(INFO_OID_LOOKUP_OUTPUT_LINE_OID.get(item.getOID()));
570        out(INFO_OID_LOOKUP_OUTPUT_LINE_NAME.get(item.getName()));
571        out(INFO_OID_LOOKUP_OUTPUT_LINE_TYPE.get(item.getType()));
572
573        final String origin = item.getOrigin();
574        if (origin != null)
575        {
576          out(INFO_OID_LOOKUP_OUTPUT_LINE_ORIGIN.get(origin));
577        }
578
579        final String url = item.getURL();
580        if (url != null)
581        {
582          out(INFO_OID_LOOKUP_OUTPUT_LINE_URL.get(url));
583        }
584      }
585    }
586
587    return ResultCode.SUCCESS;
588  }
589
590
591
592  /**
593   * Retrieves a copy of the provided OID registry that has been augmented with
594   * information read from a specified set of schema files.
595   *
596   * @param  registry     The OID registry to be augmented.
597   * @param  schemaPaths  The paths containing the schema information to use to
598   *                      augment the default registry.
599   *
600   * @return  The augmented OID registry.
601   *
602   * @throws  LDAPException  If a problem occurs while trying to read and parse
603   *                         the schema.
604   */
605  @NotNull()
606  private static OIDRegistry augmentOIDRegistry(
607               @NotNull final OIDRegistry registry,
608               @NotNull final List<File> schemaPaths)
609          throws LDAPException
610  {
611    OIDRegistry oidRegistry = registry;
612    for (final File schemaPath : schemaPaths)
613    {
614      if (schemaPath.isFile())
615      {
616        oidRegistry = augmentOIDRegistry(oidRegistry, schemaPath);
617      }
618      else if (schemaPath.isDirectory())
619      {
620        final File[] files = schemaPath.listFiles();
621        if (files != null)
622        {
623          final TreeMap<String,File> fileMap = new TreeMap<>();
624          for (final File f : files)
625          {
626            fileMap.put(f.getName(), f);
627          }
628
629          for (final File f : fileMap.values())
630          {
631            oidRegistry = augmentOIDRegistry(oidRegistry, f);
632          }
633        }
634      }
635    }
636
637    return oidRegistry;
638  }
639
640
641
642  /**
643   * Retrieves a copy of the provided OID registry that has been augmented with
644   * information read from the specified schema file.
645   *
646   * @param  registry    The OID registry to be augmented.
647   * @param  schemaFile  The file from which to read the schema elements.
648   *
649   * @return  The augmented OID registry.
650   *
651   * @throws  LDAPException  If a problem occurs while trying to read and parse
652   *                         the schema.
653 */
654  @NotNull()
655  private static OIDRegistry augmentOIDRegistry(
656               @NotNull final OIDRegistry registry,
657               @NotNull final File schemaFile)
658          throws LDAPException
659  {
660    final Schema schema;
661    try
662    {
663      schema = Schema.getSchema(schemaFile);
664    }
665    catch (final Exception e)
666    {
667      Debug.debugException(e);
668      throw new LDAPException(ResultCode.LOCAL_ERROR,
669           ERR_OID_LOOKUP_CANNOT_GET_SCHEMA_FROM_FILE.get(
670                schemaFile.getAbsolutePath(),
671                StaticUtils.getExceptionMessage(e)),
672           e);
673    }
674
675    if (schema == null)
676    {
677      return registry;
678    }
679    else
680    {
681      return registry.withSchema(schema);
682    }
683  }
684
685
686
687  /**
688   * Determines whether the provided OID registry item matches the given search
689   * string.
690   *
691   * @param  item               The item for which to make the determination.
692   * @param  lowerSearchString  The search string to match against the item.  It
693   *                            must have already been converted to lowercase.
694   * @param  exactMatch         Indicates whether to use exact matching (if
695   *                            {@code true}) or substring matching (if
696   *                            {@code false}).
697   *
698   * @return  {@code true} if the provided item matches the given search string,
699   *          or {@code false} if not.
700   */
701  private static boolean itemMatchesSearchString(
702               @NotNull final OIDRegistryItem item,
703               @NotNull final String lowerSearchString,
704               final boolean exactMatch)
705  {
706    return (matches(item.getOID(), lowerSearchString, exactMatch) ||
707         matches(item.getName(), lowerSearchString, exactMatch) ||
708         matches(item.getType(), lowerSearchString, exactMatch) ||
709         matches(item.getOrigin(), lowerSearchString, exactMatch) ||
710         matches(item.getURL(), lowerSearchString, exactMatch));
711  }
712
713
714
715  /**
716   * Indicates whether the provided item matches the given search string.
717   *
718   * @param  itemString         A string from the registry item being
719   *                            considered.  It may be {@code null}, and it may
720   *                            be mixed-case.
721   * @param  lowerSearchString  The search string to match against the item.  It
722   *                            must have already been converted to lowercase.
723   * @param  exactMatch         Indicates whether to use exact matching (if
724   *                            {@code true}) or substring matching (if
725   *                            {@code false}).
726   *
727   * @return  {@code true} if the provided item string matches the given search
728   *          string, or {@code false} if not.
729   */
730  private static boolean matches(@Nullable final String itemString,
731                                 @NotNull final String lowerSearchString,
732                                 final boolean exactMatch)
733  {
734    if (itemString == null)
735    {
736      return false;
737    }
738
739    final String lowerItemString = StaticUtils.toLowerCase(itemString);
740    if (exactMatch)
741    {
742      return lowerItemString.equals(lowerSearchString);
743    }
744    else
745    {
746      return lowerItemString.contains(lowerSearchString);
747    }
748  }
749
750
751
752  /**
753   * Writes a wrapped version of the provided message with each line preceded
754   * by "# " to indicate that it is a comment.  No output will be written if the
755   * tool is running in terse mode.
756   *
757   * @param  message  The message to write as a comment.
758   */
759  private void wrapComment(@NotNull final String message)
760  {
761    final boolean terse = ((terseArg != null) && terseArg.isPresent());
762    if (! terse)
763    {
764      for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
765      {
766        out("# ", line);
767      }
768    }
769  }
770
771
772
773  /**
774   * {@inheritDoc}
775   */
776  @Override()
777  @NotNull()
778  public LinkedHashMap<String[],String> getExampleUsages()
779  {
780    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
781
782    examples.put(
783         StaticUtils.NO_STRINGS,
784         INFO_OID_LOOKUP_EXAMPLE_1.get());
785
786    examples.put(
787         new String[]
788         {
789           "--output-format", "json",
790           "2.5.4.3"
791         },
792         INFO_OID_LOOKUP_EXAMPLE_2.get());
793
794    return examples;
795  }
796}