/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sun.jini.tool; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; import java.util.StringTokenizer; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A tool for generating "wrapper" JAR files. A wrapper JAR file contains a * <code>Class-Path</code> manifest attribute listing a group of JAR files to * be loaded from a common codebase. It may also, depending on applicability * and selected options, contain a JAR index file, a preferred class list * and/or a <code>Main-Class</code> manifest entry for the grouped JAR files. * <p> * The following items are discussed below: * <ul> * <li> <a href="#applicability">Applicability</a> * <li> <a href="#running">Using the Tool</a> * <li> <a href="#logging">Logging</a> * <li> {@linkplain #main Processing Options} * </ul> * <p> * <a name="applicability"><h3>Applicability</h3></a> * <p> * The <code>JarWrapper</code> tool is applicable in the following deployment * situations, which may overlap: * <ul> * <li> If a codebase contains multiple JAR files which declare preferred * resources, <code>JarWrapper</code> can be used to produce a wrapper * JAR file with a combined preferred list. Preferred resources are * described in the documentation for the {@link net.jini.loader.pref} * package. * <p> * <li> If a codebase contains multiple JAR files and requires integrity * protection, <code>JarWrapper</code> can be used to produce a wrapper * JAR file with a <code>Class-Path</code> attribute that uses HTTPMD * URLs. HTTPMD URLs are described in the documentation for the * {@link net.jini.url.httpmd} package. * <p> * <li> If an application or service packaged as an executable JAR file * refers to classes specified at deployment time (e.g., via a * {@link net.jini.config.Configuration Configuration}) which are not * present in the JAR file or its <code>Class-Path</code>, * <code>JarWrapper</code> can be used to produce a wrapper JAR file * which includes the extra classes in its <code>Class-Path</code> while * retaining the original <code>Main-Class</code> declaration; the * wrapper JAR file can then be executed in place of the original JAR * file. * </ul> * <p> * <a name="running"><h3>Using the Tool</h3></a> * <code>JarWrapper</code> can be run directly from the * {@linkplain #main command line} or can be invoked programmatically using the * {@link #wrap wrap} method. * <p> * To run the tool on UNIX platforms: * <blockquote><pre> * java -jar <var><b>jsk_install_dir</b></var>/lib/jarwrapper.jar <var><b>processing_options</b></var> * </pre></blockquote> * To run the tool on Microsoft Windows platforms: * <blockquote><pre> * java -jar <var><b>jsk_install_dir</b></var>\lib\jarwrapper.jar <var><b>processing_options</b></var> * </pre></blockquote> * <p> * A more specific example with options for running directly from a Unix command line might be: * <blockquote><pre> * % java -jar <var><b>install_dir</b></var>/lib/jarwrapper.jar \ * -httpmd=SHA-1 wrapper.jar base_dir src1.jar src2.jar * </pre></blockquote> * where <var><b>jsk_install_dir</b></var> is the directory where the Apache River release * is installed. This command line would result in the creation of a wrapper * JAR file, <code>wrapper.jar</code>, in the current working directory, whose * contents would be based on the source JAR files <code>src1.jar</code> and * <code>src2.jar</code> (as well as any other JAR files referenced * transitively through their <code>Class-Path</code> attributes or JAR * indexes). The paths for <code>src1.jar</code> and <code>src2.jar</code>, as * well as any transitively referenced JAR files, would be resolved relative to * the <code>base_dir</code> directory. The <code>Class-Path</code> attribute * of <code>wrapper.jar</code> would use HTTPMD URLs with SHA-1 digests. * <p> * The equivalent programmatic invocation of <code>JarWrapper</code> would be: * <blockquote><pre> * JarWrapper.wrap("wrapper.jar", "base_dir", new String[]{ "src1.jar", "src2.jar" }, "SHA-1", true, "manifest.mf" ); * </pre></blockquote> * * <p> * <a name="logging"><h3>Logging</h3></a> * <p> * <code>JarWrapper</code> uses the {@link Logger} named * <code>com.sun.jini.tool.JarWrapper</code> to log information at the * following logging levels: * <p> * <table border="1" cellpadding="5" * summary="Describes logging performed by JarWrapper at different * logging levels"> * <caption halign="center" valign="top"><b><code> * com.sun.jini.tool.JarWrapper</code></b></caption> * * <tr> <th scope="col"> Level <th scope="col"> Description </tr> * <tr> * <td> {@link Level#WARNING WARNING} * <td> Generated JAR index entries that do not end in <code>".jar"</code> * </tr> * <tr> * <td> {@link Level#FINE FINE} * <td> Names of processed source JAR files and output wrapper JAR file * </tr> * <tr> * <td> {@link Level#FINER FINER} * <td> Processing of <code>Main-Class</code> and <code>Class-Path</code> * attributes, and presence of preferred lists and JAR indexes * </tr> * <tr> * <td> {@link Level#FINEST FINEST} * <td> Processing and compilation of preferred lists and JAR indexes * </tr> * </table> * * @author Sun Microsystems, Inc. * @since 2.0 */ public class JarWrapper { private static ResourceBundle resources; private static final Object resourcesLock = new Object(); private static final Logger logger = Logger.getLogger(JarWrapper.class.getName()); private final File destJar; private final File baseDir; private final SourceJarURL[] srcJars; private final Manifest manifest; private final MessageDigest digest; private final JarIndexWriter indexWriter; private final PreferredListWriter prefWriter = new PreferredListWriter(); private final StringBuffer classPath = new StringBuffer(); private String mainClass = null; private final Set seenJars = new HashSet(); static private final String DEFAULT_HTTPMD_ALGORITHM = "SHA-1"; /** * Initializes JarWrapper based on the given values. */ private JarWrapper(String destJar, String baseDir, String[] srcJars, String httpmdAlg, boolean index, Manifest mf) { this.destJar = new File(destJar); if (this.destJar.exists()) { throw new LocalizedIllegalArgumentException( "jarwrapper.fileexists", destJar); } this.baseDir = new File(baseDir); if (!this.baseDir.isDirectory()) { throw new LocalizedIllegalArgumentException( "jarwrapper.invalidbasedir", baseDir); } this.srcJars = new SourceJarURL[srcJars.length]; for (int i = 0; i < srcJars.length; i++) { try { SourceJarURL url = new SourceJarURL(srcJars[i]); if (url.algorithm != null) { throw new LocalizedIllegalArgumentException( "jarwrapper.urlhasdigest", url); } this.srcJars[i] = url; } catch (LocalizedIOException e) { throw new LocalizedIllegalArgumentException(e); } catch (IOException e) { throw (IllegalArgumentException) new IllegalArgumentException(e.getMessage()).initCause(e); } } if (httpmdAlg != null) { try { digest = MessageDigest.getInstance(httpmdAlg); } catch (NoSuchAlgorithmException e) { throw (IllegalArgumentException) new LocalizedIllegalArgumentException( "jarwrapper.invalidhttpmdalg", httpmdAlg).initCause(e); } } else { digest = null; } manifest = mf != null ? new Manifest(mf) : new Manifest(); indexWriter = index ? new JarIndexWriter() : null; } /** * Generates a wrapper JAR file for the specified JAR files. The command * line arguments are: * <pre> * [ <var>options</var> ] <var>dest-jar</var> <var>base-dir</var> <var>src-jar</var> [ <var>src-jar</var> ...] * </pre> * The <var>dest-jar</var> argument specifies the name of the wrapper JAR * file to generate. The <var>base-dir</var> argument specifies the base * directory from which to locate source JAR files to wrap. The * <var>src-jar</var> arguments are non-absolute URLs to "top-level" source * JAR files relative to <var>base-dir</var>; they also constitute the * basis of the <code>Class-Path</code> attribute included in the generated * wrapper JAR file. JAR files not present in the command line but * indirectly referenced via JAR index or <code>Class-Path</code> entries * in source JAR files will themselves be used as source JAR files, and * will appear alongside the top-level source JAR files in the * <code>Class-Path</code> attribute of the wrapper JAR file in depth-first * order, with JAR index references appearing before * <code>Class-Path</code> references. This utility does not modify any * source JAR files. * <p> * If any of the top-level source JAR files contain preferred resources (as * indicated by a preferred list in the JAR file), then a preferred list * describing resource preferences across all source JAR files will be * included in the wrapper JAR file. The preferred list of a top-level * source JAR file is interpreted as applying to that JAR file along with * all JAR files transitively referenced by it through JAR index or * <code>Class-Path</code> entries, excluding JAR files that have already * been encountered in the processing of preceding top-level JAR files. If * a given top-level source JAR file does not contain a preferred list, * then all resources contained in it and its transitively referenced JAR * files (again, excluding those previously encountered) are considered not * preferred. Preferred lists are described further in the documentation * for {@link net.jini.loader.pref.PreferredClassLoader * PreferredClassLoader}. * <p> * If any of the top-level source JAR files declare a * <code>Main-Class</code> manifest entry, then the wrapper JAR file will * include a <code>Main-Class</code> manifest entry whose value is that of * the first top-level source JAR file listed on the command line which * defines a <code>Main-Class</code> entry. * <p> * Note that attribute values generated by this utility, such as those for the * <code>Class-Path</code> and <code>Main-Class</code> attributes described * above, do not take precedence over values for the same attributes contained * in a manifest file explicitly specified using the <code>-manifest</code> * option (described below). * <p> * Supported options for this tool include: * <p> * <dl> * <dt> <code>-verbose</code> * <dd> Sets the level of the <code>com.sun.jini.tool.JarWrapper</code> * logger to <code>Level.FINER</code>. * <p> * <dt> <code>-httpmd[=algorithm]</code> * <dd> Use (relative) HTTPMD URLs in the <code>Class-Path</code> * attribute of the generated wrapper JAR file. The default is to * use HTTP URLs. Digests for HTTPMD URLs are calculated using the * given algorithm, or SHA-1 if none is specified. * <p> * <dt> <code>-noindex</code> * <dd> Do not include a JAR index in the generated wrapper JAR file. The * default is to compile an index based on the contents of the * source JAR files. * <p> * <dt> <code>-manifest=<I>file</I></code> * <dd> Specifies a manifest file containing attribute values to * include in the manifest file inside the generated wrapper JAR file. * This allows enables users to add additional metadata or * override JarWrapper's generated values to customize the resulting * manifest. The values contained in this optional file take * precedence over the generated content. This flag is conceptually * similar to the jar utilities m flag. In the current * version there are four possible attributes that can be overridden * in the target Manifest. These are Name.MANIFEST_VERSION, * Name("Created-By"), Name.CLASS_PATH and Name.MAIN_CLASS. Any additonal * attributes beyond these four will be appended to the manifest attribute * list and will appear in the resultant MANIFEST.MF file. * * </dl> */ public static void main(String[] args) { String destJar; String baseDir; String[] srcJars; String httpmdAlg = null; boolean index = true; Manifest mf = null; int i = 0; while (i < args.length && args[i].startsWith("-")) { String s = args[i++]; if (s.equals("-help")) { System.err.println(localize("jarwrapper.usage")); System.exit(0); } else if (s.equals("-verbose")) { setLoggingLevel(Level.FINER); } else if (s.equals("-debug")) { setLoggingLevel(Level.ALL); } else if (s.equals("-httpmd") || s.startsWith("-httpmd=")) { if (httpmdAlg != null) { System.err.println(localize("jarwrapper.multiplehttpmd")); System.err.println(localize("jarwrapper.usage")); System.exit(1); } int split = s.indexOf('='); httpmdAlg = (split != -1) ? s.substring(split + 1) : DEFAULT_HTTPMD_ALGORITHM; } else if (s.startsWith("-manifest=")) { int split = s.indexOf('='); String fileName = s.substring(split + 1); try { mf = retrieveManifest(fileName); } catch (IOException ioe) { System.err.println(localize("jarwrapper.badmanifest", s)); System.exit(1); } } else if (s.equals("-noindex")) { index = false; } else { System.err.println(localize("jarwrapper.badoption", s)); System.err.println(localize("jarwrapper.usage")); System.exit(1); } } if (args.length - i < 3) { System.err.println(localize("jarwrapper.insufficientargs")); System.err.println(localize("jarwrapper.usage")); System.exit(1); } destJar = args[i++]; baseDir = args[i++]; srcJars = new String[args.length - i]; System.arraycopy(args, i, srcJars, 0, srcJars.length); try { wrap(destJar, baseDir, srcJars, httpmdAlg, index, mf); } catch (Throwable t) { if (t instanceof LocalizedIllegalArgumentException || t instanceof LocalizedIOException) { System.err.println(t.getMessage()); } else { System.err.println(localize("jarwrapper.fatalexception")); t.printStackTrace(); } System.exit(1); } } /** * Invokes {@link #wrap(String, String, String[], String, boolean, Manifest) * wrap} with the provided values and a <code>null</code> manifest. * * * @param destJar name of the wrapper JAR file to generate * @param baseDir base directory from which to locate source JAR * files to wrap * @param srcJars list of top-level source JAR files to process * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or * <code>null</code> if plain HTTP URLs should be used * @param index if <code>true</code>, generate a JAR index; if * <code>false</code>, do not generate one * @throws IOException if an I/O error occurs while processing source JAR * files or generating the wrapper JAR file * @throws IllegalArgumentException if the provided values are invalid * @throws NullPointerException if <code>destJar</code>, * <code>baseDir</code>, <code>srcJars</code>, or any element of * <code>srcJars</code> is <code>null</code> */ public static void wrap(String destJar, String baseDir, String[] srcJars, String httpmdAlg, boolean index) throws IOException { wrap(destJar, baseDir, srcJars, httpmdAlg, index, null); } /** * Generates a wrapper JAR file based on the provided values in the same * manner as described in the documentation for {@link #main}. The only * difference between this method and <code>main</code> is that it receives * its values as explicit arguments instead of in a command line, and * indicates failure by throwing an exception. * * @param destJar name of the wrapper JAR file to generate * @param baseDir base directory from which to locate source JAR * files to wrap * @param srcJars list of top-level source JAR files to process * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or * <code>null</code> if plain HTTP URLs should be used * @param index if <code>true</code>, generate a JAR index; if * <code>false</code>, do not generate one * @param mf manifest containing values to include in the manifest file * of the generated wrapper JAR file * * @throws IOException if an I/O error occurs while processing source JAR * files or generating the wrapper JAR file * @throws IllegalArgumentException if the provided values are invalid * @throws NullPointerException if <code>destJar</code>, * <code>baseDir</code>, <code>srcJars</code>, or any element of * <code>srcJars</code> is <code>null</code> * @since 2.1 */ public static void wrap(String destJar, String baseDir, String[] srcJars, String httpmdAlg, boolean index, Manifest mf) throws IOException { new JarWrapper(destJar, baseDir, srcJars, httpmdAlg, index, mf).wrap(); } /** * Processes source JAR files and outputs wrapper JAR file. */ private void wrap() throws IOException { for (int i = 0; i < srcJars.length; i++) { process(srcJars[i], null); } outputWrapperJar(); } /** * Processes source JAR file indicated by the given URL, determining * preferred resources using the provided preferred list reader. If the * preferred list reader is null, then the URL is for a top-level source * JAR file, in which case the preferred list of the JAR file should be * read, if the JAR file has not already been processed. */ private void process(SourceJarURL url, PreferredListReader prefReader) throws IOException { File file = url.toFile(baseDir); boolean seen = seenJars.contains(file); boolean checkMainClass = mainClass == null && prefReader == null; if (seen && !checkMainClass) { return; } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "processing {0}", new Object[]{ file }); } if (!file.exists()) { throw new LocalizedIOException("jarwrapper.filenotfound", file); } JarFile jar = new JarFile(file, false); if (checkMainClass) { mainClass = getMainClass(jar); } if (!seen) { seenJars.add(file); if (digest != null) { url = new SourceJarURL( url.path, digest.getAlgorithm(), getDigestString(digest, file), null); } if (classPath.length() > 0) { classPath.append(' '); } classPath.append(url); if (indexWriter != null) { indexWriter.addEntries(jar, url); } if (prefReader == null) { prefReader = new PreferredListReader(jar); } prefWriter.addEntries(jar, prefReader); List l = new ArrayList(); l.addAll(new JarIndexReader(jar).getJars()); l.addAll(getClassPath(jar)); for (Iterator i = l.iterator(); i.hasNext(); ) { SourceJarURL u = (SourceJarURL) i.next(); u = url.resolve(new SourceJarURL(u.path, null, null, null)); process(u, prefReader); } } } /** * Returns URLs contained in the Class-Path attribute (if any) of the given * JAR file, as a list of SourceJarURL instances. */ private List getClassPath(JarFile jar) throws IOException { Manifest mf = jar.getManifest(); if (mf == null) { return Collections.EMPTY_LIST; } Attributes atts = mf.getMainAttributes(); String cp = atts.getValue(Name.CLASS_PATH); if (cp == null) { return Collections.EMPTY_LIST; } if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "Class-Path: {0}", new Object[]{ cp }); } List l = new ArrayList(); for (StringTokenizer tok = new StringTokenizer(cp, " "); tok.hasMoreTokens(); ) { SourceJarURL url = new SourceJarURL(tok.nextToken()); if (digest != null && url.algorithm == null) { throw new LocalizedIOException("jarwrapper.nonhttpmdurl", url); } l.add(url); } return l; } /** * Writes wrapper JAR file based on information from processed source JARs. */ private void outputWrapperJar() throws IOException { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "writing {0}", new Object[]{ destJar }); } Attributes atts = manifest.getMainAttributes(); if (atts.get(Name.MANIFEST_VERSION) == null) atts.put(Name.MANIFEST_VERSION, "1.0"); Name creatorName = new Name("Created-By"); if (atts.get(creatorName) == null ) atts.put(creatorName, JarWrapper.class.getName()); if (atts.get(Name.CLASS_PATH) == null) atts.put(Name.CLASS_PATH, classPath.toString()); if ((atts.get(Name.MAIN_CLASS) == null) && (mainClass != null)) { atts.put(Name.MAIN_CLASS, mainClass); } boolean completed = false; try { JarOutputStream jout = new JarOutputStream(new FileOutputStream(destJar), manifest); if (indexWriter != null) { indexWriter.write(jout); } prefWriter.write(jout); jout.close(); completed = true; } finally { if (!completed) { deleteWrapperJar(); } } } /** * Attempts to delete wrapper JAR file. */ private void deleteWrapperJar() { try { if (!destJar.delete() && logger.isLoggable(Level.WARNING)) { logger.log( Level.WARNING, "failed to delete {0}", new Object[]{ destJar }); } } catch (Throwable t) { logger.log( Level.WARNING, "exception deleting wrapper JAR file", t); } } /** * Returns the value of the Main-Class attribute of the given JAR file, or * null if none is present. */ private static String getMainClass(JarFile jar) throws IOException { Manifest mf = jar.getManifest(); if (mf == null) { return null; } Attributes atts = mf.getMainAttributes(); String mc = atts.getValue(Name.MAIN_CLASS); if (mc != null && logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "Main-Class: {0}", new Object[]{ mc }); } return mc; } /** * Returns a string representation of the message digest of the given file. */ private static String getDigestString(MessageDigest digest, File file) throws IOException { FileInputStream fin = new FileInputStream(file); byte[] buf = new byte[2048]; int n; while ((n = fin.read(buf)) >= 0) { digest.update(buf, 0, n); } buf = digest.digest(); fin.close(); StringBuffer sb = new StringBuffer(buf.length * 2); for (int i = 0; i < buf.length; i++) { byte b = buf[i]; sb.append(Character.forDigit((b >> 4) & 0xf, 16)); sb.append(Character.forDigit(b & 0xf, 16)); } return sb.toString(); } /** * Sets logging and console handler output level. */ private static void setLoggingLevel(Level level) { logger.setLevel(level); for (Logger l = logger; l != null; l = l.getParent()) { Handler[] handlers = l.getHandlers(); for (int i = 0; i < handlers.length; i++) { if (handlers[i] instanceof ConsoleHandler) { handlers[i].setLevel(level); } } if (!l.getUseParentHandlers()) { break; } } } /** * Returns localized message text corresponding to the given key string. */ static String localize(String key) { String fmt = getResourceString(key); if (fmt == null) { fmt = "no text found: \"" + key + "\""; } // REMIND: format even without arguments? return MessageFormat.format(fmt, null); } /** * Returns localized message text corresponding to the given key string, * passing the provided value as an argument when formatting the message. */ static String localize(String key, Object val) { String fmt = getResourceString(key); if (fmt == null) { fmt = "no text found: \"" + key + "\" {0}"; } return MessageFormat.format(fmt, new Object[]{ val }); } /** * Returns localized format string, obtained from the resource bundle for * JarWrapper, that corresponds to the given key, or null if the resource * bundle does not contain a corresponding string. */ private static String getResourceString(String key) { synchronized (resourcesLock) { if (resources == null) { resources = ResourceBundle.getBundle( "com.sun.jini.tool.resources.jarwrapper"); } } try { return resources.getString(key); } catch (MissingResourceException e) { return null; } } /** * Returns the Manifest object derived from a manifest file as specified * on the command line. */ private static Manifest retrieveManifest(String fileName) throws IOException { FileInputStream fis = new FileInputStream(fileName); Manifest mf = new Manifest(fis); fis.close(); return mf; } /** * IllegalArgumentException with a localized detail message. */ private static class LocalizedIllegalArgumentException extends IllegalArgumentException { private static final long serialVersionUID = 0L; /** * Creates exception with localized message text corresponding to the * given key string, passing the provided value as an argument when * formatting the message. */ LocalizedIllegalArgumentException(String key, Object val) { super(localize(key, val)); } /** * Creates exception with the localized message text of the given * cause. */ LocalizedIllegalArgumentException(LocalizedIOException cause) { super(cause.getMessage()); initCause(cause); } } /** * IOException with a localized detail message. */ private static class LocalizedIOException extends IOException { private static final long serialVersionUID = 0L; /** * Creates exception with localized message text corresponding to the * given key string, passing the provided value as an argument when * formatting the message. */ LocalizedIOException(String key, Object val) { super(localize(key, val)); } } /** * Represents URL to a source JAR file. Source JAR URLs must be relative, * and may contain HTTPMD digests. */ private static class SourceJarURL { private static final Pattern httpmdPattern = Pattern.compile("(.*);(.+?)=(.+?)(?:,(.*))?$"); /** raw URL string, including HTTPMD information (if any) */ final String raw; /** URL path component, excluding any HTTPMD information */ final String path; /** HTTPMD digest algorithm, or null if non-HTTPMD URL */ final String algorithm; /** HTTPMD digest value, or null if non-HTTPMD URL */ final String digest; /** HTTPMD digest comment, or null if non-HTTPMD URL */ final String comment; /** * Creates SourceJarURL based on given raw URL string. */ SourceJarURL(String raw) throws IOException { try { this.raw = raw; Matcher m = httpmdPattern.matcher(raw); if (m.matches()) { path = m.group(1); algorithm = m.group(2); digest = m.group(3); comment = m.group(4); } else { path = raw; algorithm = null; digest = null; comment = null; } URI uri = new URI(path); if (uri.getScheme() != null) { throw new LocalizedIOException( "jarwrapper.urlhasscheme", raw); } else if (uri.getAuthority() != null) { throw new LocalizedIOException( "jarwrapper.urlhasauthority", raw); } String p = uri.getPath(); if (p == null || p.length() == 0) { throw new LocalizedIOException( "jarwrapper.urlemptypath", raw); } else if (p.startsWith("/")) { throw new LocalizedIOException( "jarwrapper.urlabsolute", raw); } } catch (URISyntaxException e) { throw (IOException) new LocalizedIOException( "jarwrapper.invalidurlsyntax", raw).initCause(e); } } /** * Creates SourceJarURL based on given components. */ SourceJarURL(String path, String algorithm, String digest, String comment) { if (algorithm != null) { raw = path + ';' + algorithm + '=' + digest + ((comment != null) ? ',' + comment : ""); } else { raw = path; } this.path = path; this.algorithm = algorithm; this.digest = digest; this.comment = comment; } /** * Resolves given URL relative to this URL. */ SourceJarURL resolve(SourceJarURL other) { try { // hack around URI bug 4548698 by temporarily prepending slash URI uri = new URI('/' + path); String p = uri.resolve(other.path).getPath().substring(1); return new SourceJarURL( p, other.algorithm, other.digest, other.comment); } catch (URISyntaxException e) { throw new AssertionError(e); } } /** * Returns file represented by this URL. */ File toFile(File base) { try { String p = new URI(path).getPath(); // decode path return new File(base, p.replace('/', File.separatorChar)); } catch (URISyntaxException e) { throw (Error) new InternalError().initCause(e); } } public boolean equals(Object obj) { return obj instanceof SourceJarURL && raw.equals(((SourceJarURL) obj).raw); } public int hashCode() { return raw.hashCode(); } public String toString() { return raw; } } /** * Parses JAR indexes. */ private static class JarIndexReader { private static final Pattern headerPattern = Pattern.compile("^JarIndex-Version:\\s*(.*?)$"); private static final Pattern versionPattern = Pattern.compile("^1(\\.\\d+)*$"); private final List jars; /** * Parses the given JAR file's JAR index, if any. */ JarIndexReader(JarFile jar) throws IOException { List l = new ArrayList(); jars = Collections.unmodifiableList(l); JarEntry ent = jar.getJarEntry("META-INF/INDEX.LIST"); if (ent == null) { return; } logger.finer("reading JAR index"); BufferedReader r = new BufferedReader( new InputStreamReader(jar.getInputStream(ent), "UTF8")); String s = r.readLine(); if (s == null) { throw new IOException("missing JAR index header"); } s = s.trim(); Matcher m = headerPattern.matcher(s); if (!m.matches()) { throw new IOException("illegal JAR index header: " + s); } s = m.group(1); if (!versionPattern.matcher(s).matches()) { throw new IOException("unsupported JAR index version: " + s); } s = r.readLine(); if (s == null) { throw new IOException("truncated JAR index"); } s = s.trim(); if (s.length() > 0) { throw new IOException( "non-empty line after JAR index header: " + s); } while ((s = r.readLine()) != null) { SourceJarURL url = new SourceJarURL(s.trim()); if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "JAR index references {0}", new Object[]{ url }); } l.add(url); do { s = r.readLine(); } while (s != null && s.trim().length() > 0); } if (l.isEmpty()) { throw new IOException("empty JAR index"); } } /** * Returns list of SourceJarURLs representing the JAR files referenced * in the JAR index. */ List getJars() { return jars; } } /** * Assembles and writes JAR indexes. */ private static class JarIndexWriter { private final List urls = new ArrayList(); private final Map contentMap = new HashMap(); JarIndexWriter() { } /** * Tabulates contents of the given JAR file, associating them with the * provided URL for the JAR file. */ void addEntries(JarFile jar, SourceJarURL url) { Set contents = new HashSet(); for (Enumeration e = jar.entries(); e.hasMoreElements();) { String name = ((JarEntry) e.nextElement()).getName(); if (!(name.startsWith("META-INF") || name.endsWith("/"))) { int pos = name.lastIndexOf("/"); contents.add((pos != -1) ? name.substring(0, pos) : name); } } if (!contents.isEmpty()) { urls.add(url); contentMap.put(url, contents); } } /** * Writes JAR index to the given output stream. */ void write(JarOutputStream jout) throws IOException { if (contentMap.isEmpty()) { logger.finer("omitting empty JAR index"); return; } logger.finer("writing JAR index"); jout.putNextEntry(new JarEntry("META-INF/INDEX.LIST")); Writer w = new BufferedWriter(new OutputStreamWriter(jout, "UTF8")); w.write("JarIndex-Version: 1.0\n\n"); // preserve original insertion order for (Iterator i = urls.iterator(); i.hasNext();) { SourceJarURL url = (SourceJarURL) i.next(); Set contents = (Set) contentMap.get(url); if (!url.raw.endsWith(".jar")) { if (url.algorithm != null) { url = new SourceJarURL( url.path, url.algorithm, url.digest, ".jar"); } else if (logger.isLoggable(Level.WARNING)) { logger.log( Level.WARNING, "JAR index entry {0} does not end in .jar", new Object[]{ url }); } } if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "writing JAR index entry {0}: {1}", new Object[]{ url, contents }); } w.write(url + "\n"); for (Iterator j = contents.iterator(); j.hasNext(); ) { w.write(j.next() + "\n"); } w.write("\n"); } w.flush(); jout.closeEntry(); } } /** * Parses preferred lists. */ private static class PreferredListReader { private static final Pattern headerPattern = Pattern.compile("^PreferredResources-Version:\\s*(.*?)$"); private static final Pattern versionPattern = Pattern.compile("^1\\.\\d+$"); private static final Pattern namePattern = Pattern.compile("^Name:\\s*(.*)$"); private static final Pattern preferredPattern = Pattern.compile("^Preferred:\\s*(.*)$"); private final boolean defaultPref; private final Map namePrefs = new HashMap(); private final Map packagePrefs = new HashMap(); private final Map subtreePrefs = new HashMap(); /** * Parses the given JAR file's preferred list, if any. */ PreferredListReader(JarFile jar) throws IOException { JarEntry ent = jar.getJarEntry("META-INF/PREFERRED.LIST"); if (ent == null) { defaultPref = false; return; } logger.finer("reading preferred list"); BufferedReader r = new BufferedReader( new InputStreamReader(jar.getInputStream(ent), "UTF8")); String s = r.readLine(); if (s == null) { throw new IOException("missing preferred list header"); } s = s.trim(); Matcher m = headerPattern.matcher(s); if (!m.matches()) { throw new IOException("illegal preferred list header: " + s); } s = m.group(1); if (!versionPattern.matcher(s).matches()) { throw new IOException( "unsupported preferred list version: " + s); } s = nextNonBlankLine(r); if (s == null) { throw new IOException("empty preferred list"); } if ((m = preferredPattern.matcher(s)).matches()) { defaultPref = Boolean.valueOf(m.group(1)).booleanValue(); s = nextNonBlankLine(r); } else { defaultPref = false; } while (s != null) { if (!(m = namePattern.matcher(s)).matches()) { throw new IOException( "expected preferred entry name: " + s); } String name = m.group(1); s = nextNonBlankLine(r); if (s == null) { throw new IOException("EOF before preferred entry"); } if (!(m = preferredPattern.matcher(s)).matches()) { throw new IOException("expected preferred entry: " + s); } Boolean pref = Boolean.valueOf(m.group(1)); String key; Map map; if (name.endsWith("/*")) { key = name.substring(0, name.length() - 2); map = packagePrefs; } else if (name.endsWith("/")) { key = name.substring(0, name.length() - 1); map = packagePrefs; } else if (name.endsWith("/-")) { key = name.substring(0, name.length() - 2); map = subtreePrefs; } else { key = name; map = namePrefs; } if (key.length() == 0) { throw new IOException( "invalid preferred entry name: " + name); } map.put(key, pref); if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "read preferred list entry {0}: {1}", new Object[]{ name, pref }); } s = nextNonBlankLine(r); } } /** * Returns true if list prefers given entry, or false otherwise. */ boolean isPreferred(String entry) { Boolean b = (Boolean) namePrefs.get(entry); if (b != null) { return b.booleanValue(); } if (entry.endsWith(".class")) { int i = entry.lastIndexOf('$'); while (i >= 0) { String outer = entry.substring(0, i) + ".class"; if ((b = (Boolean) namePrefs.get(outer)) != null) { return b.booleanValue(); } i = entry.lastIndexOf('$', i - 1); } } int i = entry.lastIndexOf('/'); if (i >= 0) { String base = entry.substring(0, i); if ((b = (Boolean) packagePrefs.get(base)) != null) { return b.booleanValue(); } for (;;) { if ((b = (Boolean) subtreePrefs.get(base)) != null) { return b.booleanValue(); } if ((i = base.lastIndexOf('/')) < 0) { break; } base = base.substring(0, i); } } return defaultPref; } /** * Returns next non-blank, non-comment line, or null if end of file has * been reached. */ private static String nextNonBlankLine(BufferedReader reader) throws IOException { String s; while ((s = reader.readLine()) != null) { s = s.trim(); if (s.length() > 0 && s.charAt(0) != '#') { return s; } } return null; } } /** * Compiles and writes combined preferred lists. */ private static class PreferredListWriter { private static final int NAME_LEN = "Name: ".length(); private static final int PREFERRED_LEN = "Preferred: ".length(); private static final int TRUE_LEN = "true".length(); private static final int FALSE_LEN = "false".length(); private static final int NEWLINE_LEN = "\n".length(); private final HashMap pathMap = new HashMap(); private final DirNode rootNode = new DirNode(""); private int numPrefs = 0; PreferredListWriter() { pathMap.put("", rootNode); } /** * Records preferred status of each file entry in the given JAR file, * determined using the provided preferred list reader. */ void addEntries(JarFile jar, PreferredListReader prefReader) throws IOException { for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { String path = ((JarEntry) e.nextElement()).getName(); if (!(path.startsWith("META-INF") || path.endsWith("/"))) { boolean pref = prefReader.isPreferred(path); if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, pref ? "preferred: {0}" : "not preferred: {0}", new Object[]{ path }); } addFile(path, pref); } } } /** * Writes minimal combined preferred list to given output stream. */ void write(JarOutputStream jout) throws IOException { if (numPrefs == 0) { logger.finer("omitting empty preferred list"); return; } logger.finer("writing preferred list"); jout.putNextEntry(new JarEntry("META-INF/PREFERRED.LIST")); Writer w = new BufferedWriter(new OutputStreamWriter(jout, "UTF8")); w.write("PreferredResources-Version: 1.0\n"); rootNode.compileList(); rootNode.writeList(w); w.flush(); jout.closeEntry(); } /** * Records the preferred setting of the given file entry. */ private void addFile(String path, boolean preferred) throws IOException { FileNode fn = (FileNode) pathMap.get(path); if (fn != null) { if (fn.preferred != preferred) { throw new LocalizedIOException( "jarwrapper.prefconflict", path); } return; } fn = new FileNode(path, preferred); pathMap.put(path, fn); if (preferred) { numPrefs++; } path = parentPath(path); DirNode dn = (DirNode) pathMap.get(path); if (dn != null) { dn.files.add(fn); return; } dn = new DirNode(path); pathMap.put(path, dn); dn.files.add(fn); for (path = parentPath(path); ; path = parentPath(path)) { DirNode pn = (DirNode) pathMap.get(path); if (pn != null) { pn.subdirs.add(dn); return; } pn = new DirNode(path); pathMap.put(path, pn); pn.subdirs.add(dn); dn = pn; } } /** * Returns path of the parent directory of the indicated JAR entry. */ private static String parentPath(String path) { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } int i = path.lastIndexOf('/'); return (i >= 0) ? path.substring(0, i + 1) : ""; } static int min(int i1, int i2, int i3) { return Math.min(i1, Math.min(i2, i3)); } /** * Returns the number of characters needed to write a preferred list * entry with the given name and preferred setting. If the given name * is null, then the length of a "default" preferred list entry (i.e., * an entry without a name) is returned. */ static int calcEntryLength(String name, boolean pref) { int len = NEWLINE_LEN; if (name != null) { len += NAME_LEN + name.length() + NEWLINE_LEN; } len += PREFERRED_LEN + (pref ? TRUE_LEN : FALSE_LEN) + NEWLINE_LEN; return len; } /** * Writes preferred list entry with the given name and preferred * setting. If the given name is null, then a "default" preferred list * entry is written. */ static void writeEntry(Writer w, String name, boolean pref) throws IOException { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "writing preferred list entry {0}: {1}", new Object[]{ (name != null) ? name : "<default>", Boolean.valueOf(pref) }); } w.write("\n"); if (name != null) { w.write("Name: " + name + "\n"); } w.write("Preferred: " + pref + "\n"); } /** * Stores file preference state. */ private static class FileNode { /* action constants */ static final int NONE = 0; static final int SKIP = 1; static final int INCLUDE = 2; final String path; final boolean preferred; int action; FileNode(String path, boolean preferred) { this.path = path; this.preferred = preferred; } } /** * Represents JAR-internal directory. */ private class DirNode { final String path; final List subdirs = new ArrayList(); final List files = new ArrayList(); /* * The length, in characters, of the preferred list covering this * directory subtree if the default preferred setting for the * entire subtree is true. */ int prefSubtreeLen; /* * The length, in characters, of the preferred list covering this * directory subtree if the default preferred setting for the * immediate directory is true, but the default preferred setting * for the subtree as a whole is false. */ int prefPackageLen; /* * The length, in characters, of the preferred list covering this * directory subtree if the default preferred setting for the * entire subtree is false. */ int unprefSubtreeLen; /* * The length, in characters, of the preferred list covering this * directory subtree if the default preferred setting for the * immediate directory is false, but the default preferred setting * for the subtree as a whole is true. */ int unprefPackageLen; DirNode(String path) { this.path = path; } /** * Computes minimal list length using dynamic programming. */ void compileList() { int prefLen = 0, unprefLen = 0; for (Iterator i = files.iterator(); i.hasNext(); ) { FileNode fn = (FileNode) i.next(); for (int j = fn.path.lastIndexOf('$'); j != -1; j = fn.path.lastIndexOf('$', j - 1)) { FileNode fn2 = (FileNode) pathMap.get( fn.path.substring(0, j) + ".class"); if (fn2 != null) { fn.action = (fn.preferred == fn2.preferred) ? FileNode.SKIP : FileNode.INCLUDE; break; } } int entryLen = calcEntryLength(fn.path, fn.preferred); if (fn.action == FileNode.SKIP) { // won't list, so don't increment length counts } else if (fn.action == FileNode.INCLUDE) { prefLen += entryLen; unprefLen += entryLen; } else if (fn.preferred) { unprefLen += entryLen; } else { prefLen += entryLen; } } prefSubtreeLen = prefLen; prefPackageLen = prefLen; unprefSubtreeLen = unprefLen; unprefPackageLen = unprefLen; for (Iterator i = subdirs.iterator(); i.hasNext();) { DirNode dn = (DirNode) i.next(); dn.compileList(); String subtreePath = dn.path + "-"; prefSubtreeLen += min( dn.prefSubtreeLen, dn.unprefSubtreeLen + calcEntryLength(subtreePath, false), dn.unprefPackageLen + calcEntryLength(dn.path, false)); prefPackageLen += min( dn.prefSubtreeLen + calcEntryLength(subtreePath, true), dn.prefPackageLen + calcEntryLength(dn.path, true), dn.unprefSubtreeLen); unprefSubtreeLen += min( dn.prefSubtreeLen + calcEntryLength(subtreePath, true), dn.prefPackageLen + calcEntryLength(dn.path, true), dn.unprefSubtreeLen); unprefPackageLen += min( dn.prefSubtreeLen, dn.unprefSubtreeLen + calcEntryLength(subtreePath, false), dn.unprefPackageLen + calcEntryLength(dn.path, false)); } } /** * Writes preferred list. This method is only called on the * root node. */ void writeList(Writer w) throws IOException { int totalPrefSubtreeLen = prefSubtreeLen + calcEntryLength(null, true); boolean defaultPref = totalPrefSubtreeLen < unprefSubtreeLen; if (defaultPref) { writeEntry(w, null, true); } writeFiles(w, defaultPref); for (Iterator i = subdirs.iterator(); i.hasNext();) { ((DirNode) i.next()).writeDir(w, defaultPref); } } /** * Writes preferred list entries (if any) for this directory, which * inherits the given preferred value as its default. */ void writeDir(Writer w, boolean contextPref) throws IOException { boolean dirPref; boolean subdirPref; String subtreePath = path + "-"; if (contextPref) { int totalUnprefPackageLen = unprefPackageLen + calcEntryLength(path, false); int totalUnprefSubtreeLen = unprefSubtreeLen + calcEntryLength(subtreePath, false); int best = min( prefSubtreeLen, totalUnprefPackageLen, totalUnprefSubtreeLen); if (best == prefSubtreeLen) { dirPref = true; subdirPref = true; } else if (best == totalUnprefPackageLen) { writeEntry(w, path, false); dirPref = false; subdirPref = true; } else { writeEntry(w, subtreePath, false); dirPref = false; subdirPref = false; } } else { int totalPrefPackageLen = prefPackageLen + calcEntryLength(path, true); int totalPrefSubtreeLen = prefSubtreeLen + calcEntryLength(subtreePath, true); int best = min( unprefSubtreeLen, totalPrefPackageLen, totalPrefSubtreeLen); if (best == unprefSubtreeLen) { dirPref = false; subdirPref = false; } else if (best == totalPrefPackageLen) { writeEntry(w, path, true); dirPref = true; subdirPref = false; } else { writeEntry(w, subtreePath, true); dirPref = true; subdirPref = true; } } writeFiles(w, dirPref); for (Iterator i = subdirs.iterator(); i.hasNext(); ) { ((DirNode) i.next()).writeDir(w, subdirPref); } } /** * Writes preferred list entries (if any) for files in this * directory, which has the given preferred value as its default. */ void writeFiles(Writer w, boolean contextPref) throws IOException { for (Iterator i = files.iterator(); i.hasNext(); ) { FileNode fn = (FileNode) i.next(); if (fn.action != FileNode.SKIP && (fn.action == FileNode.INCLUDE || fn.preferred != contextPref)) { writeEntry(w, fn.path, fn.preferred); } } } } } }