org.wurstworks.tools.pinto.AbstractPintoBean.java Source code

Java tutorial

Introduction

Here is the source code for org.wurstworks.tools.pinto.AbstractPintoBean.java

Source

/**
 * AbstractPintoBean
 * (C) 2012 Washington University School of Medicine
 * All Rights Reserved
 *
 * Released under the Simplified BSD License
 *
 * Created on 10/16/12 by rherri01
 */
package org.wurstworks.tools.pinto;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.CharBuffer;
import java.util.*;

public abstract class AbstractPintoBean {

    /**
     * Processes the incoming arguments, setting the bean's print stream to {@link System#out}.
     *
     * @param arguments Incoming parameter arguments to process.
     */
    protected AbstractPintoBean(Object parent, String[] arguments) throws PintoException {
        this(parent, arguments, System.out);
    }

    /**
     * Processes the incoming arguments, setting the bean's print stream to the submitted parameter.
     *
     * @param arguments   Incoming parameter arguments to process.
     * @param printStream Indicates the print stream to be used for printing output.
     */
    protected AbstractPintoBean(Object parent, String[] arguments, PrintStream printStream) throws PintoException {
        assert parent != null : "You must specify the parent for your pinto bean.";

        _arguments = Arrays.asList(arguments);
        _parent = parent;

        try {
            scan();
            harvest();
            prune();

            _printStream = getOutputStream();

            if (getHelp()) {
                displayHelp();
            } else if (getVersion()) {
                displayVersion();
            } else {
                validate();
            }
        } catch (PintoException exception) {
            String parameter = exception.getParameter();
            if (!StringUtils.isBlank(parameter)) {
                printStream.println("Found error with parameter: " + parameter + ":");
            }
            printStream.println("Error type: " + exception.getType());
            printStream.println(exception.getMessage());
            throw exception;
        }
    }

    /**
     * Provides an opportunity for subclasses to validate the processed parameters and their arguments.
     *
     * @throws PintoException
     */
    abstract public void validate() throws PintoException;

    public void setPrintStream(PrintStream printStream) {
        _printStream = printStream;
    }

    public PrintStream getPrintStream() {
        return _printStream;
    }

    /**
     * The setter for the help option.
     *
     * @param help Incoming parameter.
     */
    @Parameter(value = "h", longOption = "help", help = "Displays this help text.", argCount = ArgCount.StandAlone)
    public void setHelp(boolean help) {
        _help = help;
    }

    /**
     * The getter for the help option.
     *
     * @return Gets the value for the help option.
     */
    @Value("h")
    public boolean getHelp() {
        return _help;
    }

    /**
     * The setter for the version option.
     *
     * @param version Incoming parameter.
     */
    @Parameter(value = "v", longOption = "version", argCount = ArgCount.StandAlone, help = "Displays the version of this application.")
    public void setVersion(boolean version) {
        _version = version;
    }

    /**
     * The getter for the version option.
     *
     * @return Gets the value for the version option.
     */
    @Value("v")
    public boolean getVersion() {
        return _version;
    }

    /**
     * The setter for the outputStreamAdapter option.
     *
     * @param outputStreamAdapter Incoming parameter.
     */
    @Parameter(value = "osa", longOption = "outputStreamAdapter", help = "Specifies an output stream adapter implementation to handle redirecting the output from your application.")
    public void setOutputStreamAdapter(String outputStreamAdapter) {
        _outputStreamAdapter = outputStreamAdapter;
    }

    /**
     * The getter for the outputStreamAdapter option.
     *
     * @return Gets the value for the outputStreamAdapter option.
     */
    @Value("osa")
    public String getOutputStreamAdapter() {
        return _outputStreamAdapter;
    }

    /**
     * Returns any arguments that are posted on the end of the list of arguments but aren't associated with a parameter.
     *
     * @return Any arguments on the end of the list of arguments not associated with a parameter.
     */
    public List<String> getTrailingArguments() {
        return _trailing;
    }

    /**
     * Indicates whether application execution should continue based on submitted parameters. Common reasons for not
     * continuing include specifying help or version parameters.
     * @return Whether application execution should continue once the pinto bean has been constructed.
     */
    public boolean getShouldContinue() {
        return !_help && !_version;
    }

    /**
     * Display the help for each of the available command-line parameters supported by this bean. The help is printed to
     * the stream specified by the {@link #setPrintStream(java.io.PrintStream)} property or passed in through the
     * {@link AbstractPintoBean#AbstractPintoBean(Object, String[], java.io.PrintStream)} constructor.
     */
    public void displayHelp() {
        if (_parametersByShortOption == null || _parametersByShortOption.size() == 0) {
            getPrintStream().println("No parameters found for this application!");
        } else {
            // TODO: Add an annotation to put application name, copyright info, and introductory help text on the class level.
            String appName, copyright, introduction;
            PintoApplication application = _parent.getClass().getAnnotation(PintoApplication.class);
            if (application == null) {
                appName = _parent.getClass().getSimpleName();
                copyright = introduction = null;
            } else {
                appName = application.value();
                copyright = application.copyright();
                introduction = application.introduction();
            }

            getPrintStream().println(appName);
            if (!StringUtils.isBlank(copyright)) {
                getPrintStream().println(copyright);
            }
            if (!StringUtils.isBlank(introduction)) {
                getPrintStream().println(introduction);
            }
            getPrintStream().println();

            for (ParameterData parameter : _parametersByShortOption.values()) {
                StringBuilder parameterText = new StringBuilder(PREFIX);
                parameterText.append(SHORT_OPTION_DELIMITER).append(parameter.getShortOption());
                if (parameter.hasLongOption()) {
                    parameterText.append(", ").append(LONG_OPTION_DELIMITER).append(parameter.getLongOption());
                }

                // If our parameter text is so long that it will either run into the hanging indent text or directly up
                // to the hanging indent text (i.e., no space left between them)...
                final int length = parameterText.length();
                if (length > HANGING_INDENT - 1) {
                    // Then add a new line
                    parameterText.append(INDENT_FILLER);
                } else {
                    parameterText
                            .append(CharBuffer.allocate(HANGING_INDENT - length).toString().replace('\0', ' '));
                }

                parameterText
                        .append(WordUtils.wrap(parameter.getHelp(), WIDTH - HANGING_INDENT, INDENT_FILLER, true));
                getPrintStream().println(parameterText.toString());
            }
        }
    }

    /**
     * Displays the version from the {@link PintoApplication annotation} on the parent class if available. If the
     * annotation is not available, this method tries to find a <b>getVersion()</b> method on the parent class and call
     * that, along with displaying the application name as the class name.
     */
    protected void displayVersion() throws PintoException {
        String appName, version, copyright;
        PintoApplication application = _parent.getClass().getAnnotation(PintoApplication.class);
        if (application == null) {
            appName = _parent.getClass().getSimpleName();
            version = getVersionFromParent();
            copyright = null;
        } else {
            appName = resolveAttribute(application.value());
            version = application.version();
            if (StringUtils.isBlank(version)) {
                version = getVersionFromParent();
            } else {
                version = resolveAttribute(version);
            }
            copyright = resolveAttribute(application.copyright());
        }
        getPrintStream().println(appName + ", version " + version);
        if (!StringUtils.isBlank(copyright)) {
            getPrintStream().println(copyright);
        }
    }

    /**
     * Converts a string to the type indicated by the <b>type</b> parameter. The ability of a pinto bean to convert
     * strings to any arbitrary type can be extended by overriding and extending this method.
     *
     * @param type     Indicates the type to which the argument should be converted.
     * @param argument The argument to be converted.
     * @return An object of the indicated type from the given value.
     * @throws PintoException
     */
    protected Object convertStringToType(final Class<?> type, final String argument) throws PintoException {
        Object object;
        if (type == String.class) {
            object = argument;
        } else if (type == Integer.class || type == int.class) {
            object = Integer.parseInt(argument);
        } else if (type == Long.class || type == long.class) {
            object = Long.parseLong(argument);
        } else if (type == Float.class || type == float.class) {
            object = Float.parseFloat(argument);
        } else if (type == Double.class || type == double.class) {
            object = Double.parseDouble(argument);
        } else if (type == Character.class || type == char.class) {
            object = argument.toCharArray()[0];
        } else if (type == Byte.class || type == byte.class) {
            object = Byte.parseByte(argument);
        } else if (type == Short.class || type == short.class) {
            object = Short.parseShort(argument);
        } else if (type == Boolean.class || type == boolean.class) {
            object = Boolean.parseBoolean(argument);
        } else if (type == File.class) {
            object = new File(argument);
        } else if (type == URI.class) {
            try {
                object = new URI(argument);
            } catch (URISyntaxException exception) {
                throw new PintoException(PintoExceptionType.SyntaxFormat,
                        "The value " + argument + " is not a valid URI.", exception);
            }
        } else {
            throw new PintoException(PintoExceptionType.UnknownParameterTypes,
                    "I don't know how to convert to the type " + type.getName());
        }

        return object;
    }

    /**
     * This can be used by {@link #validate()} implementations to have help displayed if no arguments are specified on
     * the command line. If this is called and no arguments were passed to the application, this method will set the
     * help display flag to true.
     * @return Returns true if help should be displayed. Note that this should terminate further parameter validation.
     */
    protected boolean noArgsHelp() {
        if (_arguments == null || _arguments.size() == 0) {
            displayHelp();
            _help = true;
            return true;
        }
        return false;
    }

    /**
     * Scans the bean class for configured parameters. Parameters are configured by adding the {@link Parameter}
     * annotation to the setter method. Associated getter methods are marked with the {@link Value} annotation.
     */
    private void scan() throws PintoException {
        // TODO: For now this doesn't detect duplication in parameters when subclass method hides base class method. Maybe that's OK?
        Method[] methods = getClass().getMethods();
        for (Method method : methods) {
            Parameter annotation = method.getAnnotation(Parameter.class);
            if (annotation != null) {
                if (_log.isDebugEnabled()) {
                    _log.debug("Found command-line parameter annotation " + annotation.value() + " on "
                            + getClass().getName() + "." + method.getName() + "() method");
                }
                final ParameterData parameter = new ParameterData(method, annotation);
                if (_parametersByShortOption.containsKey(parameter.getShortOption())) {
                    throw new PintoException(PintoExceptionType.DuplicateParameter,
                            "Your application has multiple declarations of the short option "
                                    + parameter.getShortOption());
                }
                _parametersByShortOption.put(parameter.getShortOption(), parameter);
                final String longOption = parameter.getLongOption();
                if (!StringUtils.isBlank(longOption)) {
                    if (_parametersByLongOption.containsKey(parameter.getLongOption())) {
                        throw new PintoException(PintoExceptionType.DuplicateParameter,
                                "Your application has multiple declarations of the long option "
                                        + parameter.getLongOption());
                    }
                    _parametersByLongOption.put(longOption, parameter);
                }
            }
        }
    }

    /**
     * Harvests the command-line parameters and sorts them along with their arguments. Any trailing
     * arguments are assigned to the last found parameter. Trailing arguments that occur without
     * parameters are stashed in the {@link #getTrailingArguments()} property.
     *
     * @throws PintoException
     */
    private void harvest() throws PintoException {
        ParameterData parameter = null;
        for (String argument : _arguments) {
            // Is this argument a parameter?
            if (isParameter(argument)) {
                // If there are trailing arguments, then something is wrong.
                if (_trailing.size() > 0) {
                    throw new PintoException(PintoExceptionType.SyntaxFormat, argument,
                            "Trailing arguments were found prior to the parameter " + argument
                                    + ". Check that you've supplied only the expected number of arguments to each parameter.");
                }

                // Try to get the data for the indicated parameter.
                ParameterData foundParameter = getParameterData(argument);

                // Before we finish with an existing parameter...
                if (parameter != null) {
                    ArgCount argCount = parameter.getArgCount();
                    int argSize = _parameters.get(parameter.getShortOption()).size();

                    // It's OK to have a new parameter now, so bail out.
                    if (argSize == 0 && (argCount == ArgCount.ZeroToN || argCount == ArgCount.StandAlone)) {
                        break;
                    }

                    if (argCount == ArgCount.OneArgument && argSize != 1) {
                        throw new PintoException(PintoExceptionType.SyntaxFormat,
                                "Not enough arguments specified for parameter " + parameter.getShortOption()
                                        + ", requires exactly one");
                    } else if (argCount == ArgCount.OneToN && argSize == 0) {
                        throw new PintoException(PintoExceptionType.SyntaxFormat,
                                "Not enough arguments specified for parameter " + parameter.getShortOption()
                                        + ", requires one or more");
                    } else if (argCount == ArgCount.SpecificCount && argSize != parameter.getExactArgCount()) {
                        throw new PintoException(PintoExceptionType.SyntaxFormat,
                                "Not enough arguments specified for parameter " + parameter.getShortOption()
                                        + ", requires " + parameter.getExactArgCount());
                    }
                }

                parameter = foundParameter;

                // Now store the parameter option in the map of parameter data and initialize the argument cache.
                _parameters.put(parameter.getShortOption(), new ArrayList<String>());

                // If the parameter takes no args, there's no reason to keep it around.  The next tokens have to be
                // either another parameter or trailing arguments.
                if (parameter.getArgCount() == ArgCount.StandAlone) {
                    parameter = null;
                }
            } else if (parameter == null) {
                // This is a fail-safe catch for situations with no parameters, e.g., ls file1 file2 file3
                _trailing.add(argument);
            } else {
                // Add the argument to the current parameter. The last parameter will harvest all trailing data.
                // We'll handle that situation when we prune the parameter list.
                List<String> args = _parameters.get(parameter.getShortOption());
                args.add(argument);

                ArgCount argCount = parameter.getArgCount();
                // Note that we don't handle ArgCount.StandAlone here because that's cut off when the
                // stand-alone parameter is detected earlier. We also don't deal with ZeroToN and OneToN,
                // since they should be cut off by end of command or the next parameter.
                switch (argCount) {
                case OneArgument:
                    // Really we shouldn't ever get this since we're going to cut it off after this.
                    if (args.size() > 1) {
                        throw new PintoException(PintoExceptionType.SyntaxFormat,
                                "Too many arguments specified for parameter " + parameter.getShortOption());
                    }
                    parameter = null;
                    break;
                case SpecificCount:
                    if (args.size() > parameter.getExactArgCount()) {
                        throw new PintoException(PintoExceptionType.SyntaxFormat,
                                "Too many arguments specified for parameter " + parameter.getShortOption());
                    }
                }
            }
        }
    }

    /**
     * Prunes the parameters and arguments. This includes validating the arguments passed in against
     * the accepted arguments for each parameter, as well as removing trailing arguments.
     */
    private void prune() throws PintoException {
        for (String parameterId : _parameters.keySet()) {
            ParameterData parameter = _parametersByShortOption.get(parameterId);
            List<String> arguments = _parameters.get(parameterId);

            validateArgCount(parameter, arguments);

            final Method method = parameter.getMethod();
            try {
                if (parameter.getArgCount() == ArgCount.StandAlone) {
                    method.invoke(this, true);
                } else {
                    try {
                        Object[] coercedArguments = coerceArguments(method, arguments);
                        method.invoke(this, coercedArguments);
                    } catch (PintoException exception) {
                        if (exception.getType() == PintoExceptionType.UnknownParameterTypes
                                && StringUtils.isBlank(exception.getMessage())) {
                            throw new PintoException(PintoExceptionType.UnknownParameterTypes, "The parameter "
                                    + parameterId
                                    + " has unknown parameter types. Check your set method for compatible parameter types.");
                        }
                        if (exception.getType() == PintoExceptionType.SyntaxFormat) {
                            final StringBuilder message = new StringBuilder("The parameter " + parameterId
                                    + " has a syntax error. Check that your arguments match the parameter requirements.");
                            if (!StringUtils.isBlank(exception.getMessage())) {
                                message.append(" The specific error message is:\n\n")
                                        .append(exception.getMessage());
                            }
                            throw new PintoException(PintoExceptionType.SyntaxFormat, parameterId,
                                    message.toString());
                        }
                        throw exception;
                    }
                }
            } catch (IllegalAccessException exception) {
                throw new PintoException(PintoExceptionType.Configuration, parameter.getShortOption(),
                        "Unable to call the " + method.getName() + " method configured for handling parameter",
                        exception);
            } catch (InvocationTargetException exception) {
                throw new PintoException(PintoExceptionType.Configuration, parameter.getShortOption(),
                        "Unable to call the " + method.getName() + " method configured for handling parameter",
                        exception);
            }
        }
    }

    private Object[] coerceArguments(final Method method, final List<String> arguments) throws PintoException {
        Class<?>[] types = method.getParameterTypes();
        if (types == null || types.length == 0) {
            throw new PintoException(PintoExceptionType.UnknownParameterTypes);
        }
        final boolean isArrayParameter = types.length == 1 && types[0].isArray();
        if (types.length != arguments.size() && !isArrayParameter) {
            throw new PintoException(PintoExceptionType.SyntaxFormat);
        }
        Class<?> type = isArrayParameter ? types[0].getComponentType() : null;
        final List<Object> coercedArguments = new ArrayList<Object>(types.length);
        for (int index = 0; index < arguments.size(); index++) {
            if (!isArrayParameter) {
                type = types[index];
            }
            coercedArguments.add(convertStringToType(type, arguments.get(index)));
        }

        return isArrayParameter
                ? new Object[] { coercedArguments.toArray(
                        (Object[]) Array.newInstance(types[0].getComponentType(), coercedArguments.size())) }
                : coercedArguments.toArray();
    }

    private void validateArgCount(final ParameterData parameter, final List<String> arguments)
            throws PintoException {
        final String parameterId = parameter.getShortOption();
        final int argCount = arguments.size();
        final ArgCount ArgCount = parameter.getArgCount();

        switch (ArgCount) {
        case StandAlone:
            if (_log.isDebugEnabled()) {
                _log.debug("Found parameter " + parameterId + " specified as StandAlone parameter, comes with "
                        + argCount + " arguments");
            }
            if (argCount > 0) {
                throw new PintoException(PintoExceptionType.SyntaxFormat,
                        "The parameter " + parameterId + " does not accept any arguments.");
            }
            break;
        case OneArgument:
            if (_log.isDebugEnabled()) {
                _log.debug("Found parameter " + parameterId + " specified as OneArgument parameter, comes with "
                        + argCount + " arguments");
            }
            if (argCount != 1) {
                throw new PintoException(PintoExceptionType.SyntaxFormat,
                        "The parameter " + parameterId + " only accepts a single argument.");
            }
            break;
        case OneToN:
            if (_log.isDebugEnabled()) {
                _log.debug("Found parameter " + parameterId + " specified as OneToN parameter, comes with "
                        + argCount + " arguments");
            }
            if (argCount == 0) {
                throw new PintoException(PintoExceptionType.SyntaxFormat,
                        "The parameter " + parameterId + " requires one or more arguments.");
            }
            break;
        case SpecificCount:
            int acceptedArgCount = parameter.getExactArgCount();
            if (_log.isDebugEnabled()) {
                _log.debug("Found parameter " + parameterId + " specified as SpecificCount parameter with "
                        + acceptedArgCount + " arguments required, comes with " + argCount + " arguments");
            }
            if (argCount != acceptedArgCount) {
                throw new PintoException(PintoExceptionType.SyntaxFormat,
                        "The parameter " + parameterId + " requires exactly " + acceptedArgCount + " arguments.");
            }
            break;
        case ZeroToN:
            if (_log.isDebugEnabled()) {
                _log.debug("Found parameter " + parameterId + " specified as ZeroToN parameter, comes  with "
                        + argCount + " arguments");
            }
        }
    }

    private ParameterData getParameterData(String parameter) throws PintoException {
        if (parameter.startsWith(LONG_OPTION_DELIMITER)) {
            parameter = parameter.substring(LONG_OPTION_DELIMITER.length());
            if (_parametersByLongOption.containsKey(parameter)) {
                return _parametersByLongOption.get(parameter);
            }
        }
        if (parameter.startsWith(SHORT_OPTION_DELIMITER)) {
            parameter = parameter.substring(SHORT_OPTION_DELIMITER.length());
            if (_parametersByShortOption.containsKey(parameter)) {
                return _parametersByShortOption.get(parameter);
            }
        }
        // We didn't find it.
        throw new PintoException(PintoExceptionType.UnknownParameter, parameter,
                "The parameter " + parameter + " is not a valid parameter.");
    }

    private boolean isParameter(final String argument) {
        return StringUtils.startsWithAny(argument, OPTION_DELIMITERS);
    }

    private String getVersionFromParent() throws PintoException {
        Method getVersion = null;
        try {
            getVersion = _parent.getClass().getMethod("getVersion");
        } catch (NoSuchMethodException ignored) {
            // If it doesn't exist, so be it.
        }
        if (getVersion != null) {
            try {
                return (String) getVersion.invoke(_parent);
            } catch (Exception exception) {
                throw new PintoException(PintoExceptionType.Configuration, "v",
                        "Version method was found, but throws an error", exception);
            }
        }
        return getVersionFromParentProperties();
    }

    private String getVersionFromParentProperties() throws PintoException {
        final String version = getPropertyFromParentProperties("version");
        return StringUtils.isBlank(version) ? "Unknown" : version;
    }

    private String getPropertyFromParentProperties(String property) throws PintoException {
        Properties properties = null;
        Method getProperties = null;

        try {
            getProperties = _parent.getClass().getMethod("getProperties");
        } catch (NoSuchMethodException ignored) {
            // If it doesn't exist, so be it.
        }

        if (getProperties != null) {
            try {
                properties = (Properties) getProperties.invoke(_parent);
            } catch (Exception exception) {
                throw new PintoException(PintoExceptionType.Configuration, "v",
                        "Properties method was found, but throws an error", exception);
            }
        }

        if (properties == null) {
            properties = getPropertiesForClass(_parent.getClass());
        }

        return properties == null ? null : properties.getProperty(property);
    }

    public static Properties getPropertiesForClass(final Class<?> parent) {
        final String bundle = "/" + parent.getName().replace(".", "/") + ".properties";
        Properties properties = new Properties();
        try {
            properties.load(parent.getResourceAsStream(bundle));
        } catch (IOException e) {
            properties = null;
        }
        return properties;
    }

    private String resolveAttribute(final String value) {
        if (value.startsWith(PROPERTY_INDICATOR)) {
            try {
                return getPropertyFromParentProperties(value.substring(PROPERTY_INDICATOR.length()));
            } catch (PintoException ignored) {
            }
        }
        return value;
    }

    private PrintStream getOutputStream() throws PintoException {
        final String outputStreamAdapter = getOutputStreamAdapter();
        if (StringUtils.isBlank(outputStreamAdapter)) {
            return System.out;
        }
        try {
            Class<?> clazz = getClass().getClassLoader().loadClass(outputStreamAdapter);
            PintoStreamAdapter adapter = (PintoStreamAdapter) clazz.newInstance();
            return adapter.getOutputStream();
        } catch (ClassNotFoundException e) {
            throw new PintoException(PintoExceptionType.InvalidOutputStreamAdapter,
                    "Couldn't find output stream adapter class: " + outputStreamAdapter);
        } catch (InstantiationException e) {
            throw new PintoException(PintoExceptionType.InvalidOutputStreamAdapter,
                    "Couldn't create new instance of output stream adapter class: " + outputStreamAdapter);
        } catch (IllegalAccessException e) {
            throw new PintoException(PintoExceptionType.InvalidOutputStreamAdapter,
                    "Can't access constructor of output stream adapter class: " + outputStreamAdapter);
        }
    }

    private static final Logger _log = LoggerFactory.getLogger(AbstractPintoBean.class);
    /**
     * This is the text to place before each option in the help text.
     */
    private static final String PREFIX = " ";
    /**
     * This is the width of the hanging indent in the help text. Illustration ('|' is the left margin):
     * <p/>
     * <div style='font-family: "Courier New", Courier, monospace'>
     * | -h, --help         Show this help text.
     * |12345678901234567890
     * </div>
     * <p/>
     * Above shows a 20-character hanging indent.
     */
    private static final int HANGING_INDENT = 20;
    /**
     * Provides the indent filler to format the hanging indent space.
     */
    private static final String INDENT_FILLER = "\n"
            + CharBuffer.allocate(HANGING_INDENT).toString().replace('\0', ' ');
    /**
     * The overall format width for the help text.
     */
    private static final int WIDTH = 80;

    private static final String SHORT_OPTION_DELIMITER = "-";
    private static final String LONG_OPTION_DELIMITER = "--";
    private static final String[] OPTION_DELIMITERS = new String[] { LONG_OPTION_DELIMITER,
            SHORT_OPTION_DELIMITER };
    public static final String PROPERTY_INDICATOR = "property:";

    /**
     * The print stream for help text and user messages.
     */
    private PrintStream _printStream = System.out;
    private final Object _parent;

    private boolean _help;
    private boolean _version = false;
    private String _outputStreamAdapter;

    private List<String> _arguments;
    private Map<String, List<String>> _parameters = new LinkedHashMap<String, List<String>>();
    private List<String> _trailing = new ArrayList<String>();
    private Map<String, ParameterData> _parametersByShortOption = new HashMap<String, ParameterData>();
    private Map<String, ParameterData> _parametersByLongOption = new HashMap<String, ParameterData>();
}