/**
 *
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed 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.codehaus.plexus.archiver.jar;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.jar.Attributes;

import org.codehaus.plexus.archiver.ArchiverException;

/**
 * Holds the data of a jar manifest.
 * <p>
 * Manifests are processed according to the
 * <a href="http://java.sun.com/j2se/1.4/docs/guide/jar/jar.html">Jar
 * file specification.</a>
 * Specifically, a manifest element consists of
 * a set of attributes and sections. These sections in turn may contain
 * attributes. Note in particular that this may result in manifest lines
 * greater than 72 bytes (including line break) being wrapped and continued
 * on the next line. If an application can not handle the continuation
 * mechanism, it is a defect in the application, not this task.</p>
 *
 * @since Ant 1.4
 */
public class Manifest extends java.util.jar.Manifest implements Iterable<String> {

    /**
     * The Name Attribute is the first in a named section
     */
    private static final String ATTRIBUTE_NAME = ManifestConstants.ATTRIBUTE_NAME;

    /**
     * The From Header is disallowed in a Manifest
     */
    private static final String ATTRIBUTE_FROM = ManifestConstants.ATTRIBUTE_FROM;

    /**
     * Default Manifest version if one is not specified
     */
    private static final String DEFAULT_MANIFEST_VERSION = ManifestConstants.DEFAULT_MANIFEST_VERSION;

    /**
     * The max length of a line in a Manifest
     */
    private static final int MAX_LINE_LENGTH = 72;

    /**
     * Max length of a line section which is continued. Need to allow
     * for the CRLF.
     */
    private static final int MAX_SECTION_LENGTH = MAX_LINE_LENGTH - 2;

    /**
     * The End-Of-Line marker in manifests
     */
    static final String EOL = "\r\n";

    public static class BaseAttribute {

        /**
         * The attribute's name
         */
        protected String name = null;

        /**
         * Get the Attribute's name
         *
         * @return the attribute's name.
         */
        public String getName() {
            return name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof BaseAttribute)) {
                return false;
            }

            BaseAttribute that = (BaseAttribute) o;

            return !(name != null ? !name.equals(that.name) : that.name != null);
        }

        @Override
        public int hashCode() {
            return name != null ? name.hashCode() : 0;
        }
    }

    /**
     * An attribute for the manifest.
     * Those attributes that are not nested into a section will be added to the "Main" section.
     */
    public static class Attribute extends BaseAttribute implements Iterable<String> {

        /**
         * The attribute's value
         */
        private Vector<String> values = new Vector<String>();

        /**
         * For multivalued attributes, this is the index of the attribute
         * currently being defined.
         */
        private int currentIndex = 0;

        /**
         * Construct an empty attribute
         */
        public Attribute() {}

        /**
         * 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;
            setValue(value);
        }

        @Override
        public Iterator<String> iterator() {
            return values.iterator();
        }

        /**
         * @see java.lang.Object#hashCode
         */
        @Override
        public int hashCode() {
            int hashCode = super.hashCode();
            hashCode += values.hashCode();
            return hashCode;
        }

        /**
         * @see java.lang.Object#equals
         */
        @Override
        public boolean equals(Object rhs) {
            if (super.equals(rhs)) {
                return false;
            }
            if (rhs == null || rhs.getClass() != getClass()) {
                return false;
            }

            if (rhs == this) {
                return true;
            }

            Attribute rhsAttribute = (Attribute) rhs;
            String lhsKey = getKey();
            String rhsKey = rhsAttribute.getKey();
            //noinspection SimplifiableIfStatement,ConstantConditions
            if ((lhsKey == null && rhsKey != null) || (lhsKey != null && rhsKey == null) || !lhsKey.equals(rhsKey)) {
                return false;
            }

            return rhsAttribute.values != null && values.equals(rhsAttribute.values);
        }

        /**
         * Set the Attribute's name; required
         *
         * @param name the attribute's name
         */
        public void setName(String name) {
            this.name = name;
        }

        /**
         * Get the attribute's Key - its name in lower case.
         *
         * @return the attribute's key.
         */
        public String getKey() {
            return getKey(name);
        }

        /**
         * Get the key for the specified attribute name - its name in lower case.
         *
         * @return the attribute's key.
         */
        private static String getKey(String name) {
            if (name == null) {
                return null;
            }
            return name.toLowerCase(Locale.ENGLISH);
        }

        /**
         * Set the Attribute's value; required
         *
         * @param value the attribute's value
         */
        public void setValue(String value) {
            if (currentIndex >= values.size()) {
                values.addElement(value);
                currentIndex = values.size() - 1;
            } else {
                values.setElementAt(value, currentIndex);
            }
        }

        /**
         * Get the Attribute's value.
         *
         * @return the attribute's value.
         */
        public String getValue() {
            if (values.size() == 0) {
                return null;
            }

            String fullValue = "";
            for (String value : values) {
                fullValue += value + " ";
            }
            return fullValue.trim();
        }

        /**
         * Add a new value to this attribute - making it multivalued.
         *
         * @param value the attribute's additional value
         */
        public void addValue(String value) {
            currentIndex++;
            setValue(value);
        }

        /**
         * Writes the attribute out to a writer.
         *
         * @param writer the Writer to which the attribute is written
         *
         * @throws IOException if the attribute value cannot be written
         */
        void write(Writer writer) throws IOException {
            for (String value : values) {
                writeValue(writer, value);
            }
        }

        /**
         * Write a single attribute value out. Should handle multiple lines of attribute value.
         *
         * @param writer the Writer to which the attribute is written
         * @param value the attribute value
         *
         * @throws IOException if the attribute value cannot be written
         */
        private void writeValue(Writer writer, String value) throws IOException {
            String nameValue = name + ": " + value;

            StringTokenizer tokenizer = new StringTokenizer(nameValue, "\n\r");

            String prefix = "";

            while (tokenizer.hasMoreTokens()) {
                writeLine(writer, prefix + tokenizer.nextToken());
                prefix = " ";
            }
        }

        /**
         * Write a single Manifest line. Should handle more than 72 bytes of line
         *
         * @param writer the Writer to which the attribute is written
         * @param line the manifest line to be written
         *
         * @throws java.io.IOException when Io excepts
         */
        private void writeLine(Writer writer, String line) throws IOException {
            // Make sure we have at most 70 bytes in UTF-8 as specified excluding line break
            while (line.getBytes("UTF-8").length > MAX_SECTION_LENGTH) {
                // Try to find a MAX_SECTION_LENGTH
                // Use the minimum because we operate on at most chars and not bytes here otherwise
                // if we have more bytes than chars we will die in an IndexOutOfBoundsException.
                int breakIndex = Math.min(line.length(), MAX_SECTION_LENGTH);
                String section = line.substring(0, breakIndex);
                while (section.getBytes("UTF-8").length > MAX_SECTION_LENGTH && breakIndex > 0) {
                    breakIndex--;
                    section = line.substring(0, breakIndex);
                }
                if (breakIndex == 0) {
                    throw new IOException("Unable to write manifest line " + line);
                }
                writer.write(section + EOL);
                line = " " + line.substring(breakIndex);
            }
            writer.write(line + EOL);
        }
    }

    public class ExistingAttribute extends Attribute implements Iterable<String> {

        private final Attributes attributes;

        public ExistingAttribute(Attributes attributes, String name) {
            this.attributes = attributes;
            this.name = name;
        }

        @Override
        public Iterator<String> iterator() {
            return getKeys(attributes).iterator();
        }

        @Override
        public void setName(String name) {
            throw new UnsupportedOperationException("Cant do this");
        }

        @Override
        public String getKey() {
            return name;
        }

        @Override
        public void setValue(String value) {
            attributes.putValue(name, value);
        }

        @Override
        public String getValue() {
            return attributes.getValue(name);
        }

        @Override
        public void addValue(String value) {
            String value1 = getValue();
            value1 = (value1 != null) ? " " + value : value;
            setValue(value1);
        }

        @Override
        void write(Writer writer) throws IOException {
            throw new UnsupportedOperationException("Cant do this");
        }
    }

    private static Collection<String> getKeys(Attributes attributes) {
        Collection<String> result = new ArrayList<String>();
        for (Object objectObjectEntry : attributes.keySet()) {
            result.add(objectObjectEntry.toString());
        }
        return result;
    }

    /**
     * A manifest section - you can nest attribute elements into sections.
     * A section consists of a set of attribute values,
     * separated from other sections by a blank line.
     */
    public static class Section implements Iterable<String> {

        /**
         * Warnings for this section
         */
        private Vector<String> warnings = new Vector<String>();

        /**
         * The section's name if any. The main section in a
         * manifest is unnamed.
         */
        private String name = null;

        /**
         * The section's attributes.
         */
        private Hashtable<String, Attribute> attributes = new Hashtable<String, Attribute>();

        /**
         * Index used to retain the attribute ordering
         */
        private Vector<String> attributeIndex = new Vector<String>();

        /**
         * The name of the section; optional -default is the main section.
         *
         * @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;
        }

        @Override
        public Iterator<String> iterator() {
            return attributes.keySet().iterator();
        }

        /**
         * Get a attribute of the section
         *
         * @param attributeName the name of the attribute
         *
         * @return a Manifest.Attribute instance if the attribute is
         * single-valued, otherwise a Vector of Manifest.Attribute
         * instances.
         */
        public Attribute getAttribute(String attributeName) {
            return attributes.get(attributeName.toLowerCase(Locale.ENGLISH));
        }

        /**
         * Add an attribute to the section.
         *
         * @param attribute the attribute to be added to the section
         *
         * @throws ManifestException if the attribute is not valid.
         */
        public void addConfiguredAttribute(Attribute attribute) throws ManifestException {
            String check = addAttributeAndCheck(attribute);
            if (check != null) {
                throw new ManifestException(
                        "Specify the section name using " + "the \"name\" attribute of the <section> element rather "
                                + "than using a \"Name\" manifest attribute");
            }
        }

        /**
         * 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 addAttributeAndCheck(Attribute attribute) throws ManifestException {
            if (attribute.getName() == null || attribute.getValue() == null) {
                throw new ManifestException("Attributes must have name and value");
            }
            if (attribute.getKey().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.getKey().startsWith(Attribute.getKey(ATTRIBUTE_FROM))) {
                warnings.addElement("Manifest attributes should not start " + "with \"" + ATTRIBUTE_FROM + "\" in \""
                        + attribute.getName() + ": " + attribute.getValue() + "\"");
            } else {
                // classpath attributes go into a vector
                String attributeKey = attribute.getKey();
                if (attributeKey.equalsIgnoreCase(ManifestConstants.ATTRIBUTE_CLASSPATH)) {
                    Attribute classpathAttribute = attributes.get(attributeKey);

                    if (classpathAttribute == null) {
                        storeAttribute(attribute);
                    } else {
                        warnings.addElement("Multiple Class-Path attributes " + "are supported but violate the Jar "
                                + "specification and may not be correctly "
                                + "processed in all environments");

                        for (String value : attribute) {
                            classpathAttribute.addValue(value);
                        }
                    }
                } else if (attributes.containsKey(attributeKey)) {
                    throw new ManifestException("The attribute \"" + attribute.getName() + "\" may not occur more "
                            + "than once in the same section");
                } else {
                    storeAttribute(attribute);
                }
            }
            return null;
        }

        /**
         * Store an attribute and update the index.
         *
         * @param attribute the attribute to be stored
         */
        protected void storeAttribute(Attribute attribute) {
            if (attribute == null) {
                return;
            }

            String attributeKey = attribute.getKey();
            attributes.put(attributeKey, attribute);
            if (!attributeIndex.contains(attributeKey)) {
                attributeIndex.addElement(attributeKey);
            }
        }

        /**
         * Get the warnings for this section.
         *
         * @return an Enumeration of warning strings.
         */
        public Enumeration<String> getWarnings() {
            return warnings.elements();
        }

        /**
         * @see java.lang.Object#hashCode
         */
        @Override
        public int hashCode() {
            int hashCode = 0;

            if (name != null) {
                hashCode += name.hashCode();
            }

            hashCode += attributes.hashCode();
            return hashCode;
        }

        /**
         * @see java.lang.Object#equals
         */
        @Override
        public boolean equals(Object rhs) {
            if (rhs == null || rhs.getClass() != getClass()) {
                return false;
            }

            if (rhs == this) {
                return true;
            }

            Section rhsSection = (Section) rhs;

            return rhsSection.attributes != null && attributes.equals(rhsSection.attributes);
        }
    }

    public class ExistingSection implements Iterable<String> {

        private final Attributes backingAttributes;

        private final String sectionName;

        public ExistingSection(Attributes backingAttributes, String sectionName) {
            this.backingAttributes = backingAttributes;
            this.sectionName = sectionName;
        }

        @Override
        public Iterator<String> iterator() {
            return getKeys(backingAttributes).iterator();
        }

        public ExistingAttribute getAttribute(String attributeName) {
            Attributes.Name name = new Attributes.Name(attributeName);
            return backingAttributes.containsKey(name) ? new ExistingAttribute(backingAttributes, attributeName) : null;
        }

        public String getName() {
            return sectionName;
        }

        public String getAttributeValue(String attributeName) {
            return backingAttributes.getValue(attributeName);
        }

        public void removeAttribute(String attributeName) {
            backingAttributes.remove(new Attributes.Name(attributeName));
        }

        public void addConfiguredAttribute(Attribute attribute) throws ManifestException {
            backingAttributes.putValue(attribute.getName(), attribute.getValue());
        }

        public String addAttributeAndCheck(Attribute attribute) throws ManifestException {
            return remap(backingAttributes, attribute);
        }

        @Override
        public int hashCode() {
            return backingAttributes.hashCode();
        }

        @Override
        public boolean equals(Object rhs) {
            return rhs instanceof ExistingSection
                    && backingAttributes.equals(((ExistingSection) rhs).backingAttributes);
        }
    }

    @Override
    public Iterator<String> iterator() {
        return getEntries().keySet().iterator();
    }

    /**
     * The main section of this manifest
     */
    private Section mainSection = new Section();

    /**
     * Construct a manifest from Ant's default manifest file.
     *
     * @param minimalDefaultManifest
     *            indicates whether a minimal manifest will be created, thus having only
     *            {@code Manifest-Version: 1.0} in it.
     *
     * @return the default manifest.
     *
     * @throws ArchiverException
     *             if there is a problem loading the default manifest
     */
    public static Manifest getDefaultManifest(boolean minimalDefaultManifest) throws ArchiverException {
        final Manifest defaultManifest = new Manifest();
        defaultManifest.getMainAttributes().putValue("Manifest-Version", "1.0");

        if (!minimalDefaultManifest) {
            String createdBy = "Plexus Archiver";

            final String plexusArchiverVersion = JdkManifestFactory.getArchiverVersion();

            if (plexusArchiverVersion != null) {
                createdBy += " " + plexusArchiverVersion;
            }

            defaultManifest.getMainAttributes().putValue("Created-By", createdBy);
        }

        return defaultManifest;
    }

    /**
     * @see #getDefaultManifest(boolean)
     */
    public static Manifest getDefaultManifest() throws ArchiverException {
        return getDefaultManifest(false);
    }

    /**
     * Construct an empty manifest
     */
    public Manifest() {
        setManifestVersion();
    }

    private void setManifestVersion() {
        getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
    }

    /**
     * Read a manifest file from the given reader
     *
     * @param r is the reader 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.
     * @deprecated This constructor does not properly map characters to bytes. Use
     * {@link #Manifest(InputStream)}. Will be removed in 4.0.
     */
    @Deprecated
    public Manifest(Reader r) throws ManifestException, IOException {
        super(getInputStream(r));
        setManifestVersion();
    }

    public Manifest(InputStream is) throws IOException {
        super(is);
        setManifestVersion();
    }

    /**
     * Add a section to the manifest
     *
     * @param section the manifest section to be added
     *
     * @throws ManifestException if the secti0on is not valid.
     */
    public void addConfiguredSection(Section section) throws ManifestException {
        String sectionName = section.getName();
        if (sectionName == null) {
            throw new ManifestException("Sections must have a name");
        }
        Attributes attributes = getOrCreateAttributes(sectionName);
        for (String s : section.attributes.keySet()) {

            Attribute attribute = section.getAttribute(s);
            attributes.putValue(attribute.getName(), attribute.getValue());
        }
    }

    private Attributes getOrCreateAttributes(String name) {
        Attributes attributes = getAttributes(name);
        if (attributes == null) {
            attributes = new Attributes();
            getEntries().put(name, attributes);
        }
        return attributes;
    }

    /**
     * Add an attribute to the manifest - it is added to the main section.
     *
     * @param attribute the attribute to be added.
     *
     * @throws ManifestException if the attribute is not valid.
     */
    public void addConfiguredAttribute(Attribute attribute) throws ManifestException {
        remap(getMainAttributes(), attribute);
    }

    /**
     * Writes the manifest out to a writer.
     *
     * @param writer the Writer to which the manifest is written
     *
     * @throws IOException if the manifest cannot be written
     */
    public void write(Writer writer) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        super.write(byteArrayOutputStream);
        // We know that UTF-8 is the encoding of the JAR file specification
        writer.write(byteArrayOutputStream.toString("UTF-8"));
    }

    /**
     * Convert the manifest to its string representation
     *
     * @return a multiline string with the Manifest as it
     * appears in a Manifest file.
     */
    @Override
    public String toString() {
        StringWriter sw = new StringWriter();
        try {
            write(sw);
        } catch (IOException e) {
            return null;
        }
        return sw.toString();
    }

    /**
     * Get the warnings for this manifest.
     *
     * @return an enumeration of warning strings
     */
    Enumeration<String> getWarnings() {
        Vector<String> warnings = new Vector<String>();

        Enumeration<String> warnEnum = mainSection.getWarnings();
        while (warnEnum.hasMoreElements()) {
            warnings.addElement(warnEnum.nextElement());
        }

        return warnings.elements();
    }

    /**
     * Get the version of the manifest
     *
     * @return the manifest's version string
     */
    public String getManifestVersion() {
        /*
        The version of this manifest
        */
        return DEFAULT_MANIFEST_VERSION;
    }

    /**
     * Get the main section of the manifest
     *
     * @return the main section of the manifest
     */
    public ExistingSection getMainSection() {
        return new ExistingSection(getMainAttributes(), null);
    }

    /**
     * Get a particular section from the manifest
     *
     * @param name the name of the section desired.
     *
     * @return the specified section or null if that section
     * does not exist in the manifest
     */
    public ExistingSection getSection(String name) {
        Attributes attributes = getAttributes(name);
        if (attributes != null) {
            return new ExistingSection(attributes, name);
        }
        return null;
    }

    @Deprecated
    private static InputStream getInputStream(Reader r) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int read;
        while ((read = r.read()) != -1) {
            byteArrayOutputStream.write(read);
        }
        return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    }

    public static String remap(Attributes backingAttributes, Attribute attribute) throws ManifestException {
        if (attribute.getKey() == null || attribute.getValue() == null) {
            throw new ManifestException("Attributes must have name and value");
        }

        String attributeKey = attribute.getKey();
        if (attributeKey.equalsIgnoreCase(ManifestConstants.ATTRIBUTE_CLASSPATH)) {
            String classpathAttribute = backingAttributes.getValue(attributeKey);

            if (classpathAttribute == null) {
                classpathAttribute = attribute.getValue();
            } else {
                classpathAttribute += " " + attribute.getValue();
            }
            backingAttributes.putValue(ManifestConstants.ATTRIBUTE_CLASSPATH, classpathAttribute);
        } else {
            backingAttributes.putValue(attribute.getName(), attribute.getValue());
            if (attribute.getKey().equalsIgnoreCase(ATTRIBUTE_NAME)) {
                return attribute.getValue();
            }
        }
        return null;
    }
}
