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.wrapper;
13  
14  import static org.hyphenType.datastructure.annotations.ArgumentsObject.DEFAULT_DOUBLE_HYPHEN;
15  import static org.hyphenType.datastructure.annotations.ArgumentsObject.DEFAULT_EQUALS;
16  
17  import java.io.ByteArrayOutputStream;
18  import java.io.File;
19  import java.io.FileNotFoundException;
20  import java.io.IOException;
21  import java.io.PrintWriter;
22  import java.lang.reflect.Constructor;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.lang.reflect.Modifier;
26  import java.net.URISyntaxException;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.StringTokenizer;
31  import java.util.jar.JarFile;
32  import java.util.jar.Manifest;
33  
34  import org.hyphenType.datastructure.Options;
35  import org.hyphenType.debug.HTLogger;
36  import org.hyphenType.exceptions.InvalidOptionsInterfaceException;
37  import org.hyphenType.input.UserInput;
38  import org.hyphenType.lexerparser.exceptions.MandatorySimpleArgumentNotFoundException;
39  import org.hyphenType.lexerparser.exceptions.OptionValuesException;
40  import org.hyphenType.optionsextractor.OptionsExtractor;
41  import org.hyphenType.optionsextractor.OptionsExtractorException;
42  import org.hyphenType.unittesting.NonExceptionalExit;
43  
44  /**
45   * A wrapper that can be called instead of a standard main class. When called as
46   * a main class, this class will try to find which class is the actual main
47   * class of the application. After this actual main class was found, this class
48   * will parse the arguments to a options interface and give an instance of the
49   * options interface to the actual main class.
50   * 
51   * @author Aurelio Akira M. Matsui
52   */
53  public class StandAloneAppWrapper {
54  
55      /**
56       * Manifest files can have a key value pair with this key to explicitly set
57       * which is the actual main class.
58       */
59      public static final String KEY = "StandAloneAppWrapper-main-class";
60  
61      /**
62       * The especial argument that explicitly sets which is the main class.
63       */
64      public static final String ARGUMENT = DEFAULT_DOUBLE_HYPHEN + KEY + DEFAULT_EQUALS;
65  
66      /**
67       * Standard main method.
68       * 
69       * @param arguments
70       *            Arguments from the command line.
71       * @throws Throwable
72       *             Anything that can go wrong.
73       */
74      @SuppressWarnings("unchecked")
75      public static void main(final String... arguments) throws Throwable {
76  
77          /*
78           * TODO We need to conduct an extensive review of this main method.
79           * There are several security concerns to address here. We cannot rely
80           * on system properties or variables in order to find which is the main
81           * class. That could violate security as it could allow for malicious
82           * code to easily change the main class. The same problem does not
83           * happen when we read properties from the manifest file, as we assume
84           * that the integrity of the JAR cannot be violated. Also, we need a
85           * method to find the main class in which we analyze the stack trace to
86           * retrieve the current running class. There is a decision to make.
87           * Shall we restrict the valid main classes to only those that extend
88           * StandAloneAppWrapper? If we do restrict, then we can eliminate
89           * unreliable methods to find the main class. On the other hand, Java
90           * does not allow for multiple inheritance. So if a programmer needs to
91           * create a main class XSub that extends a superclass X, XSub will be
92           * not allowed to extend StandAloneAppWrapper. So what is the best
93           * design?
94           */
95  
96          String mainClassName = null;
97  
98          /*
99           * (1) key in MANIFEST.MF
100          */
101         /*
102          * (1.1) If this class was added to user's jar file. I.e. if this class
103          * is in the directly called jar file.
104          */
105         try {
106             JarFile jarFile = new JarFile(new File(StandAloneAppWrapper.class.getProtectionDomain().getCodeSource().getLocation().toURI()));
107             Manifest m = jarFile.getManifest();
108             mainClassName = m.getMainAttributes().getValue(KEY);
109         } catch (FileNotFoundException e) {
110             mainClassName = null;
111         } catch (IOException e) {
112             mainClassName = null;
113         } catch (URISyntaxException e) {
114             mainClassName = null;
115         }
116         /*
117          * (1.2) If this class is in a jar file referenced by the directly
118          * called jar file.
119          */
120         try {
121             StringTokenizer st = new StringTokenizer(System.getProperty("java.class.path"), System.getProperty("path.separator"));
122             if (st.countTokens() == 1) { // java -jar calls will have only one
123                                          // item in class path
124                 JarFile jarFile = new JarFile(new File(st.nextToken()));
125                 Manifest m = jarFile.getManifest();
126                 mainClassName = m.getMainAttributes().getValue(KEY);
127             }
128         } catch (FileNotFoundException e) {
129             mainClassName = null;
130         } catch (IOException e) {
131             mainClassName = null;
132         }
133 
134         String[] correctedArguments = arguments;
135 
136         /*
137          * (2) Extra argument
138          */
139         if (mainClassInvalid(mainClassName) && correctedArguments.length > 0 && correctedArguments[0].startsWith(ARGUMENT)) {
140             mainClassName = correctedArguments[0].substring(ARGUMENT.length(), correctedArguments[0].length());
141             correctedArguments = Arrays.copyOfRange(correctedArguments, 1, correctedArguments.length);
142         }
143 
144         /*
145          * (3) System property variable
146          */
147         if (mainClassInvalid(mainClassName)) {
148             mainClassName = System.getProperty(KEY);
149         }
150 
151         /*
152          * (4) System environment variable
153          */
154         if (mainClassInvalid(mainClassName)) {
155             mainClassName = System.getenv(KEY);
156         }
157 
158         /*
159          * (5) Reflection over jar file. This will detect cases in which this
160          * JVM was invoked using "java -jar something.jar" and this class
161          * StandAloneAppWrapper was indirectly called either because the main
162          * class is a subclass of StandAloneAppWrapper or because explicit call
163          * to StandAloneAppWrapper was made somehow. The same process will also
164          * detect direct calls to a jar file (such as "./something.jar") in
165          * Linux and Solaris environments.
166          */
167         if (mainClassInvalid(mainClassName)) {
168             try {
169                 StringTokenizer st = new StringTokenizer(System.getProperty("java.class.path"), System.getProperty("path.separator"));
170                 if (st.countTokens() == 1) { // java -jar calls will have only
171                     // one item in class path
172                     JarFile jarFile = new JarFile(new File(st.nextToken()));
173                     Manifest m = jarFile.getManifest();
174                     mainClassName = m.getMainAttributes().getValue("Main-Class");
175                 }
176             } catch (FileNotFoundException e) {
177                 mainClassName = null;
178             } catch (IOException e) {
179                 mainClassName = null;
180             }
181         }
182 
183         /*
184          * (6) Detects the main class via system property "sun.java.command".
185          */
186         if (mainClassInvalid(mainClassName)) {
187             try {
188                 String candidateClassName = System.getProperty("sun.java.command");
189                 // If there are arguments, they will come along the command. So
190                 // we need to
191                 // filter them out.
192                 if (candidateClassName.contains(" ")) {
193                     candidateClassName = candidateClassName.substring(0, candidateClassName.indexOf(" "));
194                 }
195                 if (!StandAloneAppWrapper.class.equals(StandAloneAppWrapper.class.getClassLoader().loadClass(candidateClassName))) {
196                     mainClassName = candidateClassName;
197                 }
198             } catch (ClassNotFoundException e) {
199                 mainClassName = null;
200             }
201         }
202 
203         if (mainClassInvalid(mainClassName)) {
204             System.err.println("Error: could not find the main class to execute. Possible methods are (in this order of precedence):\n" + "(1) add the property '" + KEY + "' to the MANIFEST.MF file;\n" + "(2) pass a first argument called " + ARGUMENT + " to the " + StandAloneAppWrapper.class.getName() + " class;\n" + "(3) set the system property " + KEY + " for the JVM (utilizing the parameter -D or programmatically before calling the " + StandAloneAppWrapper.class.getName() + " class);\n" + "(4) set the environment variable " + KEY + " before executing this class;\n" + "(5) make your main class extend or call " + StandAloneAppWrapper.class.getName() + ", put your main class in a jar file, and start the JVM using \"java -jar\"; or\n" + "(6) simply make your main class YourClass extend or call " + StandAloneAppWrapper.class.getName() + " and start the JVM using \"java YourClass\" (works also inside of your favorite IDE).");
205             return;
206         }
207         
208         Class clazz;
209         try {
210             clazz = StandAloneAppWrapper.class.getClassLoader().loadClass(mainClassName);
211         } catch (ClassNotFoundException e) {
212             System.err.println("Error: could not load the main class " + mainClassName);
213             return;
214         }
215 
216         new StandAloneAppWrapper().invokeMain(clazz, correctedArguments);
217     }
218 
219     protected static boolean mainClassInvalid(String mainClassName) {
220         if (mainClassName == null)
221             return true;
222         try {
223             StandAloneAppWrapper.class.getClassLoader().loadClass(mainClassName);
224         } catch (ClassNotFoundException e) {
225             return true;
226         }
227         return false;
228     }
229 
230     /**
231      * A safer main method. It is safer because this method receives which is
232      * the actual main class as a parameter.
233      * 
234      * @param mainClass
235      *            The main class to execute.
236      * @param arguments
237      *            The arguments, from command line.
238      * @throws Throwable
239      *             Anything that can go wrong.
240      */
241     public static void main(final Class<?> mainClass, final String... arguments) throws Throwable {
242 
243         new StandAloneAppWrapper().invokeMain(mainClass, arguments);
244     }
245 
246     /**
247      * Invokes the main method of the given class. It also parses the received
248      * arguments and creates an option object. This method will trap all
249      * throwables, preventing them to go uncaught to the JVM. Instead, an error
250      * message is printed.
251      *  
252      * @param mainClass
253      *            The main class to be executed.
254      * @param arguments
255      *            The arguments received from the command line.
256      * @param trapThrowable
257      *            If true, this method will prevent throwables from going
258      *            uncaught to the JVM, which causes the throwable to be
259      *            printed in user's console.
260      * @throws Throwable
261      *             Anything that can go wrong.
262      */
263     public final void invokeMain(final Class<?> mainClass, final String[] arguments) throws Throwable {
264         invokeMain(mainClass, arguments, true);
265     }
266     
267     /**
268      * Invokes the main method of the given class. It also parses the received
269      * arguments and creates an option object.
270      * 
271      * @param mainClass
272      *            The main class to be executed.
273      * @param arguments
274      *            The arguments received from the command line.
275      * @param trapThrowable
276      *            If true, this method will prevent throwables from going uncaught to the JVM, which causes the throwable to be printed in user's console.
277      * @throws Throwable
278      *             Anything that can go wrong.
279      */
280     public final void invokeMain(final Class<?> mainClass, final String[] arguments, final boolean trapThrowable) throws Throwable {
281 
282         List<String> messages = new ArrayList<String>();
283 
284         for (Method method : mainClass.getMethods()) {
285             if (method.getName().equals("main")) {
286                 if (Modifier.isStatic(method.getModifiers())) {
287                     if (method.getParameterTypes().length == 2 && method.getParameterTypes()[0].equals(Class.class) && method.getParameterTypes()[1].equals(String[].class)) {
288                         // Found myself! I.e. found this method
289                         // StandaloneAppWrapper#main(String[])
290                         continue;
291                     }
292                     if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(String[].class)) {
293                         // Found myself! I.e. found this method
294                         // StandaloneAppWrapper#main(Class, String[])
295                         continue;
296                     }
297                 }
298                 if (method.getParameterTypes().length != 1) {
299                     messages.add("Method main in class " + mainClass.getName() + " should have only one argument.");
300                     continue;
301                 }
302                 if (!method.getParameterTypes()[0].isInterface()) {
303                     messages.add("The type of the argument of the method main of the class " + mainClass.getName() + " should be an interface.");
304                     continue;
305                 }
306                 if (!Options.class.isAssignableFrom(method.getParameterTypes()[0])) {
307                     messages.add(String.format("The parameter of the main method should be of an interface that extends %s.", Options.class.getName()));
308                     continue;
309                 }
310                 // Main method found.
311                 try {
312                     Options<?> options;
313 
314                     try {
315                         options = buildOptionsObject(method.getParameterTypes()[0], arguments);
316                         systemOptions = options;
317                     } catch (MandatorySimpleArgumentNotFoundException e) {
318                         System.err.println(e.getLocalizedMessage());
319                         HTLogger.log(e);
320                         return;
321                     } catch (OptionValuesException e) {
322                         System.err.println(e.getLocalizedMessage());
323                         HTLogger.log(e);
324                         return;
325                     }
326 
327                     Object instance = null;
328 
329                     if (!Modifier.isStatic(method.getModifiers())) {
330                         Constructor<?> constructor;
331                         try {
332                             constructor = mainClass.getConstructor();
333                         } catch (NoSuchMethodException e) {
334                             messages.add(String.format("Class %s should have a default (empty) constructor.", mainClass.getName()));
335                             continue;
336                         }
337                         instance = constructor.newInstance();
338                     }
339 
340                     try {
341                         
342                         //
343                         // !!! INVOKING THE MAIN METHOD !!!
344                         //
345                         
346                         method.invoke(instance, options);
347                         
348                     } catch (InvocationTargetException e) {
349                         if(e.getCause() instanceof NonExceptionalExit)
350                             throw e.getCause();
351                         if(!options.exit(e.getCause())) {
352                             /*
353                              * No exit status constant caught the throwable, so
354                              * we print the throwable's localized message.
355                              */
356                             messages.add(e.getCause().getLocalizedMessage());
357                             printExceptionMessage(messages, e);
358                             if(!trapThrowable) {
359                                 throw e.getCause();
360                             }
361                         }
362                     }
363 
364                 } catch (InvalidOptionsInterfaceException e) {
365                     messages.add(String.format("Invalid options interface %s. Check the error trace for details:", method.getParameterTypes()[0].getName()));
366                     printExceptionMessage(messages, e);
367                 } catch (IllegalArgumentException e) {
368                     printExceptionMessage(messages, e);
369                 } catch (SecurityException e) {
370                     printExceptionMessage(messages, e);
371                 } catch (InstantiationException e) {
372                     printExceptionMessage(messages, e);
373                 } catch (IllegalAccessException e) {
374                     printExceptionMessage(messages, e);
375                 } catch (InvocationTargetException e) {
376                     printExceptionMessage(messages, e);
377                 }
378             }
379         }
380 
381         if (messages.size() > 0) {
382             System.err.println("There was an error executing the main class. This sort of error refers to the structure of the main class, instead of it's execution. Therefore, this error message should not appear when you distribute this application to the final user. Maybe the following can provide some hints on how to solve the problem:");
383             for (String problem : messages) {
384                 System.err.println(problem);
385             }
386         }
387     }
388 
389     private void printExceptionMessage(List<String> messages, Exception e) {
390         ByteArrayOutputStream baos = new ByteArrayOutputStream();
391         PrintWriter pw = new PrintWriter(baos);
392         e.printStackTrace(pw);
393         pw.flush();
394         try {
395             baos.flush();
396         } catch (IOException e1) {
397             // TODO Auto-generated catch block
398             e1.printStackTrace();
399         }
400         messages.add(baos.toString());
401     }
402 
403     /**
404      * Ready to be overridden in a subclass.
405      * 
406      * @param mainClass
407      *            The options interface type.
408      * @param arguments
409      *            The arguments, as received from the command line.
410      * @return The options object.
411      * @throws InvalidOptionsInterfaceException
412      *             If the options interface is invalid. There are many reasons
413      *             why an options interface may be invalid. For a detailed
414      *             description on the conditions for an options interface to be
415      *             invalid, please refer to
416      *             {@link InvalidOptionsInterfaceException}
417      * @throws OptionValuesException
418      *             If there is anything wrong with the arguments passed. For an
419      *             in depth description, please refer to
420      *             {@link OptionsExtractor#options(UserInput, String...)} .
421      * @throws OptionsExtractorException 
422      */
423     @SuppressWarnings("unchecked")
424     protected Options buildOptionsObject(final Class<?> mainClass, final String[] arguments) throws InvalidOptionsInterfaceException, OptionValuesException, OptionsExtractorException {
425         OptionsExtractor<?> optionsExtractor = new OptionsExtractor(mainClass);
426         Options result = optionsExtractor.options(arguments);
427         return result;
428     }
429 
430     private static Options<?> systemOptions;
431 
432     /**
433      * Gives system-wide access to the options object. If you did not use the
434      * {@link StandAloneAppWrapper} to start your application, this method will
435      * return null. Avoid using this method if you use the {@link StandAloneAppWrapper}
436      * for any purpose else than running the main class.
437      * 
438      * @param <E>
439      * @return
440      */
441     @SuppressWarnings("unchecked")
442     public static <E extends Options<?>> E options() {
443         return (E) systemOptions;
444     }
445 }