/* * RHQ Management Platform * Copyright (C) 2012 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.pluginapi.util; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import com.sun.istack.Nullable; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jetbrains.annotations.NotNull; /** * Parses a java command line and provides easy access to its parts. * <p/> * A Java command line looks like this: * <pre><code> * Usage: java [-options] class [args...] * (to execute a class) * or java [-options] -jar jarfile [args...] * (to execute a jar file) * </code></pre> * <p> * Note that this class offers the subclasses to ehance the parsing process by overriding the {@link #processClassArgument(String, String)} * method. To be able to achieve that, the evaluation of the commandline arguments needs to happen lazily. * See the {@link #parseCommandLine()} method for subclassing guidelines. * <p> * This class is <b>NOT</b> thread-safe. * * @author Ian Springer * @author Lukas Krejci */ public class JavaCommandLine { /** * When parsing command line options, specifies the valid option value delimiter(s). * * @see JavaCommandLine#getClassOption(CommandLineOption) */ public enum OptionValueDelimiter { /** * The option value is separated from the option name by whitespace (hence it is actually a separate command * line argument, e.g. "-f FILE" or "--file FILE". */ WHITESPACE, /** * The option value is separated from the option name by an equals sign, e.g. "-f=FILE" or "--file=FILE". */ EQUALS_SIGN } private static final String SHORT_OPTION_PREFIX = "-"; private static final String LONG_OPTION_PREFIX = "--"; private static final Pattern SYSTEM_PROPERTY_PATTERN = Pattern.compile("-D.+"); private static final Log log = LogFactory.getLog(JavaCommandLine.class); //These properties are passed to the constructors private final List<String> arguments; private final boolean includeSystemPropertiesFromClassArguments; private final Set<OptionValueDelimiter> shortClassOptionValueDelims; private final Set<OptionValueDelimiter> longClassOptionValueDelims; //These are lazily evaluated in the getters private boolean argumentsParsed; private File javaExecutable; private List<String> classPath; private Map<String, String> systemProperties; private List<String> javaOptions; private String mainClassName; private File executableJarFile; private List<String> classArguments; private Map<String, String> shortClassOptionNameToOptionValueMap; private Map<String, String> longClassOptionNameToOptionValueMap; /** * Same as <code>JavaCommandLine(args, false, OptionFormat.POSIX, OptionFormat.POSIX)</code> */ public JavaCommandLine(String... args) { this(args, false); } /** * Same as <code>JavaCommandLine(args, includeSystemPropertiesFromClassArguments, OptionFormat.POSIX, OptionFormat.POSIX)</code> */ public JavaCommandLine(String[] args, boolean includeSystemPropertiesFromClassArguments) { this(args, includeSystemPropertiesFromClassArguments, null, null); } public JavaCommandLine(String[] args, boolean includeSystemPropertiesFromClassArguments, Set<OptionValueDelimiter> shortClassOptionValueDelims, Set<OptionValueDelimiter> longClassOptionValueDelims) { if (args == null) { throw new IllegalArgumentException("'args' parameter is null."); } if (args.length == 0) { throw new IllegalArgumentException("'args' parameter is an empty array."); } this.includeSystemPropertiesFromClassArguments = includeSystemPropertiesFromClassArguments; // Default to GNU-style short options (e.g. "-f FILE"). this.shortClassOptionValueDelims = (shortClassOptionValueDelims != null) ? shortClassOptionValueDelims : EnumSet.of(OptionValueDelimiter.WHITESPACE); if (this.shortClassOptionValueDelims.isEmpty()) { throw new IllegalArgumentException("'shortClassOptionValueDelims' parameter is an empty set."); } // Default to GNU-style long options (e.g. "--file=FILE"). this.longClassOptionValueDelims = (longClassOptionValueDelims != null) ? longClassOptionValueDelims : EnumSet.of(OptionValueDelimiter.EQUALS_SIGN); if (this.longClassOptionValueDelims.isEmpty()) { throw new IllegalArgumentException("'longClassOptionValueDelims' parameter is an empty set."); } // Wrap as list and store as field for use by getArguments() and toString(). this.arguments = Arrays.asList(args); } /** * This method can be called to process the command line from the arguments passed in the constructor. * This is to support lazy evaluation of the parsed properties. * <p> * Any class overriding the {@link #processClassArgument(String, String)} method should make sure to call this method * if it finds that the data extracted in that method is still uninitialized. * <p> * Typically, this will happen during a getter for such data: * <pre> * <code> * public Data getDataExtractedFromCommandLine() { * if (data == null) { * parseCommandLine(); * } * * return data; * } * </code> * </pre> * * The data variable would then be initialized as part of the {@link #processClassArgument(String, String)} method * that gets called during the execution of this method. * <p> * Alternatively to the null check on the data, the subclass can use the {@link #isArgumentsParsed()} method that * returns true only if this method successfully finished. * <p> * If you are overriding this method make sure to call <code>super.parseCommandLine()</code> before any of your other * logic, otherwise you may end up with an <b>endless loop</b> (and eventually stack overflow) if you try to access * any of the getters of the data extracted from the commandline (like {@link #getClassArguments()}, * {@link #getClassPath()}, etc). */ protected void parseCommandLine() { if (log.isDebugEnabled()) { log.debug("Parsing " + this + "..."); } ListIterator<String> argIterator = arguments.listIterator(); ListIterator<String> classArgumentsIterator = arguments.listIterator(); this.javaExecutable = new File(argIterator.next()); this.classPath = new ArrayList<String>(); this.systemProperties = new LinkedHashMap<String, String>(); this.javaOptions = new ArrayList<String>(); this.classArguments = new ArrayList<String>(); boolean nextArgIsClassPath = false; boolean nextArgIsJarFile = false; while (argIterator.hasNext()) { String arg = argIterator.next(); //skip along with the main iterator... once we break out of this loop, this iterator //will point to the start of the class arguments. classArgumentsIterator.next(); if (nextArgIsClassPath) { this.classPath.addAll(Arrays.asList(arg.split(File.pathSeparator))); nextArgIsClassPath = false; } else if (nextArgIsJarFile) { this.executableJarFile = new File(arg); parseClassArguments(argIterator, true); break; } else if (arg.charAt(0) != '-') { this.mainClassName = arg; parseClassArguments(argIterator, true); break; } else if (arg.equals("-cp") || arg.equals("-classpath")) { if (!argIterator.hasNext()) { throw new IllegalArgumentException(arg + " option has no argument."); } nextArgIsClassPath = true; } else if (arg.equals("-jar")) { if (!argIterator.hasNext()) { throw new IllegalArgumentException(arg + " option has no argument."); } nextArgIsJarFile = true; } else { if (isSystemPropertyArgument(arg)) { parseSystemPropertyArgument(arg); } this.javaOptions.add(arg); } } parseClassOptions(); argumentsParsed = true; if (classArgumentsIterator.hasNext()) { parseClassArguments(classArgumentsIterator, false); } this.classPath = Collections.unmodifiableList(this.classPath); this.javaOptions = Collections.unmodifiableList(this.javaOptions); this.classArguments = Collections.unmodifiableList(this.classArguments); this.systemProperties = Collections.unmodifiableMap(this.systemProperties); } /** * @return true iff the {@link #parseCommandLine()} method was called and successfully finished. */ protected boolean isArgumentsParsed() { return argumentsParsed; } private void parseClassArguments(Iterator<String> arguments, boolean firstPass) { if (!arguments.hasNext()) { return; } //as strange as it seems, this adds each and every argument found in //arguments as a class argument. //Additionally, it will call processClassArgument() with every such class argument and the next argument in line. String classArg = arguments.next(); while (arguments.hasNext()) { String nextArg = arguments.next(); processClassArgument(classArg, nextArg, firstPass); classArg = nextArg; } processClassArgument(classArg, null, firstPass); } private void processClassArgument(String classArg, String nextArg, boolean firstPass) { if (firstPass) { //in first pass, we do the processing required by this class if (this.includeSystemPropertiesFromClassArguments && isSystemPropertyArgument(classArg)) { parseSystemPropertyArgument(classArg); } this.classArguments.add(classArg); } else { //in the second pass, we let the subclasses process the class arguments processClassArgument(classArg, nextArg); } } /** * Override this method to do additional processing of the class arguments. * This method is called during the {@link #parseCommandLine()} call but after all other properties are processed. * <p> * It is therefore safe to call {@link #getClassArguments()}, {@link #getExecutableJarFile()} and all other getters * defined by {@link JavaCommandLine}. At the time this method is called during {@link #parseCommandLine()}, the * default implementation of {@link #isArgumentsParsed()} already returns true. * <p> * This method is called at a stage during the parsing of the commandline where all the properties are still writeable * - you can modify the {@link #getSystemProperties() system properties} and other collections. * <p> * By default this method does nothing. * * @param classArg * @param nextArg */ protected void processClassArgument(String classArg, String nextArg) { //do nothing by default. } private void parseClassOptions() { this.shortClassOptionNameToOptionValueMap = new HashMap<String, String>(); this.longClassOptionNameToOptionValueMap = new HashMap<String, String>(); if (!this.classArguments.isEmpty()) { for (int i = 0, classArgumentsSize = this.classArguments.size(); i < classArgumentsSize; i++) { String classArg = this.classArguments.get(i); String optionString; // the option with the prefix stripped off Set<OptionValueDelimiter> optionValueDelims; Map<String, String> optionValueMap; // We must check if the arg starts with long prefix before short prefix, since the former is a subset of // the latter. if (classArg.startsWith(LONG_OPTION_PREFIX)) { // long opt if (classArg.length() == LONG_OPTION_PREFIX.length()) { // arg is "--", which means to stop processing options // TODO: make this configurable? break; } optionString = classArg.substring(LONG_OPTION_PREFIX.length()); optionValueDelims = this.longClassOptionValueDelims; optionValueMap = this.longClassOptionNameToOptionValueMap; } else if (classArg.startsWith(SHORT_OPTION_PREFIX)) { // short opt optionString = classArg.substring(SHORT_OPTION_PREFIX.length()); optionValueDelims = this.shortClassOptionValueDelims; optionValueMap = this.shortClassOptionNameToOptionValueMap; } else { // not an option continue; } String optionName = null; String optionValue = null; if (optionValueDelims.contains(OptionValueDelimiter.WHITESPACE)) { int equalsIndex = optionString.indexOf('='); if (equalsIndex >= 1) { if (optionValueDelims.contains(OptionValueDelimiter.EQUALS_SIGN)) { optionName = optionString.substring(0, equalsIndex); optionValue = (equalsIndex == (optionString.length() - 1)) ? "" : optionString.substring(equalsIndex + 1); } else if (optionString.charAt(0) != 'D') { // We don't log this warning for sysprops. log.warn("Option [" + classArg + "] contains an equals sign, which is not a valid class option value delimiter for this command line."); } } else { optionName = optionString; if (((i + 1) < classArgumentsSize) && !this.classArguments.get(i + 1).startsWith("-")) { // there is a next argument and it's not an option - assume it's an argument to this option // and advance our loop index. optionValue = this.classArguments.get(++i); } else { // the option has no argument - store an empty string as its value to indicate the option // was present on the command line. optionValue = ""; } } } else if (optionValueDelims.contains(OptionValueDelimiter.EQUALS_SIGN)) { int equalsIndex = optionString.indexOf('='); if (equalsIndex == -1) { // the option has no argument - store an empty string as its value to indicate the option // was present on the command line. optionName = optionString; optionValue = ""; } else if (equalsIndex >= 1) { optionName = optionString.substring(0, equalsIndex); optionValue = (equalsIndex == (optionString.length() - 1)) ? "" : optionString.substring(equalsIndex + 1); } else { log.warn("Ignoring malformed option [" + classArg + "] on command line [" + this + "]..."); } } if (optionName != null) { optionValueMap.put(optionName, optionValue); } } } } private boolean isSystemPropertyArgument(String arg) { return SYSTEM_PROPERTY_PATTERN.matcher(arg).matches(); } private void parseSystemPropertyArgument(String arg) { String argValue = arg.substring(2); int equalsSignIndex = argValue.indexOf('='); String name; String value; if (equalsSignIndex >= 0) { name = argValue.substring(0, equalsSignIndex); value = (equalsSignIndex == (argValue.length() - 1)) ? "" : argValue.substring(equalsSignIndex + 1); } else { name = argValue; value = ""; } this.systemProperties.put(name, value); } @NotNull public List<String> getArguments() { return arguments; } @NotNull public File getJavaExecutable() { if (!argumentsParsed) { parseCommandLine(); } return javaExecutable; } @NotNull public List<String> getClassPath() { if (!argumentsParsed) { parseCommandLine(); } return classPath; } @NotNull public Map<String, String> getSystemProperties() { if (!argumentsParsed) { parseCommandLine(); } return systemProperties; } @NotNull public List<String> getJavaOptions() { if (!argumentsParsed) { parseCommandLine(); } return javaOptions; } @Nullable public String getMainClassName() { if (!argumentsParsed) { parseCommandLine(); } return mainClassName; } @Nullable public File getExecutableJarFile() { if (!argumentsParsed) { parseCommandLine(); } return executableJarFile; } @NotNull public List<String> getClassArguments() { if (!argumentsParsed) { parseCommandLine(); } return classArguments; } /** * @param option the class option to look for * * @return null if the class option is not on the command line, "" if it is on the command line and * either has no value or expects no value, and otherwise the non-empty value. */ @Nullable public String getClassOption(CommandLineOption option) { if (!argumentsParsed) { parseCommandLine(); } return getClassOption(option, null); } /** * @param option the class option to look for * @param defaultValue the value to return if the specified class option is not on the command line * * @return null if the class option is not on the command line, "" if it is on the command line and * either has no value or expects no value, and otherwise the non-empty value. */ @Nullable public String getClassOption(CommandLineOption option, String defaultValue) { if (!argumentsParsed) { parseCommandLine(); } String optionValue = null; // Note, we never store null values in either of the option value maps. if ((option.getLongName() != null) && this.longClassOptionNameToOptionValueMap.containsKey(option.getLongName())) { optionValue = this.longClassOptionNameToOptionValueMap.get(option.getLongName()); if (!optionValue.isEmpty() && !option.isExpectsValue()) { // TODO: Store the delims used for each of the options in another set of maps, so we can handle // things differently here depending on what delim was used. if (this.longClassOptionValueDelims.equals(EnumSet.of(OptionValueDelimiter.EQUALS_SIGN))) { log.warn("Class option [" + option + "] does not expect a value, but a value was specified on command line [" + this + "]."); } else if (this.longClassOptionValueDelims.equals(EnumSet.of(OptionValueDelimiter.WHITESPACE))) { optionValue = ""; } } } if ((optionValue == null) && (option.getShortName() != null) && this.shortClassOptionNameToOptionValueMap.containsKey(option.getShortName())) { optionValue = this.shortClassOptionNameToOptionValueMap.get(option.getShortName()); if (!optionValue.isEmpty() && !option.isExpectsValue()) { // TODO: Store the delims used for each of the options in another set of maps, so we can handle // things differently here depending on what delim was used. if (this.shortClassOptionValueDelims.equals(EnumSet.of(OptionValueDelimiter.EQUALS_SIGN))) { log.warn("Class option [" + option + "] does not expect a value, but a value was specified on command line [" + this + "]."); } else if (this.shortClassOptionValueDelims.equals(EnumSet.of(OptionValueDelimiter.WHITESPACE))) { optionValue = ""; } } } if (optionValue != null && optionValue.isEmpty() && option.isExpectsValue()) { log.warn("Class option [" + option + "] expects a value, but no value was specified on command line [" + this + "]."); } return (optionValue != null) ? optionValue : defaultValue; } public boolean isClassOptionPresent(CommandLineOption option) { if (!argumentsParsed) { parseCommandLine(); } String optionValue = getClassOption(option); return (optionValue != null); } @Override public String toString() { return "JavaCommandLine[arguments=" + this.arguments // + ", includeSystemPropertiesFromClassArguments=" + this.includeSystemPropertiesFromClassArguments // + ", shortClassOptionFormat=" + this.shortClassOptionValueDelims // + ", longClassOptionFormat=" + this.longClassOptionValueDelims + "]"; } }