View Javadoc

1   /*
2   Copyright (C) 2003 Grid Systems, S.A.
3   
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License, version 2, as
6   published by the Free Software Foundation.
7   
8   This program is distributed in the hope that it will be useful,
9   but WITHOUT ANY WARRANTY; without even the implied warranty of
10  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  GNU General Public License for more details.
12  
13  You should have received a copy of the GNU General Public License
14  along with this program; if not, write to the Free Software
15  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16  */
17  package com.gridsystems.launcher;
18  
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.lang.reflect.InvocationTargetException;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.util.Iterator;
26  import java.util.Map;
27  import java.util.Properties;
28  import java.util.Stack;
29  
30  import javax.xml.parsers.ParserConfigurationException;
31  import javax.xml.parsers.SAXParser;
32  import javax.xml.parsers.SAXParserFactory;
33  
34  import org.xml.sax.Attributes;
35  import org.xml.sax.InputSource;
36  import org.xml.sax.SAXException;
37  import org.xml.sax.SAXParseException;
38  import org.xml.sax.helpers.DefaultHandler;
39  
40  /**
41   * Allows the execution of a java application within a custom classloader.
42   * <p>
43   * This class allows two usage styles. It can be used as the main application
44   * class, and it will use an "XML descriptor" to construct the classloader and
45   * execute the class, or it can be used directly from code.
46   *
47   * <h3>Descriptor Interface</h3>
48   *
49   * When executed as a standalone application, this class will search for an
50   * XML descriptor file according to the following rules:
51   *
52   * <ul>
53   *   <li>The default file name is "launcher.xml". It can be modified through
54   *       the "launcher.file" system property (-D flag in the command line)
55   *   <li>Firstly, it tries to construct a valid URL from the file name
56   *   <li>If is is not a valid URL, it checks if it corresponds to an existent file
57   *   <li>If no file is found, it will search a resource with the specified name
58   * </ul>
59   *
60   * If a valid source is found, it will be read and parsed. This descriptor
61   * will contain the name of the class to launch, as well as the classpath to use
62   * in the custom classloader. The syntax of this descriptor is rather simple:
63   * <p>
64   *
65   * <code>
66   * &lt;!DOCTYPE launcher SYSTEM "http://www.gridsystems.com/dtds/launcher.dtd"&gt;<br>
67   * &lt;launcher&gt;<br>
68   * &nbsp; &lt;application class="full.class.path" [method="XXX(default main)"]/&gt;<br>
69   * &nbsp; &nbsp; &lt;!-- Sets a system property with the given value --&gt;<br>
70   * &nbsp; &nbsp; &lt;property name="name" value="value"/&gt;
71   * &nbsp; &nbsp; &lt;!-- Sets a system property with the given relative path --&gt;<br>
72   * &nbsp; &nbsp; &lt;property name="name" path="path"/&gt;
73   * &nbsp; &lt;/application&gt;<br>
74   * <br>
75   * &nbsp; &lt;classpath&gt;<br>
76   * &nbsp; &nbsp; &lt;!-- Includes a single jar file --&gt;<br>
77   * &nbsp; &nbsp; &lt;include file="path"/&gt;<br>
78   * <br>
79   * &nbsp; &nbsp; &lt;!-- Includes a class directory --&gt;<br>
80   * &nbsp; &nbsp; &lt;include dir="path"/&gt;<br>
81   * <br>
82   * &nbsp; &nbsp; &lt;!-- Includes all jars in the specified path --&gt;<br>
83   * &nbsp; &nbsp; &lt;include jars="path" [recursive="true / false(default)"]/&gt;<br>
84   * <br>
85   * &nbsp; &nbsp; &lt;!-- Includes WEB-INF/classes and WEB-INF/lib/*.jar --&gt;<br>
86   * &nbsp; &nbsp; &lt;include webapp="path"/&gt;<br>
87   * &nbsp; &lt;/classpath&gt;<br>
88   * &lt;/launcher&gt;
89   * </code>
90   * <p>
91   * The DTD URL is ficticious, and Launcher will use an internal DTD located at the
92   * same directory as the class in the jar (via <code>class.getResource()</code>).
93   * <p>
94   * All relative paths in the descriptor are considered relative to the "base directory".
95   * This base directory is determined as follows:
96   *
97   * <ul>
98   *   <li>If a system property called <code>launcher.basedir</code> is set, uses
99   *       its value,
100  *   <li>If the property is not set, but the descriptor is a file, the file directory
101  *       will be used,
102  *   <li>Otherwise, the current working directory will be used
103  * </ul>
104  *
105  * <h3>Programmatic Interface</h3>
106  *
107  * This class can also be used from within another program. The following is an example
108  * of how to use it:
109  *
110  * <code>
111  * File tomcatHome = new File("[path to tomcat]");<br>
112  * <br>
113  * Launcher launcher = new Launcher();<br>
114  * launcher.setClassName("[class name]");<br>
115  * launcher.setMethodName("[method name]");<br>
116  * launcher.setBaseDir(tomcatHome);<br>
117  * launcher.addWebapp("webapps/" + contextName);<br>
118  * launcher.addDir("common/classes");<br>
119  * launcher.addJars("common/lib");<br>
120  * <br>
121  * try {<br>
122  * &nbsp; launcher.launch(args);<br>
123  * }<br>
124  * catch (LaunchException e) {<br>
125  * &nbsp; ...<br>
126  * }
127  * </code>
128  *
129  * <p>
130  * Classes in the classpath are searched for in order of addition, so the order of
131  * calls to addXXX methods is important. In the example above, classes will be searched
132  * for starting by [contextName]/WEB-INF/classes, and ending by the jars in
133  * [tomcatHome]/common/lib.
134  *
135  * @author Rodrigo Ruiz
136  * @version 1.0
137  */
138 public class Launcher extends DefaultHandler {
139   /**
140    * The Classloader builder instance.
141    */
142   private CLBuilder clb = new CLBuilder();
143 
144   /**
145    * The base directory for relative path calculations.
146    */
147   private File baseDir = getBaseDir();
148 
149   /**
150    * The context stack.
151    */
152   private Stack context = new Stack();
153 
154   /**
155    * The name of the class to launch.
156    */
157   private String className = null;
158 
159   /**
160    * The name of the method to execute.
161    */
162   private String methodName = "main";
163 
164   /**
165    * Empty constructor.
166    */
167   public Launcher() {
168   }
169 
170   /**
171    * Executes the configured class and method, passing it the specified arguments,
172    * into the built class loader. The method must have the following signature:
173    * <p>
174    * <code>static public void [methodName](String[])</code>
175    *
176    * @param args The arguments to pass to the method
177    * @throws LaunchException If an unhandled error is thrown during the execution
178    */
179   public void launch(String[] args) throws LaunchException {
180     ClassLoader cl = clb.getClassLoader();
181     Thread.currentThread().setContextClassLoader(cl);
182     try {
183       Class c = cl.loadClass(className);
184       Class[] types = { String[].class };
185       Object[] params = { args };
186       c.getMethod(methodName, types).invoke(null, params);
187     } catch (ClassNotFoundException cnfe) {
188       throw new LaunchException("Class " + className + " not found", cnfe);
189     } catch (NoSuchMethodException nsme) {
190       throw new LaunchException("Method main(String[]) not found in " + className, nsme);
191     } catch (IllegalAccessException iae) {
192       throw new LaunchException("Method main(String[]) not public in " + className, iae);
193     } catch (InvocationTargetException ite) {
194       Throwable t = ite.getCause();
195       throw new LaunchException("Unhandled exception: " + t.getMessage(), t);
196     }
197   }
198 
199   /**
200    * Sets the name of the class to execute.
201    *
202    * @param className The fully qualified class name
203    */
204   public void setClassName(String className) {
205     this.className = className;
206   }
207 
208   /**
209    * Sets the name of the method to execute. The method MUST have the following
210    * signature (or a compatible one):
211    * <p>
212    * <code>static public void [methodName](String args[]) throws Exception</code>
213    *
214    * @param methodName The name of the method to invoke
215    */
216   public void setMethodName(String methodName) {
217     this.methodName = methodName == null ? "main" : methodName;
218   }
219 
220   /**
221    * Gets the base directory for all relative paths.
222    * <p>
223    * If not explicitly set, a default base directory will be selected according to
224    * the rules explained above.
225    *
226    * @return The base directory
227    */
228   public File getBaseDir() {
229     if (this.baseDir == null) {
230       String base = System.getProperty("launcher.basedir");
231       if (base != null) {
232         this.baseDir = new File(base);
233       } else {
234         String resName = System.getProperty("launcher.file", "launcher.xml");
235         File f = new File(resName);
236         if (f.exists()) {
237           this.baseDir = f.getParentFile();
238         } else {
239           this.baseDir = new File(".");
240         }
241       }
242     }
243     return this.baseDir;
244   }
245 
246   /**
247    * Sets the base directory. Only paths added after this change will be affected;
248    * already added paths will not be modified.
249    *
250    * @param dir The new base directory
251    */
252   public void setBaseDir(File dir) {
253     this.baseDir = dir;
254   }
255 
256   /**
257    * Makes the generated classloader to be a child of the current context classloader.
258    * <p>
259    * Needed to gain access to base classes in an embedded environment.
260    */
261   public void doInherit() {
262     clb.setParent(Thread.currentThread().getContextClassLoader());
263   }
264 
265   /**
266    * Adds a single directory to the classpath.
267    *
268    * @param path A path to a directory containing .class files
269    */
270   public void addDir(String path) {
271     clb.addDir(getFile(path));
272   }
273 
274   /**
275    * Adds a single file to the classpath. Currently, only .jar files are accepted.
276    *
277    * @param path A relative
278    */
279   public void addFile(String path) {
280     clb.addFile(getFile(path));
281   }
282 
283   /**
284    * Adds all jars in the specified directory to the classpath.
285    * <p>
286    * The order of the jars in path is platform dependent, so applications should not
287    * rely on them being sorted by any specific criteria.
288    *
289    * @param path A path to a directory containing jar files
290    * @param recursive Whether the search will recurse into subdirectories or not
291    */
292   public void addJars(String path, boolean recursive) {
293     clb.addJars(getFile(path), recursive);
294   }
295 
296   /**
297    * Adds a single URL to the classpath.
298    *
299    * @param surl The string with the URL to add
300    * @throws SAXException In case of a syntax error in the URL
301    */
302   public void addUrl(String surl) throws SAXException {
303     try {
304       URL url = new URL(surl);
305       clb.addUrl(url);
306     } catch (Exception e) {
307       throw new SAXException("Malformed URL: " + surl, e);
308     }
309   }
310 
311   /**
312    * Adds a single URL to the classpath.
313    *
314    * @param url The URL to add
315    */
316   public void addUrl(URL url) {
317     clb.addUrl(url);
318   }
319 
320   /**
321    * Adds a web application to the classpath.
322    * <p>
323    * It is equivalent to:
324    * <p>
325    * <code>
326    * launcher.addDir(path + "/WEB-INF/classes");<br>
327    * launcher.addJars(path + "/WEB-INF/lib", false);
328    * </code>
329    *
330    * @param path The path to the web application context
331    */
332   public void addWebApp(String path) {
333     clb.addWebApp(getFile(path));
334   }
335 
336   /**
337    * Adds a war packed web application to the classpath.
338    *
339    * @param path Path to the .war file
340    */
341   public void addWar(String path) {
342     clb.addWar(getFile(path));
343   }
344 
345   /**
346    * Loads all properties from the specified file path and adds them as system
347    * properties.
348    *
349    * @param path  The path to the .properties file
350    * @throws IOException In case of read error
351    */
352   public void loadProperties(String path) throws IOException {
353     File f = getFile(path);
354     if (f.exists() && f.isFile()) {
355       FileInputStream fis = null;
356       try {
357         fis = new FileInputStream(f);
358         Properties p = new Properties();
359         p.load(fis);
360 
361         for (Iterator it = p.entrySet().iterator(); it.hasNext();) {
362           Map.Entry entry = (Map.Entry)it.next();
363           System.setProperty((String)entry.getKey(), (String)entry.getValue());
364         }
365       } finally {
366         try {
367           fis.close();
368         } catch (Exception ignore) {
369         }
370       }
371     }
372   }
373 
374   /**
375    * Starts the launcher in "stand-alone" mode.
376    *
377    * @param args The the command line arguments
378    */
379   public static void main(String[] args) {
380     URL descriptor = getDescriptorUrl();
381 
382     if (descriptor == null) {
383       System.err.println("FATAL: Launcher descriptor not found");
384       System.exit(1);
385     }
386 
387     Launcher launcher = new Launcher();
388 
389     try {
390       // Gets the SAX driver class
391       String driver = System.getProperty("org.xml.sax.driver");
392       if (driver == null) {
393         driver = "org.apache.crimson.parser.XMLReaderImpl";
394       }
395 
396       // Creates a parser
397       SAXParserFactory factory = SAXParserFactory.newInstance();
398       factory.setValidating(true);
399       SAXParser parser = factory.newSAXParser();
400 
401       // Parses the descriptor
402       parser.parse(descriptor.openStream(), launcher);
403     } catch (IOException ie) {
404       System.err.println("Error reading descriptor:");
405       ie.printStackTrace();
406       System.exit(1);
407     } catch (SAXException e) {
408       System.err.println("Error parsing descriptor:");
409       e.printStackTrace();
410       System.exit(1);
411     } catch (ParserConfigurationException pce) {
412       System.err.println("Parser configuration exception: ");
413       pce.printStackTrace();
414       System.exit(1);
415     }
416 
417     // Launches application
418     try {
419       launcher.launch(args);
420     } catch (LaunchException e) {
421       System.err.println("FATAL: " + e.getMessage());
422       e.printStackTrace();
423       System.exit(1);
424     }
425   }
426 
427   /**
428    * Discovers the URL to the descriptor file, and returns it.
429    *
430    * @return The URL to the XML descriptor file, or null if not found
431    */
432   private static URL getDescriptorUrl() {
433     String resName = System.getProperty("launcher.file", "launcher.xml");
434 
435     // First, try to use resName as a URL
436     try {
437       return new URL(resName);
438     } catch (MalformedURLException e) {
439     }
440 
441     // Malformed URL --> try resName as a file name/path
442     File f = new File(resName);
443     if (f.exists()) {
444       try {
445         return f.toURI().toURL();
446       } catch (Exception e) {
447       }
448     }
449 
450     // File does not exists or an exception was thrown
451     // during translation into an URL instance.
452     // Consider it as a resource
453     return ClassLoader.getSystemResource(resName);
454   }
455 
456   /**
457    * Obtains a File instance from the specified path.
458    * <p>
459    * If a relative path is specified, it is considered relative to the base directory.
460    * <p>
461    * The returned file is always "absolute"
462    *
463    * @param path The path to a file or directory
464    * @return A File instance pointing to the requested path
465    */
466   private File getFile(String path) {
467     // Parses embedded system properties
468     path = parse(path);
469     // Treat some special values
470     if (path.equals("JDK_TOOLS")) {
471       String jrePath = System.getProperty("java.home");
472       File f = new File(jrePath, "../lib/tools.jar");
473       return f;
474     } else {
475       File f = new File(path);
476       if (!f.isAbsolute()) {
477         f = new File(baseDir, path);
478       }
479       return f;
480     }
481   }
482 
483   /**
484    * Searches for substrings in the form ${name} in the specified string, and replaces
485    * them by the value of name in the system properties, if there is any value
486    * associated.
487    *
488    * @param s The string to parse
489    * @return  The parsed string
490    */
491   private String parse(String s) {
492     int pos = s.indexOf("${");
493     while (pos != -1) {
494       int pos2 = s.indexOf("}", pos + 2);
495       if (pos2 != -1) {
496         String name = s.substring(pos + 2, pos2);
497         String value = System.getProperty(name);
498         if (value != null) {
499           s = s.substring(0, pos) + value + s.substring(pos2 + 1);
500         }
501       } else {
502         break;
503       }
504     }
505     return s;
506   }
507 
508   //=============================================================================
509   // EntityResolver Interface Implementation (SAX Parser)
510   //=============================================================================
511 
512   /**
513    * {@inheritDoc}
514    */
515   public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
516     if ("-//GridSystems//launcher".equals(publicId)
517         || "http://www.gridsystems.com/dtds/launcher.dtd".equals(systemId)) {
518       try {
519         URL url = getClass().getResource("launcher.dtd");
520         return new InputSource(url.openStream());
521       } catch (IOException e) {
522         throw new SAXException("I/O Error", e);
523       }
524     }
525     return null;
526   }
527 
528   //=============================================================================
529   // ContentHandler Interface Implementation (SAX Parser)
530   //=============================================================================
531 
532   /**
533    * {@inheritDoc}
534    */
535   public void endElement(String namespaceURI, String localName, String qName)
536     throws SAXException {
537     context.pop();
538   }
539 
540   /**
541    * {@inheritDoc}
542    */
543   public void startElement(String ns, String localName, String qName, Attributes atts)
544     throws SAXException {
545     String ctx = (context.size() == 0) ? null : (String)context.peek();
546 
547     if ("".equals(localName)) {
548       localName = qName;
549     }
550 
551     if ("launcher".equals(ctx)) {
552       parseLauncher(localName, atts);
553     } else if ("application".equals(ctx)) {
554       parseApplication(localName, atts);
555     } else if ("classpath".equals(ctx)) {
556       parseClasspath(localName, atts);
557     }
558 
559     context.push(localName);
560   }
561 
562   /**
563    * Parses launcher subnodes.
564    *
565    * @param localName The subnode local name
566    * @param atts The subnod attributes
567    */
568   private void parseLauncher(String localName, Attributes atts) {
569     if ("classpath".equals(localName)) {
570       String inherit = atts.getValue("inherit");
571       if ("true".equals(inherit)) {
572         doInherit();
573       }
574     } else if ("application".equals(localName)) {
575       setClassName(atts.getValue("class"));
576 
577       setMethodName(atts.getValue("method"));
578     }
579   }
580   /**
581    * Parses application subnodes.
582    *
583    * @param localName The subnode local name
584    * @param atts The subnode attributes
585    */
586   private void parseApplication(String localName, Attributes atts) {
587     if ("property".equals(localName)) {
588       String path = atts.getValue("file");
589       if (path != null) {
590         try {
591           loadProperties(path);
592         } catch (IOException e) {
593           // just ignore
594         }
595       } else {
596         String name = atts.getValue("name");
597         String value = atts.getValue("value");
598         if (value == null) {
599           value = getFile(atts.getValue("path")).getPath();
600         }
601         System.setProperty(name, value);
602       }
603     }
604   }
605   /**
606    * Parses classpath subnodes.
607    *
608    * @param localName The subnode local name
609    * @param atts The subnode attributes
610    * @throws SAXException If a malformed URL is found
611    */
612   private void parseClasspath(String localName, Attributes atts) throws SAXException {
613     if ("include".equals(localName)) {
614       String flag = atts.getValue("recursive");
615       boolean recursive = "true".equals(flag);
616 
617       for (int i = 0; i < atts.getLength(); i++) {
618         String attrName = atts.getLocalName(i);
619         if ("".equals(attrName)) {
620           attrName = atts.getQName(i);
621         }
622 
623         String attrValue = atts.getValue(i);
624         if ("dir".equals(attrName)) {
625           addDir(attrValue);
626         } else if ("file".equals(attrName)) {
627           addFile(attrValue);
628         } else if ("jars".equals(attrName)) {
629           addJars(attrValue, recursive);
630         } else if ("url".equals(attrName)) {
631           addUrl(attrValue);
632         } else if ("war".equals(attrName)) {
633           addWar(attrValue);
634         } else if ("webapp".equals(attrName)) {
635           addWebApp(attrValue);
636         }
637       }
638     }
639   }
640 
641   //=============================================================================
642   // ErrorHandler Interface Implementation (SAX Parser)
643   //=============================================================================
644 
645   /**
646    * {@inheritDoc}
647    */
648   public void error(SAXParseException e) throws SAXException {
649     throw e;
650   }
651 
652   /**
653    * {@inheritDoc}
654    */
655   public void fatalError(SAXParseException e) throws SAXException {
656     throw e;
657   }
658 
659   /**
660    * {@inheritDoc}
661    */
662   public void warning(SAXParseException err) throws SAXException {
663     System.out.println("WARNING: line " + err.getLineNumber()
664                        + ", uri " + err.getSystemId());
665     System.out.println(" - " + err.getMessage());
666   }
667 }