/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions copyright 2012-2016 ForgeRock AS. */ package com.forgerock.opendj.cli; import static com.forgerock.opendj.cli.ArgumentConstants.*; import static com.forgerock.opendj.cli.CliMessages.*; import static com.forgerock.opendj.cli.DocGenerationHelper.*; import static com.forgerock.opendj.cli.Utils.*; import static com.forgerock.opendj.util.StaticUtils.*; import java.io.File; import java.io.FileInputStream; import java.io.OutputStream; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageBuilder; import org.forgerock.i18n.slf4j.LocalizedLogger; /** * This class defines a utility that can be used to deal with command-line * arguments for applications in a CLIP-compliant manner using either short * one-character or longer word-based arguments. It is also integrated with the * Directory Server message catalog so that it can display messages in an * internationalizable format, can automatically generate usage information, * can detect conflicts between arguments, and can interact with a properties * file to obtain default values for arguments there if they are not specified * on the command-line. */ public class ArgumentParser implements ToolRefDocContainer { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); private static final Set HOST_LONG_IDENTIFIERS = new HashSet<>(Arrays.asList( OPTION_LONG_HOST, OPTION_LONG_REFERENCED_HOST_NAME, "host1", "host2", "hostSource", "hostDestination")); /** * The name of the OpenDJ configuration direction in the user home * directory. */ public static final String DEFAULT_OPENDJ_CONFIG_DIR = ".opendj"; /** The default properties file name. */ public static final String DEFAULT_OPENDJ_PROPERTIES_FILE_NAME = "tools"; /** The default properties file extension. */ public static final String DEFAULT_OPENDJ_PROPERTIES_FILE_EXTENSION = ".properties"; /** The name of a command-line script used to launch a tool. */ public static final String PROPERTY_SCRIPT_NAME = "com.forgerock.opendj.ldap.tools.scriptName"; /** The legacy name of a command-line script used to launch a tool. */ public static final String PROPERTY_SCRIPT_NAME_LEGACY = "org.opends.server.scriptName"; /** The argument that will be used to indicate the file properties. */ private StringArgument filePropertiesPathArgument; /** The argument that will be used to indicate that we'll not look for default properties file. */ private BooleanArgument noPropertiesFileArgument; /** The argument that will be used to trigger the display of usage information. */ private Argument usageArgument; /** The argument that will be used to trigger the display of the OpenDJ version. */ private Argument versionArgument; /** The set of unnamed trailing arguments that were provided for this parser. */ private final ArrayList trailingArguments = new ArrayList<>(); /** * Indicates whether this parser will allow additional unnamed arguments at * the end of the list. */ private final boolean allowsTrailingArguments; /** Indicates whether long arguments should be treated in a case-sensitive manner. */ private final boolean longArgumentsCaseSensitive; /** Indicates whether the usage or version information has been displayed. */ private boolean usageOrVersionDisplayed; /** Indicates whether the version argument was provided. */ private boolean versionPresent; /** The handler to call to print the product version. */ private VersionHandler versionHandler = new VersionHandler() { @Override public void printVersion() { // display nothing at all } @Override public String toString() { return ""; } }; /** The set of arguments defined for this parser, referenced by short ID. */ private final Map shortIDMap = new HashMap<>(); /** The set of arguments defined for this parser, referenced by long ID. */ private final Map longIDMap = new HashMap<>(); /** The total set of arguments defined for this parser. */ private final List argumentList = new LinkedList<>(); /** The maximum number of unnamed trailing arguments that may be provided. */ private final int maxTrailingArguments; /** The minimum number of unnamed trailing arguments that may be provided. */ private final int minTrailingArguments; /** The output stream to which usage information should be printed. */ private OutputStream usageOutputStream = System.out; /** * The fully-qualified name of the Java class that should be invoked to * launch the program with which this argument parser is associated. */ private final String mainClassName; /** * A human-readable description for the tool, which will be included when * displaying usage information. */ private final LocalizableMessage toolDescription; /** A short description for this tool, suitable in a man page summary line. */ private LocalizableMessage shortToolDescription; /** The display name that will be used for the trailing arguments in the usage information. */ private final String trailingArgsDisplayName; /** Set of argument groups. */ protected final Set argumentGroups = new TreeSet<>(); /** * Group for arguments that have not been explicitly grouped. These will * appear at the top of the usage statement without a header. */ private final ArgumentGroup defaultArgGroup = new ArgumentGroup( LocalizableMessage.EMPTY, Integer.MAX_VALUE); /** * Group for arguments that are related to connection through LDAP. This * includes options like the bind DN, the port, etc. */ final ArgumentGroup ldapArgGroup = new ArgumentGroup( INFO_DESCRIPTION_LDAP_CONNECTION_ARGS.get(), Integer.MIN_VALUE + 2); /** * Group for arguments that are related to utility input/output like * properties file, no-prompt etc. These will appear toward the bottom of * the usage statement. */ protected final ArgumentGroup ioArgGroup = new ArgumentGroup( INFO_DESCRIPTION_IO_ARGS.get(), Integer.MIN_VALUE + 1); /** * Group for arguments that are general like help, version etc. These will * appear at the end of the usage statement. */ private final ArgumentGroup generalArgGroup = new ArgumentGroup( INFO_DESCRIPTION_GENERAL_ARGS.get(), Integer.MIN_VALUE); private static final String INDENT = " "; /** * Creates a new instance of this argument parser with no arguments. Unnamed * trailing arguments will not be allowed. * * @param mainClassName * The fully-qualified name of the Java class that should be * invoked to launch the program with which this argument parser * is associated. * @param toolDescription * A human-readable description for the tool, which will be * included when displaying usage information. * @param longArgumentsCaseSensitive * Indicates whether long arguments should be treated in a * case-sensitive manner. */ public ArgumentParser(final String mainClassName, final LocalizableMessage toolDescription, final boolean longArgumentsCaseSensitive) { this.mainClassName = mainClassName; this.toolDescription = toolDescription; this.longArgumentsCaseSensitive = longArgumentsCaseSensitive; allowsTrailingArguments = false; trailingArgsDisplayName = null; maxTrailingArguments = 0; minTrailingArguments = 0; initGroups(); } /** * Creates a new instance of this argument parser with no arguments that may * or may not be allowed to have unnamed trailing arguments. * * @param mainClassName * The fully-qualified name of the Java class that should be * invoked to launch the program with which this argument parser * is associated. * @param toolDescription * A human-readable description for the tool, which will be * included when displaying usage information. * @param longArgumentsCaseSensitive * Indicates whether long arguments should be treated in a * case-sensitive manner. * @param allowsTrailingArguments * Indicates whether this parser allows unnamed trailing * arguments to be provided. * @param minTrailingArguments * The minimum number of unnamed trailing arguments that must be * provided. A value less than or equal to zero indicates that no * minimum will be enforced. * @param maxTrailingArguments * The maximum number of unnamed trailing arguments that may be * provided. A value less than or equal to zero indicates that no * maximum will be enforced. * @param trailingArgsDisplayName * The display name that should be used as a placeholder for * unnamed trailing arguments in the generated usage information. */ public ArgumentParser(final String mainClassName, final LocalizableMessage toolDescription, final boolean longArgumentsCaseSensitive, final boolean allowsTrailingArguments, final int minTrailingArguments, final int maxTrailingArguments, final String trailingArgsDisplayName) { this.mainClassName = mainClassName; this.toolDescription = toolDescription; this.longArgumentsCaseSensitive = longArgumentsCaseSensitive; this.allowsTrailingArguments = allowsTrailingArguments; this.minTrailingArguments = minTrailingArguments; this.maxTrailingArguments = maxTrailingArguments; this.trailingArgsDisplayName = trailingArgsDisplayName; initGroups(); } /** * Adds the provided argument to the set of arguments handled by this * parser. * * @param argument * The argument to be added. * @throws ArgumentException * If the provided argument conflicts with another argument that * has already been defined. */ public void addArgument(final Argument argument) throws ArgumentException { addArgument(argument, null); } /** * Adds the provided argument to the set of arguments handled by this * parser. * * @param argument * The argument to be added. * @param group * The argument group to which the argument belongs. * @throws ArgumentException * If the provided argument conflicts with another argument that * has already been defined. */ public void addArgument(final Argument argument, ArgumentGroup group) throws ArgumentException { final Character shortID = argument.getShortIdentifier(); if (shortID != null && shortIDMap.containsKey(shortID)) { final String conflictingID = shortIDMap.get(shortID).getLongIdentifier(); throw new ArgumentException( ERR_ARGPARSER_DUPLICATE_SHORT_ID.get(argument.getLongIdentifier(), shortID, conflictingID)); } // JNR: what is the requirement for the following code? if (versionArgument != null && shortID != null && shortID.equals(versionArgument.getShortIdentifier())) { // Update the version argument to not display its short identifier. try { versionArgument = getVersionArgument(false); // JNR: why not call addGeneralArgument(versionArgument) here? this.generalArgGroup.addArgument(versionArgument); } catch (final ArgumentException e) { // ignore } } final String longID = formatLongIdentifier(argument.getLongIdentifier()); if (longIDMap.containsKey(longID)) { throw new ArgumentException(ERR_ARGPARSER_DUPLICATE_LONG_ID.get(argument.getLongIdentifier())); } if (shortID != null) { shortIDMap.put(shortID, argument); } if (longID != null) { longIDMap.put(longID, argument); } argumentList.add(argument); if (group == null) { group = getStandardGroup(argument); } group.addArgument(argument); argumentGroups.add(group); } private BooleanArgument getVersionArgument(final boolean displayShortIdentifier) throws ArgumentException { return BooleanArgument.builder(OPTION_LONG_PRODUCT_VERSION) .shortIdentifier(displayShortIdentifier ? OPTION_SHORT_PRODUCT_VERSION : null) .description(INFO_DESCRIPTION_PRODUCT_VERSION.get()) .buildArgument(); } /** * Adds the provided argument to the set of arguments handled by this parser * and puts the argument in the default group. * * @param argument * The argument to be added. * @throws ArgumentException * If the provided argument conflicts with another argument that * has already been defined. */ protected void addDefaultArgument(final Argument argument) throws ArgumentException { addArgument(argument, defaultArgGroup); } /** * Adds the provided argument to the set of arguments handled by this parser * and puts the argument in the LDAP connection group. * * @param argument * The argument to be added. * @throws ArgumentException * If the provided argument conflicts with another argument that * has already been defined. */ public void addLdapConnectionArgument(final Argument argument) throws ArgumentException { addArgument(argument, ldapArgGroup); } /** * Indicates whether this parser will allow unnamed trailing arguments. * These will be arguments at the end of the list that are not preceded by * either a long or short identifier and will need to be manually parsed by * the application using this parser. Note that once an unnamed trailing * argument has been identified, all remaining arguments will be classified * as such. * * @return true if this parser allows unnamed trailing * arguments, or false if it does not. */ boolean allowsTrailingArguments() { return allowsTrailingArguments; } /** * Check if we have a properties file. * * @return The properties found in the properties file or null. * @throws ArgumentException * If a problem was encountered while parsing the provided * arguments. */ Properties checkExternalProperties() throws ArgumentException { // We don't look for properties file. if (noPropertiesFileArgument != null && noPropertiesFileArgument.isPresent()) { return null; } // Check if we have a properties file argument if (filePropertiesPathArgument == null) { return null; } // check if the properties file argument has been set. If not look for default location. String propertiesFilePath; if (filePropertiesPathArgument.isPresent()) { propertiesFilePath = filePropertiesPathArgument.getValue(); } else { // Check in "user home"/.opendj directory final String userDir = System.getProperty("user.home"); propertiesFilePath = findPropertiesFile(userDir + File.separator + DEFAULT_OPENDJ_CONFIG_DIR); } // We don't have a properties file location if (propertiesFilePath == null) { return null; } // We have a location for the properties file. try { final Properties argumentProperties = new Properties(); final String scriptName = getScriptName(); final Properties p = new Properties(); try (final FileInputStream fis = new FileInputStream(propertiesFilePath)) { p.load(fis); } for (final Enumeration e = p.propertyNames(); e.hasMoreElements();) { final String currentPropertyName = (String) e.nextElement(); String propertyName = currentPropertyName; // Property name form