package org.gambi.tapestry5.cli.services.impl; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.ValidationException; import javax.validation.Validator; import org.apache.commons.cli.BasicParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.gambi.tapestry5.cli.data.ApplicationConfiguration; import org.gambi.tapestry5.cli.data.CLIOption; import org.gambi.tapestry5.cli.services.CLIParser; import org.gambi.tapestry5.cli.services.CLIValidator; import org.gambi.tapestry5.cli.services.internal.ApplicationConfigurationSource; import org.gambi.tapestry5.cli.services.internal.BridgeCLIOptionProvider; import org.gambi.tapestry5.cli.utils.CLIDefaultOptions; import org.slf4j.Logger; public class CLIParserImpl implements CLIParser { private Logger logger; private ApplicationConfigurationSource applicationConfigurationSource; private Validator validator; private CLIValidator cliValidator; private BridgeCLIOptionProvider bridgeCLIOptionProvider; // User Contributions. This is also used to store the parsed inputs if any private Collection<CLIOption> cliOptions; // User contribution... must be this the args[0] element ? private String commandName; // Internal implementation private CommandLineParser parser; private CommandLine parsedOptions; private HelpFormatter formatter; // Configure this via Symbol ? private PrintWriter pw; public CLIParserImpl(Logger logger, ApplicationConfigurationSource applicationBeanSource, Validator validator, CLIValidator cliValidator, String commandName, BridgeCLIOptionProvider bridgeCLIOptionProvider, Collection<CLIOption> _options) { this.logger = logger; this.validator = validator; this.cliValidator = cliValidator; this.applicationConfigurationSource = applicationBeanSource; this.commandName = commandName; this.bridgeCLIOptionProvider = bridgeCLIOptionProvider; this.formatter = new HelpFormatter(); this.pw = new PrintWriter(System.out); this.parser = new BasicParser(); validateAndMerge(_options); } /** * Print the help message and call System.exit */ /* * Note: this should be implemented by using a special command called help: * each commadn should be executed from scratch: add to wish list! */ // FIXME Kind of bad ! private void prindUsage(String[] args) { final Options options = setupParsing(); try { // THIS IS TAKEN FROM // http://stackoverflow.com/questions/14309467/how-can-i-avoid-a-parserexception-for-required-options-when-user-just-wants-to-p final Options helpOptions = new Options(); helpOptions.addOption(CLIDefaultOptions.HELP_OPTION); CommandLine tmpLine = parser.parse(helpOptions, args, true); if (tmpLine.hasOption(CLIDefaultOptions.HELP_OPTION.getLongOpt())) { formatter.printHelp(commandName, options); // TODO !! // System.exit(0); } } catch (Exception e) { logger.error("", e); formatter.printHelp(commandName, options); // TODO // System.exit(1); } } /** * This check for duplicate entries and merge the ones that can be merged */ private void validateAndMerge(Collection<CLIOption> _options) { ArrayList<CLIOption> cliOptions = new ArrayList<CLIOption>(); for (CLIOption cliOption : _options) { if (!cliOptions.contains(cliOption)) { logger.info("Adding " + cliOption); cliOptions.add(cliOption); } else { logger.info("\t Merging options " + cliOptions.get(cliOptions.indexOf(cliOption)) + " with " + cliOption); cliOptions.get(cliOptions.indexOf(cliOption)).merge(cliOption); } } // Validate (ideally here there are few options to check); for (CLIOption cliOption1 : cliOptions) { for (CLIOption cliOption2 : cliOptions) { if (cliOption1.conflicts(cliOption2)) { throw new IllegalArgumentException( "Found conflicting CLIOptions: Option " + cliOption1 + " conflicts with " + cliOption2 + ". Please revise your contributions !"); } } } // Now update the variable this.cliOptions = cliOptions; logger.info("Validate and merge : " + this.cliOptions); } /* * Initialize the objects to parse the command line * * @return */ private Options setupParsing() { Options configuration = new Options(); for (CLIOption cliOption : cliOptions) { @SuppressWarnings("static-access") Option option = OptionBuilder.withLongOpt(cliOption.getLongOpt()) .hasArgs(cliOption.getnArgs()) .isRequired(cliOption.isRequired()) .withDescription(cliOption.getDescription()) .create(cliOption.getShortOpt()); logger.debug("Created option " + option); configuration.addOption(option); } return configuration; } // Maybe some useful message here as well private void printAndReThrow(Throwable t) throws ParseException, ValidationException { Options options = setupParsing(); if (t instanceof ParseException) { // TODO Add some more info on error formatter.printHelp(commandName, options); throw (ParseException) t; } else if (t instanceof ValidationException) { // TODO Add some more info on error formatter.printHelp(commandName, options); throw (ValidationException) t; } else { // TODO Add some more info on error formatter.printHelp(commandName, options); // By default we wrap everything inside a ValidationException throw new ValidationException(t); } } // Maybe this should be a pipeline thing ? public void parse(String[] args) throws ParseException, ValidationException { // If --help is present then print usage and exit(0) // prindUsage(args); try { logger.debug("Parsing Input"); ApplicationConfiguration application = parseTheInput(args); logger.debug("Basic Validation of Input " + application); validate(application); // This is actually only for better readability logger.debug("Prepare CLIInput"); List<String> inputs = prepareCLIInputs(); logger.debug("\t" + inputs); logger.debug("Prepare CLIOptions"); Map<CLIOption, String> options = prepareCLIOptions(); logger.debug("\t" + options); logger.debug("CLI Validation"); validate(options, inputs); // Setup the CLIOptionSource bridgeCLIOptionProvider.add(options); bridgeCLIOptionProvider.add(inputs); } catch (Exception e) { printAndReThrow(e); } } private Option findOptionByName(String name) { for (Option option : parsedOptions.getOptions()) { if (option.getOpt().equals(name) || option.getLongOpt().equals(name)) { return option; } } logger.warn("Cannot find option " + name); return null; } private ApplicationConfiguration parseTheInput(String[] args) throws ParseException { Options options = setupParsing(); logger.debug("Parsing " + Arrays.toString(args)); // Parse the input line parsedOptions = parser.parse(options, args); List<String> parsedInputs = parsedOptions.getArgList(); for (CLIOption cliOption : cliOptions) { logger.debug("Processing " + cliOption.toString()); Option theOption = findOptionByName(cliOption.getShortOpt()); if (theOption != null) { if (!theOption.hasArg()) { // FLAG: this may override the previous value cliOption.setValue("true"); } else { if (!theOption.hasArgs()) { // 1 arg cliOption.setValue(theOption.getValue()); } else { // 2+ args cliOption.setValues(theOption.getValues()); } } logger.debug("Done Option" + cliOption.toString()); } else { // Set default values if any logger.debug(String.format("CLIOption %s is not set.", cliOption.toString())); if (cliOption.getnArgs() == 0) { // FLAG Options have default value to false. They become // true ONLY if they were specified on the command line logger.debug("CLIParserImpl.parse(): Default setting " + cliOption.getLongOpt() + " == false "); cliOption.setDefaultValue("false"); cliOption.setValue("false"); } else { if (cliOption.getnArgs() == 1) { logger.debug(String.format( "Force default value %s for Option %s", cliOption.getDefaultValue(), cliOption.toString())); cliOption.setValue(cliOption.getDefaultValue()); logger.debug("The value now is " + cliOption.getValue()); } else if (cliOption.getnArgs() > 1) { logger.debug("Force default values %s for Option %s", Arrays.toString(cliOption.getDefaultValues()), cliOption.toString()); cliOption.setValues(cliOption.getDefaultValues()); logger.debug("The values now are " + Arrays.toString(cliOption.getValues())); } } } } return applicationConfigurationSource.get(cliOptions, parsedInputs); } private void validate(ApplicationConfiguration applicationConfiguration) throws ValidationException { boolean isValid = true; StringBuffer errorBuffer = new StringBuffer(); errorBuffer.append("\t Violation Messages: \n"); try { for (Object property : applicationConfiguration.getAllProperties()) { Set<ConstraintViolation<Object>> result = validator .validate(property); for (ConstraintViolation<Object> viol : result) { logger.debug("CLIParserImpl.validate() : " + viol.getMessage()); logger.debug("CLIParserImpl.validate() : " + viol.getConstraintDescriptor()); logger.debug("CLIParserImpl.validate() : " + viol.getInvalidValue()); errorBuffer.append("-"); errorBuffer.append(viol.getMessage()); errorBuffer.append("\n"); } if (result.size() > 0) { isValid = false; } } errorBuffer.append("\n"); errorBuffer.append("\n"); } catch (Exception e) { formatter.printWrapped(pw, formatter.getWidth(), errorBuffer.toString()); pw.flush(); throw new ValidationException("Error during validation", e); } if (!isValid) { formatter.printWrapped(pw, formatter.getWidth(), errorBuffer.toString()); pw.flush(); throw new ValidationException( "The provided command line input is not valid"); } } private void validate(Map<CLIOption, String> options, List<String> inputs) throws ValidationException { boolean isValid = true; StringBuffer errorBuffer = new StringBuffer(); errorBuffer.append("\tViolation Messages: \n"); try { List<String> result = new ArrayList<String>(); cliValidator.validate(options, inputs, result); for (String violation : result) { logger.warn("CLIParserImpl.validate() Input Violation : " + violation); errorBuffer.append("-"); errorBuffer.append(violation); errorBuffer.append("\n"); } if (result.size() > 0) { isValid = false; } errorBuffer.append("\n"); errorBuffer.append("\n"); } catch (Throwable e) { formatter.printWrapped(pw, formatter.getWidth(), errorBuffer.toString()); pw.flush(); throw new ValidationException("Error during validation", e); } if (!isValid) { formatter.printWrapped(pw, formatter.getWidth(), errorBuffer.toString()); pw.flush(); throw new ValidationException( "The provided command line input is not valid"); } } /** * User the parsed command line to build the structure that contains the * ORDERED user inputs */ @SuppressWarnings("unchecked") private List<String> prepareCLIInputs() { try { List<String> result = new ArrayList<String>(); result.addAll(parsedOptions.getArgList()); return result; } catch (Throwable e) { logger.error("Error while preparing CLI Inputs", e); throw new RuntimeException(e); } } /** * Use the parsed options and the CLIOptions definitions to create a map of * values that corresponds to the final data to be validated * * @return */ private Map<CLIOption, String> prepareCLIOptions() { Map<CLIOption, String> result = new HashMap<CLIOption, String>(); try { for (CLIOption cliOption : cliOptions) { if (cliOption.getValue() != null) { result.put(cliOption, cliOption.getValue()); } else if (cliOption.getValues() != null) { result.put(cliOption, Arrays.toString(cliOption.getValues())); } } return result; } catch (Exception e) { logger.error("Error while preparing CLIOptions", e); throw new RuntimeException(e); } } }