All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.plugins.ear.EarMojo Maven / Gradle / Ivy

The newest version!
/*
 * 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.ear;

import javax.inject.Inject;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ProviderMismatchException;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.plugins.ear.util.EarMavenArchiver;
import org.apache.maven.plugins.ear.util.JavaEEVersion;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.filtering.FilterWrapper;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.filtering.MavenResourcesExecution;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.apache.maven.shared.mapping.MappingUtils;
import org.apache.maven.shared.utils.io.FileUtils;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.ear.EarArchiver;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.jar.Manifest;
import org.codehaus.plexus.archiver.jar.Manifest.Attribute;
import org.codehaus.plexus.archiver.jar.ManifestException;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.components.io.filemappers.FileMapper;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.StringUtils;

/**
 * Builds J2EE Enterprise Archive (EAR) files.
 *
 * @author Stephane Nicoll
 */
@Mojo(
        name = "ear",
        defaultPhase = LifecyclePhase.PACKAGE,
        threadSafe = true,
        requiresDependencyResolution = ResolutionScope.TEST)
public class EarMojo extends AbstractEarMojo {
    /**
     * Default file name mapping used by artifacts located in local repository.
     */
    private static final String ARTIFACT_DEFAULT_FILE_NAME_MAPPING =
            "@{artifactId}@-@{version}@@{dashClassifier?}@.@{extension}@";

    /**
     * Single directory for extra files to include in the EAR.
     */
    @Parameter(defaultValue = "${basedir}/src/main/application", required = true)
    private File earSourceDirectory;

    /**
     * The comma separated list of tokens to include in the EAR.
     */
    @Parameter(alias = "includes", defaultValue = "**")
    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.
     *
     * @since 2.3.2
     */
    @Parameter(defaultValue = "false")
    private boolean filtering;

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

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

    /**
     * To escape interpolated value with Windows path c:\foo\bar will be replaced with c:\\foo\\bar.
     *
     * @since 2.3.2
     */
    @Parameter(defaultValue = "false")
    private boolean escapedBackslashesInFilePath;

    /**
     * Expression preceded with this String won't be interpolated \${foo} will be replaced with ${foo}.
     *
     * @since 2.3.2
     */
    @Parameter
    protected String escapeString;

    /**
     * In case of using the {@link #skinnyWars} and {@link #defaultLibBundleDir} usually the classpath will be modified.
     * By settings this option {@code true} you can change this and keep the classpath untouched. This option has been
     * introduced to keep the backward compatibility with earlier versions of the plugin.
     *
     * @since 2.10
     */
    @Parameter(defaultValue = "false")
    private boolean skipClassPathModification;

    /**
     * The location of a custom application.xml file to be used within the EAR file.
     */
    @Parameter
    private File applicationXml;

    /**
     * The directory for the generated EAR.
     */
    @Parameter(defaultValue = "${project.build.directory}", required = true)
    private String outputDirectory;

    /**
     * The name of the EAR file to generate.
     */
    @Parameter(defaultValue = "${project.build.finalName}", required = true, readonly = true)
    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;

    /**
     * A comma separated list of tokens to exclude when packaging the EAR. By default nothing is excluded. Note that you
     * can use the Java Regular Expressions engine to include and exclude specific pattern using the expression
     * %regex[]. Hint: read the about (?!Pattern).
     *
     * @since 2.7
     */
    @Parameter
    private String packagingExcludes;

    /**
     * A comma separated list of tokens to include when packaging the EAR. By default everything is included. Note that
     * you can use the Java Regular Expressions engine to include and exclude specific pattern using the expression
     * %regex[].
     *
     * @since 2.7
     */
    @Parameter
    private String packagingIncludes;

    /**
     * Whether to create skinny WARs or not. A skinny WAR is a WAR that does not have all of its dependencies in
     * WEB-INF/lib. Instead those dependencies are shared between the WARs through the EAR.
     *
     * @since 2.7
     */
    @Parameter(defaultValue = "false")
    private boolean skinnyWars;

    /**
     * Whether to create skinny EAR modules or not. A skinny EAR module is a WAR, SAR, HAR, RAR or WSR module that
     * does not contain all of its dependencies in it. Instead those dependencies are shared between the WARs, SARs,
     * HARs, RARs and WSRs through the EAR. This option takes precedence over {@link #skinnyWars} option. That is if
     * skinnyModules is {@code true} but {@link #skinnyWars} is {@code false} (explicitly or by default) then all
     * modules including WARs are skinny.
     *
     * @since 3.2.0
     */
    @Parameter(defaultValue = "false")
    private boolean skinnyModules;

    /**
     * The Plexus EAR archiver to create the output archive.
     */
    private EarArchiver earArchiver;

    /**
     * The Plexus JAR archiver to create the output archive if no EAR application descriptor is provided (JavaEE 5+).
     */
    private JarArchiver jarArchiver;

    /**
     * The archive configuration to use. See Maven Archiver
     * Reference.
     */
    @Parameter
    private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();

    /**
     * Timestamp for reproducible output archive entries, either formatted as ISO 8601
     * yyyy-MM-dd'T'HH:mm:ssXXX or as an int representing seconds since the epoch (like
     * SOURCE_DATE_EPOCH).
     *
     * @since 3.1.0
     */
    @Parameter(defaultValue = "${project.build.outputTimestamp}")
    private String outputTimestamp;

    private MavenProjectHelper projectHelper;

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

    private MavenFileFilter mavenFileFilter;

    private MavenResourcesFiltering mavenResourcesFiltering;

    /**
     * @since 2.3.2
     */
    @Parameter(defaultValue = "${session}", readonly = true, required = true)
    private MavenSession session;

    private List filterWrappers;

    @Inject
    public EarMojo(
            EarArchiver earArchiver,
            JarArchiver jarArchiver,
            MavenProjectHelper projectHelper,
            ArchiverManager archiverManager,
            MavenFileFilter mavenFileFilter,
            MavenResourcesFiltering mavenResourcesFiltering) {
        this.earArchiver = earArchiver;
        this.jarArchiver = jarArchiver;
        this.projectHelper = projectHelper;
        this.archiverManager = archiverManager;
        this.mavenFileFilter = mavenFileFilter;
        this.mavenResourcesFiltering = mavenResourcesFiltering;
    }

    /** {@inheritDoc} */
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        // Initializes ear modules
        super.execute();

        File earFile = getEarFile(outputDirectory, finalName, classifier);
        MavenArchiver archiver = new EarMavenArchiver(getModules());
        File ddFile = new File(getWorkDirectory(), APPLICATION_XML_URI);

        JarArchiver theArchiver;
        if (ddFile.exists()) {
            earArchiver.setAppxml(ddFile);
            theArchiver = earArchiver;
        } else {
            // current Plexus EarArchiver does not support application.xml-less JavaEE 5+ case
            // => fallback to Plexus Jar archiver
            theArchiver = jarArchiver;
        }
        getLog().debug("Ear archiver implementation [" + theArchiver.getClass().getName() + "]");
        archiver.setArchiver(theArchiver);
        archiver.setOutputFile(earFile);
        archiver.setCreatedBy("Maven EAR Plugin", "org.apache.maven.plugins", "maven-ear-plugin");

        // configure for Reproducible Builds based on outputTimestamp value
        archiver.configureReproducibleBuild(outputTimestamp);

        final JavaEEVersion javaEEVersion = JavaEEVersion.getJavaEEVersion(version);

        final Collection outdatedResources = initOutdatedResources();

        // Initializes unpack types
        List unpackTypesList = createUnpackList();

        // Copy modules
        copyModules(javaEEVersion, unpackTypesList, outdatedResources);

        // Copy source files
        try {
            File earSourceDir = earSourceDirectory;

            if (earSourceDir.exists()) {
                getLog().info("Copy ear sources to " + getWorkDirectory().getAbsolutePath());
                String[] fileNames = getEarFiles(earSourceDir);
                for (String fileName : fileNames) {
                    copyFile(new File(earSourceDir, fileName), new File(getWorkDirectory(), fileName));
                    outdatedResources.remove(Paths.get(fileName).toString());
                }
            }

            if (applicationXml != null) {
                // rename to application.xml
                getLog().info("Including custom application.xml[" + applicationXml + "]");
                File metaInfDir = new File(getWorkDirectory(), META_INF);
                copyFile(applicationXml, new File(metaInfDir, "/application.xml"));
                outdatedResources.remove(Paths.get("META-INF/application.xml").toString());
            }
        } catch (IOException e) {
            throw new MojoExecutionException("Error copying EAR sources", e);
        } catch (MavenFilteringException e) {
            throw new MojoExecutionException("Error filtering EAR sources", e);
        }

        // Check if deployment descriptor is there
        if (!ddFile.exists() && (javaEEVersion.lt(JavaEEVersion.FIVE))) {
            throw new MojoExecutionException("Deployment descriptor: " + ddFile.getAbsolutePath() + " does not exist.");
        }
        // no need to check timestamp for descriptors: removing if outdated does not really make sense
        outdatedResources.remove(Paths.get(APPLICATION_XML_URI).toString());
        if (getJbossConfiguration() != null) {
            outdatedResources.remove(Paths.get("META-INF/jboss-app.xml").toString());
        }

        deleteOutdatedResources(outdatedResources);

        try {
            getLog().debug("Excluding " + Arrays.asList(getPackagingExcludes()) + " from the generated EAR.");
            getLog().debug("Including " + Arrays.asList(getPackagingIncludes()) + " in the generated EAR.");

            archiver.getArchiver().addDirectory(getWorkDirectory(), getPackagingIncludes(), getPackagingExcludes());

            archiver.createArchive(session, getProject(), archive);
        } catch (ManifestException | IOException | DependencyResolutionRequiredException e) {
            throw new MojoExecutionException("Error assembling EAR", e);
        }

        if (classifier != null) {
            projectHelper.attachArtifact(getProject(), "ear", classifier, earFile);
        } else {
            getProject().getArtifact().setFile(earFile);
        }
    }

    private void copyModules(
            final JavaEEVersion javaEEVersion, List unpackTypesList, Collection outdatedResources)
            throws MojoExecutionException, MojoFailureException {
        try {
            for (EarModule module : getModules()) {
                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()
                            + "]");
                    // FIXME: Shouldn't that result in a build failure!?
                    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()))
                        || (module.shouldUnpack() != null && module.shouldUnpack())) {
                    getLog().info("Copying artifact [" + module + "] to [" + module.getUri() + "] (unpacked)");
                    // Make sure that the destination is a directory to avoid plexus nasty stuff :)
                    if (!destinationFile.isDirectory() && !destinationFile.mkdirs()) {
                        throw new MojoExecutionException("Error creating " + destinationFile);
                    }
                    unpack(sourceFile, destinationFile, outdatedResources);

                    if (module.changeManifestClasspath()) {
                        changeManifestClasspath(module, destinationFile, javaEEVersion, outdatedResources);
                    }
                } else {
                    if (sourceFile.lastModified() > destinationFile.lastModified()) {
                        getLog().info("Copying artifact [" + module + "] to [" + module.getUri() + "]");
                        createParentIfNecessary(destinationFile);
                        Files.copy(
                                sourceFile.toPath(),
                                destinationFile.toPath(),
                                LinkOption.NOFOLLOW_LINKS,
                                StandardCopyOption.REPLACE_EXISTING);
                        if (module.changeManifestClasspath()) {
                            changeManifestClasspath(module, destinationFile, javaEEVersion, outdatedResources);
                        }
                    } else {
                        getLog().debug("Skipping artifact [" + module + "], as it is already up to date at ["
                                + module.getUri() + "]");
                    }
                    removeFromOutdatedResources(destinationFile.toPath(), outdatedResources);
                }
            }
        } 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);
        }
    }

    private List createUnpackList() throws MojoExecutionException {
        List unpackTypesList = new ArrayList<>();
        if (unpackTypes != null) {
            unpackTypesList = Arrays.asList(unpackTypes.split(","));
            for (String type : unpackTypesList) {
                if (!EarModuleFactory.isStandardArtifactType(type)) {
                    throw new MojoExecutionException("Invalid type [" + type + "] supported types are "
                            + EarModuleFactory.getStandardArtifactTypes());
                }
            }
            getLog().debug("Initialized unpack types " + unpackTypesList);
        }
        return unpackTypesList;
    }

    /**
     * @return {@link #applicationXml}
     */
    public File getApplicationXml() {
        return applicationXml;
    }

    /**
     * @param applicationXml {@link #applicationXml}
     */
    public void setApplicationXml(File 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 excludeList.toArray(new String[excludeList.size()]);
    }

    /**
     * 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(Objects.toString(earSourceIncludes, ""), ",");
    }

    /**
     * @return The array with the packaging excludes.
     */
    public String[] getPackagingExcludes() {
        if (packagingExcludes == null || packagingExcludes.isEmpty()) {
            return new String[0];
        } else {
            return StringUtils.split(packagingExcludes, ",");
        }
    }

    /**
     * @param packagingExcludes {@link #packagingExcludes}
     */
    public void setPackagingExcludes(String packagingExcludes) {
        this.packagingExcludes = packagingExcludes;
    }

    /**
     * @return the arrays with the includes
     */
    public String[] getPackagingIncludes() {
        if (packagingIncludes == null || packagingIncludes.isEmpty()) {
            return new String[] {"**"};
        } else {
            return StringUtils.split(packagingIncludes, ",");
        }
    }

    /**
     * @param packagingIncludes {@link #packagingIncludes}
     */
    public void setPackagingIncludes(String packagingIncludes) {
        this.packagingIncludes = packagingIncludes;
    }

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

    /**
     * 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 where to put the unpacked files
     * @param outdatedResources currently outdated resources
     * @throws ArchiverException a corrupt archive
     * @throws NoSuchArchiverException if we don't have an appropriate archiver
     * @throws IOException in case of a general IOException
     */
    public void unpack(File source, final File destDir, final Collection outdatedResources)
            throws ArchiverException, NoSuchArchiverException, IOException {
        Path destPath = destDir.toPath();

        UnArchiver unArchiver = archiverManager.getUnArchiver("zip");
        unArchiver.setSourceFile(source);
        unArchiver.setDestDirectory(destDir);
        unArchiver.setFileMappers(new FileMapper[] {
            pName -> {
                removeFromOutdatedResources(destPath.resolve(pName), outdatedResources);
                return pName;
            }
        });

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

    private void copyFile(File source, File target)
            throws MavenFilteringException, IOException, MojoExecutionException {
        createParentIfNecessary(target);
        if (filtering && !isNonFilteredExtension(source.getName())) {
            mavenFileFilter.copyFile(source, target, true, getFilterWrappers(), encoding);
        } else {
            Files.copy(
                    source.toPath(), target.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
        }
    }

    private void createParentIfNecessary(File target) throws IOException {
        // Silly that we have to do this ourselves
        File parentDirectory = target.getParentFile();
        if (parentDirectory != null && !parentDirectory.exists()) {
            Files.createDirectories(parentDirectory.toPath());
        }
    }

    /**
     * @param fileName the name of the file which should be checked
     * @return {@code true} if the name is part of the non filtered extensions; {@code false} otherwise
     */
    public boolean isNonFilteredExtension(String fileName) {
        return !mavenResourcesFiltering.filteredFileExtension(fileName, nonFilteredFileExtensions);
    }

    private List getFilterWrappers() throws MojoExecutionException {
        if (filterWrappers == null) {
            try {
                MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution();
                mavenResourcesExecution.setMavenProject(getProject());
                mavenResourcesExecution.setEscapedBackslashesInFilePath(escapedBackslashesInFilePath);
                mavenResourcesExecution.setFilters(filters);
                mavenResourcesExecution.setEscapeString(escapeString);

                filterWrappers = mavenFileFilter.getDefaultFilterWrappers(mavenResourcesExecution);
            } catch (MavenFilteringException e) {
                getLog().error("Fail to build filtering wrappers " + e.getMessage());
                throw new MojoExecutionException(e.getMessage(), e);
            }
        }
        return filterWrappers;
    }

    private void changeManifestClasspath(
            EarModule module, File original, JavaEEVersion javaEEVersion, Collection outdatedResources)
            throws MojoFailureException {
        final String moduleLibDir = module.getLibDir();
        if (!((moduleLibDir == null) || skinnyModules || (skinnyWars && module instanceof WebModule))) {
            return;
        }

        // for new created items
        FileTime outputFileTime = MavenArchiver.parseBuildOutputTimestamp(outputTimestamp)
                .map(FileTime::from)
                .orElse(null);

        FileSystem fileSystem = null;

        try {
            Path workDirectory;

            // Handle the case that the destination might be a directory (project-038)
            // We can get FileSystems only for files
            if (original.isFile()) {
                fileSystem = FileSystems.newFileSystem(
                        original.toPath(), Thread.currentThread().getContextClassLoader());
                workDirectory = fileSystem.getRootDirectories().iterator().next();
            } else {
                workDirectory = original.toPath();
            }

            // Create a META-INF/MANIFEST.MF file if it doesn't exist (project-038)
            Path metaInfDirectory = workDirectory.resolve("META-INF");
            if (!Files.exists(metaInfDirectory)) {
                Files.createDirectory(metaInfDirectory);
                if (outputFileTime != null) {
                    Files.setLastModifiedTime(metaInfDirectory, outputFileTime);
                }
                getLog().debug(
                                "This project did not have a META-INF directory before, so a new directory was created.");
            }
            Path manifestFile = metaInfDirectory.resolve("MANIFEST.MF");
            if (!Files.exists(manifestFile)) {
                Files.createFile(manifestFile);
                if (outputFileTime != null) {
                    Files.setLastModifiedTime(manifestFile, outputFileTime);
                }
                getLog().debug(
                                "This project did not have a META-INF/MANIFEST.MF file before, so a new file was created.");
            }

            Manifest mf = readManifest(manifestFile);
            Attribute classPath = mf.getMainSection().getAttribute("Class-Path");
            List classPathElements = new ArrayList<>();

            boolean classPathExists;
            if (classPath != null) {
                classPathExists = true;
                classPathElements.addAll(Arrays.asList(classPath.getValue().split(" ")));
            } else {
                classPathExists = false;
                classPath = new Attribute("Class-Path", "");
            }

            if ((moduleLibDir != null) && (skinnyModules || (skinnyWars && module instanceof WebModule))) {
                // Remove modules
                for (EarModule otherModule : getAllEarModules()) {
                    if (module.equals(otherModule)) {
                        continue;
                    }
                    // MEAR-189:
                    // We use the original name, cause in case of outputFileNameMapping
                    // we could not not delete it and it will end up in the resulting EAR and the WAR
                    // will not be cleaned up.
                    final Path workLibDir = workDirectory.resolve(moduleLibDir);
                    Path artifact =
                            workLibDir.resolve(module.getArtifact().getFile().getName());

                    // MEAR-217
                    // If WAR contains files with timestamps, but EAR strips them away (useBaseVersion=true)
                    // the artifact is not found. Therefore, respect the current fileNameMapping additionally.

                    if (!Files.exists(artifact)) {
                        getLog().debug("module does not exist with original file name.");
                        artifact = workLibDir.resolve(otherModule.getBundleFileName());
                        getLog().debug("Artifact with mapping: " + artifact.toAbsolutePath());
                    }

                    if (!Files.exists(artifact)) {
                        getLog().debug("Artifact with mapping does not exist.");
                        artifact = workLibDir.resolve(
                                otherModule.getArtifact().getFile().getName());
                        getLog().debug("Artifact with original file name: " + artifact.toAbsolutePath());
                    }

                    if (!Files.exists(artifact)) {
                        getLog().debug("Artifact with original file name does not exist.");
                        final Artifact otherModuleArtifact = otherModule.getArtifact();
                        if (otherModuleArtifact.isSnapshot()) {
                            try {
                                artifact = workLibDir.resolve(MappingUtils.evaluateFileNameMapping(
                                        ARTIFACT_DEFAULT_FILE_NAME_MAPPING, otherModuleArtifact));
                                getLog().debug("Artifact with default mapping file name: " + artifact.toAbsolutePath());
                            } catch (InterpolationException e) {
                                getLog().warn("Failed to evaluate file name for [" + otherModule
                                        + "] module using mapping: " + ARTIFACT_DEFAULT_FILE_NAME_MAPPING);
                            }
                        }
                    }

                    if (Files.exists(artifact)) {
                        getLog().debug(" -> Artifact to delete: " + artifact);
                        Files.delete(artifact);
                    }
                }
            }

            // Modify the classpath entries in the manifest
            final boolean forceClassPathModification =
                    javaEEVersion.lt(JavaEEVersion.FIVE) || defaultLibBundleDir == null;
            final boolean classPathExtension = !skipClassPathModification || forceClassPathModification;
            for (EarModule otherModule : getModules()) {
                if (module.equals(otherModule)) {
                    continue;
                }
                final int moduleClassPathIndex = findModuleInClassPathElements(classPathElements, otherModule);
                if (moduleClassPathIndex != -1) {
                    if (otherModule.isClassPathItem()) {
                        classPathElements.set(moduleClassPathIndex, otherModule.getUri());
                    } else {
                        classPathElements.remove(moduleClassPathIndex);
                    }
                } else if (otherModule.isClassPathItem() && classPathExtension) {
                    classPathElements.add(otherModule.getUri());
                }
            }

            // Remove provided modules from classpath
            for (EarModule otherModule : getProvidedEarModules()) {
                final int moduleClassPathIndex = findModuleInClassPathElements(classPathElements, otherModule);
                if (moduleClassPathIndex != -1) {
                    classPathElements.remove(moduleClassPathIndex);
                }
            }

            if (!skipClassPathModification || !classPathElements.isEmpty() || classPathExists) {
                classPath.setValue(StringUtils.join(classPathElements.iterator(), " "));
                mf.getMainSection().addConfiguredAttribute(classPath);

                // Write the manifest to disk, preserve timestamp
                FileTime lastModifiedTime = Files.getLastModifiedTime(manifestFile);
                try (BufferedWriter writer = Files.newBufferedWriter(
                        manifestFile,
                        StandardCharsets.UTF_8,
                        StandardOpenOption.WRITE,
                        StandardOpenOption.CREATE,
                        StandardOpenOption.TRUNCATE_EXISTING)) {
                    mf.write(writer);
                }
                Files.setLastModifiedTime(manifestFile, lastModifiedTime);
                removeFromOutdatedResources(manifestFile, outdatedResources);
            }

            if (fileSystem != null) {
                fileSystem.close();
                fileSystem = null;
            }
        } catch (ManifestException | IOException | ArchiverException e) {
            throw new MojoFailureException(e.getMessage(), e);
        } finally {
            if (fileSystem != null) {
                try {
                    fileSystem.close();
                } catch (IOException e) {
                    // ignore here
                }
            }
        }
    }

    private static Manifest readManifest(Path manifestFile) throws IOException {
        // Read the manifest from disk
        try (InputStream in = Files.newInputStream(manifestFile)) {
            return new Manifest(in);
        }
    }

    private Collection initOutdatedResources() {
        final Collection outdatedResources = new ArrayList<>();

        if (getWorkDirectory().exists()) {
            try {
                Files.walkFileTree(getWorkDirectory().toPath(), new SimpleFileVisitor() {
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        outdatedResources.add(
                                getWorkDirectory().toPath().relativize(file).toString());
                        return super.visitFile(file, attrs);
                    }
                });
            } catch (IOException e) {
                getLog().warn("Can't detect outdated resources", e);
            }
        }

        getLog().debug("initOutdatedResources: " + outdatedResources);
        return outdatedResources;
    }

    private void deleteOutdatedResources(final Collection outdatedResources) {
        getLog().debug("deleteOutdatedResources: " + outdatedResources);
        final long startTime = session.getStartTime().getTime();

        getLog().debug("deleteOutdatedResources session startTime: " + startTime);

        for (String outdatedResource : outdatedResources) {
            File resourceFile = new File(getWorkDirectory(), outdatedResource);
            if (resourceFile.lastModified() < startTime) {
                getLog().info("deleting outdated resource " + outdatedResource);
                getLog().debug(outdatedResource + " last modified: " + resourceFile.lastModified());
                resourceFile.delete();
            }
        }
    }

    private void removeFromOutdatedResources(Path destination, Collection outdatedResources) {
        Path relativeDestFile;
        try {
            relativeDestFile = getWorkDirectory().toPath().relativize(destination.normalize());
        } catch (ProviderMismatchException e) {
            relativeDestFile = destination.normalize();
        }

        if (outdatedResources.remove(relativeDestFile.toString())) {
            getLog().debug("Remove from outdatedResources: " + relativeDestFile);
        }
    }

    /**
     * Searches for the given JAR module in the list of classpath elements. If JAR module is found among specified
     * classpath elements then returns index of first matching element. Returns -1 otherwise.
     *
     * @param classPathElements classpath elements to search among
     * @param module module to find among classpath elements defined by {@code classPathElements}
     * @return -1 if {@code module} was not found in {@code classPathElements} or index of item of
     * {@code classPathElements} which matches {@code module}
     */
    private int findModuleInClassPathElements(final List classPathElements, final EarModule module) {
        if (classPathElements.isEmpty()) {
            return -1;
        }
        int moduleClassPathIndex = classPathElements.indexOf(module.getBundleFileName());
        if (moduleClassPathIndex != -1) {
            return moduleClassPathIndex;
        }
        final Artifact artifact = module.getArtifact();
        moduleClassPathIndex = classPathElements.indexOf(artifact.getFile().getName());
        if (moduleClassPathIndex != -1) {
            return moduleClassPathIndex;
        }
        if (artifact.isSnapshot()) {
            try {
                moduleClassPathIndex = classPathElements.indexOf(
                        MappingUtils.evaluateFileNameMapping(ARTIFACT_DEFAULT_FILE_NAME_MAPPING, artifact));
                if (moduleClassPathIndex != -1) {
                    return moduleClassPathIndex;
                }
            } catch (InterpolationException e) {
                getLog().warn("Failed to evaluate file name for [" + module + "] module using mapping: "
                        + ARTIFACT_DEFAULT_FILE_NAME_MAPPING);
            }
        }
        return classPathElements.indexOf(module.getUri());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy