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}