package eu.fbk.knowledgestore.internal; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.annotation.Nullable; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.slf4j.Logger; import ch.qos.logback.classic.Level; import eu.fbk.knowledgestore.data.Data; public final class CommandLine { private final List<String> args; private final List<String> options; private final Map<String, List<String>> optionValues; private CommandLine(final List<String> args, final Map<String, List<String>> optionValues) { final List<String> options = Lists.newArrayList(); for (final String letterOrName : optionValues.keySet()) { if (letterOrName.length() > 1) { options.add(letterOrName); } } this.args = args; this.options = Ordering.natural().immutableSortedCopy(options); this.optionValues = optionValues; } public <T> List<T> getArgs(final Class<T> type) { return convert(this.args, type); } public <T> T getArg(final int index, final Class<T> type) { return convert(this.args.get(index), type); } public <T> T getArg(final int index, final Class<T> type, final T defaultValue) { try { return convert(this.args.get(index), type); } catch (final Throwable ex) { return defaultValue; } } public int getArgCount() { return this.args.size(); } public List<String> getOptions() { return this.options; } public boolean hasOption(final String letterOrName) { return this.optionValues.containsKey(letterOrName); } public <T> List<T> getOptionValues(final String letterOrName, final Class<T> type) { final List<String> strings = MoreObjects.firstNonNull(this.optionValues.get(letterOrName), ImmutableList.<String>of()); return convert(strings, type); } @Nullable public <T> T getOptionValue(final String letterOrName, final Class<T> type) { final List<String> strings = this.optionValues.get(letterOrName); if (strings == null || strings.isEmpty()) { return null; } if (strings.size() > 1) { throw new Exception("Multiple values for option '" + letterOrName + "': " + Joiner.on(", ").join(strings), null); } try { return convert(strings.get(0), type); } catch (final Throwable ex) { throw new Exception("'" + strings.get(0) + "' is not a valid " + type.getSimpleName(), ex); } } @Nullable public <T> T getOptionValue(final String letterOrName, final Class<T> type, @Nullable final T defaultValue) { final List<String> strings = this.optionValues.get(letterOrName); if (strings == null || strings.isEmpty() || strings.size() > 1) { return defaultValue; } try { return convert(strings.get(0), type); } catch (final Throwable ex) { return defaultValue; } } public int getOptionCount() { return this.options.size(); } private static <T> T convert(final String string, final Class<T> type) { try { return Data.convert(string, type); } catch (final Throwable ex) { throw new Exception("'" + string + "' is not a valid " + type.getSimpleName(), ex); } } @SuppressWarnings("unchecked") private static <T> List<T> convert(final List<String> strings, final Class<T> type) { if (type == String.class) { return (List<T>) strings; } final List<T> list = Lists.newArrayList(); for (final String string : strings) { list.add(convert(string, type)); } return ImmutableList.copyOf(list); } public static void fail(final Throwable throwable) { if (throwable instanceof Exception) { if (throwable.getMessage() == null) { System.exit(0); } else { System.err.println("SYNTAX ERROR: " + throwable.getMessage()); } System.exit(-2); } else { System.err.println("EXECUTION FAILED: " + throwable.getMessage()); throwable.printStackTrace(); System.exit(-1); } } public static Parser parser() { return new Parser(); } public static final class Parser { @Nullable private String name; @Nullable private String header; @Nullable private String footer; @Nullable private Logger logger; private final Options options; private final Map<Option, Type> optionTypes; private final Set<String> mandatoryOptions; public Parser() { this.name = null; this.header = null; this.footer = null; this.options = new Options(); this.optionTypes = new HashMap<>(); this.mandatoryOptions = new HashSet<>(); } public Parser withName(@Nullable final String name) { this.name = name; return this; } public Parser withHeader(@Nullable final String header) { this.header = header; return this; } public Parser withFooter(@Nullable final String footer) { this.footer = footer; return this; } public Parser withLogger(@Nullable final Logger logger) { this.logger = logger; return this; } public Parser withOption(@Nullable final String letter, final String name, final String description) { Preconditions.checkNotNull(name); Preconditions.checkArgument(name.length() > 1); Preconditions.checkNotNull(description); final Option option = new Option(letter, name, false, description); this.options.addOption(option); return this; } public Parser withOption(@Nullable final String letter, final String name, final String description, final String argName, @Nullable final Type argType, final boolean argRequired, final boolean multiValue, final boolean mandatory) { Preconditions.checkNotNull(name); Preconditions.checkArgument(name.length() > 1); Preconditions.checkNotNull(description); Preconditions.checkNotNull(argName); final Option option = new Option(letter, name, true, description); option.setArgName(argName); option.setOptionalArg(!argRequired); option.setArgs(multiValue ? Short.MAX_VALUE : 1); this.options.addOption(option); if (argType != null) { this.optionTypes.put(option, argType); } if (mandatory) { this.mandatoryOptions.add(name); } return this; } public CommandLine parse(final String... args) { try { // Add additional options if (this.logger != null) { this.options.addOption("V", "verbose", false, "enable verbose output"); } this.options.addOption("v", "version", false, "display version information and terminate"); this.options.addOption("h", "help", false, "display this help message and terminate"); // Parse options org.apache.commons.cli.CommandLine cmd = null; try { cmd = new DefaultParser().parse(this.options, args); } catch (final Throwable ex) { System.err.println("SYNTAX ERROR: " + ex.getMessage()); printHelp(); throw new Exception(null); } // Handle verbose mode if (cmd.hasOption('V')) { try { ((ch.qos.logback.classic.Logger) this.logger).setLevel(Level.DEBUG); } catch (final Throwable ex) { // ignore } } // Handle version and help commands. Throw an exception to halt execution if (cmd.hasOption('v')) { printVersion(); throw new Exception(null); } else if (cmd.hasOption('h')) { printHelp(); throw new Exception(null); } // Check that mandatory options have been specified for (final String name : this.mandatoryOptions) { if (!cmd.hasOption(name)) { System.err.println("SYNTAX ERROR: missing mandatory option " + name); printHelp(); throw new Exception(null); } } // Extract options and their arguments final Map<String, List<String>> optionValues = Maps.newHashMap(); for (final Option option : cmd.getOptions()) { final List<String> valueList = Lists.newArrayList(); final String[] values = cmd.getOptionValues(option.getLongOpt()); if (values != null) { final Type type = this.optionTypes.get(option); for (final String value : values) { if (type != null) { Type.validate(value, type); } valueList.add(value); } } final List<String> valueSet = ImmutableList.copyOf(valueList); optionValues.put(option.getLongOpt(), valueSet); if (option.getOpt() != null) { optionValues.put(option.getOpt(), valueSet); } } // Create and return the resulting CommandLine object return new CommandLine(ImmutableList.copyOf(cmd.getArgList()), optionValues); } catch (final Throwable ex) { throw new Exception(ex.getMessage(), ex); } } private void printVersion() { String version = "(development)"; final URL url = CommandLine.class.getClassLoader().getResource( "META-INF/maven/eu.fbk.nafview/nafview/pom.properties"); if (url != null) { try { final InputStream stream = url.openStream(); try { final Properties properties = new Properties(); properties.load(stream); version = properties.getProperty("version").trim(); } finally { stream.close(); } } catch (final IOException ex) { version = "(unknown)"; } } final String name = MoreObjects.firstNonNull(this.name, "Version"); System.out.println(String.format("%s %s\nJava %s bit (%s) %s\n", name, version, System.getProperty("sun.arch.data.model"), System.getProperty("java.vendor"), System.getProperty("java.version"))); } private void printHelp() { final HelpFormatter formatter = new HelpFormatter(); final PrintWriter out = new PrintWriter(System.out); final String name = MoreObjects.firstNonNull(this.name, "java"); formatter.printUsage(out, 80, name, this.options); if (this.header != null) { out.println(); formatter.printWrapped(out, 80, this.header); } out.println(); formatter.printOptions(out, 80, this.options, 2, 2); if (this.footer != null) { out.println(); out.println(this.footer); // formatter.printWrapped(out, 80, this.footer); } out.flush(); } } public static final class Exception extends RuntimeException { private static final long serialVersionUID = 1L; public Exception(final String message) { super(message); } public Exception(final String message, final Throwable cause) { super(message, cause); } } public enum Type { STRING, INTEGER, POSITIVE_INTEGER, NON_NEGATIVE_INTEGER, FLOAT, POSITIVE_FLOAT, NON_NEGATIVE_FLOAT, FILE, FILE_EXISTING, DIRECTORY, DIRECTORY_EXISTING; public boolean validate(final String string) { // Polymorphism not used for performance reasons return validate(string, this); } private static boolean validate(final String string, final Type type) { if (type == Type.INTEGER || type == Type.POSITIVE_INTEGER || type == Type.NON_NEGATIVE_INTEGER) { try { final long n = Long.parseLong(string); if (type == Type.POSITIVE_INTEGER) { return n > 0L; } else if (type == Type.NON_NEGATIVE_INTEGER) { return n >= 0L; } } catch (final Throwable ex) { return false; } } else if (type == Type.FLOAT || type == Type.POSITIVE_FLOAT || type == Type.NON_NEGATIVE_FLOAT) { try { final double n = Double.parseDouble(string); if (type == Type.POSITIVE_FLOAT) { return n > 0.0; } else if (type == Type.NON_NEGATIVE_FLOAT) { return n >= 0.0; } } catch (final Throwable ex) { return false; } } else if (type == FILE) { final File file = new File(string); return !file.exists() || file.isFile(); } else if (type == FILE_EXISTING) { final File file = new File(string); return file.exists() && file.isFile(); } else if (type == DIRECTORY) { final File dir = new File(string); return !dir.exists() || dir.isDirectory(); } else if (type == DIRECTORY_EXISTING) { final File dir = new File(string); return dir.exists() && dir.isDirectory(); } return true; } } }