/* * 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-2014 ForgeRock AS. */ package com.forgerock.opendj.cli; import static com.forgerock.opendj.cli.CliMessages.*; import static com.forgerock.opendj.cli.CliConstants.*; import static com.forgerock.opendj.cli.Utils.PROPERTY_SCRIPT_NAME; import static com.forgerock.opendj.cli.Utils.wrapText; import static com.forgerock.opendj.util.StaticUtils.EOL; import static com.forgerock.opendj.util.StaticUtils.getBytes; import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage; import static com.forgerock.opendj.util.StaticUtils.toLowerCase; import java.io.File; import java.io.FileInputStream; import java.io.IOException; 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; /** * 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 { /** * 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; // 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 set of arguments defined for this parser, referenced by short // ID. private final HashMap shortIDMap; // The set of arguments defined for this parser, referenced by // argument name. private final HashMap argumentMap; // The set of arguments defined for this parser, referenced by long // ID. private final HashMap longIDMap; // 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 total set of arguments defined for this parser. private final LinkedList argumentList; // The output stream to which usage information should be printed. private OutputStream usageOutputStream; // 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 Set argumentGroups; /** * 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. */ private 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. */ private 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 final static String INDENT = " "; private final static int MAX_LENGTH = 80; /** * 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; argumentList = new LinkedList(); argumentMap = new HashMap(); shortIDMap = new HashMap(); longIDMap = new HashMap(); allowsTrailingArguments = false; usageOrVersionDisplayed = false; versionPresent = false; trailingArgsDisplayName = null; maxTrailingArguments = 0; minTrailingArguments = 0; trailingArguments = new ArrayList(); rawArguments = null; usageArgument = null; filePropertiesPathArgument = null; noPropertiesFileArgument = null; usageOutputStream = System.out; 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; argumentList = new LinkedList(); argumentMap = new HashMap(); shortIDMap = new HashMap(); longIDMap = new HashMap(); trailingArguments = new ArrayList(); usageOrVersionDisplayed = false; versionPresent = false; rawArguments = null; usageArgument = null; usageOutputStream = System.out; 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(); final LocalizableMessage message = ERR_ARGPARSER_DUPLICATE_SHORT_ID.get(argument.getName(), String .valueOf(shortID), conflictingName); throw new ArgumentException(message); } if (versionArgument != null) { if (shortID != null && shortID.equals(versionArgument.getShortIdentifier())) { // Update the version argument to not display its short // identifier. try { versionArgument = new BooleanArgument(OPTION_LONG_PRODUCT_VERSION, null, OPTION_LONG_PRODUCT_VERSION, INFO_DESCRIPTION_PRODUCT_VERSION .get()); 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(); final LocalizableMessage message = ERR_ARGPARSER_DUPLICATE_LONG_ID.get(argument.getName(), argument .getLongIdentifier(), conflictingName); throw new ArgumentException(message); } } 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); } /** * Adds the provided argument to the set of arguments handled by this parser * and puts the arguement 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. */ 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 arguement in the general group. * * @param argument * The argument to be added. * @throws ArgumentException * If the provided argument conflicts with another argument that * has already been defined. */ void addGeneralArgument(final Argument argument) throws ArgumentException { addArgument(argument, generalArgGroup); } /** * Adds the provided argument to the set of arguments handled by this parser * and puts the argument in the input/output group. * * @param argument * The argument to be added. * @throws ArgumentException * If the provided argument conflicts with another argument that * has already been defined. */ void addInputOutputArgument(final Argument argument) throws ArgumentException { addArgument(argument, ioArgGroup); } /** * 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 = System.getProperty(PROPERTY_SCRIPT_NAME); 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