/*
 *  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
 *
 *      https://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.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.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
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.ant.util.StreamUtils;
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.taskcategory="packaging"
/** * Creates a JAR archive. * * @since Ant 1.1 * * @ant.task category="packaging" */
public class Jar extends Zip {
The index file name.
/** The index file name. */
private static final String INDEX_NAME = "META-INF/INDEX.LIST";
The manifest file name.
/** The manifest file name. */
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
List of all known SPI Services
/** * List of all known SPI Services */
private List<Service> serviceList = new ArrayList<>();
merged manifests added through addConfiguredManifest
/** merged manifests added through addConfiguredManifest */
private Manifest configuredManifest;
shadow of the above if upToDate check alters the value
/** shadow of the above if upToDate check alters the value */
private Manifest savedConfiguredManifest;
merged manifests added through filesets
/** merged manifests added through filesets */
private Manifest filesetManifest;
Manifest of original archive, will be set to null if not in update mode.
/** * 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'
/** * 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'
/** * 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
/** the manifest specified by the 'manifest' attribute **/
private Manifest manifest;
The encoding to use when reading in a manifest file
/** 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
/** * 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
/** jar index is JDK 1.3+ only */
private boolean index = false;
Whether to index META-INF/ and its children
/** 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.
/** * 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
/** * 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 List<String> rootEntries;
Path containing jars that shall be indexed in addition to this archive.
Since:Ant 1.6.2
/** * 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 Also:
/** * Strict mode for checking rules of the JAR-Specification. * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/versioning/spec/versioning2.html#wp89936"></a> */
private StrictMode strict = new StrictMode("ignore"); // CheckStyle:LineLength ON
whether to merge Class-Path attributes.
/** * whether to merge Class-Path attributes. */
private boolean mergeClassPaths = false;
whether to flatten Class-Path attributes into a single one.
/** * 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
/** * 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
/** constructor */
public Jar() { super(); archiveType = "jar"; emptyBehavior = "create"; setEncoding("UTF8"); setZip64Mode(Zip64ModeAttribute.NEVER); rootEntries = new Vector<>(); }
Not used for jar files.
Params:
  • we – not used
@ant.attributeignore="true"
/** * Not used for jar files. * @param we not used * @ant.attribute ignore="true" */
@Override 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: fail (throw an exception and halt the build); skip (do not create any archive, but issue a warning); create (make an archive with only a manifest file). Default is create;
Params:
  • we – a WhenEmpty enumerated value
/** * 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 true a BuildException will be thrown if the Jar-Packaging specification was broken.
Params:
  • strict – New value of the strict mode.
Since:Ant 1.7.1
/** * 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.
Params:
  • jarFile – the destination file
Deprecated:since 1.5.x. Use setDestFile(File) instead.
/** * Set the destination file. * @param jarFile the destination file * @deprecated since 1.5.x. * Use setDestFile(File) instead. */
@Deprecated 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.
Params:
  • flag – a boolean value
/** * 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.

Doesn't have any effect if index is false.

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.

Params:
  • flag – a boolean value, defaults to false
See Also:
Since:Ant 1.8.0
/** * 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="https://bugs.openjdk.java.net/browse/JDK-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.
Params:
  • manifestEncoding – the character encoding
/** * 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.
Params:
  • newManifest – an embedded manifest element
Throws:
/** * 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.
Params:
  • manifestFile – the manifest file to use.
/** * 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) { try (InputStreamReader isr = new InputStreamReader( Files.newInputStream(manifestFile.toPath()), getManifestCharset())) { return getManifest(isr); } catch (IOException e) { throw new BuildException("Unable to read manifest file: " + manifestFile + " (" + e.getMessage() + ")", e); } }
Returns:null if jarFile doesn't contain a manifest, the manifest otherwise.
Since:Ant 1.5.2
/** * @return null if jarFile doesn't contain a manifest, the * manifest otherwise. * @since Ant 1.5.2 */
private Manifest getManifestFromJar(File jarFile) throws IOException { try (ZipFile zf = new ZipFile(jarFile)) { // must not use getEntry as "well behaving" applications // must accept the manifest in any capitalization ZipEntry ze = StreamUtils.enumerationAsStream(zf.entries()) .filter(entry -> MANIFEST_NAME.equalsIgnoreCase(entry.getName())) .findFirst().orElse(null); if (ze == null) { return null; } try (InputStreamReader isr = new InputStreamReader(zf.getInputStream(ze), StandardCharsets.UTF_8)) { return getManifest(isr); } } } private Manifest getManifest(Reader r) { try { return 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); } } private boolean jarHasIndex(File jarFile) throws IOException { try (ZipFile zf = new ZipFile(jarFile)) { return StreamUtils.enumerationAsStream(zf.entries()) .anyMatch(ze -> INDEX_NAME.equalsIgnoreCase(ze.getName())); } }
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
Params:
  • config – setting for found manifest behavior.
/** * 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 = config != null && "merge".equals(config.getValue()); if (filesetManifestConfig != null && !"skip".equals(filesetManifestConfig.getValue())) { doubleFilePass = true; } }
Adds a zipfileset to include in the META-INF directory.
Params:
  • fs – zipfileset to add
/** * 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.
Params:
  • p – a path
Since:Ant 1.6.2
/** * 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.
Params:
  • service – the nested element.
Since:Ant 1.7
/** * 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
/** * Write SPI Information to JAR */
private void writeServices(ZipOutputStream zOut) throws IOException { for (Service service : serviceList) { try (InputStream is = service.getAsStream()) { //stolen from writeManifest super.zipFile(is, zOut, "META-INF/services/" + service.getType(), System.currentTimeMillis(), null, ZipFileSet.DEFAULT_FILE_MODE); } } }
Whether to merge Class-Path attributes.
Params:
  • b – boolean
Since:Ant 1.8.0
/** * Whether to merge Class-Path attributes. * * @param b boolean * @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.
Params:
  • b – boolean
Since:Ant 1.8.0
/** * Whether to flatten multi-valued attributes (i.e. Class-Path) * into a single one. * * @param b boolean * @since Ant 1.8.0 */
public void setFlattenAttributes(boolean b) { flattenClassPaths = b; }
Initialize the zip output stream.
Params:
  • zOut – the zip output stream
Throws:
/** * Initialize the zip output stream. * @param zOut the zip output stream * @throws IOException on I/O errors * @throws BuildException on other errors */
@Override 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 && 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 { StreamUtils.enumerationAsStream(manifest.getWarnings()) .forEach(warning -> log("Manifest warning: " + warning, 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_CHARSET); 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.
Params:
  • zOut – the zip output stream
Throws:
/** * 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 */
@Override 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 the JAR index specification for more details.
Params:
  • 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.
/** * Create the index list to speed up classloading. * This is a JDK 1.3+ specific feature and is enabled by default. See * <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Index"> * 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, StandardCharsets.UTF_8)); // version-info blankline writer.println("JarIndex-Version: 1.0"); writer.println(); // header newline writer.println(zipFile.getName()); writeIndexLikeList(new ArrayList<>(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(); } } for (String indexJarEntry : indexJars.list()) { String name = findJarName(indexJarEntry, cpEntries); if (name != null) { ArrayList<String> dirs = new ArrayList<>(); ArrayList<String> files = new ArrayList<>(); grabFilesAndDirs(indexJarEntry, 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(); try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) { super.zipFile(bais, zOut, INDEX_NAME, System.currentTimeMillis(), null, ZipFileSet.DEFAULT_FILE_MODE); } }
Overridden from Zip class to deal with manifests and index lists.
Params:
  • is – the stream to read data for the entry from. The caller of the method is responsible for closing the stream.
  • zOut – the zip output stream
  • vPath – the name this entry shall have in the archive
  • lastModified – last modification time for the entry.
  • fromArchive – the original archive we are copying this entry from, will be null if we are not copying from an archive.
  • mode – the Unix permissions to set.
Throws:
/** * 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 */
@Override 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.contains("/")) { rootEntries.add(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); if (is == null) { manifest = getManifest(file); } else { try (InputStreamReader isr = new InputStreamReader(is, getManifestCharset())) { manifest = getManifest(isr); } } } else if (filesetManifestConfig != null && !"skip".equals(filesetManifestConfig.getValue())) { // we add this to our group of fileset manifests logWhenWriting("Found manifest to merge in file " + file, Project.MSG_VERBOSE); try { Manifest newManifest; if (is == null) { newManifest = getManifest(file); } else { try (InputStreamReader isr = new InputStreamReader(is, getManifestCharset())) { newManifest = getManifest(isr); } } 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.

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 super.getResourcesToAdd and indicate with the third arg if they already know that the archive is out-of-date.

Params:
  • rcs – The resource collections to grab resources from
  • zipFile – intended archive file (may or may not exist)
  • 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.
Throws:
Returns:an array of resources to add for each fileset passed in as well as a flag that indicates whether the archive is uptodate.
/** * 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 */
@Override 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 (Resource[] mf : manifests) { count += mf.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.
Params:
  • zipFile – the file to create
Throws:
Returns:true for historic reasons
/** * Create an empty jar file. * @param zipFile the file to create * @return true for historic reasons * @throws BuildException on error */
@Override protected boolean createEmptyZip(File zipFile) throws BuildException { if (!createEmpty) { return true; } if ("skip".equals(emptyBehavior)) { if (!skipWriting) { log("Warning: skipping " + archiveType + " archive " + zipFile + " because no files were included.", Project.MSG_WARN); } return true; } if ("fail".equals(emptyBehavior)) { throw new BuildException("Cannot create " + archiveType + " archive " + zipFile + ": no files were included.", getLocation()); } if (!skipWriting) { log("Building MANIFEST-only jar: " + getDestFile().getAbsolutePath()); try (ZipOutputStream 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 { createEmpty = false; } } return true; }
Make sure we don't think we already have a MANIFEST next time this task gets executed.
See Also:
  • cleanUp.cleanUp
/** * Make sure we don't think we already have a MANIFEST next time this task * gets executed. * * @see Zip#cleanUp */
@Override 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.clear(); } // CheckStyle:LineLength OFF - Link is too long.
Check against packaging spec
See Also:
  • https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html
/** * Check against packaging spec * @see "https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html" */
// CheckStyle:LineLength ON private void checkJarSpec() { StringBuilder message = new StringBuilder(); 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(String.format("%nLocation: %s%n", getLocation())); if ("fail".equalsIgnoreCase(strict.getValue())) { throw new BuildException(message.toString(), getLocation()); } logWhenWriting(message.toString(), strict.getLogLevel()); } }
reset to default values.
See Also:
  • reset.reset
Since:1.44, Ant 1.5
/** * reset to default values. * * @see Zip#reset * * @since 1.44, Ant 1.5 */
@Override public void reset() { super.reset(); emptyBehavior = "create"; configuredManifest = null; filesetManifestConfig = null; mergeManifestsMain = false; manifestFile = null; index = false; }
The manifest config enumerated type.
/** * The manifest config enumerated type. */
public static class FilesetManifestConfig extends EnumeratedAttribute {
Get the list of valid strings.
Returns:the list of values - "skip", "merge" and "mergewithoutmain"
/** * Get the list of valid strings. * @return the list of values - "skip", "merge" and "mergewithoutmain" */
@Override 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.
Params:
  • dirs – a list of directories
  • files – a list of files
  • writer – the writer to write to
Since:Ant 1.6.2
/** * 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 * @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 // https://bugs.openjdk.java.net/browse/JDK-4408526 if (!indexMetaInf && dir.startsWith("META-INF")) { continue; } // name newline writer.println(dir); } files.forEach(writer::println); }
try to guess the name of the given file.

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.

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.

if there is a classpath and the given file doesn't match any of its entries, return null.

Params:
  • fileName – the name to look for
  • classpath – the classpath to look in (may be null)
Returns:the matching entry, or null if the file is not found
Since:Ant 1.6.2
/** * 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, '/'); SortedMap<String, String> matches = new TreeMap<>(Comparator .<String> comparingInt(s -> s == null ? 0 : s.length()).reversed()); for (String element : classpath) { String candidate = element; while (true) { if (fileName.endsWith(candidate)) { matches.put(candidate, element); break; } int slash = candidate.indexOf('/'); if (slash < 0) { break; } candidate = candidate.substring(slash + 1); } } return matches.isEmpty() ? null : matches.get(matches.firstKey()); }
Grab lists of all root-level files and all directories contained in the given archive.
Params:
  • file – the zip file to examine
  • dirs – where to place the directories found
  • files – where to place the files found
Throws:
Since:Ant 1.7
/** * 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 { try (org.apache.tools.zip.ZipFile zf = new org.apache.tools.zip.ZipFile(file, "utf-8")) { Set<String> dirSet = new HashSet<>(); StreamUtils.enumerationAsStream(zf.getEntries()).forEach(ze -> { String name = ze.getName(); if (ze.isDirectory()) { dirSet.add(name); } else if (!name.contains("/")) { 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); } } private Resource[][] grabManifests(ResourceCollection[] rcs) { Resource[][] manifests = new Resource[rcs.length][]; for (int i = 0; i < rcs.length; i++) { Resource[][] resources; 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 (!afs.getFullpath(getProject()).isEmpty()) { name = afs.getFullpath(getProject()); } else if (!afs.getPrefix(getProject()).isEmpty()) { String prefix = afs.getPrefix(getProject()); if (!prefix.endsWith("/") && !prefix.endsWith("\\")) { prefix += "/"; } name = prefix + name; } } if (MANIFEST_NAME.equalsIgnoreCase(name)) { manifests[i] = new Resource[] {resources[0][j]}; break; } } if (manifests[i] == null) { manifests[i] = new Resource[0]; } } return manifests; } private Charset getManifestCharset() { if (manifestEncoding == null) { return Charset.defaultCharset(); } try { return Charset.forName(manifestEncoding); } catch (IllegalArgumentException e) { throw new BuildException( "Unsupported encoding while reading manifest: " + e.getMessage(), e); } }
The strict enumerated type.
/** The strict enumerated type. */
public static class StrictMode extends EnumeratedAttribute {
Public no arg constructor.
/** Public no arg constructor. */
public StrictMode() { }
Constructor with an arg.
Params:
  • value – the enumerated value as a string.
/** * Constructor with an arg. * @param value the enumerated value as a string. */
public StrictMode(String value) { setValue(value); }
Get List of valid strings.
Returns:the list of values.
/** * Get List of valid strings. * @return the list of values. */
@Override public String[] getValues() { return new String[] {"fail", "warn", "ignore"}; }
Returns:The log level according to the strict mode.
/** * @return The log level according to the strict mode. */
public int getLogLevel() { return "ignore".equals(getValue()) ? Project.MSG_VERBOSE : Project.MSG_WARN; } } }