/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.tools.ant.taskdefs.optional.ejb;

// Standard java imports
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.xml.sax.SAXException;

/**
 * Provides automated EJB JAR file creation.
 * <p>
 * Extends the
 * MatchingTask class provided in the default ant distribution to provide a
 * directory scanning EJB jarfile generator.
 * </p>
 *
 * <p>
 * The task works by taking the deployment descriptors one at a time and
 * parsing them to locate the names of the classes which should be placed in
 * the jar. The classnames are translated to java.io.Files by replacing
 * periods with File.separatorChar and resolving the generated filename as a
 * relative path under the srcDir attribute. All necessary files are then
 * assembled into a jarfile. One jarfile is constructed for each deployment
 * descriptor found.
 * </p>
 *
 * */
public class EjbJar extends MatchingTask {

    /**
     * Inner class used to record information about the location of a local DTD
     */
    public static class DTDLocation
        extends org.apache.tools.ant.types.DTDLocation {
    }

    /**
     * A class which contains the configuration state of the ejbjar task.
     * This state is passed to the deployment tools for configuration
     */
    static class Config {
        // CheckStyle:VisibilityModifier OFF - bc
        /**
         * Stores a handle to the directory under which to search for class
         * files
         */
        public File srcDir;

        /**
         * Stores a handle to the directory under which to search for
         * deployment descriptors
         */
        public File descriptorDir;

        /** Instance variable that marks the end of the 'basename' */
        public String baseNameTerminator = "-";

        /** Stores a handle to the destination EJB Jar file */
        public String baseJarName;

        /**
         * Instance variable that determines whether to use a package structure
         * of a flat directory as the destination for the jar files.
         */
        public boolean flatDestDir = false;

        /**
         * The classpath to use when loading classes
         */
        public Path classpath;

        /**
         * A Fileset of support classes
         */
        public List<FileSet> supportFileSets = new ArrayList<>();

        /**
         * The list of configured DTD locations
         */
        public ArrayList<DTDLocation> dtdLocations = new ArrayList<>();

        /**
         * The naming scheme used to determine the generated jar name
         * from the descriptor information
         */
        public NamingScheme namingScheme;

        /**
         * The Manifest file
         */
        public File manifest;

        /**
         * The dependency analyzer to use to add additional classes to the jar
         */
        public String analyzer;
        // CheckStyle:VisibilityModifier ON
    }

    /**
     * An EnumeratedAttribute class for handling different EJB jar naming
     * schemes
     */
    public static class NamingScheme extends EnumeratedAttribute {
        /**
         * Naming scheme where generated jar is determined from the ejb-name in
         * the deployment descriptor
         */
        public static final String EJB_NAME = "ejb-name";

        /**
         * Naming scheme where the generated jar name is based on the
         * name of the directory containing the deployment descriptor
         */
        public static final String DIRECTORY = "directory";

        /**
         * Naming scheme where the generated jar name is based on the name of
         * the deployment descriptor file
         */
        public static final String DESCRIPTOR = "descriptor";

        /**
         * Naming scheme where the generated jar is named by the basejarname
         * attribute
         */
        public static final String BASEJARNAME = "basejarname";

        /**
         * Gets the values of the NamingScheme
         *
         * @return an array of the values of this attribute class.
         */
        @Override
        public String[] getValues() {
            return new String[] {EJB_NAME, DIRECTORY, DESCRIPTOR, BASEJARNAME};
        }
    }

    /**
     * CMP versions supported
     * valid CMP versions are 1.0 and 2.0
     * @since ant 1.6
     */
    public static class CMPVersion extends EnumeratedAttribute {
        /** 1.0 value */
        public static final String CMP1_0 = "1.0";
        /** 2.0 value */
        public static final String CMP2_0 = "2.0";
        /** {@inheritDoc}. */
        @Override
        public String[] getValues() {
            return new String[] {
                CMP1_0,
                CMP2_0,
            };
        }
    }

    /**
     * The config which is built by this task and used by the various deployment
     * tools to access the configuration of the ejbjar task
     */
    private Config config = new Config();

    /**
     * Stores a handle to the directory to put the Jar files in. This is
     * only used by the generic deployment descriptor tool which is created
     * if no other deployment descriptor tools are provided. Normally each
     * deployment tool will specify the destination dir itself.
     */
    private File destDir;

    /** Instance variable that stores the suffix for the generated jarfile. */
    private String genericJarSuffix = "-generic.jar";

    /** Instance variable that stores the CMP version for the JBoss jarfile. */
    private String cmpVersion = CMPVersion.CMP1_0;

    /** The list of deployment tools we are going to run. */
    private List<EJBDeploymentTool> deploymentTools = new ArrayList<>();

    /**
     * Add a deployment tool to the list of deployment tools that will be
     * processed
     *
     * @param deploymentTool a deployment tool instance to which descriptors
     *        will be passed for processing.
     */
    protected void addDeploymentTool(EJBDeploymentTool deploymentTool) {
        deploymentTool.setTask(this);
        deploymentTools.add(deploymentTool);
    }

    /**
     * Create a orion nested element used to configure a
     * deployment tool for Orion server.
     *
     * @return the deployment tool instance to be configured.
     * @since Ant 1.10.2
     */
    public OrionDeploymentTool createOrion() {
        OrionDeploymentTool tool = new OrionDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for WebLogic server.
     *
     * @return the deployment tool instance to be configured.
     */
    public WeblogicDeploymentTool createWeblogic() {
        WeblogicDeploymentTool tool = new WeblogicDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for WebSphere 4.0 server.
     *
     * @return the deployment tool instance to be configured.
     */
    public WebsphereDeploymentTool createWebsphere() {
        WebsphereDeploymentTool tool = new WebsphereDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for Borland server.
     *
     * @return the deployment tool instance to be configured.
     */
    public BorlandDeploymentTool createBorland() {
        log("Borland deployment tools",  Project.MSG_VERBOSE);

        BorlandDeploymentTool tool = new BorlandDeploymentTool();
        tool.setTask(this);
        deploymentTools.add(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for iPlanet Application Server.
     *
     * @return the deployment tool instance to be configured.
     */
    public IPlanetDeploymentTool createIplanet() {
        log("iPlanet Application Server deployment tools", Project.MSG_VERBOSE);

        IPlanetDeploymentTool tool = new IPlanetDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for JBoss server.
     *
     * @return the deployment tool instance to be configured.
     */
    public JbossDeploymentTool createJboss() {
        JbossDeploymentTool tool = new JbossDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for JOnAS server.
     *
     * @return the deployment tool instance to be configured.
     */
    public JonasDeploymentTool createJonas() {
        log("JOnAS deployment tools",  Project.MSG_VERBOSE);

        JonasDeploymentTool tool = new JonasDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds a deployment tool for WebLogic when using the TOPLink
     * Object-Relational mapping.
     *
     * @return the deployment tool instance to be configured.
     */
    public WeblogicTOPLinkDeploymentTool createWeblogictoplink() {
        log("The <weblogictoplink> element is no longer required. Please use "
            + "the <weblogic> element and set newCMP=\"true\"",
            Project.MSG_INFO);
        WeblogicTOPLinkDeploymentTool tool
            = new WeblogicTOPLinkDeploymentTool();
        addDeploymentTool(tool);
        return tool;
    }

    /**
     * Adds to the classpath used to locate the super classes and
     * interfaces of the classes that will make up the EJB JAR.
     *
     * @return the path to be configured.
     */
    public Path createClasspath() {
        if (config.classpath == null) {
            config.classpath = new Path(getProject());
        }
        return config.classpath.createPath();
    }

    /**
     * Create a DTD location record. This stores the location of a DTD. The
     * DTD is identified by its public Id. The location may either be a file
     * location or a resource location.
     *
     * @return the DTD location object to be configured by Ant
     */
    public DTDLocation createDTD() {
        DTDLocation dtdLocation = new DTDLocation();
        config.dtdLocations.add(dtdLocation);

        return dtdLocation;
    }

    /**
     * Adds a fileset for support elements.
     *
     * @return a fileset which can be populated with support files.
     */
    public FileSet createSupport() {
        FileSet supportFileSet = new FileSet();
        config.supportFileSets.add(supportFileSet);
        return supportFileSet;
    }

    /**
     * Set the Manifest file to use when jarring. As of EJB 1.1, manifest
     * files are no longer used to configure the EJB. However, they still
     * have a vital importance if the EJB is intended to be packaged in an
     * EAR file. By adding "Class-Path" settings to a Manifest file, the EJB
     * can look for classes inside the EAR file itself, allowing for easier
     * deployment. This is outlined in the J2EE specification, and all J2EE
     * components are meant to support it.
     *
     * @param manifest the manifest to be used in the EJB jar
     */
     public void setManifest(File manifest) {
         config.manifest = manifest;
     }

    /**
     * Sets the source directory, which is the directory that
     * contains the classes that will be added to the EJB jar. Typically
     * this will include the home and remote interfaces and the bean class.
     *
     * @param inDir the source directory.
     */
    public void setSrcdir(File inDir) {
        config.srcDir = inDir;
    }

    /**
     * Set the descriptor directory. The descriptor directory contains the
     * EJB deployment descriptors. These are XML files that declare the
     * properties of a bean in a particular deployment scenario. Such
     * properties include, for example, the transactional nature of the bean
     * and the security access control to the bean's methods.
     *
     * @param inDir the directory containing the deployment descriptors.
     */
    public void setDescriptordir(File inDir) {
        config.descriptorDir = inDir;
    }

    /**
     * Set the analyzer to use when adding in dependencies to the JAR.
     *
     * @param analyzer the name of the dependency analyzer or a class.
     */
    public void setDependency(String analyzer) {
        config.analyzer = analyzer;
    }

    /**
     * Set the base name of the EJB JAR that is to be created if it is not
     * to be determined from the name of the deployment descriptor files.
     *
     * @param inValue the basename that will be used when writing the jar
     *      file containing the EJB
     */
    public void setBasejarname(String inValue) {
        config.baseJarName = inValue;
        if (config.namingScheme == null) {
            config.namingScheme = new NamingScheme();
            config.namingScheme.setValue(NamingScheme.BASEJARNAME);
        } else if (!NamingScheme.BASEJARNAME.equals(config.namingScheme.getValue())) {
            throw new BuildException(
                "The basejarname attribute is not compatible with the %s naming scheme",
                config.namingScheme.getValue());
        }
    }

    /**
     * Set the naming scheme used to determine the name of the generated jars
     * from the deployment descriptor
     *
     * @param namingScheme the naming scheme to be used
     */
    public void setNaming(NamingScheme namingScheme) {
        config.namingScheme = namingScheme;
        if (!NamingScheme.BASEJARNAME.equals(config.namingScheme.getValue())
            && config.baseJarName != null) {
            throw new BuildException(
                "The basejarname attribute is not compatible with the %s naming scheme",
                config.namingScheme.getValue());
        }
    }

    /**
     * Gets the destination directory.
     *
     * @return destination directory
     * @since ant 1.6
     */
    public File getDestdir() {
        return this.destDir;
    }

    /**
     * Set the destination directory. The EJB jar files will be written into
     * this directory. The jar files that exist in this directory are also
     * used when determining if the contents of the jar file have changed.
     * Note that this parameter is only used if no deployment tools are
     * specified. Typically each deployment tool will specify its own
     * destination directory.
     *
     * @param inDir the destination directory in which to generate jars
     */
    public void setDestdir(File inDir) {
        this.destDir = inDir;
    }

    /**
     * Gets the CMP version.
     *
     * @return CMP version
     * @since ant 1.6
     */
    public String getCmpversion() {
        return this.cmpVersion;
    }

    /**
     * Sets the CMP version.
     * Must be either <code>1.0</code> or <code>2.0</code>.
     * Default is <code>1.0</code>.
     * Initially, only the JBoss implementation does something specific for CMP 2.0.
     *
     * @param version CMP version.
     * @since ant 1.6
     */
    public void setCmpversion(CMPVersion version) {
        this.cmpVersion = version.getValue();
    }

    /**
     * Set the classpath to use when resolving classes for inclusion in the jar.
     *
     * @param classpath the classpath to use.
     */
    public void setClasspath(Path classpath) {
        config.classpath = classpath;
    }

    /**
     * Controls whether the
     * destination JARs are written out in the destination directory with
     * the same hierarchical structure from which the deployment descriptors
     * have been read. If this is set to true the generated EJB jars are
     * written into the root of the destination directory, otherwise they
     * are written out in the same relative position as the deployment
     * descriptors in the descriptor directory.
     *
     * @param inValue the new value of the flatdestdir flag.
     */
    public void setFlatdestdir(boolean inValue) {
        config.flatDestDir = inValue;
    }

    /**
     * Set the suffix for the generated jar file. When generic jars are
     * generated, they have a suffix which is appended to the the bean name
     * to create the name of the jar file. Note that this suffix includes
     * the extension fo te jar file and should therefore end with an
     * appropriate extension such as .jar or .ear
     *
     * @param inString the string to use as the suffix.
     */
    public void setGenericjarsuffix(String inString) {
        this.genericJarSuffix = inString;
    }

    /**
     * The string which terminates the bean name.
     * The convention used by this task is
     * that bean descriptors are named as the BeanName with some suffix. The
     * baseNameTerminator string separates the bean name and the suffix and
     * is used to determine the bean name.
     *
     * @param inValue a string which marks the end of the basename.
     */
    public void setBasenameterminator(String inValue) {
        config.baseNameTerminator = inValue;
    }

    /**
     * Validate the config that has been configured from the build file
     *
     * @throws BuildException if the config is not valid
     */
    private void validateConfig() throws BuildException {
        if (config.srcDir == null) {
            throw new BuildException("The srcDir attribute must be specified");
        }

        if (config.descriptorDir == null) {
            config.descriptorDir = config.srcDir;
        }

        if (config.namingScheme == null) {
            config.namingScheme = new NamingScheme();
            config.namingScheme.setValue(NamingScheme.DESCRIPTOR);
        } else if (NamingScheme.BASEJARNAME.equals(config.namingScheme.getValue())
                    && config.baseJarName == null) {
            throw new BuildException(
                "The basejarname attribute must be specified with the basejarname naming scheme");
        }
    }

    /**
     * Invoked by Ant after the task is prepared, when it is ready to execute
     * this task.
     *
     * This will configure all of the nested deployment tools to allow them to
     * process the jar. If no deployment tools have been configured a generic
     * tool is created to handle the jar.
     *
     * A parser is configured and then each descriptor found is passed to all
     * the deployment tool elements for processing.
     *
     * @exception BuildException thrown whenever a problem is
     *            encountered that cannot be recovered from, to signal to ant
     *            that a major problem occurred within this task.
     */
    @Override
    public void execute() throws BuildException {
        validateConfig();

        if (deploymentTools.isEmpty()) {
            GenericDeploymentTool genericTool = new GenericDeploymentTool();
            genericTool.setTask(this);
            genericTool.setDestdir(destDir);
            genericTool.setGenericJarSuffix(genericJarSuffix);
            deploymentTools.add(genericTool);
        }

        for (EJBDeploymentTool tool : deploymentTools) {
            tool.configure(config);
            tool.validateConfigured();
        }

        try {
            // Create the parser using whatever parser the system dictates
            SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
            saxParserFactory.setValidating(true);
            SAXParser saxParser = saxParserFactory.newSAXParser();

            DirectoryScanner ds = getDirectoryScanner(config.descriptorDir);
            ds.scan();
            String[] files = ds.getIncludedFiles();

            log(files.length + " deployment descriptors located.",
                Project.MSG_VERBOSE);

            // Loop through the files. Each file represents one deployment
            // descriptor, and hence one bean in our model.
            for (String file : files) {
                // process the deployment descriptor in each tool
                for (EJBDeploymentTool tool : deploymentTools) {
                    tool.processDescriptor(file, saxParser);
                }
            }
        } catch (SAXException se) {
            throw new BuildException("SAXException while creating parser.", se);
        } catch (ParserConfigurationException pce) {
            throw new BuildException(
                "ParserConfigurationException while creating parser. ", pce);
        }
    } // end of execute()

}
