001: /*
002: * <copyright>
003: *
004: * Copyright 1997-2004 BBNT Solutions, LLC
005: * under sponsorship of the Defense Advanced Research Projects
006: * Agency (DARPA).
007: *
008: * You can redistribute this software and/or modify it under the
009: * terms of the Cougaar Open Source License as published on the
010: * Cougaar Open Source Website (www.cougaar.org).
011: *
012: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
013: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
014: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
015: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
016: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
018: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
019: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
020: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
021: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
022: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
023: *
024: * </copyright>
025: */
026: package org.cougaar.util;
027:
028: import java.io.BufferedReader;
029: import java.io.File;
030: import java.io.FileNotFoundException;
031: import java.io.IOException;
032: import java.io.InputStream;
033: import java.io.InputStreamReader;
034: import java.net.MalformedURLException;
035: import java.net.URL;
036: import java.util.ArrayList;
037: import java.util.Collections;
038: import java.util.Enumeration;
039: import java.util.HashMap;
040: import java.util.Iterator;
041: import java.util.List;
042: import java.util.Map;
043: import java.util.zip.ZipEntry;
044: import java.util.zip.ZipException;
045: import java.util.zip.ZipFile;
046:
047: import javax.xml.parsers.DocumentBuilder;
048: import javax.xml.parsers.DocumentBuilderFactory;
049: import javax.xml.parsers.ParserConfigurationException;
050:
051: import org.cougaar.bootstrap.SystemProperties;
052: import org.cougaar.util.log.Logger;
053: import org.cougaar.util.log.Logging;
054: import org.w3c.dom.Document;
055: import org.xml.sax.EntityResolver;
056: import org.xml.sax.InputSource;
057: import org.xml.sax.SAXException;
058:
059: /**
060: * ConfigFinder provides utilitites to search for a named file in
061: * several specified locations, returning the first location where a
062: * file by that name is found.
063: * <p>
064: * Files are found and opened by the open() method. open() tries to
065: * find the file using each of the elements of org.cougaar.config.path. The
066: * elements of org.cougaar.config.path are separated by semicolons and
067: * interpreted as URLs. The URLs in org.cougaar.config.path are interpreted
068: * relative to the directory specified by org.cougaar.install.path. Several
069: * special tokens may appear in these URLs:
070: * <pre>
071: * $RUNTIME signifies <org.cougaar.runtime.path>
072: * $SOCIETY signifies <org.cougaar.society.path>
073: * $INSTALL signifies <org.cougaar.install.path>
074: * $CONFIG signifies <org.cougaar.config>
075: * $CWD signifies <user.dir>
076: * $HOME signifies <user.home>
077: * $MOD signifies the name of a Cougaar module - a sub-directory of $INSTALL
078: * </pre>
079: * The default value for org.cougaar.config.path is defined in the static
080: * variable DEFAULT_CONFIG_PATH:
081: * $CWD;\
082: * $RUNTIME/configs/$CONFIG;\
083: * $RUNTIME/configs/common;\
084: * $SOCIETY/configs/$CONFIG;\
085: * $SOCIETY/configs/common;\
086: * $INSTALL/configs/$CONFIG;\
087: * $INSTALL/configs/common
088: * <p>
089: * If a value is specified for org.cougaar.config.path that ends with a
090: * semicolon, the above default is appended to the specified
091: * value. The URLs in org.cougaar.config.path are interpreted relative to
092: * $INSTALL. URLs may be absolute in which case some or all of the
093: * base URL may be ignored.
094: * <p>
095: * By default, $MOD is not set. However, when an object requests
096: * a ConfigFinder, it may specify a String value for $MOD. If specified,
097: * the search path used is augmented, adding 9 directories to the start
098: * of the search path:
099: * <ul>
100: * <li>$RUNTIME/$MOD/configs/$CONFIG</li>
101: * <li>$RUNTIME/$MOD/configs</li>
102: * <li>$RUNTIME/$MOD/data/$CONFIG</li>
103: * <li>$RUNTIME/$MOD/data</li>
104: * <li>$SOCIETY/$MOD/configs/$CONFIG</li>
105: * <li>$SOCIETY/$MOD/configs</li>
106: * <li>$SOCIETY/$MOD/data/$CONFIG</li>
107: * <li>$SOCIETY/$MOD/data</li>
108: * <li>$INSTALL/$MOD/configs/$CONFIG</li>
109: * <li>$INSTALL/$MOD/configs</li>
110: * <li>$INSTALL/$MOD/data/$CONFIG</li>
111: * <li>$INSTALL/$MOD/data</li>
112: * </ul>
113: * <br>
114: *
115: * Enable INFO level logging on org.cougaar.core.util.ConfigFinder to turn on
116: * additional information on usage of ConfigFinder.
117: * @property org.cougaar.install.path Used as the base path for config file finding.
118: * @property org.cougaar.config.path The search path for config files. See the class
119: * documentation for details.
120: * @property org.cougaar.core.util.ConfigFinder.verbose When set to <em>true</em>, report
121: * progress while finding each config file.
122: * @property org.cougaar.config The configuration being run, for example minitestconfig or small-135.
123: * Setting this property means that CIP/configs/<value of this property> will be searched before configs/common
124: * @property org.cougaar.util.ConfigFinder.ClassName The class to use instead of ConfigFinder, presumably an
125: * extension.
126: **/
127: public class ConfigFinder {
128: private List configPath;
129: private final Map properties; // initialized by all constructors
130:
131: /** Cache of the String to URL mappings found.
132: * @note that access should be synchronized on the cache itself.
133: **/
134: protected final HashMap urlCache = new HashMap(89);
135:
136: // logger support
137: private Logger logger = null; // use getLogger to access
138:
139: protected final synchronized Logger getLogger() {
140: if (logger == null) {
141: logger = Logging.getLogger(ConfigFinder.class);
142: }
143: return logger;
144: }
145:
146: /**
147: * Alias for ConfigFinder(null, null, null)
148: **/
149: public ConfigFinder() {
150: this (null, null, null);
151: }
152:
153: /**
154: * Alias for ConfigFinder(null, path, null)
155: **/
156: public ConfigFinder(String configpath) {
157: this (null, configpath, null);
158: }
159:
160: /**
161: * Alias for ConfigFinder(module, configpath, null)
162: **/
163: public ConfigFinder(String module, String configpath) {
164: this (module, configpath, null);
165: }
166:
167: /**
168: * Alias for ConfigFinder(null, configpath, props)
169: **/
170: public ConfigFinder(String configpath, Map props) {
171: this (null, configpath, props);
172: }
173:
174: /**
175: * Construct a ConfigFinder that will first search within
176: * the specified module, and then in the directories on the
177: * given search path, using the given Property substitutions.<br>
178: *
179: * When searching the given module, we search the following 8
180: * directories (if defined) before any other directories:
181: * <ul>
182: * <li>$RUNTIME/$module/configs/$CONFIG</li>
183: * <li>$RUNTIME/$module/configs</li>
184: * <li>$RUNTIME/$module/data/$CONFIG</li>
185: * <li>$RUNTIME/$module/data</li>
186: * <li>$SOCIETY/$module/configs/$CONFIG</li>
187: * <li>$SOCIETY/$module/configs</li>
188: * <li>$SOCIETY/$module/data/$CONFIG</li>
189: * <li>$SOCIETY/$module/data</li>
190: * <li>$INSTALL/$module/configs/$CONFIG</li>
191: * <li>$INSTALL/$module/configs</li>
192: * <li>$INSTALL/$module/data/$CONFIG</li>
193: * <li>$INSTALL/$module/data</li>
194: * </ul>
195: *
196: * @param module name of the module to use for module-specific configs. If null,
197: * no module-specific paths are added.
198: * @param configpath configuration path string. If null, defaults to Configuration.getConfigPath();
199: * @param props properties to use for configpath variable substitutions.
200: **/
201: public ConfigFinder(String module, String configpath, Map props) {
202: String s = configpath;
203: if (s == null)
204: s = Configuration.getConfigPath();
205: if (props == null)
206: props = Configuration.getDefaultProperties();
207:
208: if (getLogger().isDebugEnabled()) {
209: getLogger().debug(
210: "ConfigFinder class: " + this .getClass().getName());
211: }
212:
213: properties = new HashMap(89);
214: if (props != null)
215: properties.putAll(props);
216:
217: if (s == null) {
218: s = Configuration.getConfigPath();
219: } else {
220: s = s.replace('\\', '/'); // Make sure its a URL and not a file path
221: // append the default if we end with a ';'
222: if (s.endsWith(";"))
223: s += Configuration.getConfigPath();
224: }
225:
226: List v = new ArrayList();
227:
228: // add module paths
229: if (module != null) {
230: properties.put("MOD", module);
231: properties.put("MODULE", module);
232: // tack on to the front of the search path
233: for (int i = 0; i < 3; i++) {
234: String base = (i == 0 ? "RUNTIME" : i == 1 ? "SOCIETY"
235: : "INSTALL");
236: if (!properties.containsKey(base))
237: continue;
238: boolean has_config = properties.containsKey("CONFIG");
239: if (has_config) {
240: v.add("$" + base + "/$MOD/configs/$CONFIG");
241: }
242: v.add("$" + base + "/$MOD/configs");
243: if (has_config) {
244: v.add("$" + base + "/$MOD/data/$CONFIG");
245: }
246: v.add("$" + base + "/$MOD/data");
247: }
248: }
249:
250: // split the specified path up
251: if (s != null) {
252: String[] els = s.trim().split("\\s*;\\s*");
253: for (int i = 0; i < els.length; i++) {
254: v.add(els[i]);
255: }
256: }
257:
258: // resolve and remove duplicates
259: List expanded = new ArrayList(v.size());
260: List resolved = new ArrayList(v.size());
261: for (int i = 0; i < v.size(); i++) {
262: String vi = (String) v.get(i);
263: String ei = Configuration.substituteProperties(vi,
264: properties);
265: if (expanded.contains(ei)) {
266: continue;
267: }
268: URL ui;
269: try {
270: ui = Configuration.urlify(ei);
271: } catch (MalformedURLException mue) {
272: getLogger().error(
273: "Bad ConfigPath element \"" + vi + "\" -> \""
274: + ei + "\"", mue);
275: continue;
276: }
277: resolved.add(ui);
278: }
279:
280: // save
281: configPath = Collections.unmodifiableList(resolved);
282:
283: if (getLogger().isInfoEnabled()) {
284: String tmp = configPath.toString();
285: getLogger().info(
286: "ConfigPath = "
287: + tmp.substring(1, tmp.length() - 1));
288: }
289: }
290:
291: /** get the config path as an unmodifiable List of URL instances
292: * which describes, in order, the set of base locations searched by
293: * this instance of the ConfigFinder.
294: * Contrast with Configuration.getConfigPath() which returns the
295: * vm's default path.
296: **/
297: public List getConfigPath() {
298: return configPath;
299: }
300:
301: /** Do variable expansion/substitution on the argument.
302: * Essentially calls Configuration.substituteProperties(s, myproperties);
303: **/
304: protected final String substituteProperties(String s) {
305: return Configuration.substituteProperties(s, properties);
306: }
307:
308: /**
309: * Locate an actual file in the config path. This will skip over
310: * elements of org.cougaar.config.path that are not file: urls.
311: **/
312: public File locateFile(String aFilename) {
313: synchronized (urlCache) {
314: URL u = (URL) urlCache.get(aFilename);
315: if (u != null) {
316: return new File(u.getFile());
317: }
318: }
319:
320: for (int i = 0; i < configPath.size(); i++) {
321: URL base = (URL) configPath.get(i);
322: try {
323: URL url = newURL(base, aFilename);
324: if (url == null)
325: continue;
326: File result = new File(url.getFile());
327: if (!result.exists())
328: continue;
329: traceLog(aFilename, base);
330: synchronized (urlCache) {
331: urlCache.put(aFilename, url);
332: }
333: return result;
334: } catch (MalformedURLException mue) {
335: continue;
336: }
337: }
338: traceLog(aFilename, null);
339: return null;
340: }
341:
342: /**
343: * Resolve a logical reference to a URL, e.g.
344: * will convert "$INSTALL/configs/common/foo.txt" to
345: * "file:/opt/cougaar/20030331/configs/common/foo.txt"
346: * or somesuch.
347: * @return null if unresolvable.
348: **/
349: public URL resolveName(String logicalName)
350: throws MalformedURLException {
351: return Configuration.urlify(Configuration.substituteProperties(
352: logicalName, properties));
353: }
354:
355: /**
356: * Opens an InputStream to access the named file. The file is sought
357: * in all the places specified in configPath.
358: * @throws IOException if the resource cannot be found.
359: **/
360: public InputStream open(String aURL) throws IOException {
361: synchronized (urlCache) {
362: URL u = (URL) urlCache.get(aURL);
363: if (u != null) {
364: return u.openStream();
365: }
366: }
367:
368: for (int i = 0, l = configPath.size(); i < l; i++) {
369: URL base = (URL) configPath.get(i);
370: try {
371: URL url = newURL(base, aURL);
372: if (url == null)
373: continue;
374: InputStream is = url.openStream();
375: if (is == null)
376: continue; // Don't return null
377: traceLog(aURL, base);
378: synchronized (urlCache) {
379: urlCache.put(aURL, url);
380: }
381: return is;
382: } catch (MalformedURLException mue) {
383: if (getLogger().isDebugEnabled()) {
384: getLogger().debug(
385: "Exception while looking for " + aURL
386: + " at " + base, mue);
387: }
388: continue;
389: } catch (IOException ioe) {
390: if (getLogger().isDebugEnabled()) {
391: getLogger().debug(
392: "Exception while looking for " + aURL
393: + " at " + base, ioe);
394: }
395: continue;
396: } catch (RuntimeException rte) {
397: if (getLogger().isDebugEnabled()) {
398: getLogger().debug(
399: "Exception while looking for " + aURL
400: + " at " + base, rte);
401: }
402: continue;
403: }
404: }
405:
406: traceLog(aURL, null);
407:
408: throw new FileNotFoundException(aURL);
409: }
410:
411: /**
412: * Attempt to find the URL which would be opened by the open method.
413: * Note that this must actually attempt to open the various URLs
414: * under consideration, so this is <em>not</em> an inexpensive operation.
415: **/
416: public URL find(String aURL) throws IOException {
417: synchronized (urlCache) {
418: URL u = (URL) urlCache.get(aURL);
419: if (u != null) {
420: return u;
421: }
422: }
423: for (int i = 0; i < configPath.size(); i++) {
424: URL base = (URL) configPath.get(i);
425: try {
426: URL url = newURL(base, aURL);
427: if (url == null)
428: continue;
429: InputStream is = url.openStream();
430: if (is == null)
431: continue; // Don't return null
432: is.close();
433: traceLog(aURL, base);
434: synchronized (urlCache) {
435: urlCache.put(aURL, url);
436: }
437: return url;
438: } catch (MalformedURLException mue) {
439: continue;
440: } catch (IOException ioe) {
441: continue;
442: }
443: }
444: //if (getLogger().isInfoEnabled())
445: {
446: // it isn't really an error, but I think we'd like to see these.
447: getLogger().warn("Failed to find " + aURL);
448: }
449: traceLog(aURL, null);
450:
451: return null;
452: }
453:
454: private URL newURL(URL base, String s) throws MalformedURLException {
455: if (base == null || s == null) {
456: return null;
457: }
458: if (!"file".equals(base.getProtocol())
459: || !base.getPath().startsWith("/IN_COUGAAR_JARS/")) {
460: return new URL(base, s);
461: }
462: String path = (s.startsWith("/") ? s : (base.getPath()
463: .substring("/IN_COUGAAR_JARS/".length() - 1) + s));
464: return getClass().getResource(path);
465: }
466:
467: /** Read and parse an XML file somewhere in the configpath **/
468: public Document parseXMLConfigFile(String xmlfile)
469: throws IOException {
470: InputStream istream = null;
471: istream = open(xmlfile);
472: if (istream == null) {
473: throw new RuntimeException(
474: "Got null InputStream opening file " + xmlfile);
475: }
476: return parseXMLConfigFile(istream, xmlfile);
477: }
478:
479: private final ConfigResolver _configResolver = new ConfigResolver();
480:
481: protected ConfigResolver getConfigResolver() {
482: return _configResolver;
483: }
484:
485: /** parse an XML stream in the context of the current configuration environment.
486: * This means that embedded references to relative XML objects must be resolved
487: * via the configfinder rather than the stream itself.
488: **/
489: protected Document parseXMLConfigFile(InputStream isstream,
490: String xmlfile) {
491: try {
492: DocumentBuilderFactory dbf = DocumentBuilderFactory
493: .newInstance();
494: DocumentBuilder parser = dbf.newDocumentBuilder();
495: parser.setEntityResolver(getConfigResolver());
496: InputSource is = new InputSource(isstream);
497: return parser.parse(is);
498: } catch (Exception e) {
499: throw new RuntimeException("Unable to parse XML file \""
500: + xmlfile + "\"", e);
501: }
502: }
503:
504: private static Class getConfigFinderClass() {
505: String configFinderClassName = SystemProperties
506: .getProperty("org.cougaar.util.ConfigFinder.ClassName");
507: Class theClass = ConfigFinder.class;
508: if (configFinderClassName != null) {
509: try {
510: theClass = Class.forName(configFinderClassName);
511: } catch (Exception e) {
512: throw new RuntimeException(
513: "Not a valid ConfigFinder class: "
514: + configFinderClassName, e);
515: }
516: if (!ConfigFinder.class.isAssignableFrom(theClass)) {
517: throw new RuntimeException(ConfigFinder.class.getName()
518: + " should be a superclass of "
519: + configFinderClassName);
520: }
521: }
522: return theClass;
523: }
524:
525: private static ConfigFinder getConfigFinderInstance(String module,
526: String path) {
527: Class cls = getConfigFinderClass();
528: Class paramCls[] = new Class[] { String.class, String.class };
529: Object paramVal[] = new Object[] { module, path };
530:
531: try {
532: return (ConfigFinder) cls.getConstructor(paramCls)
533: .newInstance(paramVal);
534: } catch (Exception e) {
535: throw new RuntimeException(
536: "Unable to instantiate ConfigFinder");
537: }
538: }
539:
540: // Singleton pattern
541: private static ConfigFinder defaultConfigFinder = null;
542:
543: private synchronized static ConfigFinder getDefaultConfigFinder() {
544: if (defaultConfigFinder == null) {
545: try {
546: defaultConfigFinder = (ConfigFinder) getConfigFinderClass()
547: .newInstance();
548: } catch (Exception e) {
549: throw new Error("Unable to instantiate ConfigFinder", e);
550: }
551: }
552: return defaultConfigFinder;
553: }
554:
555: /**
556: * Return the default static instance of the ConfigFinder,
557: * configured using the system properties.
558: **/
559: public static ConfigFinder getInstance() {
560: return getDefaultConfigFinder();
561: }
562:
563: // hash of the default module config finders
564: private static Map moduleConfigFinders = new HashMap(11);
565:
566: /**
567: * Return a new ConfigFinder that uses the system properties
568: * for most configuration details, adding the four module-specific
569: * directories to the front of the search path.
570: **/
571: public static ConfigFinder getInstance(String module) {
572: if (module == null || module.equals("")) {
573: return getDefaultConfigFinder();
574: }
575:
576: String config_path = SystemProperties
577: .getProperty("org.cougaar.config.path");
578: if (config_path != null && config_path.charAt(0) == '"'
579: && config_path.charAt(config_path.length() - 1) == '"')
580: config_path = config_path.substring(1,
581: config_path.length() - 1);
582:
583: ConfigFinder mcf = (ConfigFinder) moduleConfigFinders
584: .get(module);
585: if (mcf == null) {
586: mcf = getConfigFinderInstance(module, config_path);
587: moduleConfigFinders.put(module, mcf);
588: }
589: return mcf;
590: }
591:
592: /** Support class for parsing of XML files
593: **/
594: protected class ConfigResolver implements EntityResolver {
595: public InputSource resolveEntity(String publicId,
596: String systemId) {
597: URL url = null;
598:
599: try {
600: url = new URL(systemId);
601: } catch (Exception e) {
602: }
603:
604: String filename = url.getFile();
605:
606: // Convert any '\'s to '/'s.
607: filename = filename.replace('\\', '/');
608:
609: filename = filename.substring(
610: filename.lastIndexOf("/") + 1, filename.length());
611:
612: InputSource is = null;
613: try {
614: InputStream istream = open(filename);
615: if (istream == null) {
616: throw new RuntimeException(
617: "Got null input stream opening file "
618: + filename);
619: }
620: is = new InputSource(istream);
621: } catch (IOException e) {
622: getLogger().error(
623: "Error getting input source for file \""
624: + filename + "\"", e);
625: }
626:
627: if (is == null) {
628: getLogger().error(
629: "Null InputSource for file \"" + filename
630: + "\"");
631: }
632:
633: return is;
634: }
635: }
636:
637: /** Hack for logging nice messages when tracing is enabled.
638: * Looks back up the stack for method and origin points.
639: **/
640: // this could be static except that the logger isn't static right now.
641: private void traceLog(Object name, Object value) {
642: if (getLogger().isInfoEnabled()) {
643: Throwable t = new Throwable();
644: StackTraceElement[] st = t.getStackTrace();
645: String frame = "unknown";
646: String method = "unknown";
647: for (int i = 0; i < st.length; i++) {
648: StackTraceElement se = st[i];
649: if (i == 1) {
650: method = se.getMethodName();
651: }
652: String cn = se.getClassName();
653: if ((!cn.startsWith("org.cougaar.util."))
654: && (!cn.startsWith("org.apache."))) {
655: frame = se.toString();
656: break;
657: }
658: }
659: getLogger().info(
660: method + "(" + name.toString() + ")=" + value
661: + " from " + frame);
662: }
663: }
664:
665: }
|