1   package eu.fbk.knowledgestore.internal;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.io.PrintWriter;
7   import java.net.URL;
8   import java.util.HashMap;
9   import java.util.HashSet;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.Properties;
13  import java.util.Set;
14  
15  import javax.annotation.Nullable;
16  
17  import com.google.common.base.Joiner;
18  import com.google.common.base.MoreObjects;
19  import com.google.common.base.Preconditions;
20  import com.google.common.collect.ImmutableList;
21  import com.google.common.collect.Lists;
22  import com.google.common.collect.Maps;
23  import com.google.common.collect.Ordering;
24  
25  import org.apache.commons.cli.DefaultParser;
26  import org.apache.commons.cli.HelpFormatter;
27  import org.apache.commons.cli.Option;
28  import org.apache.commons.cli.Options;
29  import org.slf4j.Logger;
30  
31  import ch.qos.logback.classic.Level;
32  
33  import eu.fbk.knowledgestore.data.Data;
34  
35  public final class CommandLine {
36  
37      private final List<String> args;
38  
39      private final List<String> options;
40  
41      private final Map<String, List<String>> optionValues;
42  
43      private CommandLine(final List<String> args, final Map<String, List<String>> optionValues) {
44  
45          final List<String> options = Lists.newArrayList();
46          for (final String letterOrName : optionValues.keySet()) {
47              if (letterOrName.length() > 1) {
48                  options.add(letterOrName);
49              }
50          }
51  
52          this.args = args;
53          this.options = Ordering.natural().immutableSortedCopy(options);
54          this.optionValues = optionValues;
55      }
56  
57      public <T> List<T> getArgs(final Class<T> type) {
58          return convert(this.args, type);
59      }
60  
61      public <T> T getArg(final int index, final Class<T> type) {
62          return convert(this.args.get(index), type);
63      }
64  
65      public <T> T getArg(final int index, final Class<T> type, final T defaultValue) {
66          try {
67              return convert(this.args.get(index), type);
68          } catch (final Throwable ex) {
69              return defaultValue;
70          }
71      }
72  
73      public int getArgCount() {
74          return this.args.size();
75      }
76  
77      public List<String> getOptions() {
78          return this.options;
79      }
80  
81      public boolean hasOption(final String letterOrName) {
82          return this.optionValues.containsKey(letterOrName);
83      }
84  
85      public <T> List<T> getOptionValues(final String letterOrName, final Class<T> type) {
86          final List<String> strings = MoreObjects.firstNonNull(this.optionValues.get(letterOrName),
87                  ImmutableList.<String>of());
88          return convert(strings, type);
89      }
90  
91      @Nullable
92      public <T> T getOptionValue(final String letterOrName, final Class<T> type) {
93          final List<String> strings = this.optionValues.get(letterOrName);
94          if (strings == null || strings.isEmpty()) {
95              return null;
96          }
97          if (strings.size() > 1) {
98              throw new Exception("Multiple values for option '" + letterOrName + "': "
99                      + Joiner.on(", ").join(strings), null);
100         }
101         try {
102             return convert(strings.get(0), type);
103         } catch (final Throwable ex) {
104             throw new Exception("'" + strings.get(0) + "' is not a valid " + type.getSimpleName(),
105                     ex);
106         }
107     }
108 
109     @Nullable
110     public <T> T getOptionValue(final String letterOrName, final Class<T> type,
111             @Nullable final T defaultValue) {
112         final List<String> strings = this.optionValues.get(letterOrName);
113         if (strings == null || strings.isEmpty() || strings.size() > 1) {
114             return defaultValue;
115         }
116         try {
117             return convert(strings.get(0), type);
118         } catch (final Throwable ex) {
119             return defaultValue;
120         }
121     }
122 
123     public int getOptionCount() {
124         return this.options.size();
125     }
126 
127     private static <T> T convert(final String string, final Class<T> type) {
128         try {
129             return Data.convert(string, type);
130         } catch (final Throwable ex) {
131             throw new Exception("'" + string + "' is not a valid " + type.getSimpleName(), ex);
132         }
133     }
134 
135     @SuppressWarnings("unchecked")
136     private static <T> List<T> convert(final List<String> strings, final Class<T> type) {
137         if (type == String.class) {
138             return (List<T>) strings;
139         }
140         final List<T> list = Lists.newArrayList();
141         for (final String string : strings) {
142             list.add(convert(string, type));
143         }
144         return ImmutableList.copyOf(list);
145     }
146 
147     public static void fail(final Throwable throwable) {
148         if (throwable instanceof Exception) {
149             if (throwable.getMessage() == null) {
150                 System.exit(0);
151             } else {
152                 System.err.println("SYNTAX ERROR: " + throwable.getMessage());
153             }
154             System.exit(-2);
155         } else {
156             System.err.println("EXECUTION FAILED: " + throwable.getMessage());
157             throwable.printStackTrace();
158             System.exit(-1);
159         }
160     }
161 
162     public static Parser parser() {
163         return new Parser();
164     }
165 
166     public static final class Parser {
167 
168         @Nullable
169         private String name;
170 
171         @Nullable
172         private String header;
173 
174         @Nullable
175         private String footer;
176 
177         @Nullable
178         private Logger logger;
179 
180         private final Options options;
181 
182         private final Map<Option, Type> optionTypes;
183 
184         private final Set<String> mandatoryOptions;
185 
186         public Parser() {
187             this.name = null;
188             this.header = null;
189             this.footer = null;
190             this.options = new Options();
191             this.optionTypes = new HashMap<>();
192             this.mandatoryOptions = new HashSet<>();
193         }
194 
195         public Parser withName(@Nullable final String name) {
196             this.name = name;
197             return this;
198         }
199 
200         public Parser withHeader(@Nullable final String header) {
201             this.header = header;
202             return this;
203         }
204 
205         public Parser withFooter(@Nullable final String footer) {
206             this.footer = footer;
207             return this;
208         }
209 
210         public Parser withLogger(@Nullable final Logger logger) {
211             this.logger = logger;
212             return this;
213         }
214 
215         public Parser withOption(@Nullable final String letter, final String name,
216                 final String description) {
217 
218             Preconditions.checkNotNull(name);
219             Preconditions.checkArgument(name.length() > 1);
220             Preconditions.checkNotNull(description);
221 
222             final Option option = new Option(letter, name, false, description);
223             this.options.addOption(option);
224 
225             return this;
226         }
227 
228         public Parser withOption(@Nullable final String letter, final String name,
229                 final String description, final String argName, @Nullable final Type argType,
230                 final boolean argRequired, final boolean multiValue, final boolean mandatory) {
231 
232             Preconditions.checkNotNull(name);
233             Preconditions.checkArgument(name.length() > 1);
234             Preconditions.checkNotNull(description);
235             Preconditions.checkNotNull(argName);
236 
237             final Option option = new Option(letter, name, true, description);
238             option.setArgName(argName);
239             option.setOptionalArg(!argRequired);
240             option.setArgs(multiValue ? Short.MAX_VALUE : 1);
241             this.options.addOption(option);
242 
243             if (argType != null) {
244                 this.optionTypes.put(option, argType);
245             }
246 
247             if (mandatory) {
248                 this.mandatoryOptions.add(name);
249             }
250 
251             return this;
252         }
253 
254         public CommandLine parse(final String... args) {
255 
256             try {
257                 // Add additional options
258                 if (this.logger != null) {
259                     this.options.addOption("V", "verbose", false, "enable verbose output");
260                 }
261                 this.options.addOption("v", "version", false,
262                         "display version information and terminate");
263                 this.options.addOption("h", "help", false,
264                         "display this help message and terminate");
265 
266                 // Parse options
267                 org.apache.commons.cli.CommandLine cmd = null;
268                 try {
269                     cmd = new DefaultParser().parse(this.options, args);
270                 } catch (final Throwable ex) {
271                     System.err.println("SYNTAX ERROR: " + ex.getMessage());
272                     printHelp();
273                     throw new Exception(null);
274                 }
275 
276                 // Handle verbose mode
277                 if (cmd.hasOption('V')) {
278                     try {
279                         ((ch.qos.logback.classic.Logger) this.logger).setLevel(Level.DEBUG);
280                     } catch (final Throwable ex) {
281                         // ignore
282                     }
283                 }
284 
285                 // Handle version and help commands. Throw an exception to halt execution
286                 if (cmd.hasOption('v')) {
287                     printVersion();
288                     throw new Exception(null);
289 
290                 } else if (cmd.hasOption('h')) {
291                     printHelp();
292                     throw new Exception(null);
293                 }
294 
295                 // Check that mandatory options have been specified
296                 for (final String name : this.mandatoryOptions) {
297                     if (!cmd.hasOption(name)) {
298                         System.err.println("SYNTAX ERROR: missing mandatory option " + name);
299                         printHelp();
300                         throw new Exception(null);
301                     }
302                 }
303 
304                 // Extract options and their arguments
305                 final Map<String, List<String>> optionValues = Maps.newHashMap();
306                 for (final Option option : cmd.getOptions()) {
307                     final List<String> valueList = Lists.newArrayList();
308                     final String[] values = cmd.getOptionValues(option.getLongOpt());
309                     if (values != null) {
310                         final Type type = this.optionTypes.get(option);
311                         for (final String value : values) {
312                             if (type != null) {
313                                 Type.validate(value, type);
314                             }
315                             valueList.add(value);
316                         }
317                     }
318                     final List<String> valueSet = ImmutableList.copyOf(valueList);
319                     optionValues.put(option.getLongOpt(), valueSet);
320                     if (option.getOpt() != null) {
321                         optionValues.put(option.getOpt(), valueSet);
322                     }
323                 }
324 
325                 // Create and return the resulting CommandLine object
326                 return new CommandLine(ImmutableList.copyOf(cmd.getArgList()), optionValues);
327 
328             } catch (final Throwable ex) {
329                 throw new Exception(ex.getMessage(), ex);
330             }
331         }
332 
333         private void printVersion() {
334             String version = "(development)";
335             final URL url = CommandLine.class.getClassLoader().getResource(
336                     "META-INF/maven/eu.fbk.nafview/nafview/pom.properties");
337             if (url != null) {
338                 try {
339                     final InputStream stream = url.openStream();
340                     try {
341                         final Properties properties = new Properties();
342                         properties.load(stream);
343                         version = properties.getProperty("version").trim();
344                     } finally {
345                         stream.close();
346                     }
347 
348                 } catch (final IOException ex) {
349                     version = "(unknown)";
350                 }
351             }
352             final String name = MoreObjects.firstNonNull(this.name, "Version");
353             System.out.println(String.format("%s %s\nJava %s bit (%s) %s\n", name, version,
354                     System.getProperty("sun.arch.data.model"), System.getProperty("java.vendor"),
355                     System.getProperty("java.version")));
356         }
357 
358         private void printHelp() {
359             final HelpFormatter formatter = new HelpFormatter();
360             final PrintWriter out = new PrintWriter(System.out);
361             final String name = MoreObjects.firstNonNull(this.name, "java");
362             formatter.printUsage(out, 80, name, this.options);
363             if (this.header != null) {
364                 out.println();
365                 formatter.printWrapped(out, 80, this.header);
366             }
367             out.println();
368             formatter.printOptions(out, 80, this.options, 2, 2);
369             if (this.footer != null) {
370                 out.println();
371                 out.println(this.footer);
372                 // formatter.printWrapped(out, 80, this.footer);
373             }
374             out.flush();
375         }
376 
377     }
378 
379     public static final class Exception extends RuntimeException {
380 
381         private static final long serialVersionUID = 1L;
382 
383         public Exception(final String message) {
384             super(message);
385         }
386 
387         public Exception(final String message, final Throwable cause) {
388             super(message, cause);
389         }
390 
391     }
392 
393     public enum Type {
394 
395         STRING,
396 
397         INTEGER,
398 
399         POSITIVE_INTEGER,
400 
401         NON_NEGATIVE_INTEGER,
402 
403         FLOAT,
404 
405         POSITIVE_FLOAT,
406 
407         NON_NEGATIVE_FLOAT,
408 
409         FILE,
410 
411         FILE_EXISTING,
412 
413         DIRECTORY,
414 
415         DIRECTORY_EXISTING;
416 
417         public boolean validate(final String string) {
418             // Polymorphism not used for performance reasons
419             return validate(string, this);
420         }
421 
422         private static boolean validate(final String string, final Type type) {
423 
424             if (type == Type.INTEGER || type == Type.POSITIVE_INTEGER
425                     || type == Type.NON_NEGATIVE_INTEGER) {
426                 try {
427                     final long n = Long.parseLong(string);
428                     if (type == Type.POSITIVE_INTEGER) {
429                         return n > 0L;
430                     } else if (type == Type.NON_NEGATIVE_INTEGER) {
431                         return n >= 0L;
432                     }
433                 } catch (final Throwable ex) {
434                     return false;
435                 }
436 
437             } else if (type == Type.FLOAT || type == Type.POSITIVE_FLOAT
438                     || type == Type.NON_NEGATIVE_FLOAT) {
439                 try {
440                     final double n = Double.parseDouble(string);
441                     if (type == Type.POSITIVE_FLOAT) {
442                         return n > 0.0;
443                     } else if (type == Type.NON_NEGATIVE_FLOAT) {
444                         return n >= 0.0;
445                     }
446                 } catch (final Throwable ex) {
447                     return false;
448                 }
449 
450             } else if (type == FILE) {
451                 final File file = new File(string);
452                 return !file.exists() || file.isFile();
453 
454             } else if (type == FILE_EXISTING) {
455                 final File file = new File(string);
456                 return file.exists() && file.isFile();
457 
458             } else if (type == DIRECTORY) {
459                 final File dir = new File(string);
460                 return !dir.exists() || dir.isDirectory();
461 
462             } else if (type == DIRECTORY_EXISTING) {
463                 final File dir = new File(string);
464                 return dir.exists() && dir.isDirectory();
465             }
466 
467             return true;
468         }
469 
470     }
471 
472 }