From 590936af269c77bda5fb410fd81e19958062efe6 Mon Sep 17 00:00:00 2001 From: Conor MacNeill Date: Mon, 30 Jul 2001 11:15:17 +0000 Subject: [PATCH] Manifest is no longer in a fileset and so needs an uptodate check of its own Also now log warnings about malformed manifests git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@269407 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/tools/ant/taskdefs/Jar.java | 92 ++++-- .../apache/tools/ant/taskdefs/Manifest.java | 268 +++++++++++++++--- .../tools/ant/taskdefs/ManifestException.java | 73 +++++ .../org/apache/tools/ant/taskdefs/Zip.java | 56 ++-- 4 files changed, 404 insertions(+), 85 deletions(-) create mode 100644 src/main/org/apache/tools/ant/taskdefs/ManifestException.java diff --git a/src/main/org/apache/tools/ant/taskdefs/Jar.java b/src/main/org/apache/tools/ant/taskdefs/Jar.java index f1ad682dc..cef230304 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Jar.java +++ b/src/main/org/apache/tools/ant/taskdefs/Jar.java @@ -59,6 +59,7 @@ import org.apache.tools.ant.types.ZipFileSet; import org.apache.tools.zip.*; import java.io.*; +import java.util.Enumeration; /** * Creates a JAR archive. @@ -67,6 +68,7 @@ import java.io.*; */ public class Jar extends Zip { + private File manifestFile; private Manifest manifest; private Manifest execManifest; @@ -86,6 +88,8 @@ public class Jar extends Zip { throw new BuildException("Manifest file: " + manifestFile + " does not exist.", getLocation()); } + + this.manifestFile = manifestFile; InputStream is = null; try { @@ -96,6 +100,10 @@ public class Jar extends Zip { } manifest.merge(newManifest); } + 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: " + manifestFile, e); } @@ -120,25 +128,39 @@ public class Jar extends Zip { protected void initZipOutputStream(ZipOutputStream zOut) throws IOException, BuildException { - // If no manifest is specified, add the default one. - if (manifest == null) { - execManifest = null; + try { + // If no manifest is specified, add the default one. + if (manifest == null) { + execManifest = null; + } + else { + execManifest = new Manifest(); + execManifest.merge(manifest); + } + zipDir(null, zOut, "META-INF/"); + super.initZipOutputStream(zOut); } - else { - execManifest = new Manifest(); - execManifest.merge(manifest); + catch (ManifestException e) { + log("Manifest is invalid: " + e.getMessage(), Project.MSG_ERR); + throw new BuildException("Invalid Manifest", e, getLocation()); } - zipDir(null, zOut, "META-INF/"); - super.initZipOutputStream(zOut); } - - private Manifest getDefaultManifest() throws IOException { - String s = "/org/apache/tools/ant/defaultManifest.mf"; - InputStream in = this.getClass().getResourceAsStream(s); - if (in == null) { - throw new BuildException("Could not find: " + s); + + private Manifest getDefaultManifest() { + try { + String s = "/org/apache/tools/ant/defaultManifest.mf"; + InputStream in = this.getClass().getResourceAsStream(s); + if (in == null) { + throw new BuildException("Could not find default manifest: " + s); + } + return new Manifest(in); + } + catch (ManifestException e) { + throw new BuildException("Default manifest is invalid !!"); + } + catch (IOException e) { + throw new BuildException("Unable to read default manifest", e); } - return new Manifest(in); } protected void finalizeZipOutputStream(ZipOutputStream zOut) @@ -147,6 +169,10 @@ public class Jar extends Zip { if (execManifest == null) { execManifest = getDefaultManifest(); } + + for (Enumeration e = execManifest.getWarnings(); e.hasMoreElements(); ) { + log("Manifest warning: " + (String)e.nextElement(), Project.MSG_WARN); + } // time to write the manifest ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -157,7 +183,6 @@ public class Jar extends Zip { ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); super.zipFile(bais, zOut, "META-INF/MANIFEST.MF", System.currentTimeMillis()); super.finalizeZipOutputStream(zOut); - } /** @@ -169,11 +194,17 @@ public class Jar extends Zip { * and not the old one from the JAR we are updating */ private void zipManifestEntry(InputStream is) throws IOException { - if (execManifest == null) { - execManifest = new Manifest(is); + try { + if (execManifest == null) { + execManifest = new Manifest(is); + } + else if (isAddingNewFiles()) { + execManifest.merge(new Manifest(is)); + } } - else if (isAddingNewFiles()) { - execManifest.merge(new Manifest(is)); + catch (ManifestException e) { + log("Manifest is invalid: " + e.getMessage(), Project.MSG_ERR); + throw new BuildException("Invalid Manifest", e, getLocation()); } } @@ -223,6 +254,27 @@ public class Jar extends Zip { } } + /** + * Check whether the archive is up-to-date; + * @param scanners list of prepared scanners containing files to archive + * @param zipFile intended archive file (may or may not exist) + * @return true if nothing need be done (may have done something already); false if + * archive creation should proceed + * @exception BuildException if it likes + */ + protected boolean isUpToDate(FileScanner[] scanners, File zipFile) throws BuildException { + // need to handle manifest as a special check + if (manifestFile != null && manifestFile.lastModified() > zipFile.lastModified()) { + return false; + } + return super.isUpToDate(scanners, zipFile); + } + + protected boolean createEmptyZip(File zipFile) { + // Jar files always contain a manifest and can never be empty + return false; + } + /** * Make sure we don't think we already have a MANIFEST next time this task * gets executed. diff --git a/src/main/org/apache/tools/ant/taskdefs/Manifest.java b/src/main/org/apache/tools/ant/taskdefs/Manifest.java index 30756df96..ef74a5a37 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Manifest.java +++ b/src/main/org/apache/tools/ant/taskdefs/Manifest.java @@ -63,64 +63,132 @@ import java.io.*; * @author Conor MacNeill */ public class Manifest { - static public final String ATTR_MANIFEST_VERSION = "Manifest-Version"; - static public final String ATTR_SIGNATURE_VERSION = "Signature-Version"; - static public final String ATTR_NAME = "Name"; - static public final String ATTR_FROM = "From"; + /** The standard manifest version header */ + static public final String ATTRIBUTE_MANIFEST_VERSION = "Manifest-Version"; + + /** The standard Signature Version header */ + static public final String ATTRIBUTE_SIGNATURE_VERSION = "Signature-Version"; + + /** The Name Attribute is the first in a named section */ + static public final String ATTRIBUTE_NAME = "Name"; + + /** THe From Header is disallowed in a Manifest */ + static public final String ATTRIBUTE_FROM = "From"; + + /** Default Manifest version if one is not specified */ static public final String DEFAULT_MANIFEST_VERSION = "1.0"; + + /** The max length of a line in a Manifest */ static public final int MAX_LINE_LENGTH = 70; /** * Class to hold manifest attributes */ - static private class Attribute { + private class Attribute { /** The attribute's name */ private String name = null; /** The attribute's value */ private String value = null; + /** + * Construct an empty attribute */ public Attribute() { } - public Attribute(String line) throws IOException { + /** + * Construct an attribute by parsing a line from the Manifest + * + * @param line the line containing the attribute name and value + * + * @throws ManifestException if the line is not valid + */ + public Attribute(String line) throws ManifestException { parse(line); } + /** + * Construct a manifest by specifying its name and value + * + * @param name the attribute's name + * @param value the Attribute's value + */ public Attribute(String name, String value) { this.name = name; this.value = value; } - public void parse(String line) throws IOException { + /** + * Parse a line into name and value pairs + * + * @param line the line to be parsed + * + * @throws ManifestException if the line does not contain a colon + * separating the name and value + */ + public void parse(String line) throws ManifestException { int index = line.indexOf(": "); if (index == -1) { - throw new IOException("Manifest line \"" + line + "\" is not valid"); + throw new ManifestException("Manifest line \"" + line + "\" is not valid"); } name = line.substring(0, index); value = line.substring(index + 2); } + /** + * Set the Attribute's name + * + * @param name the attribute's name + */ public void setName(String name) { this.name = name; } + /** + * Get the Attribute's name + * + * @return the attribute's name. + */ public String getName() { return name; } + /** + * Set the Attribute's value + * + * @param value the attribute's value + */ public void setValue(String value) { this.value = value; } + /** + * Get the Attribute's value + * + * @return the attribute's value. + */ public String getValue() { return value; } + /** + * Add a continuation line from the Manifest file + * + * When lines are too long in a manifest, they are continued on the + * next line by starting with a space. This method adds the continuation + * data to the attribute value by skipping the first character. + */ public void addContinuation(String line) { value += line.substring(1); } + /** + * Write the attribute out to a print writer. + * + * @param writer the Writer to which the attribute is written + * + * @throws IOException if the attribte value cannot be written + */ public void write(PrintWriter writer) throws IOException { String line = name + ": " + value; while (line.getBytes().length > MAX_LINE_LENGTH) { @@ -143,22 +211,46 @@ public class Manifest { /** * Class to represent an individual section in the - * Manifest + * Manifest. A section consists of a set of attribute values, + * separated from other sections by a blank line. */ - static private class Section { + private class Section { + /** The section's name if any. The main section in a manifest is unnamed.*/ private String name = null; + /** The section's attributes.*/ private Hashtable attributes = new Hashtable(); + /** + * Set the Section's name + * + * @param name the section's name + */ public void setName(String name) { this.name = name; } + /** + * Get the Section's name + * + * @return the section's name. + */ public String getName() { return name; } - public String read(BufferedReader reader) throws IOException { + /** + * Read a section through a reader + * + * @param reader the reader from which the section is read + * + * @return the name of the next section if it has been read as part of this + * section - This only happens if the Manifest is malformed. + * + * @throws ManifestException if the section is not valid according to the JAR spec + * @throws IOException if the section cannot be read from the reader. + */ + public String read(BufferedReader reader) throws ManifestException, IOException { Attribute attribute = null; while (true) { String line = reader.readLine(); @@ -168,29 +260,31 @@ public class Manifest { if (line.charAt(0) == ' ') { // continuation line if (attribute == null) { - throw new IOException("Can't start an attribute with a continuation line " + line); + throw new ManifestException("Can't start an attribute with a continuation line " + line); } attribute.addContinuation(line); } else { attribute = new Attribute(line); - if (attribute.getName().equalsIgnoreCase(ATTR_NAME)) { - return attribute.getValue(); + String nameReadAhead = addAttribute(attribute); + if (nameReadAhead != null) { + return nameReadAhead; } - - if (attribute.getName().toLowerCase().startsWith(ATTR_FROM.toLowerCase())) { - throw new IOException("Attribute names may not start with " + ATTR_FROM); - } - - addAttribute(attribute); } } } - public void merge(Section section) throws IOException { + /** + * Merge in another section + * + * @param section the section to be merged with this one. + * + * @throws ManifestException if the sections cannot be merged. + */ + public void merge(Section section) throws ManifestException { if (name == null && section.getName() != null || name != null && !(name.equalsIgnoreCase(section.getName()))) { - throw new IOException("Unable to merge sections with different names"); + throw new ManifestException("Unable to merge sections with different names"); } for (Enumeration e = section.attributes.keys(); e.hasMoreElements();) { @@ -200,9 +294,16 @@ public class Manifest { } } + /** + * Write the section out to a print writer. + * + * @param writer the Writer to which the section is written + * + * @throws IOException if the section cannot be written + */ public void write(PrintWriter writer) throws IOException { if (name != null) { - Attribute nameAttr = new Attribute(ATTR_NAME, name); + Attribute nameAttr = new Attribute(ATTRIBUTE_NAME, name); nameAttr.write(writer); } for (Enumeration e = attributes.elements(); e.hasMoreElements();) { @@ -212,6 +313,14 @@ public class Manifest { writer.println(); } + /** + * Get the value of the attribute with the name given. + * + * @param attributeName the name of the attribute to be returned. + * + * @return the attribute's value or null if the attribute does not exist + * in the section + */ public String getAttributeValue(String attributeName) { Attribute attribute = (Attribute)attributes.get(attributeName.toLowerCase()); if (attribute == null) { @@ -219,25 +328,61 @@ public class Manifest { } return attribute.getValue(); } - + + /** + * Remove tge given attribute from the section + * + * @param attributeName the name of the attribute to be removed. + */ public void removeAttribute(String attributeName) { attributes.remove(attributeName.toLowerCase()); } - public void addAttribute(Attribute attribute) throws IOException { - if (attributes.containsKey(attribute.getName().toLowerCase())) { - throw new IOException("The attribute \"" + attribute.getName() + "\" may not occur more than" + - " once in the same section"); + /** + * Add an attribute to the section + * + * @param attribute the attribute to be added. + * + * @return the value of the attribute if it is a name attribute - null other wise + * + * @throws ManifestException if the attribute already exists in this section. + */ + public String addAttribute(Attribute attribute) throws ManifestException { + if (attribute.getName().equalsIgnoreCase(ATTRIBUTE_NAME)) { + warnings.addElement("\"" + ATTRIBUTE_NAME + "\" attributes should not occur in the " + + "main section and must be the first element in all " + + "other sections: \"" +attribute.getName() + ": " + attribute.getValue() + "\""); + return attribute.getValue(); + } + + if (attribute.getName().toLowerCase().startsWith(ATTRIBUTE_FROM.toLowerCase())) { + warnings.addElement("Manifest attributes should not start with \"" + + ATTRIBUTE_FROM + "\" in \"" +attribute.getName() + ": " + attribute.getValue() + "\""); } - attributes.put(attribute.getName().toLowerCase(), attribute); + else if (attributes.containsKey(attribute.getName().toLowerCase())) { + throw new ManifestException("The attribute \"" + attribute.getName() + "\" may not " + + "occur more than once in the same section"); + } + else { + attributes.put(attribute.getName().toLowerCase(), attribute); + } + return null; } } - + /** The version of this manifest */ private String manifestVersion = DEFAULT_MANIFEST_VERSION; + + /** The main section of this manifest */ private Section mainSection = new Section(); + + /** The named sections of this manifest */ private Hashtable sections = new Hashtable(); + /** Warnings for this manifest file */ + private Vector warnings = new Vector(); + + /** Construct an empty manifest */ public Manifest() { } @@ -245,8 +390,11 @@ public class Manifest { * Read a manifest file from the given input stream * * @param is the input stream from which the Manifest is read + * + * @throws ManifestException if the manifest is not valid according to the JAR spec + * @throws IOException if the manifest cannot be read from the reader. */ - public Manifest(InputStream is) throws IOException { + public Manifest(InputStream is) throws ManifestException, IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line = reader.readLine(); if (line == null) { @@ -255,10 +403,10 @@ public class Manifest { // This should be the manifest version String nextSectionName = mainSection.read(reader); - String readManifestVersion = mainSection.getAttributeValue(ATTR_MANIFEST_VERSION); + String readManifestVersion = mainSection.getAttributeValue(ATTRIBUTE_MANIFEST_VERSION); if (readManifestVersion != null) { manifestVersion = readManifestVersion; - mainSection.removeAttribute(ATTR_MANIFEST_VERSION); + mainSection.removeAttribute(ATTRIBUTE_MANIFEST_VERSION); } while ((line = reader.readLine()) != null) { @@ -269,9 +417,9 @@ public class Manifest { Section section = new Section(); if (nextSectionName == null) { Attribute sectionName = new Attribute(line); - if (!sectionName.getName().equalsIgnoreCase(ATTR_NAME)) { - throw new IOException("Manifest sections should start with a \"" + ATTR_NAME + - "\" attribute and not \"" + sectionName.getName() + "\""); + if (!sectionName.getName().equalsIgnoreCase(ATTRIBUTE_NAME)) { + throw new ManifestException("Manifest sections should start with a \"" + ATTRIBUTE_NAME + + "\" attribute and not \"" + sectionName.getName() + "\""); } nextSectionName = sectionName.getValue(); } @@ -291,8 +439,13 @@ public class Manifest { /** * Merge the contents of the given manifest into this manifest + * + * @param other the Manifest to be merged with this one. + * + * @throws ManifestException if there is a problem merging the manfest according + * to the Manifest spec. */ - public void merge(Manifest other) throws IOException { + public void merge(Manifest other) throws ManifestException { manifestVersion = other.manifestVersion; mainSection.merge(other.mainSection); for (Enumeration e = other.sections.keys(); e.hasMoreElements();) { @@ -306,18 +459,35 @@ public class Manifest { ourSection.merge(otherSection); } } + + // add in the warnings + for (Enumeration e = other.warnings.elements(); e.hasMoreElements();) { + warnings.addElement(e.nextElement()); + } } + /** + * Write the manifest out to a print writer. + * + * @param writer the Writer to which the manifest is written + * + * @throws IOException if the manifest cannot be written + */ public void write(PrintWriter writer) throws IOException { - writer.println(ATTR_MANIFEST_VERSION + ": " + manifestVersion); - String signatureVersion = mainSection.getAttributeValue(ATTR_SIGNATURE_VERSION); + writer.println(ATTRIBUTE_MANIFEST_VERSION + ": " + manifestVersion); + String signatureVersion = mainSection.getAttributeValue(ATTRIBUTE_SIGNATURE_VERSION); if (signatureVersion != null) { - writer.println(ATTR_SIGNATURE_VERSION + ": " + signatureVersion); - mainSection.removeAttribute(ATTR_SIGNATURE_VERSION); + writer.println(ATTRIBUTE_SIGNATURE_VERSION + ": " + signatureVersion); + mainSection.removeAttribute(ATTRIBUTE_SIGNATURE_VERSION); } mainSection.write(writer); if (signatureVersion != null) { - mainSection.addAttribute(new Attribute(ATTR_SIGNATURE_VERSION, signatureVersion)); + try { + mainSection.addAttribute(new Attribute(ATTRIBUTE_SIGNATURE_VERSION, signatureVersion)); + } + catch (ManifestException e) { + // shouldn't happen - ignore + } } for (Enumeration e = sections.elements(); e.hasMoreElements();) { @@ -326,6 +496,11 @@ public class Manifest { } } + /** + * Convert the manifest to its string representation + * + * @return a multiline string with the Manifest as it appears in a Manifest file. + */ public String toString() { StringWriter sw = new StringWriter(); try { @@ -336,4 +511,13 @@ public class Manifest { } return sw.toString(); } + + /** + * Get the warnings for this manifest. + * + * @return an enumeration of warning strings + */ + public Enumeration getWarnings() { + return warnings.elements(); + } } diff --git a/src/main/org/apache/tools/ant/taskdefs/ManifestException.java b/src/main/org/apache/tools/ant/taskdefs/ManifestException.java new file mode 100644 index 000000000..27b087f1c --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/ManifestException.java @@ -0,0 +1,73 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2001 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Ant", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ +package org.apache.tools.ant.taskdefs; + + +import java.io.*; + +/** + * Exception thrown indicating problems in a JAR Manifest + * + * @author Conor MacNeill + */ +public class ManifestException extends Exception { + + /** + * Constructs an exception with the given descriptive message. + * @param msg Description of or information about the exception. + */ + public ManifestException(String msg) { + super(msg); + } +} diff --git a/src/main/org/apache/tools/ant/taskdefs/Zip.java b/src/main/org/apache/tools/ant/taskdefs/Zip.java index 33ca4d394..3070d3380 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Zip.java +++ b/src/main/org/apache/tools/ant/taskdefs/Zip.java @@ -431,6 +431,38 @@ public class Zip extends MatchingTask { throws IOException, BuildException { } + + /** + * Create an empty zip file + * + * @return true if the file is then considered up to date. + */ + protected boolean createEmptyZip(File zipFile) { + // In this case using java.util.zip will not work + // because it does not permit a zero-entry archive. + // Must create it manually. + log("Note: creating empty "+archiveType+" archive " + zipFile, Project.MSG_INFO); + try { + OutputStream os = new FileOutputStream(zipFile); + try { + // Cf. PKZIP specification. + byte[] empty = new byte[22]; + empty[0] = 80; // P + empty[1] = 75; // K + empty[2] = 5; + empty[3] = 6; + // remainder zeros + os.write(empty); + } finally { + os.close(); + } + } catch (IOException ioe) { + throw new BuildException("Could not create empty ZIP archive", ioe, location); + } + return true; + } + + /** * Check whether the archive is up-to-date; and handle behavior for empty archives. * @param scanners list of prepared scanners containing files to archive @@ -453,29 +485,7 @@ public class Zip extends MatchingTask { ": no files were included.", location); } else { // Create. - if (zipFile.exists()) return true; - // In this case using java.util.zip will not work - // because it does not permit a zero-entry archive. - // Must create it manually. - log("Note: creating empty "+archiveType+" archive " + zipFile, Project.MSG_INFO); - try { - OutputStream os = new FileOutputStream(zipFile); - try { - // Cf. PKZIP specification. - byte[] empty = new byte[22]; - empty[0] = 80; // P - empty[1] = 75; // K - empty[2] = 5; - empty[3] = 6; - // remainder zeros - os.write(empty); - } finally { - os.close(); - } - } catch (IOException ioe) { - throw new BuildException("Could not create empty ZIP archive", ioe, location); - } - return true; + return createEmptyZip(zipFile); } } else { for (int i = 0; i < files.length; ++i) {