package org.apache.maven.plugin.ear;

/*
 * 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.
 */

import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenResourcesExecution;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.apache.maven.execution.MavenSession;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * Builds J2EE Enteprise Archive (EAR) files.
 *
 * @author <a href="snicoll@apache.org">Stephane Nicoll</a>
 * @version $Id: EarMojo.java 746740 2009-02-22 15:44:32Z snicoll $
 * @goal ear
 * @phase package
 * @requiresDependencyResolution test
 */
public class EarMojo
    extends AbstractEarMojo
{
    private static final String[] EMPTY_STRING_ARRAY = {};


    /**
     * Single directory for extra files to include in the EAR.
     *
     * @parameter expression="${basedir}/src/main/application"
     * @required
     */
    private File earSourceDirectory;

    /**
     * The comma separated list of tokens to include in the EAR.
     *
     * @parameter alias="includes" default-value="**"
     */
    private String earSourceIncludes;

    /**
     * The comma separated list of tokens to exclude from the EAR.
     *
     * @parameter alias="excludes"
     */
    private String earSourceExcludes;

    /**
     * Specify that the ear sources should be filtered.
     *
     * @parameter default-value="false"
     * @since 2.3.2     
     */
    private boolean filtering;

    /**
     * Filters (property files) to include during the interpolation of the pom.xml.
     *
     * @parameter
     * @since 2.3.2
     */
    private List filters;

    /**
     * A list of file extensions that should not be filtered if
     * filtering is actived.
     *
     * @parameter
     * @since 2.3.2
     */
    private List nonFilteredFileExtensions;

    /**
     * To escape interpolated value with windows path
     * c:\foo\bar will be replaced with c:\\foo\\bar
     *
     * @parameter expression="${maven.ear.escapedBackslashesInFilePath}" default-value="false"
     * @since 2.3.2
     */
    private boolean escapedBackslashesInFilePath;

    /**
     * Expression preceded with the String won't be interpolated
     * \${foo} will be replaced with ${foo}
     *
     * @parameter expression="${maven.ear.escapeString}"
     * @since 2.3.2
     */
    protected String escapeString;

    /**
     * The location of the manifest file to be used within the ear file.
     *
     * @parameter expression="${basedir}/src/main/application/META-INF/MANIFEST.MF"
     */
    private File manifestFile;

    /**
     * The location of a custom application.xml file to be used
     * within the ear file.
     *
     * @parameter
     */
    private String applicationXml;

    /**
     * The directory for the generated EAR.
     *
     * @parameter expression="${project.build.directory}"
     * @required
     */
    private String outputDirectory;

    /**
     * The name of the EAR file to generate.
     *
     * @parameter alias="earName" expression="${project.build.finalName}"
     * @required
     */
    private String finalName;

    /**
     * The comma separated list of artifact's type(s) to unpack
     * by default.
     *
     * @parameter
     */
    private String unpackTypes;

    /**
     * Classifier to add to the artifact generated. If given, the artifact will
     * be an attachment instead.
     *
     * @parameter
     */
    private String classifier;

    /**
     * The directory to get the resources from.
     *
     * @parameter
     * @deprecated please use earSourcesDirectory instead
     */
    private File resourcesDir;

    /**
     * The Jar archiver.
     *
     * @component role="org.codehaus.plexus.archiver.Archiver" role-hint="jar"
     */
    private JarArchiver jarArchiver;

    /**
     * The archive configuration to use.
     * See <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven Archiver Reference</a>.
     *
     * @parameter
     */
    private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();

    /**
     * @component
     */
    private MavenProjectHelper projectHelper;

    /**
     * The archive manager.
     *
     * @component
     */
    private ArchiverManager archiverManager;

    /**
     *
     * @component role="org.apache.maven.shared.filtering.MavenFileFilter" role-hint="default"
     * @required
     */
    private MavenFileFilter mavenFileFilter;

    /**
     *
     * @component role="org.apache.maven.shared.filtering.MavenResourcesFiltering" role-hint="default"
     * @required
     */
    private MavenResourcesFiltering mavenResourcesFiltering;

    /**
     * @parameter expression="${session}"
     * @readonly
     * @required
     * @since 2.3.2
     */
    private MavenSession session;


    private List filterWrappers;


    public void execute()
        throws MojoExecutionException, MojoFailureException
    {
        // Initializes ear modules
        super.execute();

        // Initializes unpack types
        List unpackTypesList = new ArrayList();
        if ( unpackTypes != null )
        {
            unpackTypesList = Arrays.asList( unpackTypes.split( "," ) );
            final Iterator it = unpackTypesList.iterator();
            while ( it.hasNext() )
            {
                String type = (String) it.next();
                if ( !EarModuleFactory.standardArtifactTypes.contains( type ) )
                {
                    throw new MojoExecutionException(
                        "Invalid type[" + type + "] supported types are " + EarModuleFactory.standardArtifactTypes );
                }
            }
            getLog().debug( "Initialized unpack types " + unpackTypesList );
        }

        // Copy modules
        try
        {
            for ( Iterator iter = getModules().iterator(); iter.hasNext(); )
            {
                EarModule module = (EarModule) iter.next();
                if ( module instanceof JavaModule )
                {
                    getLog().warn( "JavaModule is deprecated (" + module + "), please use JarModule instead." );
                }
                if ( module instanceof Ejb3Module )
                {
                    getLog().warn( "Ejb3Module is deprecated (" + module + "), please use EjbModule instead." );
                }
                final File sourceFile = module.getArtifact().getFile();
                final File destinationFile = buildDestinationFile( getWorkDirectory(), module.getUri() );
                if ( !sourceFile.isFile() )
                {
                    throw new MojoExecutionException( "Cannot copy a directory: " + sourceFile.getAbsolutePath() +
                        "; Did you package/install " + module.getArtifact() + "?" );
                }

                if ( destinationFile.getCanonicalPath().equals( sourceFile.getCanonicalPath() ) )
                {
                    getLog().info(
                        "Skipping artifact[" + module + "], as it already exists at[" + module.getUri() + "]" );
                    continue;
                }

                // If the module is within the unpack list, make sure that no unpack wasn't forced (null or true)
                // If the module is not in the unpack list, it should be true
                if ( ( unpackTypesList.contains( module.getType() ) &&
                    ( module.shouldUnpack() == null || module.shouldUnpack().booleanValue() ) ) ||
                    ( module.shouldUnpack() != null && module.shouldUnpack().booleanValue() ) )
                {
                    getLog().info( "Copying artifact[" + module + "] to[" + module.getUri() + "] (unpacked)" );
                    // Make sure that the destination is a directory to avoid plexus nasty stuff :)
                    destinationFile.mkdirs();
                    unpack( sourceFile, destinationFile );
                }
                else
                {
                    if ( sourceFile.lastModified() > destinationFile.lastModified() )
                    {
                        getLog().info( "Copying artifact[" + module + "] to[" + module.getUri() + "]" );
                        FileUtils.copyFile( sourceFile, destinationFile );
                    }
                    else
                    {
                        getLog().debug( "Skipping artifact[" + module + "], as it is already up to date at[" +
                            module.getUri() + "]" );
                    }
                }
            }
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Error copying EAR modules", e );
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "Error unpacking EAR modules", e );
        }
        catch ( NoSuchArchiverException e )
        {
            throw new MojoExecutionException( "No Archiver found for EAR modules", e );
        }

        // Copy source files
        try
        {
            File earSourceDir = earSourceDirectory;
            if ( earSourceDir.exists() )
            {
                getLog().info( "Copy ear sources to " + getWorkDirectory().getAbsolutePath() );
                String[] fileNames = getEarFiles( earSourceDir );
                for ( int i = 0; i < fileNames.length; i++ )
                {
                    copyFile( new File( earSourceDir, fileNames[i] ),
                              new File( getWorkDirectory(), fileNames[i] ) );
                }
            }

            if ( applicationXml != null && !"".equals( applicationXml ) )
            {
                //rename to application.xml
                getLog().info( "Including custom application.xml[" + applicationXml + "]" );
                File metaInfDir = new File( getWorkDirectory(), META_INF );
                copyFile( new File( applicationXml ), new File( metaInfDir, "/application.xml" ) );
            }

        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Error copying EAR sources", e );
        }
        catch ( MavenFilteringException e )
        {
            throw new MojoExecutionException( "Error filetering EAR sources", e );
        }

        // Copy resources files
        try
        {
            if ( resourcesDir != null && resourcesDir.exists() )
            {
                getLog().warn( "resourcesDir is deprecated. Please use the earSourceDirectory property instead.");
                getLog().info( "Copy ear resources to " + getWorkDirectory().getAbsolutePath() );
                String[] fileNames = getEarFiles( resourcesDir );
                for ( int i = 0; i < fileNames.length; i++ )
                {
                    FileUtils.copyFile( new File( resourcesDir, fileNames[i] ),
                                        new File( getWorkDirectory(), fileNames[i] ) );
                }
            }
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Error copying EAR resources", e );
        }

        // Check if deployment descriptor is there
        File ddFile = new File( getWorkDirectory(), APPLICATION_XML_URI );
        if ( !ddFile.exists() && !version.equals(VERSION_5 ) )
        {
            throw new MojoExecutionException(
                "Deployment descriptor: " + ddFile.getAbsolutePath() + " does not exist." );
        }

        try
        {
            File earFile = getEarFile( outputDirectory, finalName, classifier );
            MavenArchiver archiver = new MavenArchiver();
            final JarArchiver jarArchiver = getJarArchiver();
            getLog().debug( "Jar archiver implementation[" + jarArchiver.getClass().getName() + "]" );
            archiver.setArchiver( jarArchiver );
            archiver.setOutputFile( earFile );

            // Include custom manifest if necessary
            includeCustomManifestFile();

            archiver.getArchiver().addDirectory( getWorkDirectory() );
            archiver.createArchive( getProject(), archive );

            if ( classifier != null )
            {
                projectHelper.attachArtifact( getProject(), "ear", classifier, earFile );
            }
            else
            {
                getProject().getArtifact().setFile( earFile );
            }
        }
        catch ( Exception e )
        {
            throw new MojoExecutionException( "Error assembling EAR", e );
        }
    }

    public String getApplicationXml()
    {
        return applicationXml;
    }

    public void setApplicationXml( String applicationXml )
    {
        this.applicationXml = applicationXml;
    }

    /**
     * Returns a string array of the excludes to be used
     * when assembling/copying the ear.
     *
     * @return an array of tokens to exclude
     */
    protected String[] getExcludes()
    {
        List excludeList = new ArrayList( FileUtils.getDefaultExcludesAsList() );
        if ( earSourceExcludes != null && !"".equals( earSourceExcludes ) )
        {
            excludeList.addAll( Arrays.asList( StringUtils.split( earSourceExcludes, "," ) ) );
        }

        // if applicationXml is specified, omit the one in the source directory
        if ( getApplicationXml() != null && !"".equals( getApplicationXml() ) )
        {
            excludeList.add( "**/" + META_INF + "/application.xml" );
        }

        return (String[]) excludeList.toArray( EMPTY_STRING_ARRAY );
    }

    /**
     * Returns a string array of the includes to be used
     * when assembling/copying the ear.
     *
     * @return an array of tokens to include
     */
    protected String[] getIncludes()
    {
        return StringUtils.split( StringUtils.defaultString( earSourceIncludes ), "," );
    }

    private static File buildDestinationFile( File buildDir, String uri )
    {
        return new File( buildDir, uri );
    }

    private void includeCustomManifestFile()
    {
        File customManifestFile = manifestFile;

        if ( !customManifestFile.exists() )
        {
            getLog().info( "Could not find manifest file: " + manifestFile + " - Generating one" );
        }
        else
        {
            getLog().info( "Including custom manifest file[" + customManifestFile + "]" );
            archive.setManifestFile( customManifestFile );
        }
    }

    /**
     * Returns the EAR file to generate, based on an optional classifier.
     *
     * @param basedir    the output directory
     * @param finalName  the name of the ear file
     * @param classifier an optional classifier
     * @return the EAR file to generate
     */
    private static File getEarFile( String basedir, String finalName, String classifier )
    {
        if ( classifier == null )
        {
            classifier = "";
        }
        else if ( classifier.trim().length() > 0 && !classifier.startsWith( "-" ) )
        {
            classifier = "-" + classifier;
        }

        return new File( basedir, finalName + classifier + ".ear" );
    }

    /**
     * Returns a list of filenames that should be copied
     * over to the destination directory.
     *
     * @param sourceDir the directory to be scanned
     * @return the array of filenames, relative to the sourceDir
     */
    private String[] getEarFiles( File sourceDir )
    {
        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir( sourceDir );
        scanner.setExcludes( getExcludes() );
        scanner.addDefaultExcludes();

        scanner.setIncludes( getIncludes() );

        scanner.scan();

        return scanner.getIncludedFiles();
    }

    /**
     * Unpacks the module into the EAR structure.
     *
     * @param source  File to be unpacked.
     * @param destDir Location where to put the unpacked files.
     */
    public void unpack( File source, File destDir )
        throws NoSuchArchiverException, IOException, ArchiverException
    {
        UnArchiver unArchiver = archiverManager.getUnArchiver( "zip" );
        unArchiver.setSourceFile( source );
        unArchiver.setDestDirectory( destDir );

        // Extract the module
        unArchiver.extract();
    }

    /**
     * Returns the {@link JarArchiver} implementation used
     * to package the EAR file.
     * <p/>
     * By default the archiver is obtained from the Plexus container.
     *
     * @return the archiver
     */
    protected JarArchiver getJarArchiver()
    {
        return jarArchiver;
    }

    private void copyFile(File source, File target)
            throws MavenFilteringException, IOException, MojoExecutionException {
        if ( filtering && !isNonFilteredExtension( source.getName() ) )
        {
           mavenFileFilter.copyFile( source, target, true, getFilterWrappers(), null );
        }
        else
        {
             FileUtils.copyFile(source,target);
        }
    }

    public boolean isNonFilteredExtension( String fileName )
    {
        return !mavenResourcesFiltering.filteredFileExtension( fileName, nonFilteredFileExtensions );
    }

    private List getFilterWrappers() throws MojoExecutionException {
        if (filterWrappers == null)
        {
            try
            {
                MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution();
                mavenResourcesExecution.setEscapeString( escapeString );
                filterWrappers = mavenFileFilter.getDefaultFilterWrappers( project, filters,
                        escapedBackslashesInFilePath, this.session, mavenResourcesExecution );
            }
            catch ( MavenFilteringException e )
            {
                getLog().error( "fail to build filering wrappers " + e.getMessage() );
                throw new MojoExecutionException( e.getMessage(), e );
            }
        }
        return filterWrappers;
    }
}
