// ======================================================================== // Copyright (c) Webtide LLC // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.apache.org/licenses/LICENSE-2.0.txt // // You may elect to redistribute this code under either of these licenses. // ======================================================================== package org.eclipse.jetty.start; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.net.URL; import java.text.CollationKey; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; /** * <p> * It allows an application to be started with the command <code>"java -jar start.jar"</code>. * </p> * * <p> * The behaviour of Main is controlled by the <code>"org/eclipse/start/start.config"</code> file obtained as a resource * or file. This can be overridden with the START system property. The format of each line in this file is: * </p> * * <p> * Each line contains entry in the format: * </p> * * <pre> * SUBJECT [ [!] CONDITION [AND|OR] ]* * </pre> * * <p> * where SUBJECT: * </p> * <ul> * <li>ends with <code>".class"</code> is the Main class to run.</li> * <li>ends with <code>".xml"</code> is a configuration file for the command line</li> * <li>ends with <code>"/"</code> is a directory from which to add all jar and zip files.</li> * <li>ends with <code>"/*"</code> is a directory from which to add all unconsidered jar and zip files.</li> * <li>ends with <code>"/**"</code> is a directory from which to recursively add all unconsidered jar and zip files.</li> * <li>Containing <code>=</code> are used to assign system properties.</li> * <li>Containing <code>~=</code> are used to assign start properties.</li> * <li>Containing <code>/=</code> are used to assign a canonical path.</li> * <li>all other subjects are treated as files to be added to the classpath.</li> * </ul> * * <p> * property expansion: * </p> * <ul> * <li><code>${name}</code> is expanded to a start property</li> * <li><code>$(name)</code> is expanded to either a start property or a system property.</li> * <li>The start property <code>${version}</code> is defined as the version of the start.jar</li> * </ul> * * <p> * Files starting with <code>"/"</code> are considered absolute, all others are relative to the home directory. * </p> * * <p> * CONDITION is one of: * </p> * <ul> * <li><code>always</code></li> * <li><code>never</code></li> * <li><code>available classname</code> - true if class on classpath</li> * <li><code>property name</code> - true if set as start property</li> * <li><code>system name</code> - true if set as system property</li> * <li><code>exists file</code> - true if file/dir exists</li> * <li><code>java OPERATOR version</code> - java version compared to literal</li> * <li><code>nargs OPERATOR number</code> - number of command line args compared to literal</li> * <li>OPERATOR := one of <code>"<"</code>,<code>">"</code>,<code>"<="</code>,<code>">="</code>, * <code>"=="</code>,<code>"!="</code></li> * </ul> * * <p> * CONDITIONS can be combined with <code>AND</code> <code>OR</code> or <code>!</code>, with <code>AND</code> being the * assume operator for a list of CONDITIONS. * </p> * * <p> * Classpath operations are evaluated on the fly, so once a class or jar is added to the classpath, subsequent available * conditions will see that class. * </p> * * <p> * The configuration file may be divided into sections with option names like: [ssl,default] * </p> * * <p> * Note: a special discovered section identifier <code>[=path_to_directory/*]</code> is allowed to auto-create section * IDs, based on directory names found in the path specified in the "path_to_directory/" part of the identifier. * </p> * * <p> * Clauses after a section header will only be included if they match one of the tags in the options property. By * default options are set to "default,*" or the OPTIONS property may be used to pass in a list of tags, eg. : * </p> * * <pre> * java -jar start.jar OPTIONS=jetty,jsp,ssl * </pre> * * <p> * The tag '*' is always appended to the options, so any section with the * tag is always applied. * </p> * * <p> * The property map maintained by this class is static and shared between all instances in the same classloader * </p> */ public class Config { public static final String DEFAULT_SECTION = ""; static { String ver = System.getProperty("jetty.version", null); if(ver == null) { Package pkg = Config.class.getPackage(); if (pkg != null && "Eclipse.org - Jetty".equals(pkg.getImplementationVendor()) && (pkg.getImplementationVersion() != null)) { ver = pkg.getImplementationVersion(); } } if (ver == null) { ver = "Unknown"; } _version = ver; } /** * Natural language sorting for key names. */ private final Comparator<String> keySorter = new Comparator<String>() { private final Collator collator = Collator.getInstance(); public int compare(String o1, String o2) { CollationKey key1 = collator.getCollationKey(o1); CollationKey key2 = collator.getCollationKey(o2); return key1.compareTo(key2); } }; private static final String _version; private static boolean DEBUG = false; private static final Map<String, String> __properties = new HashMap<String, String>(); private final Map<String, Classpath> _classpaths = new HashMap<String, Classpath>(); private final List<String> _xml = new ArrayList<String>(); private String _classname = null; private int argCount = 0; private final Set<String> _activeOptions = new TreeSet<String>(new Comparator<String>() { // Make sure "*" is always at the end of the list public int compare(String o1, String o2) { if ("*".equals(o1)) { return 1; } if ("*".equals(o2)) { return -1; } return o1.compareTo(o2); } }); private boolean addClasspathComponent(List<String> sections, String component) { for (String section : sections) { Classpath cp = _classpaths.get(section); if (cp == null) cp = new Classpath(); boolean added = cp.addComponent(component); _classpaths.put(section,cp); if (!added) { // First failure means all failed. return false; } } return true; } private boolean addClasspathPath(List<String> sections, String path) { for (String section : sections) { Classpath cp = _classpaths.get(section); if (cp == null) { cp = new Classpath(); } if (!cp.addClasspath(path)) { // First failure means all failed. return false; } _classpaths.put(section,cp); } return true; } private void addJars(List<String> sections, File dir, boolean recurse) throws IOException { List<File> entries = new ArrayList<File>(); File[] files = dir.listFiles(); if (files == null) { // No files found, skip it. return; } entries.addAll(Arrays.asList(files)); Collections.sort(entries,FilenameComparator.INSTANCE); for (File entry : entries) { if (entry.isDirectory()) { if (recurse) addJars(sections,entry,recurse); } else { String name = entry.getName().toLowerCase(); if (name.endsWith(".jar") || name.endsWith(".zip")) { String jar = entry.getCanonicalPath(); boolean added = addClasspathComponent(sections,jar); debug((added?" CLASSPATH+=":" !") + jar); } } } } private void close(InputStream stream) { if (stream == null) return; try { stream.close(); } catch (IOException ignore) { /* ignore */ } } private void close(Reader reader) { if (reader == null) return; try { reader.close(); } catch (IOException ignore) { /* ignore */ } } public static boolean isDebug() { return DEBUG; } public static void debug(String msg) { if (DEBUG) { System.err.println(msg); } } public static void debug(Throwable t) { if (DEBUG) { t.printStackTrace(System.err); } } private String expand(String s) { int i1 = 0; int i2 = 0; while (s != null) { i1 = s.indexOf("$(",i2); if (i1 < 0) break; i2 = s.indexOf(")",i1 + 2); if (i2 < 0) break; String name = s.substring(i1 + 2,i2); String property = getProperty(name); s = s.substring(0,i1) + property + s.substring(i2 + 1); } i1 = 0; i2 = 0; while (s != null) { i1 = s.indexOf("${",i2); if (i1 < 0) break; i2 = s.indexOf("}",i1 + 2); if (i2 < 0) break; String name = s.substring(i1 + 2,i2); String property = getProperty(name); s = s.substring(0,i1) + property + s.substring(i2 + 1); } return s; } /** * Get the default classpath. * * @return the default classpath */ public Classpath getClasspath() { return _classpaths.get(DEFAULT_SECTION); } /** * Get the active classpath, as dictated by OPTIONS= entries. * * @return the Active classpath * @see #getCombinedClasspath(Collection) */ public Classpath getActiveClasspath() { return getCombinedClasspath(_activeOptions); } /** * Get the combined classpath representing the default classpath plus all named sections. * * NOTE: the default classpath will be prepended, and the '*' classpath will be appended. * * @param optionIds * the list of section ids to fetch * @return the {@link Classpath} representing combination all of the selected sectionIds, combined with the default * section id, and '*' special id. */ public Classpath getCombinedClasspath(Collection<String> optionIds) { Classpath cp = new Classpath(); cp.overlay(_classpaths.get(DEFAULT_SECTION)); for (String optionId : optionIds) { Classpath otherCp = _classpaths.get(optionId); if (otherCp == null) { throw new IllegalArgumentException("No such OPTIONS: " + optionId); } cp.overlay(otherCp); } cp.overlay(_classpaths.get("*")); return cp; } public String getMainClassname() { return _classname; } public static void clearProperties() { __properties.clear(); } public static Properties getProperties() { Properties properties = new Properties(); // Add System Properties First Enumeration<?> ensysprop = System.getProperties().propertyNames(); while(ensysprop.hasMoreElements()) { String name = (String)ensysprop.nextElement(); properties.put(name, System.getProperty(name)); } // Add Config Properties Next (overwriting any System Properties that exist) for (String key : __properties.keySet()) { properties.put(key,__properties.get(key)); } return properties; } public static String getProperty(String name) { if ("version".equalsIgnoreCase(name)) { return _version; } // Search Config Properties First if (__properties.containsKey(name)) { return __properties.get(name); } // Return what exists in System.Properties otherwise. return System.getProperty(name); } public static String getProperty(String name, String defaultValue) { // Search Config Properties First if (__properties.containsKey(name)) return __properties.get(name); // Return what exists in System.Properties otherwise. return System.getProperty(name, defaultValue); } /** * Get the classpath for the named section * * @param sectionId * @return the classpath for the specified section id */ public Classpath getSectionClasspath(String sectionId) { return _classpaths.get(sectionId); } /** * Get the list of section Ids. * * @return the set of unique section ids */ public Set<String> getSectionIds() { Set<String> ids = new TreeSet<String>(keySorter); ids.addAll(_classpaths.keySet()); return ids; } public List<String> getXmlConfigs() { return _xml; } private boolean isAvailable(List<String> options, String classname) { // Try default/parent class loader first. try { Class.forName(classname); return true; } catch (NoClassDefFoundError e) { debug(e); } catch (ClassNotFoundException e) { debug("ClassNotFoundException (parent class loader): " + classname); } // Try option classloaders instead ClassLoader loader; Classpath classpath; for (String optionId : options) { classpath = _classpaths.get(optionId); if (classpath == null) { // skip, no classpath continue; } loader = classpath.getClassLoader(); try { loader.loadClass(classname); return true; } catch (NoClassDefFoundError e) { debug(e); } catch (ClassNotFoundException e) { debug("ClassNotFoundException (section class loader: " + optionId + "): " + classname); } } return false; } /** * Parse the configuration * * @param buf * @throws IOException */ public void parse(CharSequence buf) throws IOException { parse(new StringReader(buf.toString())); } /** * Parse the configuration * * @param stream the stream to read from * @throws IOException */ public void parse(InputStream stream) throws IOException { InputStreamReader reader = null; try { reader = new InputStreamReader(stream); parse(reader); } finally { close(reader); } } /** */ public void parse(Reader reader) throws IOException { BufferedReader buf = null; try { buf = new BufferedReader(reader); List<String> options = new ArrayList<String>(); options.add(DEFAULT_SECTION); _classpaths.put(DEFAULT_SECTION,new Classpath()); Version java_version = new Version(System.getProperty("java.version")); Version ver = new Version(); String line = null; while ((line = buf.readLine()) != null) { String trim = line.trim(); if (trim.length() == 0) // empty line continue; if (trim.startsWith("#")) // comment continue; // handle options if (trim.startsWith("[") && trim.endsWith("]")) { String identifier = trim.substring(1,trim.length() - 1); // Normal case: section identifier (possibly separated by commas) options = Arrays.asList(identifier.split(",")); List<String> option_ids=new ArrayList<String>(); // Ensure section classpaths exist for (String optionId : options) { if (optionId.charAt(0) == '=') continue; if (!_classpaths.containsKey(optionId)) _classpaths.put(optionId,new Classpath()); if (!option_ids.contains(optionId)) option_ids.add(optionId); } // Process Dynamic for (String optionId : options) { if (optionId.charAt(0) != '=') continue; option_ids = processDynamicSectionIdentifier(optionId.substring(1),option_ids); } options = option_ids; continue; } try { StringTokenizer st = new StringTokenizer(line); String subject = st.nextToken(); boolean expression = true; boolean not = false; String condition = null; // Evaluate all conditions while (st.hasMoreTokens()) { condition = st.nextToken(); if (condition.equalsIgnoreCase("!")) { not = true; continue; } if (condition.equalsIgnoreCase("OR")) { if (expression) break; expression = true; continue; } if (condition.equalsIgnoreCase("AND")) { if (!expression) break; continue; } boolean eval = true; if (condition.equals("true") || condition.equals("always")) { eval = true; } else if (condition.equals("false") || condition.equals("never")) { eval = false; } else if (condition.equals("available")) { String class_to_check = st.nextToken(); eval = isAvailable(options,class_to_check); } else if (condition.equals("exists")) { try { eval = false; File file = new File(expand(st.nextToken())); eval = file.exists(); } catch (Exception e) { debug(e); } } else if (condition.equals("property")) { String property = getProperty(st.nextToken()); eval = property != null && property.length() > 0; } else if (condition.equals("system")) { String property = System.getProperty(st.nextToken()); eval = property != null && property.length() > 0; } else if (condition.equals("java")) { String operator = st.nextToken(); String version = st.nextToken(); ver.parse(version); eval = (operator.equals("<") && java_version.compare(ver) < 0) || (operator.equals(">") && java_version.compare(ver) > 0) || (operator.equals("<=") && java_version.compare(ver) <= 0) || (operator.equals("=<") && java_version.compare(ver) <= 0) || (operator.equals("=>") && java_version.compare(ver) >= 0) || (operator.equals(">=") && java_version.compare(ver) >= 0) || (operator.equals("==") && java_version.compare(ver) == 0) || (operator.equals("!=") && java_version.compare(ver) != 0); } else if (condition.equals("nargs")) { String operator = st.nextToken(); int number = Integer.parseInt(st.nextToken()); eval = (operator.equals("<") && argCount < number) || (operator.equals(">") && argCount > number) || (operator.equals("<=") && argCount <= number) || (operator.equals("=<") && argCount <= number) || (operator.equals("=>") && argCount >= number) || (operator.equals(">=") && argCount >= number) || (operator.equals("==") && argCount == number) || (operator.equals("!=") && argCount != number); } else { System.err.println("ERROR: Unknown condition: " + condition); eval = false; } expression &= not?!eval:eval; not = false; } String file = expand(subject); debug((expression?"T ":"F ") + line); if (!expression) continue; // Setting of a start property if (subject.indexOf("~=") > 0) { int i = file.indexOf("~="); String property = file.substring(0,i); String value = fixPath(file.substring(i + 2)); debug(" " + property + "~=" + value); setProperty(property,value); continue; } // Setting of start property with canonical path if (subject.indexOf("/=") > 0) { int i = file.indexOf("/="); String property = file.substring(0,i); String value = fixPath(file.substring(i + 2)); String canonical = new File(value).getCanonicalPath(); debug(" " + property + "/=" + value + "==" + canonical); setProperty(property,canonical); continue; } // Setting of system property if (subject.indexOf("=") > 0) { int i = file.indexOf("="); String property = file.substring(0,i); String value = fixPath(file.substring(i + 1)); debug(" " + property + "=" + value); System.setProperty(property,value); continue; } // Add all unconsidered JAR and ZIP files to classpath if (subject.endsWith("/*")) { // directory of JAR files - only add jars and zips within the directory File dir = new File(fixPath(file.substring(0,file.length() - 1))); addJars(options,dir,false); continue; } // Recursively add all unconsidered JAR and ZIP files to classpath if (subject.endsWith("/**")) { //directory hierarchy of jar files - recursively add all jars and zips in the hierarchy File dir = new File(fixPath(file.substring(0,file.length() - 2))); addJars(options,dir,true); continue; } // Add raw classpath directory to classpath if (subject.endsWith("/")) { // class directory File cd = new File(fixPath(file)); String d = cd.getCanonicalPath(); boolean added = addClasspathComponent(options,d); debug((added?" CLASSPATH+=":" !") + d); continue; } // Add XML configuration if (subject.toLowerCase().endsWith(".xml")) { // Config file File f = new File(fixPath(file)); if (f.exists()) _xml.add(f.getCanonicalPath()); debug(" ARGS+=" + f); continue; } // Set the main class to execute (overrides any previously set) if (subject.toLowerCase().endsWith(".class")) { // Class String cn = expand(subject.substring(0,subject.length() - 6)); if (cn != null && cn.length() > 0) { debug(" CLASS=" + cn); _classname = cn; } continue; } // Add raw classpath entry if (subject.toLowerCase().endsWith(".path")) { // classpath (jetty.class.path?) to add to runtime classpath String cn = expand(subject.substring(0,subject.length() - 5)); if (cn != null && cn.length() > 0) { debug(" PATH=" + cn); addClasspathPath(options,cn); } continue; } // single JAR file File f = new File(fixPath(file)); if (f.exists()) { String d = f.getCanonicalPath(); boolean added = addClasspathComponent(options,d); if (!added) { added = addClasspathPath(options,expand(subject)); } debug((added?" CLASSPATH+=":" !") + d); } } catch (Exception e) { System.err.println("on line: '" + line + "'"); e.printStackTrace(); } } } finally { close(buf); } } private List<String> processDynamicSectionIdentifier(String dynamicPathId,List<String> sections) throws IOException { String rawPath; boolean deep; if (dynamicPathId.endsWith("/*")) { deep=false; rawPath = fixPath(dynamicPathId.substring(0,dynamicPathId.length() - 1)); } else if (dynamicPathId.endsWith("/**")) { deep=true; rawPath = fixPath(dynamicPathId.substring(0,dynamicPathId.length() - 2)); } else { String msg = "Illegal dynamic path [" + dynamicPathId + "]"; throw new IOException(msg); } File parentDir = new File(expand(rawPath)); if (!parentDir.exists()) return sections; debug("dynamic: " + parentDir); File dirs[] = parentDir.listFiles(new FileFilter() { public boolean accept(File path) { return path.isDirectory(); } }); List<String> dyn_sections = new ArrayList<String>(); List<String> super_sections = new ArrayList<String>(); if (sections!=null) super_sections.addAll(sections); for (File dir : dirs) { String id = dir.getName(); if (!_classpaths.keySet().contains(id)) _classpaths.put(id, new Classpath()); dyn_sections.clear(); if (sections!=null) dyn_sections.addAll(sections); dyn_sections.add(id); super_sections.add(id); debug("dynamic: " + dyn_sections); addJars(dyn_sections,dir,deep); } return super_sections; } private String fixPath(String path) { return path.replace('/',File.separatorChar); } public void parse(URL url) throws IOException { InputStream stream = null; InputStreamReader reader = null; try { stream = url.openStream(); reader = new InputStreamReader(stream); parse(reader); } finally { close(reader); close(stream); } } public void setArgCount(int argCount) { this.argCount = argCount; } public void setProperty(String name, String value) { if (name.equals("DEBUG")) { DEBUG = Boolean.parseBoolean(value); if (DEBUG) { System.setProperty("org.eclipse.jetty.util.log.stderr.DEBUG","true"); System.setProperty("org.eclipse.jetty.start.DEBUG","true"); } } if (name.equals("OPTIONS")) { _activeOptions.clear(); String ids[] = value.split(","); for (String id : ids) { addActiveOption(id); } } __properties.put(name,value); } public void addActiveOption(String option) { _activeOptions.add(option); __properties.put("OPTIONS",join(_activeOptions,",")); } public Set<String> getActiveOptions() { return _activeOptions; } public void removeActiveOption(String option) { _activeOptions.remove(option); __properties.put("OPTIONS",join(_activeOptions,",")); } private String join(Collection<?> coll, String delim) { StringBuffer buf = new StringBuffer(); Iterator<?> i = coll.iterator(); boolean hasNext = i.hasNext(); while (hasNext) { buf.append(String.valueOf(i.next())); hasNext = i.hasNext(); if (hasNext) buf.append(delim); } return buf.toString(); } }