| /* |
| * 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 org.apache.tools.ant.taskdefs; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.StringTokenizer; |
| import java.util.TreeMap; |
| import java.util.Vector; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.taskdefs.Manifest.Section; |
| import org.apache.tools.ant.types.ArchiveFileSet; |
| import org.apache.tools.ant.types.EnumeratedAttribute; |
| import org.apache.tools.ant.types.FileSet; |
| import org.apache.tools.ant.types.Path; |
| import org.apache.tools.ant.types.Resource; |
| import org.apache.tools.ant.types.ResourceCollection; |
| import org.apache.tools.ant.types.ZipFileSet; |
| import org.apache.tools.ant.types.spi.Service; |
| import org.apache.tools.ant.util.FileUtils; |
| import org.apache.tools.zip.JarMarker; |
| import org.apache.tools.zip.ZipExtraField; |
| import org.apache.tools.zip.ZipOutputStream; |
| |
| /** |
| * Creates a JAR archive. |
| * |
| * @since Ant 1.1 |
| * |
| * @ant.task category="packaging" |
| */ |
| public class Jar extends Zip { |
| /** The index file name. */ |
| private static final String INDEX_NAME = "META-INF/INDEX.LIST"; |
| |
| /** The manifest file name. */ |
| private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; |
| |
| /** |
| * List of all known SPI Services |
| */ |
| private List<Service> serviceList = new ArrayList<Service>(); |
| |
| /** merged manifests added through addConfiguredManifest */ |
| private Manifest configuredManifest; |
| |
| /** shadow of the above if upToDate check alters the value */ |
| private Manifest savedConfiguredManifest; |
| |
| /** merged manifests added through filesets */ |
| private Manifest filesetManifest; |
| |
| /** |
| * Manifest of original archive, will be set to null if not in |
| * update mode. |
| */ |
| private Manifest originalManifest; |
| |
| /** |
| * whether to merge fileset manifests; |
| * value is true if filesetmanifest is 'merge' or 'mergewithoutmain' |
| */ |
| private FilesetManifestConfig filesetManifestConfig; |
| |
| /** |
| * whether to merge the main section of fileset manifests; |
| * value is true if filesetmanifest is 'merge' |
| */ |
| private boolean mergeManifestsMain = true; |
| |
| /** the manifest specified by the 'manifest' attribute **/ |
| private Manifest manifest; |
| |
| /** The encoding to use when reading in a manifest file */ |
| private String manifestEncoding; |
| |
| /** |
| * The file found from the 'manifest' attribute. This can be |
| * either the location of a manifest, or the name of a jar added |
| * through a fileset. If its the name of an added jar, the |
| * manifest is looked for in META-INF/MANIFEST.MF |
| */ |
| private File manifestFile; |
| |
| /** jar index is JDK 1.3+ only */ |
| private boolean index = false; |
| |
| /** Whether to index META-INF/ and its children */ |
| private boolean indexMetaInf = false; |
| |
| /** |
| * whether to really create the archive in createEmptyZip, will |
| * get set in getResourcesToAdd. |
| */ |
| private boolean createEmpty = false; |
| |
| /** |
| * Stores all files that are in the root of the archive (i.e. that |
| * have a name that doesn't contain a slash) so they can get |
| * listed in the index. |
| * |
| * Will not be filled unless the user has asked for an index. |
| * |
| * @since Ant 1.6 |
| */ |
| private Vector<String> rootEntries; |
| |
| /** |
| * Path containing jars that shall be indexed in addition to this archive. |
| * |
| * @since Ant 1.6.2 |
| */ |
| private Path indexJars; |
| |
| // CheckStyle:LineLength OFF - Link is too long. |
| /** |
| * Strict mode for checking rules of the JAR-Specification. |
| * @see http://java.sun.com/j2se/1.3/docs/guide/versioning/spec/VersioningSpecification.html#PackageVersioning |
| */ |
| private StrictMode strict = new StrictMode("ignore"); |
| |
| // CheckStyle:LineLength ON |
| |
| /** |
| * whether to merge Class-Path attributes. |
| */ |
| private boolean mergeClassPaths = false; |
| |
| /** |
| * whether to flatten Class-Path attributes into a single one. |
| */ |
| private boolean flattenClassPaths = false; |
| |
| /** |
| * Extra fields needed to make Solaris recognize the archive as a jar file. |
| * |
| * @since Ant 1.6.3 |
| */ |
| private static final ZipExtraField[] JAR_MARKER = new ZipExtraField[] { |
| JarMarker.getInstance() |
| }; |
| |
| /** constructor */ |
| public Jar() { |
| super(); |
| archiveType = "jar"; |
| emptyBehavior = "create"; |
| setEncoding("UTF8"); |
| setZip64Mode(Zip64ModeAttribute.NEVER); |
| rootEntries = new Vector<String>(); |
| } |
| |
| /** |
| * Not used for jar files. |
| * @param we not used |
| * @ant.attribute ignore="true" |
| */ |
| public void setWhenempty(WhenEmpty we) { |
| log("JARs are never empty, they contain at least a manifest file", |
| Project.MSG_WARN); |
| } |
| |
| /** |
| * Indicates if a jar file should be created when it would only contain a |
| * manifest file. |
| * Possible values are: <code>fail</code> (throw an exception |
| * and halt the build); <code>skip</code> (do not create |
| * any archive, but issue a warning); <code>create</code> |
| * (make an archive with only a manifest file). |
| * Default is <code>create</code>; |
| * @param we a <code>WhenEmpty</code> enumerated value |
| */ |
| public void setWhenmanifestonly(WhenEmpty we) { |
| emptyBehavior = we.getValue(); |
| } |
| |
| /** |
| * Activate the strict mode. When set to <i>true</i> a BuildException |
| * will be thrown if the Jar-Packaging specification was broken. |
| * @param strict New value of the strict mode. |
| * @since Ant 1.7.1 |
| */ |
| public void setStrict(StrictMode strict) { |
| this.strict = strict; |
| } |
| |
| /** |
| * Set the destination file. |
| * @param jarFile the destination file |
| * @deprecated since 1.5.x. |
| * Use setDestFile(File) instead. |
| */ |
| public void setJarfile(File jarFile) { |
| setDestFile(jarFile); |
| } |
| |
| /** |
| * Set whether or not to create an index list for classes. |
| * This may speed up classloading in some cases. |
| * @param flag a <code>boolean</code> value |
| */ |
| public void setIndex(boolean flag) { |
| index = flag; |
| } |
| |
| /** |
| * Set whether or not to add META-INF and its children to the index. |
| * |
| * <p>Doesn't have any effect if index is false.</p> |
| * |
| * <p>Sun's jar implementation used to skip the META-INF directory |
| * and Ant followed that example. The behavior has been changed |
| * with Java 5. In order to avoid problems with Ant generated |
| * jars on Java 1.4 or earlier Ant will not include META-INF |
| * unless explicitly asked to.</p> |
| * |
| * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4408526"> |
| * jar -i omits service providers in index.list</a> |
| * @since Ant 1.8.0 |
| * @param flag a <code>boolean</code> value, defaults to false |
| */ |
| public void setIndexMetaInf(boolean flag) { |
| indexMetaInf = flag; |
| } |
| |
| /** |
| * The character encoding to use in the manifest file. |
| * |
| * @param manifestEncoding the character encoding |
| */ |
| public void setManifestEncoding(String manifestEncoding) { |
| this.manifestEncoding = manifestEncoding; |
| } |
| |
| /** |
| * Allows the manifest for the archive file to be provided inline |
| * in the build file rather than in an external file. |
| * |
| * @param newManifest an embedded manifest element |
| * @throws ManifestException on error |
| */ |
| public void addConfiguredManifest(Manifest newManifest) |
| throws ManifestException { |
| if (configuredManifest == null) { |
| configuredManifest = newManifest; |
| } else { |
| configuredManifest.merge(newManifest, false, mergeClassPaths); |
| } |
| savedConfiguredManifest = configuredManifest; |
| } |
| |
| /** |
| * The manifest file to use. This can be either the location of a manifest, |
| * or the name of a jar added through a fileset. If its the name of an added |
| * jar, the task expects the manifest to be in the jar at META-INF/MANIFEST.MF. |
| * |
| * @param manifestFile the manifest file to use. |
| */ |
| public void setManifest(File manifestFile) { |
| if (!manifestFile.exists()) { |
| throw new BuildException("Manifest file: " + manifestFile |
| + " does not exist.", getLocation()); |
| } |
| |
| this.manifestFile = manifestFile; |
| } |
| |
| private Manifest getManifest(File manifestFile) { |
| |
| Manifest newManifest = null; |
| FileInputStream fis = null; |
| InputStreamReader isr = null; |
| try { |
| fis = new FileInputStream(manifestFile); |
| if (manifestEncoding == null) { |
| isr = new InputStreamReader(fis); |
| } else { |
| isr = new InputStreamReader(fis, manifestEncoding); |
| } |
| newManifest = getManifest(isr); |
| } catch (UnsupportedEncodingException e) { |
| throw new BuildException("Unsupported encoding while reading manifest: " |
| + e.getMessage(), e); |
| } catch (IOException e) { |
| throw new BuildException("Unable to read manifest file: " |
| + manifestFile |
| + " (" + e.getMessage() + ")", e); |
| } finally { |
| FileUtils.close(isr); |
| } |
| return newManifest; |
| } |
| |
| /** |
| * @return null if jarFile doesn't contain a manifest, the |
| * manifest otherwise. |
| * @since Ant 1.5.2 |
| */ |
| private Manifest getManifestFromJar(File jarFile) throws IOException { |
| ZipFile zf = null; |
| try { |
| zf = new ZipFile(jarFile); |
| |
| // must not use getEntry as "well behaving" applications |
| // must accept the manifest in any capitalization |
| Enumeration<? extends ZipEntry> e = zf.entries(); |
| while (e.hasMoreElements()) { |
| ZipEntry ze = e.nextElement(); |
| if (ze.getName().equalsIgnoreCase(MANIFEST_NAME)) { |
| InputStreamReader isr = |
| new InputStreamReader(zf.getInputStream(ze), "UTF-8"); |
| return getManifest(isr); |
| } |
| } |
| return null; |
| } finally { |
| if (zf != null) { |
| try { |
| zf.close(); |
| } catch (IOException e) { |
| // TODO - log an error? throw an exception? |
| } |
| } |
| } |
| } |
| |
| private Manifest getManifest(Reader r) { |
| |
| Manifest newManifest = null; |
| try { |
| newManifest = new Manifest(r); |
| } catch (ManifestException e) { |
| log("Manifest is invalid: " + e.getMessage(), Project.MSG_ERR); |
| throw new BuildException("Invalid Manifest: " + manifestFile, |
| e, getLocation()); |
| } catch (IOException e) { |
| throw new BuildException("Unable to read manifest file" |
| + " (" + e.getMessage() + ")", e); |
| } |
| return newManifest; |
| } |
| |
| private boolean jarHasIndex(File jarFile) throws IOException { |
| ZipFile zf = null; |
| try { |
| zf = new ZipFile(jarFile); |
| Enumeration<? extends ZipEntry> e = zf.entries(); |
| while (e.hasMoreElements()) { |
| ZipEntry ze = e.nextElement(); |
| if (ze.getName().equalsIgnoreCase(INDEX_NAME)) { |
| return true; |
| } |
| } |
| return false; |
| } finally { |
| if (zf != null) { |
| try { |
| zf.close(); |
| } catch (IOException e) { |
| // TODO - log an error? throw an exception? |
| } |
| } |
| } |
| } |
| |
| /** |
| * Behavior when a Manifest is found in a zipfileset or zipgroupfileset file. |
| * Valid values are "skip", "merge", and "mergewithoutmain". |
| * "merge" will merge all of manifests together, and merge this into any |
| * other specified manifests. |
| * "mergewithoutmain" merges everything but the Main section of the manifests. |
| * Default value is "skip". |
| * |
| * Note: if this attribute's value is not "skip", the created jar will not |
| * be readable by using java.util.jar.JarInputStream |
| * |
| * @param config setting for found manifest behavior. |
| */ |
| public void setFilesetmanifest(FilesetManifestConfig config) { |
| filesetManifestConfig = config; |
| mergeManifestsMain = "merge".equals(config.getValue()); |
| |
| if (filesetManifestConfig != null |
| && !filesetManifestConfig.getValue().equals("skip")) { |
| |
| doubleFilePass = true; |
| } |
| } |
| |
| /** |
| * Adds a zipfileset to include in the META-INF directory. |
| * |
| * @param fs zipfileset to add |
| */ |
| public void addMetainf(ZipFileSet fs) { |
| // We just set the prefix for this fileset, and pass it up. |
| fs.setPrefix("META-INF/"); |
| super.addFileset(fs); |
| } |
| |
| /** |
| * Add a path to index jars. |
| * @param p a path |
| * @since Ant 1.6.2 |
| */ |
| public void addConfiguredIndexJars(Path p) { |
| if (indexJars == null) { |
| indexJars = new Path(getProject()); |
| } |
| indexJars.append(p); |
| } |
| |
| /** |
| * A nested SPI service element. |
| * @param service the nested element. |
| * @since Ant 1.7 |
| */ |
| public void addConfiguredService(Service service) { |
| // Check if the service is configured correctly |
| service.check(); |
| serviceList.add(service); |
| } |
| |
| /** |
| * Write SPI Information to JAR |
| */ |
| private void writeServices(ZipOutputStream zOut) throws IOException { |
| for (Service service : serviceList) { |
| InputStream is = null; |
| try { |
| is = service.getAsStream(); |
| //stolen from writeManifest |
| super.zipFile(is, zOut, |
| "META-INF/services/" + service.getType(), |
| System.currentTimeMillis(), null, |
| ZipFileSet.DEFAULT_FILE_MODE); |
| } finally { |
| // technically this is unnecessary since |
| // Service.getAsStream returns a ByteArrayInputStream |
| // and not closing it wouldn't do any harm. |
| FileUtils.close(is); |
| } |
| } |
| } |
| |
| /** |
| * Whether to merge Class-Path attributes. |
| * |
| * @since Ant 1.8.0 |
| */ |
| public void setMergeClassPathAttributes(boolean b) { |
| mergeClassPaths = b; |
| } |
| |
| /** |
| * Whether to flatten multi-valued attributes (i.e. Class-Path) |
| * into a single one. |
| * |
| * @since Ant 1.8.0 |
| */ |
| public void setFlattenAttributes(boolean b) { |
| flattenClassPaths = b; |
| } |
| |
| /** |
| * Initialize the zip output stream. |
| * @param zOut the zip output stream |
| * @throws IOException on I/O errors |
| * @throws BuildException on other errors |
| */ |
| protected void initZipOutputStream(ZipOutputStream zOut) |
| throws IOException, BuildException { |
| |
| if (!skipWriting) { |
| Manifest jarManifest = createManifest(); |
| writeManifest(zOut, jarManifest); |
| writeServices(zOut); |
| } |
| } |
| |
| private Manifest createManifest() |
| throws BuildException { |
| try { |
| if (manifest == null) { |
| if (manifestFile != null) { |
| // if we haven't got the manifest yet, attempt to |
| // get it now and have manifest be the final merge |
| manifest = getManifest(manifestFile); |
| } |
| } |
| |
| // fileset manifest must come even before the default |
| // manifest if mergewithoutmain is selected and there is |
| // no explicit manifest specified - otherwise the Main |
| // section of the fileset manifest is still merged to the |
| // final manifest. |
| boolean mergeFileSetFirst = !mergeManifestsMain |
| && filesetManifest != null |
| && configuredManifest == null && manifest == null; |
| |
| Manifest finalManifest; |
| if (mergeFileSetFirst) { |
| finalManifest = new Manifest(); |
| finalManifest.merge(filesetManifest, false, mergeClassPaths); |
| finalManifest.merge(Manifest.getDefaultManifest(), |
| true, mergeClassPaths); |
| } else { |
| finalManifest = Manifest.getDefaultManifest(); |
| } |
| |
| /* |
| * Precedence: manifestFile wins over inline manifest, |
| * over manifests read from the filesets over the original |
| * manifest. |
| * |
| * merge with null argument is a no-op |
| */ |
| |
| if (isInUpdateMode()) { |
| finalManifest.merge(originalManifest, false, mergeClassPaths); |
| } |
| if (!mergeFileSetFirst) { |
| finalManifest.merge(filesetManifest, false, mergeClassPaths); |
| } |
| finalManifest.merge(configuredManifest, !mergeManifestsMain, |
| mergeClassPaths); |
| finalManifest.merge(manifest, !mergeManifestsMain, |
| mergeClassPaths); |
| |
| return finalManifest; |
| |
| } catch (ManifestException e) { |
| log("Manifest is invalid: " + e.getMessage(), Project.MSG_ERR); |
| throw new BuildException("Invalid Manifest", e, getLocation()); |
| } |
| } |
| |
| private void writeManifest(ZipOutputStream zOut, Manifest manifest) |
| throws IOException { |
| for (Enumeration<String> e = manifest.getWarnings(); |
| e.hasMoreElements();) { |
| log("Manifest warning: " + e.nextElement(), |
| Project.MSG_WARN); |
| } |
| |
| zipDir((Resource) null, zOut, "META-INF/", ZipFileSet.DEFAULT_DIR_MODE, |
| JAR_MARKER); |
| // time to write the manifest |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| OutputStreamWriter osw = new OutputStreamWriter(baos, Manifest.JAR_ENCODING); |
| PrintWriter writer = new PrintWriter(osw); |
| manifest.write(writer, flattenClassPaths); |
| if (writer.checkError()) { |
| throw new IOException("Encountered an error writing the manifest"); |
| } |
| writer.close(); |
| |
| ByteArrayInputStream bais = |
| new ByteArrayInputStream(baos.toByteArray()); |
| try { |
| super.zipFile(bais, zOut, MANIFEST_NAME, |
| System.currentTimeMillis(), null, |
| ZipFileSet.DEFAULT_FILE_MODE); |
| } finally { |
| // not really required |
| FileUtils.close(bais); |
| } |
| super.initZipOutputStream(zOut); |
| } |
| |
| /** |
| * Finalize the zip output stream. |
| * This creates an index list if the index attribute is true. |
| * @param zOut the zip output stream |
| * @throws IOException on I/O errors |
| * @throws BuildException on other errors |
| */ |
| protected void finalizeZipOutputStream(ZipOutputStream zOut) |
| throws IOException, BuildException { |
| |
| if (index) { |
| createIndexList(zOut); |
| } |
| } |
| |
| /** |
| * Create the index list to speed up classloading. |
| * This is a JDK 1.3+ specific feature and is enabled by default. See |
| * <a href="http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#JAR%20Index"> |
| * the JAR index specification</a> for more details. |
| * |
| * @param zOut the zip stream representing the jar being built. |
| * @throws IOException thrown if there is an error while creating the |
| * index and adding it to the zip stream. |
| */ |
| private void createIndexList(ZipOutputStream zOut) throws IOException { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| // encoding must be UTF8 as specified in the specs. |
| PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, |
| "UTF8")); |
| |
| // version-info blankline |
| writer.println("JarIndex-Version: 1.0"); |
| writer.println(); |
| |
| // header newline |
| writer.println(zipFile.getName()); |
| |
| writeIndexLikeList(new ArrayList<String>(addedDirs.keySet()), |
| rootEntries, writer); |
| writer.println(); |
| |
| if (indexJars != null) { |
| Manifest mf = createManifest(); |
| Manifest.Attribute classpath = |
| mf.getMainSection().getAttribute(Manifest.ATTRIBUTE_CLASSPATH); |
| String[] cpEntries = null; |
| if (classpath != null && classpath.getValue() != null) { |
| StringTokenizer tok = new StringTokenizer(classpath.getValue(), |
| " "); |
| cpEntries = new String[tok.countTokens()]; |
| int c = 0; |
| while (tok.hasMoreTokens()) { |
| cpEntries[c++] = tok.nextToken(); |
| } |
| } |
| String[] indexJarEntries = indexJars.list(); |
| for (int i = 0; i < indexJarEntries.length; i++) { |
| String name = findJarName(indexJarEntries[i], cpEntries); |
| if (name != null) { |
| ArrayList<String> dirs = new ArrayList<String>(); |
| ArrayList<String> files = new ArrayList<String>(); |
| grabFilesAndDirs(indexJarEntries[i], dirs, files); |
| if (dirs.size() + files.size() > 0) { |
| writer.println(name); |
| writeIndexLikeList(dirs, files, writer); |
| writer.println(); |
| } |
| } |
| } |
| } |
| |
| if (writer.checkError()) { |
| throw new IOException("Encountered an error writing jar index"); |
| } |
| writer.close(); |
| ByteArrayInputStream bais = |
| new ByteArrayInputStream(baos.toByteArray()); |
| try { |
| super.zipFile(bais, zOut, INDEX_NAME, System.currentTimeMillis(), |
| null, ZipFileSet.DEFAULT_FILE_MODE); |
| } finally { |
| // not really required |
| FileUtils.close(bais); |
| } |
| } |
| |
| /** |
| * Overridden from Zip class to deal with manifests and index lists. |
| * @param is the stream to read data for the entry from. The |
| * caller of the method is responsible for closing the stream. |
| * @param zOut the zip output stream |
| * @param vPath the name this entry shall have in the archive |
| * @param lastModified last modification time for the entry. |
| * @param fromArchive the original archive we are copying this |
| * entry from, will be null if we are not copying from an archive. |
| * @param mode the Unix permissions to set. |
| * @throws IOException on error |
| */ |
| protected void zipFile(InputStream is, ZipOutputStream zOut, String vPath, |
| long lastModified, File fromArchive, int mode) |
| throws IOException { |
| if (MANIFEST_NAME.equalsIgnoreCase(vPath)) { |
| if (isFirstPass()) { |
| filesetManifest(fromArchive, is); |
| } |
| } else if (INDEX_NAME.equalsIgnoreCase(vPath) && index) { |
| logWhenWriting("Warning: selected " + archiveType |
| + " files include a " + INDEX_NAME + " which will" |
| + " be replaced by a newly generated one.", |
| Project.MSG_WARN); |
| } else { |
| if (index && vPath.indexOf("/") == -1) { |
| rootEntries.addElement(vPath); |
| } |
| super.zipFile(is, zOut, vPath, lastModified, fromArchive, mode); |
| } |
| } |
| |
| private void filesetManifest(File file, InputStream is) throws IOException { |
| if (manifestFile != null && manifestFile.equals(file)) { |
| // If this is the same name specified in 'manifest', this |
| // is the manifest to use |
| log("Found manifest " + file, Project.MSG_VERBOSE); |
| try { |
| if (is != null) { |
| InputStreamReader isr; |
| if (manifestEncoding == null) { |
| isr = new InputStreamReader(is); |
| } else { |
| isr = new InputStreamReader(is, manifestEncoding); |
| } |
| manifest = getManifest(isr); |
| } else { |
| manifest = getManifest(file); |
| } |
| } catch (UnsupportedEncodingException e) { |
| throw new BuildException("Unsupported encoding while reading " |
| + "manifest: " + e.getMessage(), e); |
| } |
| } else if (filesetManifestConfig != null |
| && !filesetManifestConfig.getValue().equals("skip")) { |
| // we add this to our group of fileset manifests |
| logWhenWriting("Found manifest to merge in file " + file, |
| Project.MSG_VERBOSE); |
| |
| try { |
| Manifest newManifest = null; |
| if (is != null) { |
| InputStreamReader isr; |
| if (manifestEncoding == null) { |
| isr = new InputStreamReader(is); |
| } else { |
| isr = new InputStreamReader(is, manifestEncoding); |
| } |
| newManifest = getManifest(isr); |
| } else { |
| newManifest = getManifest(file); |
| } |
| |
| if (filesetManifest == null) { |
| filesetManifest = newManifest; |
| } else { |
| filesetManifest.merge(newManifest, false, mergeClassPaths); |
| } |
| } catch (UnsupportedEncodingException e) { |
| throw new BuildException("Unsupported encoding while reading " |
| + "manifest: " + e.getMessage(), e); |
| } catch (ManifestException e) { |
| log("Manifest in file " + file + " is invalid: " |
| + e.getMessage(), Project.MSG_ERR); |
| throw new BuildException("Invalid Manifest", e, getLocation()); |
| } |
| } else { |
| // assuming 'skip' otherwise |
| // don't warn if skip has been requested explicitly, warn if user |
| // didn't set the attribute |
| |
| // Hide warning also as it makes no sense since |
| // the filesetmanifest attribute itself has been |
| // hidden |
| |
| //int logLevel = filesetManifestConfig == null ? |
| // Project.MSG_WARN : Project.MSG_VERBOSE; |
| //log("File " + file |
| // + " includes a META-INF/MANIFEST.MF which will be ignored. " |
| // + "To include this file, set filesetManifest to a value other " |
| // + "than 'skip'.", logLevel); |
| } |
| } |
| |
| /** |
| * Collect the resources that are newer than the corresponding |
| * entries (or missing) in the original archive. |
| * |
| * <p>If we are going to recreate the archive instead of updating |
| * it, all resources should be considered as new, if a single one |
| * is. Because of this, subclasses overriding this method must |
| * call <code>super.getResourcesToAdd</code> and indicate with the |
| * third arg if they already know that the archive is |
| * out-of-date.</p> |
| * |
| * @param rcs The resource collections to grab resources from |
| * @param zipFile intended archive file (may or may not exist) |
| * @param needsUpdate whether we already know that the archive is |
| * out-of-date. Subclasses overriding this method are supposed to |
| * set this value correctly in their call to |
| * super.getResourcesToAdd. |
| * @return an array of resources to add for each fileset passed in as well |
| * as a flag that indicates whether the archive is uptodate. |
| * |
| * @exception BuildException if it likes |
| */ |
| protected ArchiveState getResourcesToAdd(ResourceCollection[] rcs, |
| File zipFile, |
| boolean needsUpdate) |
| throws BuildException { |
| |
| if (skipWriting) { |
| // this pass is only there to construct the merged |
| // manifest this means we claim an update was needed and |
| // only include the manifests, skipping any uptodate |
| // checks here deferring them for the second run |
| Resource[][] manifests = grabManifests(rcs); |
| int count = 0; |
| for (int i = 0; i < manifests.length; i++) { |
| count += manifests[i].length; |
| } |
| log("found a total of " + count + " manifests in " |
| + manifests.length + " resource collections", |
| Project.MSG_VERBOSE); |
| return new ArchiveState(true, manifests); |
| } |
| |
| // need to handle manifest as a special check |
| if (zipFile.exists()) { |
| // if it doesn't exist, it will get created anyway, don't |
| // bother with any up-to-date checks. |
| |
| try { |
| originalManifest = getManifestFromJar(zipFile); |
| if (originalManifest == null) { |
| log("Updating jar since the current jar has" |
| + " no manifest", Project.MSG_VERBOSE); |
| needsUpdate = true; |
| } else { |
| Manifest mf = createManifest(); |
| if (!mf.equals(originalManifest)) { |
| log("Updating jar since jar manifest has" |
| + " changed", Project.MSG_VERBOSE); |
| needsUpdate = true; |
| } |
| } |
| } catch (Throwable t) { |
| log("error while reading original manifest in file: " |
| + zipFile.toString() + " due to " + t.getMessage(), |
| Project.MSG_WARN); |
| needsUpdate = true; |
| } |
| |
| } else { |
| // no existing archive |
| needsUpdate = true; |
| } |
| |
| createEmpty = needsUpdate; |
| if (!needsUpdate && index) { |
| try { |
| needsUpdate = !jarHasIndex(zipFile); |
| } catch (IOException e) { |
| //if we couldn't read it, we might as well recreate it? |
| needsUpdate = true; |
| } |
| } |
| return super.getResourcesToAdd(rcs, zipFile, needsUpdate); |
| } |
| |
| /** |
| * Create an empty jar file. |
| * @param zipFile the file to create |
| * @return true for historic reasons |
| * @throws BuildException on error |
| */ |
| protected boolean createEmptyZip(File zipFile) throws BuildException { |
| if (!createEmpty) { |
| return true; |
| } |
| |
| if (emptyBehavior.equals("skip")) { |
| if (!skipWriting) { |
| log("Warning: skipping " + archiveType + " archive " |
| + zipFile + " because no files were included.", |
| Project.MSG_WARN); |
| } |
| return true; |
| } else if (emptyBehavior.equals("fail")) { |
| throw new BuildException("Cannot create " + archiveType |
| + " archive " + zipFile |
| + ": no files were included.", |
| getLocation()); |
| } |
| |
| ZipOutputStream zOut = null; |
| try { |
| if (!skipWriting) { |
| log("Building MANIFEST-only jar: " |
| + getDestFile().getAbsolutePath()); |
| } |
| zOut = new ZipOutputStream(getDestFile()); |
| |
| zOut.setEncoding(getEncoding()); |
| zOut.setUseZip64(getZip64Mode().getMode()); |
| if (isCompress()) { |
| zOut.setMethod(ZipOutputStream.DEFLATED); |
| } else { |
| zOut.setMethod(ZipOutputStream.STORED); |
| } |
| initZipOutputStream(zOut); |
| finalizeZipOutputStream(zOut); |
| } catch (IOException ioe) { |
| throw new BuildException("Could not create almost empty JAR archive" |
| + " (" + ioe.getMessage() + ")", ioe, |
| getLocation()); |
| } finally { |
| // Close the output stream. |
| FileUtils.close(zOut); |
| createEmpty = false; |
| } |
| return true; |
| } |
| |
| /** |
| * Make sure we don't think we already have a MANIFEST next time this task |
| * gets executed. |
| * |
| * @see Zip#cleanUp |
| */ |
| protected void cleanUp() { |
| super.cleanUp(); |
| checkJarSpec(); |
| |
| // we want to save this info if we are going to make another pass |
| if (!doubleFilePass || !skipWriting) { |
| manifest = null; |
| configuredManifest = savedConfiguredManifest; |
| filesetManifest = null; |
| originalManifest = null; |
| } |
| rootEntries.removeAllElements(); |
| } |
| |
| // CheckStyle:LineLength OFF - Link is too long. |
| /** |
| * Check against packaging spec |
| * @see "http://java.sun.com/j2se/1.3/docs/guide/versioning/spec/VersioningSpecification.html#PackageVersioning" |
| */ |
| // CheckStyle:LineLength ON |
| private void checkJarSpec() { |
| String br = System.getProperty("line.separator"); |
| StringBuffer message = new StringBuffer(); |
| Section mainSection = (configuredManifest == null) |
| ? null |
| : configuredManifest.getMainSection(); |
| |
| if (mainSection == null) { |
| message.append("No Implementation-Title set."); |
| message.append("No Implementation-Version set."); |
| message.append("No Implementation-Vendor set."); |
| } else { |
| if (mainSection.getAttribute("Implementation-Title") == null) { |
| message.append("No Implementation-Title set."); |
| } |
| if (mainSection.getAttribute("Implementation-Version") == null) { |
| message.append("No Implementation-Version set."); |
| } |
| if (mainSection.getAttribute("Implementation-Vendor") == null) { |
| message.append("No Implementation-Vendor set."); |
| } |
| } |
| |
| if (message.length() > 0) { |
| message.append(br); |
| message.append("Location: ").append(getLocation()); |
| message.append(br); |
| if (strict.getValue().equalsIgnoreCase("fail")) { |
| throw new BuildException(message.toString(), getLocation()); |
| } else { |
| logWhenWriting(message.toString(), strict.getLogLevel()); |
| } |
| } |
| } |
| |
| /** |
| * reset to default values. |
| * |
| * @see Zip#reset |
| * |
| * @since 1.44, Ant 1.5 |
| */ |
| public void reset() { |
| super.reset(); |
| emptyBehavior = "create"; |
| configuredManifest = null; |
| filesetManifestConfig = null; |
| mergeManifestsMain = false; |
| manifestFile = null; |
| index = false; |
| } |
| |
| /** |
| * The manifest config enumerated type. |
| */ |
| public static class FilesetManifestConfig extends EnumeratedAttribute { |
| /** |
| * Get the list of valid strings. |
| * @return the list of values - "skip", "merge" and "mergewithoutmain" |
| */ |
| public String[] getValues() { |
| return new String[] {"skip", "merge", "mergewithoutmain"}; |
| } |
| } |
| |
| /** |
| * Writes the directory entries from the first and the filenames |
| * from the second list to the given writer, one entry per line. |
| * |
| * @param dirs a list of directories |
| * @param files a list of files |
| * @param writer the writer to write to |
| * @throws IOException on error |
| * @since Ant 1.6.2 |
| */ |
| protected final void writeIndexLikeList(List<String> dirs, List<String> files, |
| PrintWriter writer) { |
| // JarIndex is sorting the directories by ascending order. |
| // it has no value but cosmetic since it will be read into a |
| // hashtable by the classloader, but we'll do so anyway. |
| Collections.sort(dirs); |
| Collections.sort(files); |
| for (String dir : dirs) { |
| // try to be smart, not to be fooled by a weird directory name |
| dir = dir.replace('\\', '/'); |
| if (dir.startsWith("./")) { |
| dir = dir.substring(2); |
| } |
| while (dir.startsWith("/")) { |
| dir = dir.substring(1); |
| } |
| int pos = dir.lastIndexOf('/'); |
| if (pos != -1) { |
| dir = dir.substring(0, pos); |
| } |
| |
| // looks like nothing from META-INF should be added |
| // and the check is not case insensitive. |
| // see sun.misc.JarIndex |
| // see also |
| // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4408526 |
| if (!indexMetaInf && dir.startsWith("META-INF")) { |
| continue; |
| } |
| // name newline |
| writer.println(dir); |
| } |
| |
| for (String file : files) { |
| writer.println(file); |
| } |
| } |
| |
| /** |
| * try to guess the name of the given file. |
| * |
| * <p>If this jar has a classpath attribute in its manifest, we |
| * can assume that it will only require an index of jars listed |
| * there. try to find which classpath entry is most likely the |
| * one the given file name points to.</p> |
| * |
| * <p>In the absence of a classpath attribute, assume the other |
| * files will be placed inside the same directory as this jar and |
| * use their basename.</p> |
| * |
| * <p>if there is a classpath and the given file doesn't match any |
| * of its entries, return null.</p> |
| * |
| * @param fileName the name to look for |
| * @param classpath the classpath to look in (may be null) |
| * @return the matching entry, or null if the file is not found |
| * @since Ant 1.6.2 |
| */ |
| protected static String findJarName(String fileName, |
| String[] classpath) { |
| if (classpath == null) { |
| return (new File(fileName)).getName(); |
| } |
| fileName = fileName.replace(File.separatorChar, '/'); |
| TreeMap<String, String> matches = new TreeMap<String, String>(new Comparator<Object>() { |
| // longest match comes first |
| public int compare(Object o1, Object o2) { |
| if (o1 instanceof String && o2 instanceof String) { |
| return ((String) o2).length() |
| - ((String) o1).length(); |
| } |
| return 0; |
| } |
| }); |
| |
| for (int i = 0; i < classpath.length; i++) { |
| if (fileName.endsWith(classpath[i])) { |
| matches.put(classpath[i], classpath[i]); |
| } else { |
| int slash = classpath[i].indexOf("/"); |
| String candidate = classpath[i]; |
| while (slash > -1) { |
| candidate = candidate.substring(slash + 1); |
| if (fileName.endsWith(candidate)) { |
| matches.put(candidate, classpath[i]); |
| break; |
| } |
| slash = candidate.indexOf("/"); |
| } |
| } |
| } |
| |
| return matches.size() == 0 |
| ? null : (String) matches.get(matches.firstKey()); |
| } |
| |
| /** |
| * Grab lists of all root-level files and all directories |
| * contained in the given archive. |
| * @param file the zip file to examine |
| * @param dirs where to place the directories found |
| * @param files where to place the files found |
| * @since Ant 1.7 |
| * @throws IOException on error |
| */ |
| protected static void grabFilesAndDirs(String file, List<String> dirs, |
| List<String> files) |
| throws IOException { |
| org.apache.tools.zip.ZipFile zf = null; |
| try { |
| zf = new org.apache.tools.zip.ZipFile(file, "utf-8"); |
| Enumeration<org.apache.tools.zip.ZipEntry> entries = zf.getEntries(); |
| HashSet<String> dirSet = new HashSet<String>(); |
| while (entries.hasMoreElements()) { |
| org.apache.tools.zip.ZipEntry ze = |
| entries.nextElement(); |
| String name = ze.getName(); |
| if (ze.isDirectory()) { |
| dirSet.add(name); |
| } else if (name.indexOf("/") == -1) { |
| files.add(name); |
| } else { |
| // a file, not in the root |
| // since the jar may be one without directory |
| // entries, add the parent dir of this file as |
| // well. |
| dirSet.add(name.substring(0, name.lastIndexOf("/") + 1)); |
| } |
| } |
| dirs.addAll(dirSet); |
| } finally { |
| if (zf != null) { |
| zf.close(); |
| } |
| } |
| } |
| |
| private Resource[][] grabManifests(ResourceCollection[] rcs) { |
| Resource[][] manifests = new Resource[rcs.length][]; |
| for (int i = 0; i < rcs.length; i++) { |
| Resource[][] resources = null; |
| if (rcs[i] instanceof FileSet) { |
| resources = grabResources(new FileSet[] {(FileSet) rcs[i]}); |
| } else { |
| resources = grabNonFileSetResources(new ResourceCollection[] { |
| rcs[i] |
| }); |
| } |
| for (int j = 0; j < resources[0].length; j++) { |
| String name = resources[0][j].getName().replace('\\', '/'); |
| if (rcs[i] instanceof ArchiveFileSet) { |
| ArchiveFileSet afs = (ArchiveFileSet) rcs[i]; |
| if (!"".equals(afs.getFullpath(getProject()))) { |
| name = afs.getFullpath(getProject()); |
| } else if (!"".equals(afs.getPrefix(getProject()))) { |
| String prefix = afs.getPrefix(getProject()); |
| if (!prefix.endsWith("/") && !prefix.endsWith("\\")) { |
| prefix += "/"; |
| } |
| name = prefix + name; |
| } |
| } |
| if (name.equalsIgnoreCase(MANIFEST_NAME)) { |
| manifests[i] = new Resource[] {resources[0][j]}; |
| break; |
| } |
| } |
| if (manifests[i] == null) { |
| manifests[i] = new Resource[0]; |
| } |
| } |
| return manifests; |
| } |
| |
| /** The strict enumerated type. */ |
| public static class StrictMode extends EnumeratedAttribute { |
| /** Public no arg constructor. */ |
| public StrictMode() { |
| } |
| /** |
| * Constructor with an arg. |
| * @param value the enumerated value as a string. |
| */ |
| public StrictMode(String value) { |
| setValue(value); |
| } |
| /** |
| * Get List of valid strings. |
| * @return the list of values. |
| */ |
| public String[] getValues() { |
| return new String[]{"fail", "warn", "ignore"}; |
| } |
| /** |
| * @return The log level according to the strict mode. |
| */ |
| public int getLogLevel() { |
| return (getValue().equals("ignore")) ? Project.MSG_VERBOSE : Project.MSG_WARN; |
| } |
| } |
| } |