org.lib4j.cli.Options.java Source code

Java tutorial

Introduction

Here is the source code for org.lib4j.cli.Options.java

Source

/* Copyright (c) 2008 lib4j
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * You should have received a copy of The MIT License (MIT) along with this
 * program. If not, see <http://opensource.org/licenses/MIT/>.
 */

package org.lib4j.cli;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.FixedHelpFormatter;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.lib4j.cli_2_1_7.Cli;
import org.lib4j.cli_2_1_7.Use;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

public final class Options {
    private static final Logger logger = LoggerFactory.getLogger(Options.class);
    private static Schema schema;

    private static String formatArgumentName(final String label, final int maxOccurs, final char valueSeparator) {
        if (maxOccurs == 1)
            return label;

        final StringBuilder buffer = new StringBuilder(label);
        buffer.append(1).append(valueSeparator);

        if (maxOccurs == 2)
            return buffer.append(label).append(2).toString();

        if (maxOccurs == Integer.MAX_VALUE)
            return buffer.append(label).append(2).append("...").toString();

        return buffer.append("...").append(valueSeparator).append(label).append(maxOccurs).toString();
    }

    private static void printHelp(final org.apache.commons.cli.Options apacheOptions,
            final Cli.Arguments cliArguments, final PrintStream ps) {
        final HelpFormatter formatter = new FixedHelpFormatter();
        final PrintWriter pw = new PrintWriter(ps);
        final StringBuilder args = new StringBuilder(apacheOptions.getOptions().size() > 0 ? " [options]" : "");
        if (cliArguments != null) {
            for (short i = 1; i <= cliArguments.getMinOccurs(); i++)
                args.append(" <").append(cliArguments.getLabel()).append(i != 1 ? i : "").append('>');

            final boolean maxUnbounded = "unbounded".equals(cliArguments.getMaxOccurs());
            final int argsMax = maxUnbounded ? 2 + cliArguments.getMinOccurs()
                    : Short.parseShort(cliArguments.getMaxOccurs());
            for (int i = cliArguments.getMinOccurs() + 1; i <= argsMax; i++)
                args.append(" [").append(cliArguments.getLabel()).append(i != 1 ? i : "").append(']');

            if (maxUnbounded)
                args.append(" [...]");
        }

        formatter.printHelp(pw, HelpFormatter.DEFAULT_WIDTH, " ", args.substring(1), apacheOptions,
                HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, null, false);
        pw.flush();
    }

    private static void trapPrintHelp(final org.apache.commons.cli.Options apacheOptions,
            final Cli.Arguments cliArguments, final String message, final PrintStream ps) {
        if (message != null)
            ps.println(message);

        printHelp(apacheOptions, cliArguments, ps);
        System.exit(1);
    }

    public static Options parse(final File cliFile, final Class<?> mainClass, final String[] args) {
        try {
            return parse(cliFile.toURI().toURL(), mainClass, args);
        } catch (final MalformedURLException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static Options parse(final URL cliURL, final Class<?> mainClass, final String[] args) {
        try {
            final Unmarshaller unmarshaller = JAXBContext.newInstance(Cli.class).createUnmarshaller();
            unmarshaller
                    .setSchema(
                            Options.schema == null
                                    ? Options.schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
                                            .newSchema(Thread.currentThread().getContextClassLoader()
                                                    .getResource("cli.xsd"))
                                    : Options.schema);

            try (final InputStream in = cliURL.openStream()) {
                final JAXBElement<Cli> element = unmarshaller
                        .unmarshal(XMLInputFactory.newInstance().createXMLStreamReader(in), Cli.class);
                return parse(element.getValue(), mainClass, args);
            }
        } catch (final FactoryConfigurationError e) {
            throw new UnsupportedOperationException(e);
        } catch (final IOException e) {
            throw new RuntimeException(e);
        } catch (final JAXBException | SAXException | XMLStreamException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static Options parse(final Cli binding, final Class<?> mainClass, final String[] args) {
        final Set<String> requiredNames = new HashSet<>();
        final Map<String, String> nameToAltName = new HashMap<>();
        final org.apache.commons.cli.Options apacheOptions = new org.apache.commons.cli.Options();
        apacheOptions.addOption(null, "help", false, "Print help and usage.");
        short argumentsMinOccurs = 0;
        short argumentsMaxOccurs = 0;
        final Cli.Arguments cliArguments;
        if (binding != null) {
            cliArguments = binding.getArguments();
            if (cliArguments != null) {
                argumentsMinOccurs = cliArguments.getMinOccurs();
                argumentsMaxOccurs = "unbounded".equals(cliArguments.getMaxOccurs()) ? Short.MAX_VALUE
                        : Short.parseShort(cliArguments.getMaxOccurs());
                if (argumentsMaxOccurs < argumentsMinOccurs) {
                    logger.error("minOccurs > maxOccurs on <arguments> element");
                    System.exit(1);
                }
            }

            if (binding.getOption() != null) {
                for (final Cli.Option option : binding.getOption()) {
                    final Cli.Option.Name optionName = option.getName();
                    final String longName = optionName.getLong() == null ? null : optionName.getLong();
                    final String shortName = optionName.getShort() == null ? null : optionName.getShort();
                    final String name = longName != null ? longName : shortName;
                    if (longName == null && shortName == null) {
                        logger.error("both [long] and [short] option names are null in cli spec");
                        System.exit(1);
                    }

                    nameToAltName.put(name, shortName != null ? shortName : longName);
                    OptionBuilder.withLongOpt(name == longName ? longName : null);

                    // Record which options are required
                    if (option.getArgument() != null) {
                        final Cli.Option.Argument argument = option.getArgument();
                        final boolean isRequired = Use.REQUIRED == argument.getUse();
                        if (isRequired) {
                            OptionBuilder.isRequired();
                            requiredNames.add(longName);
                        }

                        final int maxOccurs = argument.getMaxOccurs() == null ? 1
                                : "unbounded".equals(argument.getMaxOccurs()) ? Integer.MAX_VALUE
                                        : Integer.parseInt(argument.getMaxOccurs());
                        if (maxOccurs == 1) {
                            if (isRequired)
                                OptionBuilder.hasArgs(1);
                            else
                                OptionBuilder.hasOptionalArgs(1);
                        } else if (maxOccurs == Integer.MAX_VALUE) {
                            if (isRequired)
                                OptionBuilder.hasArgs();
                            else
                                OptionBuilder.hasOptionalArgs();
                        } else {
                            if (isRequired)
                                OptionBuilder.hasArgs(maxOccurs);
                            else
                                OptionBuilder.hasOptionalArgs(maxOccurs);
                        }

                        final char valueSeparator = argument.getValueSeparator() != null
                                ? argument.getValueSeparator().charAt(0)
                                : ' ';
                        OptionBuilder
                                .withArgName(formatArgumentName(argument.getLabel(), maxOccurs, valueSeparator));
                        OptionBuilder.withValueSeparator(valueSeparator);
                        if (option.getDescription() == null) {
                            logger.error("missing <description> for " + name + " option");
                            System.exit(1);
                        }

                        final StringBuilder description = new StringBuilder(option.getDescription());
                        if (option.getArgument().getDefault() != null)
                            description.append("\nDefault: ").append(option.getArgument().getDefault());

                        OptionBuilder.withDescription(description.toString());
                    }

                    apacheOptions.addOption(OptionBuilder.create(shortName));
                }
            }
        } else {
            cliArguments = null;
        }

        final Map<String, Option> optionsMap = new HashMap<>();
        final Set<String> specifiedLongNames;
        CommandLine commandLine = null;
        if (args != null && args.length != 0) {
            specifiedLongNames = new HashSet<>();
            final CommandLineParser parser = new PosixParser();
            do {
                try {
                    commandLine = parser.parse(apacheOptions, args);
                } catch (final UnrecognizedOptionException e) {
                    if (e.getMessage().startsWith("Unrecognized option: ")) {
                        final String unrecognizedOption = e.getMessage().substring(21);
                        logger.error("Unrecognized option: " + unrecognizedOption);
                        for (int i = 0; i < args.length; i++)
                            if (args[i].equals(unrecognizedOption))
                                args[i] = "--help";
                    } else {
                        throw new IllegalArgumentException(e);
                    }
                } catch (final org.apache.commons.cli.ParseException e) {
                    Options.trapPrintHelp(apacheOptions, cliArguments, null, System.err);
                }
            } while (commandLine == null);
        } else {
            specifiedLongNames = null;
        }

        final Collection<String> arguments = commandLine != null ? commandLine.getArgList() : null;
        if (arguments != null && arguments.size() > 0) {
            if (argumentsMaxOccurs < arguments.size() || arguments.size() < argumentsMinOccurs) {
                Options.trapPrintHelp(apacheOptions, cliArguments, null, System.err);
            }
        } else if (argumentsMinOccurs > 0) {
            Options.trapPrintHelp(apacheOptions, cliArguments, null, System.err);
        }

        if (commandLine != null) {
            for (final org.apache.commons.cli.Option option : commandLine.getOptions()) {
                specifiedLongNames.add(option.getLongOpt());
                if ("help".equals(option.getLongOpt()))
                    Options.trapPrintHelp(apacheOptions, cliArguments, null, System.out);

                final String optionName = option.getLongOpt() != null ? option.getLongOpt() : option.getOpt();
                optionsMap.put(optionName,
                        option.getValue() != null
                                ? new Option(optionName, option.getValueSeparator(), option.getValues())
                                : new Option(optionName, option.getValueSeparator(), "true"));
            }
        }

        // See if some arguments are missing
        if (requiredNames.size() != 0) {
            if (specifiedLongNames != null)
                requiredNames.removeAll(specifiedLongNames);

            if (requiredNames.size() != 0) {
                final StringBuilder builder = new StringBuilder();
                for (final String longName : requiredNames) {
                    final String shortName = nameToAltName.get(longName);
                    if (shortName.equals(longName))
                        builder.append("\nMissing argument: -").append(shortName);
                    else
                        builder.append("\nMissing argument: -").append(shortName).append(",--").append(longName);
                }

                Options.trapPrintHelp(apacheOptions, cliArguments, builder.substring(1), System.out);
            }
        }

        // Include default values for options that are not specified
        if (binding.getOption() != null) {
            for (final Cli.Option option : binding.getOption()) {
                if (option.getArgument() != null && option.getArgument().getDefault() != null) {
                    final String optionName = option.getName().getLong() != null ? option.getName().getLong()
                            : option.getName().getShort();
                    if (!optionsMap.containsKey(optionName)) {
                        final String valueSeparator = option.getArgument().getValueSeparator();
                        final String defaultValue = option.getArgument().getDefault();
                        optionsMap.put(optionName,
                                valueSeparator != null
                                        ? new Option(optionName, valueSeparator.charAt(0), defaultValue)
                                        : new Option(optionName, defaultValue));
                    }
                }
            }
        }

        // Check pattern for specified and default options
        if (binding.getOption() != null) {
            final StringBuilder builder = new StringBuilder();
            for (final Cli.Option option : binding.getOption()) {
                if (option.getArgument() != null && option.getArgument().getPattern() != null) {
                    final String optionName = option.getName().getLong() != null ? option.getName().getLong()
                            : option.getName().getShort();
                    final Option opt = optionsMap.get(optionName);
                    if (opt != null) {
                        for (final String value : opt.getValues()) {
                            if (!value.matches(option.getArgument().getPattern())) {
                                if (option.getName().getLong() == null || option.getName().getShort() == null)
                                    builder.append("\nIncorrect argument form: -").append(optionName);
                                else
                                    builder.append("\nIncorrect argument form: -")
                                            .append(option.getName().getShort()).append(",--")
                                            .append(option.getName().getLong());

                                builder.append(' ').append(value).append("\n  Required: ")
                                        .append(option.getArgument().getPattern());
                            }
                        }
                    }
                }
            }

            if (builder.length() > 0)
                Options.trapPrintHelp(apacheOptions, cliArguments, builder.substring(1), System.out);
        }

        return new Options(mainClass, args, optionsMap.values(), arguments == null || arguments.size() == 0 ? null
                : arguments.toArray(new String[arguments.size()]));
    }

    private final Map<String, Option> optionNameToOption = new HashMap<>();
    private final Class<?> mainClass;
    private final String[] args;
    private final Collection<Option> options;
    private final String[] arguments;

    private Options(final Class<?> mainClass, final String[] args, final Collection<Option> options,
            final String[] arguments) {
        this.mainClass = mainClass;
        this.args = args;
        this.options = options == null ? Collections.<Option>emptyList()
                : Collections.<Option>unmodifiableCollection(options);
        this.arguments = arguments;
        for (final Option option : options)
            optionNameToOption.put(option.getName(), option);
    }

    /**
     * @return Returns an array of unnamed arguments, in original order.
     *         Returns null in case there are no unnamed arguments.
     */
    public String[] getArguments() {
        return arguments;
    }

    public Collection<Option> getOptions() {
        return options;
    }

    public String getOption(final String name) {
        final Option options = optionNameToOption.get(name);
        if (options == null || options.getValues().length == 0)
            return null;

        if (options.getValues().length == 1)
            return options.getValues()[0];

        return Arrays.stream(options.getValues()).reduce(String.valueOf(options.getValueSeparator()),
                String::concat);
    }

    public String[] getOptions(final String name) {
        final Option reqOption = optionNameToOption.get(name);
        return reqOption != null ? reqOption.getValues() : null;
    }

    public void printCommand(final PrintStream ps, final Class<?> callerClass) {
        ps.print("java " + callerClass.getName());
        for (final String arg : args)
            ps.print(" " + arg);
    }

    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder(mainClass.getName());
        if (args.length == 0)
            return buffer.toString();

        for (final String arg : args)
            buffer.append(' ').append(arg);

        return buffer.toString();
    }
}