View Javadoc

1   /*
2    * This file is part of hyphenType. hyphenType is free software: you can
3    * redistribute it and/or modify it under the terms of the GNU General Public
4    * License as published by the Free Software Foundation, either version 3 of the
5    * License, or (at your option) any later version. hyphenType is distributed in
6    * the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
8    * the GNU General Public License for more details. You should have received a
9    * copy of the GNU General Public License along with hyphenType. If not, see
10   * <http://www.gnu.org/licenses/>.
11   */
12  package org.hyphenType.lexerparser;
13  
14  import java.lang.annotation.Annotation;
15  import java.lang.reflect.Method;
16  import java.util.ArrayList;
17  import java.util.Arrays;
18  import java.util.Collections;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.regex.Pattern;
22  
23  import org.hyphenType.datastructure.Options;
24  import org.hyphenType.datastructure.annotations.ArgumentsObject;
25  import org.hyphenType.datastructure.annotations.InputChannel;
26  import org.hyphenType.datastructure.annotations.Option;
27  import org.hyphenType.datastructure.annotations.OptionArgument;
28  import org.hyphenType.datastructure.annotations.OptionMapValue;
29  import org.hyphenType.datastructure.annotations.OptionValue;
30  import org.hyphenType.datastructure.annotations.SimpleArgument;
31  import org.hyphenType.datastructure.lexer.LexToken;
32  import org.hyphenType.datastructure.lexer.option.LexOption;
33  import org.hyphenType.datastructure.lexer.option.LexOptionMapValue;
34  import org.hyphenType.datastructure.lexer.option.LexOptionValue;
35  import org.hyphenType.datastructure.lexer.simple.LexArgument;
36  import org.hyphenType.datastructure.parser.StructureElement;
37  import org.hyphenType.datastructure.parser.option.StructureOption;
38  import org.hyphenType.datastructure.parser.option.StructureOptionArgument;
39  import org.hyphenType.datastructure.parser.option.StructureOptionMapValue;
40  import org.hyphenType.datastructure.parser.option.StructureOptionValue;
41  import org.hyphenType.datastructure.parser.simple.StructureSimpleArgument;
42  import org.hyphenType.exceptions.InvalidOptionsInterfaceException;
43  import org.hyphenType.util.DefaultAnnotation;
44  import org.hyphenType.util.I18NResourceBundle;
45  
46  /**
47   * A LexerParser is an object that can parse an array of strings and create a
48   * list of tokens from it. To do so, the LexerParser's constructor performs a
49   * deep analysis on the options interface.<br>
50   * <br>
51   * Therefore, invoking the LexerParser
52   * constructor can also be used to validate the options interface.
53   * 
54   * @author Aurelio Akira M. Matsui
55   * @param <T>
56   */
57  public class LexerParser<T extends org.hyphenType.datastructure.Options<?>> {
58  
59      /**
60       * 
61       */
62      private final ArgumentsObject argumentsObject;
63      /**
64       * 
65       */
66      private final Class<T> optionsInterface;
67      /**
68       * 
69       */
70      private final List<StructureOption> parsedOptions;
71      /**
72       * 
73       */
74      private final List<StructureSimpleArgument> simpleArguments;
75  
76      /**
77       * @param optionsInterfaceClass
78       * @throws InvalidOptionsInterfaceException
79       */
80      public LexerParser(final Class<T> optionsInterfaceClass) throws InvalidOptionsInterfaceException {
81          this(optionsInterfaceClass, true);
82      }
83      
84      /**
85       * @param optionsInterfaceClass
86       * @param loadFromResourceBundle
87       * @throws InvalidOptionsInterfaceException
88       */
89      @SuppressWarnings("unchecked")
90      public LexerParser(final Class<T> optionsInterfaceClass, boolean loadFromResourceBundle) throws InvalidOptionsInterfaceException {
91  
92          // CHECKING AND BUILDING DATA STRUCTURE
93  
94          optionsInterface = optionsInterfaceClass;
95          
96          if (!optionsInterfaceClass.isInterface()) {
97              throw new InvalidOptionsInterfaceException(optionsInterfaceClass.getName() + " is not an interface.");
98          }
99  
100         argumentsObject = DefaultAnnotation.getAnnotation(optionsInterfaceClass, ArgumentsObject.class);
101 
102         I18NResourceBundle i18nrb = null;
103         if(loadFromResourceBundle) {
104             i18nrb = new I18NResourceBundle(optionsInterfaceClass);
105             DefaultAnnotation.fillWithResourceBundle(argumentsObject, i18nrb);
106         }
107 
108         boolean foundOptionsInterface = false;
109         for (Class c : optionsInterfaceClass.getInterfaces()) {
110             if (c.equals(Options.class)) {
111                 foundOptionsInterface = true;
112             }
113         }
114         if (!foundOptionsInterface) {
115             throw new InvalidOptionsInterfaceException("Interface " + optionsInterfaceClass.getName() + " should extend the interface " + Options.class.getName() + ".");
116         }
117 
118         if (!argumentsObject.statusCodeEnum().isEnum()) {
119             throw new InvalidOptionsInterfaceException("The status code class " + argumentsObject.statusCodeEnum().getName() + " is not an enumeration.");
120         }
121 
122         // Verification over. SUCCESS!
123 
124         ArrayList<StructureOption> tempOptions = new ArrayList<StructureOption>();
125         ArrayList<StructureSimpleArgument> tempSimpleArguments = new ArrayList<StructureSimpleArgument>();
126 
127         Method[] methods = optionsInterfaceClass.getMethods();
128 
129         // Checking if all the methods have one and only one of our annotations.
130         a: for (Method m : methods) {
131             for (Method m2 : Options.class.getMethods()) {
132                 if (m.equals(m2)) {
133                     continue a;
134                 }
135             }
136             int annotations = 0;
137             if (m.isAnnotationPresent(Option.class)) {
138                 annotations++;
139             }
140             if (m.isAnnotationPresent(OptionArgument.class)) {
141                 annotations++;
142             }
143             if (m.isAnnotationPresent(OptionMapValue.class)) {
144                 annotations++;
145             }
146             if (m.isAnnotationPresent(OptionValue.class)) {
147                 annotations++;
148             }
149             if (m.isAnnotationPresent(SimpleArgument.class)) {
150                 annotations++;
151             }
152             if (annotations == 0) {
153                 throw new InvalidOptionsInterfaceException("Method " + m + " has no annotation.");
154             }
155             if (annotations > 1) {
156                 throw new InvalidOptionsInterfaceException("Method " + m + " has more than one annotation.");
157             }
158         }
159 
160         // Extracting StructureOption objects
161         for (Method m : methods) {
162 
163             if (m.isAnnotationPresent((Class<? extends Annotation>) Option.class)) {
164 
165                 if (m.getParameterTypes().length > 0) {
166                     throw new InvalidOptionsInterfaceException("Method " + m + " should have no parameters.");
167                 }
168 
169                 if (!m.getReturnType().equals(boolean.class) && !m.getReturnType().equals(Boolean.class) && !m.getReturnType().equals(int.class) && !m.getReturnType().equals(Integer.class)) {
170                     throw new InvalidOptionsInterfaceException("Method " + m + " should return boolean, " + Boolean.class.getName() + ", int, or " + Integer.class.getName() + ".");
171                 }
172 
173                 ArrayList<String> alternatives = new ArrayList<String>();
174                 Option opt = DefaultAnnotation.getAnnotation(m, Option.class);
175                 if(loadFromResourceBundle) {
176                     DefaultAnnotation.fillWithResourceBundle(opt, i18nrb);
177                 }
178                 if (opt.names().length == 1 && opt.names()[0].equals("")) {
179                     alternatives.add(m.getName());
180                 } else {
181                     for (String alternative : opt.names()) {
182                         alternatives.add(alternative);
183                     }
184                 }
185 
186                 StructureOptionValue value = null;
187                 for (Method m2 : methods) {
188                     if (m2.isAnnotationPresent((Class<? extends Annotation>) OptionValue.class)) {
189                         OptionValue optionValue = DefaultAnnotation.getAnnotation(m2, OptionValue.class);
190                         if(loadFromResourceBundle) {
191                             DefaultAnnotation.fillWithResourceBundle(optionValue, i18nrb);
192                         }
193                         if (optionValue.option().equals(m.getName())) {
194                             if (value == null) {
195                                 if (optionValue.name().equals("")) {
196                                     value = new StructureOptionValue(m2, m2.getName(), optionValue.mandatory(), optionValue.arraySeparator(), optionValue.arrayUseFileSeparator());
197                                 } else {
198                                     value = new StructureOptionValue(m2, optionValue.name(), optionValue.mandatory(), optionValue.arraySeparator(), optionValue.arrayUseFileSeparator());
199                                 }
200                             } else {
201                                 throw new InvalidOptionsInterfaceException("The option " + optionValue.option() + " has more than one option value. Options should have only one option value.");
202                             }
203                         }
204                     }
205                 }
206 
207                 StructureOptionMapValue map = null;
208                 for (Method m2 : methods) {
209                     if (m2.isAnnotationPresent((Class<? extends Annotation>) OptionMapValue.class)) {
210                         OptionMapValue optionMapValue = DefaultAnnotation.getAnnotation(m2, OptionMapValue.class);
211                         if(loadFromResourceBundle) {
212                             DefaultAnnotation.fillWithResourceBundle(optionMapValue, i18nrb);
213                         }
214                         if (!m2.getReturnType().equals(Map.class)) {
215                             throw new InvalidOptionsInterfaceException("The method " + m2 + " is an option map, therefore its return type should be " + Map.class.getName() + ".");
216                         }
217                         if (optionMapValue.option().equals(m.getName())) {
218                             if (map == null) {
219                                 map = new StructureOptionMapValue(m2, optionMapValue.keyName(), optionMapValue.valueName(), optionMapValue.valueType(), optionMapValue.mandatory());
220                             } else {
221                                 throw new InvalidOptionsInterfaceException("The option " + optionMapValue.option() + " has more than one option map. Options should have only one option map.");
222                             }
223                         }
224                     }
225                 }
226 
227                 List<StructureOptionArgument> optionArguments = new ArrayList<StructureOptionArgument>();
228                 boolean alreadyHasArrayArgument = false;
229                 boolean somethingFound = false;
230                 boolean alreadyHasOptional = false;
231                 boolean alreadyHasOptionWithoutArgumentInputChannel = false;
232                 do {
233                     somethingFound = false;
234                     for (Method m2 : methods) {
235                         if (m2.isAnnotationPresent((Class<? extends Annotation>) OptionArgument.class)) {
236                             OptionArgument optionArgument = DefaultAnnotation.getAnnotation(m2, OptionArgument.class);
237                             if(loadFromResourceBundle) {
238                                 DefaultAnnotation.fillWithResourceBundle(optionArgument, i18nrb);
239                             }
240                             if (optionArgument.option().equals(m.getName()) && optionArgument.index() == optionArguments.size()) {
241 
242                                 somethingFound = true;
243 
244                                 if (optionArgument.mandatory()) {
245                                     if (alreadyHasOptional) {
246                                         throw new InvalidOptionsInterfaceException("LexUnknown " + m2 + " is mandatory after an optional argument. No mandatory argument can follow an optional argument.");
247                                     }
248                                 } else {
249                                     alreadyHasOptional = true;
250                                 }
251 
252                                 if (alreadyHasArrayArgument) {
253                                     throw new InvalidOptionsInterfaceException("Arguments referring to option " + m + " can only have one array and the array argument should be the last one. To solve this problem you can remove agument " + m2);
254                                 }
255                                 if (m2.getReturnType().isArray()) {
256                                     alreadyHasArrayArgument = true;
257                                 }
258                                 String name = optionArgument.name();
259                                 if (name.equals("")) {
260                                     name = m2.getName();
261                                 }
262                                 if (optionArgument.channels().length == 0) {
263                                     throw new InvalidOptionsInterfaceException("Option argument " + m2 + " should have at least one input channel.");
264                                 }
265                                 if (Arrays.binarySearch(optionArgument.channels(), InputChannel.ARGUMENT, null) >= 0) {
266                                     if (alreadyHasOptionWithoutArgumentInputChannel) {
267                                         throw new InvalidOptionsInterfaceException("Option argument " + m2 + " should not have the " + InputChannel.ARGUMENT + " input channel. Arguments containing the " + InputChannel.ARGUMENT + " input channel should not succeed arguments without this input channel.");
268                                     }
269                                 } else {
270                                     alreadyHasOptionWithoutArgumentInputChannel = true;
271                                 }
272 
273                                 if (!optionArgument.mandatory() && Arrays.binarySearch(optionArgument.channels(), InputChannel.GUI, null) >= 0) {
274                                     throw new InvalidOptionsInterfaceException("Option argument " + m2 + " is optional. So it should not have the " + InputChannel.GUI + " input channel.");
275                                 }
276                                 if (!optionArgument.mandatory() && Arrays.binarySearch(optionArgument.channels(), InputChannel.TEXT, null) >= 0) {
277                                     throw new InvalidOptionsInterfaceException("Option argument " + m2 + " is optional. So it should not have the " + InputChannel.TEXT + " input channel.");
278                                 }
279 
280                                 optionArguments.add(new StructureOptionArgument(name, m2, optionArgument.mandatory(), optionArgument.index(), optionArgument.regex(), Arrays.asList(optionArgument.channels()), optionArgument.description()));
281                             }
282                         }
283                     }
284                 } while (somethingFound);
285 
286                 Collections.sort(optionArguments);
287                 tempOptions.add(new StructureOption(m, opt.description(), alternatives, value, map, Collections.unmodifiableList(optionArguments)));
288             }
289         }
290 
291         // Extracting ParsedParsedSimple objects
292         for (Method m : methods) {
293             if (m.isAnnotationPresent((Class<? extends Annotation>) SimpleArgument.class)) {
294                 
295                 if (m.getParameterTypes().length > 0) {
296                     throw new InvalidOptionsInterfaceException("Method " + m + " should have no parameters.");
297                 }
298                 
299                 SimpleArgument annotation = DefaultAnnotation.getAnnotation(m, SimpleArgument.class);
300                 if(loadFromResourceBundle) {
301                     DefaultAnnotation.fillWithResourceBundle(annotation, i18nrb);
302                 }
303                 
304                 String name;
305                 if (annotation.name().equals("")) {
306                     name = m.getName();
307                 } else {
308                     name = annotation.name();
309                 }
310                 tempSimpleArguments.add(new StructureSimpleArgument(m, name, annotation.mandatory(), annotation.index(), annotation.regex(), Arrays.asList(annotation.channels()), annotation.description()));
311             }
312         }
313 
314         Collections.sort(tempSimpleArguments);
315 
316         boolean alreadyHasOptional = false;
317         boolean alreadyHasArray = false;
318 
319         for (StructureSimpleArgument a : tempSimpleArguments) {
320             if (alreadyHasArray) {
321                 throw new InvalidOptionsInterfaceException("Cannot have any argument after an array argument. Array arguments will consume all remaining arguments.");
322             }
323             if (a.isMandatory()) {
324                 if (alreadyHasOptional) {
325                     throw new InvalidOptionsInterfaceException("The parsedOptions interface should not have a mandatory agument after an optional one.");
326                 }
327             } else {
328                 alreadyHasOptional = true;
329             }
330             if (a.method.getReturnType().isArray()) {
331                 if (alreadyHasArray) {
332                     throw new InvalidOptionsInterfaceException("Only the last argument can be an array argument.");
333                 }
334                 alreadyHasArray = true;
335             }
336         }
337 
338         /*
339          * Sorting the options and simple arguments. We need to sort those lists
340          * because we will want to print documentations of them in a sorted
341          * order.
342          */
343 
344         Collections.sort(tempOptions);
345         parsedOptions = Collections.unmodifiableList(tempOptions);
346         Collections.sort(tempSimpleArguments);
347         simpleArguments = Collections.unmodifiableList(tempSimpleArguments);
348 
349         if (parsedOptions.isEmpty() && simpleArguments.isEmpty()) {
350             throw new InvalidOptionsInterfaceException("Empty parsedOptions interface: " + optionsInterfaceClass.getName());
351         }
352     }
353 
354     /**
355      * @return TODO
356      */
357     public final ArgumentsObject getArgsObject() {
358         return argumentsObject;
359     }
360 
361     /**
362      * @return TODO
363      */
364     public final Class<T> getOptionsInterface() {
365         return optionsInterface;
366     }
367 
368     /**
369      * Classifies arguments and creates tokens based on it.
370      * <ol>
371      * <li>{@link LexOption} (as "version" in "-version")</li>
372      * <li>{@link LexOptionValue} (as "12" in "-x=12")</li>
373      * <li>{@link LexOptionMapValue} (as "a=b" in "-xa=b")</li>
374      * <li>{@link LexUnknown} (as "aa" and "bb" in "-x aa bb")</li>
375      * </ol>
376      * 
377      * @param arguments
378      *            The arguments to be converted into lexer tokens.
379      * @return A list of tokens in the order they appear in the arguments.
380      */
381     public final List<LexToken> lexArguments(final String... arguments) {
382 
383         ArrayList<LexToken> expandedArgs = new ArrayList<LexToken>();
384 
385         // Solely to make the source code more readable
386 
387         final String hyphen = argumentsObject.singleHyphen();
388         final String hyphenHyphen = argumentsObject.doubleHyphen();
389         final String equals = argumentsObject.equals();
390 
391         // index to iterate over the simple arguments
392         int simpleArgumentIndex = 0;
393 
394         a: for (String argument : arguments) {
395 
396             if (argument.equals(hyphen)) {
397                 expandedArgs.add(new LexArgument(hyphen));
398                 continue a;
399             }
400 
401             if (argument.equals(hyphenHyphen)) {
402                 expandedArgs.add(new LexArgument(hyphenHyphen));
403                 continue a;
404             }
405 
406             if (simpleArguments.size() > 0 && simpleArgumentIndex < simpleArguments.size()) {
407                 StructureSimpleArgument simpleArgument = simpleArguments.get(simpleArgumentIndex);
408                 String regex = simpleArgument.getRegex();
409                 regex = regex.replace("\\h", argumentsObject.singleHyphen());
410                 regex = regex.replace("\\H", argumentsObject.doubleHyphen());
411                 if (Pattern.matches(regex, argument)) {
412                     expandedArgs.add(new LexArgument(argument));
413                     if (!simpleArgument.method.getReturnType().isArray()) {
414                         simpleArgumentIndex++;
415                     }
416                     continue a;
417                 }
418             }
419 
420             if (argumentsObject.doubleHyphenInLongOptions()) {
421                 if (argument.startsWith(hyphen) && !argument.startsWith(hyphenHyphen)) {
422                     StructureOption option = searchOption(argument.substring(hyphen.length(), hyphen.length() + 1));
423                     if (option == null || option.map == null) {
424                         int i = hyphen.length();
425                         while (i < argument.length() && argument.indexOf(equals) != i) {
426                             expandedArgs.add(new LexOption(String.valueOf(argument.charAt(i))));
427                             if (i + 1 < argument.length() && argument.indexOf(equals) == i + 1) {
428                                 expandedArgs.add(new LexOptionValue(argument.substring(i + 1 + equals.length())));
429                             }
430                             i++;
431                         }
432                     } else {
433                         if (argument.contains(equals)) {
434                             expandedArgs.add(new LexOption(String.valueOf(argument.charAt(hyphen.length()))));
435                             expandedArgs.add(new LexOptionMapValue(argument.substring(hyphen.length() + equals.length())));
436                         } else {
437                             for (int i = 1; i < argument.length(); i++) {
438                                 expandedArgs.add(new LexOption(String.valueOf(argument.charAt(i))));
439                             }
440                         }
441                     }
442                 } else if (argument.startsWith(hyphenHyphen)) {
443                     if (argument.contains(equals)) {
444                         expandedArgs.add(new LexOption(argument.substring(hyphenHyphen.length(), argument.indexOf(equals))));
445                         expandedArgs.add(new LexOptionValue(argument.substring(argument.indexOf(equals) + equals.length(), argument.length())));
446                     } else {
447                         expandedArgs.add(new LexOption(argument.substring(hyphenHyphen.length())));
448                     }
449                 } else {
450                     expandedArgs.add(new LexArgument(argument));
451                 }
452             } else {
453                 if (argument.startsWith(hyphen)) {
454                     if (argument.contains(equals)) {
455                         String prefix = searchOptionPrefixing(argument);
456                         if (prefix != null) {
457                             expandedArgs.add(new LexOption(prefix));
458                             if (argument.indexOf(equals) == prefix.length() + 1) {
459                                 expandedArgs.add(new LexOptionValue(argument.substring(prefix.length() + 1 + equals.length())));
460                             } else {
461                                 expandedArgs.add(new LexOptionMapValue(argument.substring(prefix.length() + 1)));
462                             }
463                         } else {
464                             expandedArgs.add(new LexOption(argument.substring(1, argument.indexOf(equals))));
465                             expandedArgs.add(new LexOptionValue(argument.substring(argument.indexOf(equals) + 1, argument.length())));
466                         }
467                     } else {
468                         expandedArgs.add(new LexOption(String.valueOf(argument.substring(hyphen.length()))));
469                     }
470                 } else {
471                     expandedArgs.add(new LexArgument(argument));
472                 }
473             }
474         }
475 
476         return expandedArgs;
477     }
478 
479     /**
480      * @return TODO
481      */
482     public final List<StructureOption> getParsedOptions() {
483         return Collections.unmodifiableList(parsedOptions);
484     }
485 
486     /**
487      * @return TODO
488      */
489     public final List<StructureSimpleArgument> getSimpleArguments() {
490         return Collections.unmodifiableList(simpleArguments);
491     }
492 
493     /**
494      * @param name
495      *            TODO
496      * @return TODO
497      */
498     public final StructureOption searchOption(final String name) {
499         for (StructureOption parsedOption : parsedOptions) {
500             if (parsedOption.alternatives.contains(name)) {
501                 return parsedOption;
502             }
503         }
504         return null;
505     }
506 
507     /**
508      * @param name
509      *            TODO
510      * @return TODO
511      */
512     public final String searchOptionPrefixing(final String name) {
513         String filteredName = name.substring(1);
514         for (StructureOption option : parsedOptions) {
515             for (String alternative : option.alternatives) {
516                 if (filteredName.startsWith(alternative)) {
517                     return alternative;
518                 }
519             }
520         }
521         return null;
522     }
523 
524     /**
525      * @param name
526      *            TODO
527      * @return TODO
528      */
529     public final StructureSimpleArgument searchArgument(final String name) {
530         for (StructureSimpleArgument simpleArgument : simpleArguments) {
531             if (simpleArgument.getName().equals(name)) {
532                 return simpleArgument;
533             }
534         }
535         return null;
536     }
537 
538     /**
539      * @param method
540      *            TODO
541      * @return TODO
542      */
543     public final StructureElement searchElement(final Method method) {
544         for (StructureOption option : parsedOptions) {
545             if (option.method.equals(method)) {
546                 return option;
547             }
548             if (option.value != null && option.value.method.equals(method)) {
549                 return option.value;
550             }
551             if (option.map != null && option.map.method.equals(method)) {
552                 return option.map;
553             }
554             for (StructureOptionArgument optionArgument : option.arguments) {
555                 if (optionArgument.method.equals(method)) {
556                     return optionArgument;
557                 }
558             }
559         }
560         for (StructureSimpleArgument simpleArgument : simpleArguments) {
561             if (simpleArgument.method.equals(method)) {
562                 return simpleArgument;
563             }
564         }
565 
566         return null;
567     }
568 }