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}