1   /*
2    * RDFpro - An extensible tool for building stream-oriented RDF processing libraries.
3    * 
4    * Written in 2014 by Francesco Corcoglioniti with support by Marco Amadori, Michele Mostarda,
5    * Alessio Palmero Aprosio and Marco Rospocher. Contact info on http://rdfpro.fbk.eu/
6    * 
7    * To the extent possible under law, the authors have dedicated all copyright and related and
8    * neighboring rights to this software to the public domain worldwide. This software is
9    * distributed without any warranty.
10   * 
11   * You should have received a copy of the CC0 Public Domain Dedication along with this software.
12   * If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
13   */
14  package eu.fbk.rdfpro.util;
15  
16  import java.util.ArrayList;
17  import java.util.Collections;
18  import java.util.HashMap;
19  import java.util.List;
20  import java.util.Map;
21  
22  import javax.annotation.Nullable;
23  
24  public final class Options {
25  
26      private final List<String> positionalArgs;
27  
28      private final List<String> options;
29  
30      private final Map<String, List<String>> optionArgs;
31  
32      public static Options parse(final String spec, final String... args) {
33  
34          final String[][] argsToOptions = new String[args.length][];
35          final int[] argsToMinCard = new int[args.length];
36          final int[] argsToMaxCard = new int[args.length];
37          final List<String> mandatoryOptions = new ArrayList<String>();
38          int minPositionalCard = 0;
39          int maxPositionalCard = 0;
40  
41          for (final String tokenUntrimmed : spec.split("\\|")) {
42              final String token = tokenUntrimmed.trim();
43              final int len = token.length();
44              if (len == 0) {
45                  continue;
46              }
47              final char last = token.charAt(len - 1);
48              final int minCard = last == '!' || last == '+' ? 1 : 0;
49              final int maxCard = last == '?' || last == '!' ? 1
50                      : last == '+' || last == '*' ? Integer.MAX_VALUE : 0;
51              final boolean mandatory = minCard >= 1 && len >= 2 && token.charAt(len - 2) == last;
52              final String tokenOptions = token.substring(0, maxCard == 0 ? len
53                      : mandatory ? len - 2 : len - 1);
54              if (tokenOptions.length() == 0) {
55                  minPositionalCard = minCard;
56                  maxPositionalCard = maxCard;
57              } else {
58                  final String[] options = tokenOptions.split(",");
59                  for (int i = 0; i < options.length; ++i) {
60                      options[i] = options[i].trim();
61                      if (mandatory) {
62                          mandatoryOptions.add(options[i]);
63                      }
64                  }
65                  for (int i = 0; i < options.length; ++i) {
66                      final String optionExt = (options[i].length() == 1 ? "-" : "--") + options[i];
67                      for (int j = 0; j < args.length; ++j) {
68                          if (args[j].equals(optionExt)) {
69                              argsToOptions[j] = options;
70                              argsToMinCard[j] = minCard;
71                              argsToMaxCard[j] = maxCard;
72                          }
73                      }
74                  }
75              }
76          }
77  
78          final List<String> positionalArgs = new ArrayList<String>();
79          final List<String> optionList = new ArrayList<String>();
80          final Map<String, List<String>> optionArgs = new HashMap<String, List<String>>();
81          int j = 0;
82          while (j < args.length) {
83              final String[] options = argsToOptions[j];
84              if (options != null) {
85                  final String optionWithDashes = args[j];
86                  final int minCard = argsToMinCard[j];
87                  final int maxCard = argsToMaxCard[j];
88                  ++j;
89                  List<String> values = optionArgs.get(options[0]);
90                  if (values == null) {
91                      values = new ArrayList<String>();
92                      optionList.add(options[0]);
93                      for (int i = 0; i < options.length; ++i) {
94                          optionArgs.put(options[i], values);
95                      }
96                  }
97                  for (int k = 0; k < maxCard; ++k) {
98                      if (j < args.length && argsToOptions[j] == null) {
99                          values.add(args[j++]);
100                     } else if (k < minCard) {
101                         throw new IllegalArgumentException("Expected at least " + minCard
102                                 + " arguments for option '" + optionWithDashes + "'");
103                     } else {
104                         break;
105                     }
106                 }
107             } else if (args[j].startsWith("-") && !args[j].contains(" ")) {
108                 throw new IllegalArgumentException("Unrecognized option '" + args[j] + "'");
109             } else {
110                 positionalArgs.add(args[j++]);
111             }
112         }
113 
114         if (positionalArgs.size() < minPositionalCard) {
115             throw new IllegalArgumentException("Expected at least " + minPositionalCard
116                     + " positional arguments");
117         } else if (positionalArgs.size() > maxPositionalCard) {
118             throw new IllegalArgumentException("Expected at most " + maxPositionalCard
119                     + " positional arguments");
120         }
121 
122         for (final String option : mandatoryOptions) {
123             if (!optionArgs.containsKey(option)) {
124                 throw new IllegalArgumentException("Missing mandatory option '" + option + "'");
125             }
126         }
127 
128         return new Options(positionalArgs, optionList, optionArgs);
129     }
130 
131     private Options(final List<String> args, final List<String> options,
132             final Map<String, List<String>> optionValues) {
133 
134         this.positionalArgs = args;
135         this.options = new ArrayList<String>(options);
136         this.optionArgs = optionValues;
137 
138         Collections.sort(this.options);
139     }
140 
141     public <T> List<T> getPositionalArgs(final Class<T> type) {
142         return convert(this.positionalArgs, type);
143     }
144 
145     public <T> T getPositionalArg(final int index, final Class<T> type) {
146         return convert(this.positionalArgs.get(index), type);
147     }
148 
149     public <T> T getPositionalArg(final int index, final Class<T> type, final T defaultValue) {
150         try {
151             return convert(this.positionalArgs.get(index), type);
152         } catch (final Throwable ex) {
153             return defaultValue;
154         }
155     }
156 
157     public int getPositionalArgCount() {
158         return this.positionalArgs.size();
159     }
160 
161     public List<String> getOptions() {
162         return this.options;
163     }
164 
165     public boolean hasOption(final String optionName) {
166         return this.optionArgs.containsKey(optionName);
167     }
168 
169     public <T> List<T> getOptionArgs(final String optionName, final Class<T> type) {
170         final List<String> strings = this.optionArgs.get(optionName);
171         if (strings != null) {
172             return convert(strings, type);
173         }
174         return Collections.emptyList();
175     }
176 
177     @Nullable
178     public <T> T getOptionArg(final String optionName, final Class<T> type) {
179         final List<String> strings = this.optionArgs.get(optionName);
180         if (strings == null || strings.isEmpty()) {
181             return null;
182         }
183         if (strings.size() > 1) {
184             throw new IllegalArgumentException("Multiple args for option '" + optionName + "': "
185                     + String.join(", ", strings));
186         }
187         try {
188             return convert(strings.get(0), type);
189         } catch (final Throwable ex) {
190             throw new IllegalArgumentException("'" + strings.get(0) + "' is not a valid "
191                     + type.getSimpleName(), ex);
192         }
193     }
194 
195     @Nullable
196     public <T> T getOptionArg(final String optionName, final Class<T> type,
197             @Nullable final T defaultValue) {
198         final List<String> strings = this.optionArgs.get(optionName);
199         if (strings == null || strings.isEmpty() || strings.size() > 1) {
200             return defaultValue;
201         }
202         try {
203             return convert(strings.get(0), type);
204         } catch (final Throwable ex) {
205             return defaultValue;
206         }
207     }
208 
209     public int getOptionCount() {
210         return this.options.size();
211     }
212 
213     @Override
214     public boolean equals(final Object object) {
215         if (object == this) {
216             return true;
217         }
218         if (!(object instanceof Options)) {
219             return false;
220         }
221         final Options other = (Options) object;
222         return this.positionalArgs.equals(other.positionalArgs)
223                 && this.optionArgs.equals(other.optionArgs);
224     }
225 
226     @Override
227     public int hashCode() {
228         return this.positionalArgs.hashCode() * 37 + this.optionArgs.hashCode();
229     }
230 
231     @Override
232     public String toString() {
233         final StringBuilder builder = new StringBuilder();
234         for (final String option : this.options) {
235             builder.append(builder.length() != 0 ? " " : "").append(option);
236             final List<String> args = this.optionArgs.get(option);
237             if (args != null) {
238                 for (final String arg : args) {
239                     builder.append(' ').append(arg);
240                 }
241             }
242         }
243         for (final String arg : this.positionalArgs) {
244             builder.append(builder.length() != 0 ? " " : "").append(arg);
245         }
246         return builder.toString();
247     }
248 
249     private static <T> T convert(final String string, final Class<T> type) {
250         try {
251             return Statements.convert(string, type);
252         } catch (final Throwable ex) {
253             throw new IllegalArgumentException("'" + string + "' is not a valid "
254                     + type.getSimpleName(), ex);
255         }
256     }
257 
258     @SuppressWarnings("unchecked")
259     private static <T> List<T> convert(final List<String> strings, final Class<T> type) {
260         if (type == String.class) {
261             return (List<T>) strings;
262         }
263         final List<T> list = new ArrayList<T>();
264         for (final String string : strings) {
265             list.add(convert(string, type));
266         }
267         return Collections.unmodifiableList(list);
268     }
269 
270 }