/*
 * 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.maven.plugins.war.packaging;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;

import org.apache.commons.io.input.XmlStreamReader;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.war.util.PathSet;
import org.apache.maven.plugins.war.util.WebappStructure;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.mapping.MappingUtils;
import org.apache.maven.shared.utils.StringUtils;
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.NoSuchArchiverException;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;

/**
 * @author Stephane Nicoll
 */
public abstract class AbstractWarPackagingTask implements WarPackagingTask {
    /**
     * The default list of includes.
     */
    public static final String[] DEFAULT_INCLUDES = {"**/**"};

    /**
     * The {@code WEB-INF} path.
     */
    public static final String WEB_INF_PATH = "WEB-INF";

    /**
     * The {@code META-INF} path.
     */
    public static final String META_INF_PATH = "META-INF";

    /**
     * The {@code classes} path.
     */
    public static final String CLASSES_PATH = "WEB-INF/classes/";

    /**
     * The {@code lib} path.
     */
    public static final String LIB_PATH = "WEB-INF/lib/";

    /**
     * Copies the files if possible with an optional target prefix.
     *
     * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method
     * makes sure to update the list of protected files which gives the list of files that have already been copied.
     *
     * If the structure of the source directory is not the same as the root of the webapp, use the {@code targetPrefix}
     * parameter to specify in which particular directory the files should be copied. Use {@code null} to copy the
     * files with the same structure
     *
     * @param sourceId the source id
     * @param context the context to use
     * @param sourceBaseDir the base directory from which the {@code sourceFilesSet} will be copied
     * @param sourceFilesSet the files to be copied
     * @param targetPrefix the prefix to add to the target file name
     * @param filtered filter or not.
     * @throws IOException if an error occurred while copying the files
     * @throws MojoExecutionException if an error occurs.
     */
    protected void copyFiles(
            String sourceId,
            WarPackagingContext context,
            File sourceBaseDir,
            PathSet sourceFilesSet,
            String targetPrefix,
            boolean filtered)
            throws IOException, MojoExecutionException {
        for (String fileToCopyName : sourceFilesSet.paths()) {
            final File sourceFile = new File(sourceBaseDir, fileToCopyName);

            String destinationFileName;
            if (targetPrefix == null) {
                destinationFileName = fileToCopyName;
            } else {
                destinationFileName = targetPrefix + fileToCopyName;
            }

            if (filtered && !context.isNonFilteredExtension(sourceFile.getName())) {
                copyFilteredFile(sourceId, context, sourceFile, destinationFileName);
            } else {
                copyFile(sourceId, context, sourceFile, destinationFileName);
            }
        }
    }

    /**
     * Copies the files if possible as is.
     *
     * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method
     * makes sure to update the list of protected files which gives the list of files that have already been copied.
     *
     * @param sourceId the source id
     * @param context the context to use
     * @param sourceBaseDir the base directory from which the {@code sourceFilesSet} will be copied
     * @param sourceFilesSet the files to be copied
     * @param filtered filter or not.
     * @throws IOException if an error occurred while copying the files
     * @throws MojoExecutionException break the build.
     */
    protected void copyFiles(
            String sourceId, WarPackagingContext context, File sourceBaseDir, PathSet sourceFilesSet, boolean filtered)
            throws IOException, MojoExecutionException {
        copyFiles(sourceId, context, sourceBaseDir, sourceFilesSet, null, filtered);
    }

    /**
     * Copy the specified file if the target location has not yet already been used.
     *
     * The {@code targetFileName} is the relative path according to the root of the generated web application.
     *
     * @param sourceId the source id
     * @param context the context to use
     * @param file the file to copy
     * @param targetFilename the relative path according to the root of the webapp
     * @throws IOException if an error occurred while copying
     */
    // CHECKSTYLE_OFF: LineLength
    protected void copyFile(String sourceId, final WarPackagingContext context, final File file, String targetFilename)
            throws IOException
                // CHECKSTYLE_ON: LineLength
            {
        final File targetFile = new File(context.getWebappDirectory(), targetFilename);

        if (file.isFile()) {
            context.getWebappStructure()
                    .registerFile(sourceId, targetFilename, new WebappStructure.RegistrationCallback() {
                        public void registered(String ownerId, String targetFilename) throws IOException {
                            copyFile(context, file, targetFile, targetFilename, false);
                        }

                        public void alreadyRegistered(String ownerId, String targetFilename) throws IOException {
                            copyFile(context, file, targetFile, targetFilename, true);
                        }

                        public void refused(String ownerId, String targetFilename, String actualOwnerId)
                                throws IOException {
                            context.getLog()
                                    .debug(" - "
                                            + targetFilename
                                            + " wasn't copied because it has "
                                            + "already been packaged for overlay ["
                                            + actualOwnerId + "].");
                        }

                        public void superseded(String ownerId, String targetFilename, String deprecatedOwnerId)
                                throws IOException {
                            context.getLog()
                                    .info("File ["
                                            + targetFilename
                                            + "] belonged to overlay ["
                                            + deprecatedOwnerId
                                            + "] so it will be overwritten.");
                            copyFile(context, file, targetFile, targetFilename, false);
                        }

                        public void supersededUnknownOwner(String ownerId, String targetFilename, String unknownOwnerId)
                                throws IOException {
                            // CHECKSTYLE_OFF: LineLength
                            context.getLog()
                                    .warn("File ["
                                            + targetFilename
                                            + "] belonged to overlay ["
                                            + unknownOwnerId
                                            + "] which does not exist anymore in the current project. It is recommended to invoke "
                                            + "clean if the dependencies of the project changed.");
                            // CHECKSTYLE_ON: LineLength
                            copyFile(context, file, targetFile, targetFilename, false);
                        }
                    });
        } else if (!targetFile.exists() && !targetFile.mkdirs()) {
            context.getLog().info("Failed to create directory " + targetFile.getAbsolutePath());
        }
    }

    /**
     * Copy the specified file if the target location has not yet already been used and filter its content with the
     * configured filter properties.
     *
     * The {@code targetFileName} is the relative path according to the root of the generated web application.
     *
     * @param sourceId the source id
     * @param context the context to use
     * @param file the file to copy
     * @param targetFilename the relative path according to the root of the webapp
     * @return true if the file has been copied, false otherwise
     * @throws IOException if an error occurred while copying
     * @throws MojoExecutionException if an error occurred while retrieving the filter properties
     */
    protected boolean copyFilteredFile(
            String sourceId, final WarPackagingContext context, File file, String targetFilename)
            throws IOException, MojoExecutionException {
        context.addResource(targetFilename);

        if (context.getWebappStructure().registerFile(sourceId, targetFilename)) {
            final File targetFile = new File(context.getWebappDirectory(), targetFilename);
            final String encoding;
            try {
                if (isXmlFile(file)) {
                    // For xml-files we extract the encoding from the files
                    encoding = getEncoding(file);
                } else if (isPropertiesFile(file) && StringUtils.isNotEmpty(context.getPropertiesEncoding())) {
                    encoding = context.getPropertiesEncoding();
                } else {
                    // For all others we use the configured encoding
                    encoding = context.getResourceEncoding();
                }
                // fix for MWAR-36, ensures that the parent dir are created first
                targetFile.getParentFile().mkdirs();

                context.getMavenFileFilter().copyFile(file, targetFile, true, context.getFilterWrappers(), encoding);
            } catch (MavenFilteringException e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }
            // CHECKSTYLE_OFF: LineLength
            // Add the file to the protected list
            context.getLog().debug(" + " + targetFilename + " has been copied (filtered encoding='" + encoding + "').");
            // CHECKSTYLE_ON: LineLength
            return true;
        } else {
            context.getLog()
                    .debug(" - " + targetFilename + " wasn't copied because it has already been packaged (filtered).");
            return false;
        }
    }

    /**
     * Unpacks the specified file to the specified directory.
     *
     * @param context the packaging context
     * @param file the file to unpack
     * @param unpackDirectory the directory to use for th unpacked file
     * @throws MojoExecutionException if an error occurred while unpacking the file
     */
    protected void doUnpack(WarPackagingContext context, File file, File unpackDirectory)
            throws MojoExecutionException {
        String archiveExt = FileUtils.getExtension(file.getAbsolutePath()).toLowerCase();

        try {
            UnArchiver unArchiver = context.getArchiverManager().getUnArchiver(archiveExt);
            unArchiver.setSourceFile(file);
            unArchiver.setDestDirectory(unpackDirectory);
            unArchiver.setOverwrite(true);
            unArchiver.extract();
        } catch (ArchiverException e) {
            throw new MojoExecutionException(
                    "Error unpacking file [" + file.getAbsolutePath() + "]" + " to ["
                            + unpackDirectory.getAbsolutePath() + "]",
                    e);
        } catch (NoSuchArchiverException e) {
            context.getLog()
                    .warn("Skip unpacking dependency file [" + file.getAbsolutePath() + " with unknown extension ["
                            + archiveExt + "]");
        }
    }

    /**
     * Copy file from source to destination. The directories up to <code>destination</code> will be created if they
     * don't already exist. if the <code>onlyIfModified</code> flag is {@code false}, <code>destination</code> will be
     * overwritten if it already exists. If the flag is {@code true} destination will be overwritten if it's not up to
     * date.
     *
     * @param context the packaging context
     * @param source an existing non-directory <code>File</code> to copy bytes from
     * @param destination a non-directory <code>File</code> to write bytes to (possibly overwriting).
     * @param targetFilename the relative path of the file from the webapp root directory
     * @param onlyIfModified if true, copy the file only if the source has changed, always copy otherwise
     * @return true if the file has been copied/updated, false otherwise
     * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
     *             IO error occurs during copying
     */
    protected boolean copyFile(
            WarPackagingContext context, File source, File destination, String targetFilename, boolean onlyIfModified)
            throws IOException {
        context.addResource(targetFilename);

        BasicFileAttributes readAttributes = Files.readAttributes(source.toPath(), BasicFileAttributes.class);
        if (onlyIfModified
                && destination.lastModified()
                        >= readAttributes.lastModifiedTime().toMillis()) {
            context.getLog().debug(" * " + targetFilename + " is up to date.");
            return false;
        } else {
            if (readAttributes.isDirectory()) {
                context.getLog().warn(" + " + targetFilename + " is packaged from the source folder");

                try {
                    JarArchiver archiver = context.getJarArchiver();
                    archiver.addDirectory(source);
                    archiver.setDestFile(destination);
                    archiver.createArchive();
                } catch (ArchiverException e) {
                    String msg = "Failed to create " + targetFilename;
                    context.getLog().error(msg, e);
                    throw new IOException(msg, e);
                }
            } else {
                FileUtils.copyFile(source.getCanonicalFile(), destination);
                // preserve timestamp
                destination.setLastModified(readAttributes.lastModifiedTime().toMillis());
                context.getLog().debug(" + " + targetFilename + " has been copied.");
            }
            return true;
        }
    }

    /**
     * Get the encoding from an XML-file.
     *
     * @param webXml the XML-file
     * @return The encoding of the XML-file, or UTF-8 if it's not specified in the file
     * @throws java.io.IOException if an error occurred while reading the file
     */
    protected String getEncoding(File webXml) throws IOException {
        try (XmlStreamReader xmlReader = new XmlStreamReader(webXml)) {
            return xmlReader.getEncoding();
        }
    }

    /**
     * Returns the file to copy. If the includes are {@code null} or empty, the default includes are used.
     *
     * @param baseDir the base directory to start from
     * @param includes the includes
     * @param excludes the excludes
     * @return the files to copy
     */
    protected PathSet getFilesToIncludes(File baseDir, String[] includes, String[] excludes) {
        return getFilesToIncludes(baseDir, includes, excludes, false);
    }

    /**
     * Returns the file to copy. If the includes are {@code null} or empty, the default includes are used.
     *
     * @param baseDir the base directory to start from
     * @param includes the includes
     * @param excludes the excludes
     * @param includeDirectories include directories yes or not.
     * @return the files to copy
     */
    // CHECKSTYLE_OFF: LineLength
    protected PathSet getFilesToIncludes(
            File baseDir, String[] includes, String[] excludes, boolean includeDirectories)
                // CHECKSTYLE_ON: LineLength
            {
        final DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir(baseDir);

        if (excludes != null) {
            scanner.setExcludes(excludes);
        }
        scanner.addDefaultExcludes();

        if (includes != null && includes.length > 0) {
            scanner.setIncludes(includes);
        } else {
            scanner.setIncludes(DEFAULT_INCLUDES);
        }

        scanner.scan();

        PathSet pathSet = new PathSet(scanner.getIncludedFiles());

        if (includeDirectories) {
            pathSet.addAll(scanner.getIncludedDirectories());
        }

        return pathSet;
    }

    /**
     * Returns the final name of the specified artifact.
     *
     * If the {@code outputFileNameMapping} is set, it is used, otherwise the standard naming scheme is used.
     *
     * @param context the packaging context
     * @param artifact the artifact
     * @return the converted filename of the artifact
     * @throws InterpolationException in case of interpolation problem.
     */
    protected String getArtifactFinalName(WarPackagingContext context, Artifact artifact)
            throws InterpolationException {
        if (context.getOutputFileNameMapping() != null) {
            return MappingUtils.evaluateFileNameMapping(context.getOutputFileNameMapping(), artifact);
        }

        String classifier = artifact.getClassifier();
        if ((classifier != null) && !("".equals(classifier.trim()))) {
            return MappingUtils.evaluateFileNameMapping(MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact);
        } else {
            return MappingUtils.evaluateFileNameMapping(MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact);
        }
    }

    /**
     * Determine whether a file is of a certain type, by looking at the file extension.
     *
     * @param file The file to check
     * @param extension The extension for a file type, including the '.'
     * @return <code>true</code> if the file is a file of the specified type, otherwise <code>false</code>
     * @since 3.4.0
     */
    private boolean isFileOfType(File file, String extension) {
        return file != null && file.isFile() && file.getName().endsWith(extension);
    }

    /**
     * Determine whether a file is a properties file or not.
     *
     * @param file The file to check
     * @return <code>true</code> if the file is a properties file, otherwise <code>false</code>
     * @since 3.4.0
     */
    private boolean isPropertiesFile(File file) {
        return isFileOfType(file, ".properties");
    }

    /**
     * Returns <code>true</code> if the <code>File</code>-object is a file (not a directory) that is not
     * <code>null</code> and has a file name that ends in ".xml".
     *
     * @param file The file to check
     * @return <code>true</code> if the file is an xml-file, otherwise <code>false</code>
     * @since 2.3
     */
    private boolean isXmlFile(File file) {
        return isFileOfType(file, ".xml");
    }
}
