/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions copyright 2012-2015 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.Utils.*; import static com.forgerock.opendj.util.StaticUtils.*; import java.io.File; import java.io.FileInputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; 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; import org.forgerock.util.Utils; /** * 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 { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * 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; /** The set of arguments defined for this parser, referenced by short ID. */ private final HashMap shortIDMap = new HashMap(); /** The set of arguments defined for this parser, referenced by long ID. */ private final HashMap longIDMap = new HashMap(); /** The set of arguments defined for this parser, referenced by argument name. */ private final HashMap argumentMap = new HashMap(); /** The total set of arguments defined for this parser. */ private final LinkedList 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; /** * The display name that will be used for the trailing arguments in the * usage information. */ private final String trailingArgsDisplayName; /** The raw set of command-line arguments that were provided. */ private String[] rawArguments; /** 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 conflictingName = shortIDMap.get(shortID).getName(); throw new ArgumentException( ERR_ARGPARSER_DUPLICATE_SHORT_ID.get(argument.getName(), shortID, conflictingName)); } // 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 } } String longID = argument.getLongIdentifier(); if (longID != null) { if (!longArgumentsCaseSensitive) { longID = toLowerCase(longID); } if (longIDMap.containsKey(longID)) { final String conflictingName = longIDMap.get(longID).getName(); throw new ArgumentException(ERR_ARGPARSER_DUPLICATE_LONG_ID.get( argument.getName(), argument.getLongIdentifier(), conflictingName)); } } 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 new BooleanArgument(OPTION_LONG_PRODUCT_VERSION, displayShortIdentifier ? OPTION_SHORT_PRODUCT_VERSION : null, OPTION_LONG_PRODUCT_VERSION, INFO_DESCRIPTION_PRODUCT_VERSION.get()); } /** * 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 = null; 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. final Properties argumentProperties = new Properties(); final String scriptName = getScriptName(); try { final Properties p = new Properties(); final FileInputStream fis = new FileInputStream(propertiesFilePath); p.load(fis); fis.close(); for (final Enumeration e = p.propertyNames(); e.hasMoreElements();) { final String currentPropertyName = (String) e.nextElement(); String propertyName = currentPropertyName; // Property name form