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 * <!DOCTYPE launcher SYSTEM "http://www.gridsystems.com/dtds/launcher.dtd"><br>
67 * <launcher><br>
68 * <application class="full.class.path" [method="XXX(default main)"]/><br>
69 * <!-- Sets a system property with the given value --><br>
70 * <property name="name" value="value"/>
71 * <!-- Sets a system property with the given relative path --><br>
72 * <property name="name" path="path"/>
73 * </application><br>
74 * <br>
75 * <classpath><br>
76 * <!-- Includes a single jar file --><br>
77 * <include file="path"/><br>
78 * <br>
79 * <!-- Includes a class directory --><br>
80 * <include dir="path"/><br>
81 * <br>
82 * <!-- Includes all jars in the specified path --><br>
83 * <include jars="path" [recursive="true / false(default)"]/><br>
84 * <br>
85 * <!-- Includes WEB-INF/classes and WEB-INF/lib/*.jar --><br>
86 * <include webapp="path"/><br>
87 * </classpath><br>
88 * </launcher>
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 * launcher.launch(args);<br>
123 * }<br>
124 * catch (LaunchException e) {<br>
125 * ...<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 }