org.fluidity.deployment.maven.DependenciesSupportImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.fluidity.deployment.maven.DependenciesSupportImpl.java

Source

/*
 * Copyright (c) 2006-2016 Tibor Adam Varga (tibor.adam.varga on gmail)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.fluidity.deployment.maven;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import javax.inject.Inject;

import org.fluidity.foundation.Archives;
import org.fluidity.foundation.Deferred;
import org.fluidity.foundation.Exceptions;
import org.fluidity.foundation.Lists;
import org.fluidity.foundation.Proxies;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.DefaultArtifactHandler;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.eclipse.aether.AbstractForwardingRepositorySystemSession;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.ArtifactProperties;
import org.eclipse.aether.artifact.DefaultArtifactType;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.collection.DependencyCollectionContext;
import org.eclipse.aether.collection.DependencySelector;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.DependencyVisitor;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.WorkspaceReader;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.graph.selector.StaticDependencySelector;

/**
 * @author Tibor Vara
 */
@Component(role = DependenciesSupport.class)
final class DependenciesSupportImpl implements DependenciesSupport {

    private static final StaticDependencySelector NO_SELECTOR = new StaticDependencySelector(false);

    private static String artifactSpecification(final org.eclipse.aether.artifact.Artifact artifact) {
        return artifact.getGroupId() + ':' + artifact.getArtifactId();
    }

    private static String artifactSpecification(final org.eclipse.aether.graph.Exclusion artifact) {
        return artifact.getGroupId() + ':' + artifact.getArtifactId();
    }

    /**
     * The entry point to Aether, i.e. the component doing all the work.
     */
    private final RepositorySystem system;

    @Inject
    public DependenciesSupportImpl(final RepositorySystem system) {
        this.system = system;
    }

    @Override
    public Dependency dependency(final Class<?> type, final Collection<Dependency> dependencies)
            throws MojoExecutionException {
        final String[] spec = projectId(type);

        for (final Dependency artifact : dependencies) {
            if (spec[0].equals(artifact.getGroupId()) && spec[1].equals(artifact.getArtifactId())) {
                return artifact;
            }
        }

        return null;
    }

    @Override
    public Artifact artifact(final Class<?> type, final Collection<Artifact> dependencies)
            throws MojoExecutionException {
        final String[] spec = projectId(type);

        for (final Artifact artifact : dependencies) {
            if (spec[0].equals(artifact.getGroupId()) && spec[1].equals(artifact.getArtifactId())) {
                return artifact;
            }
        }

        return null;
    }

    @Override
    public Collection<Artifact> dependencyClosure(final RepositorySystemSession session,
            final List<RemoteRepository> repositories, final Artifact artifact, final boolean compile,
            final boolean optionals, final Collection<Exclusion> exclusions) throws MojoExecutionException {
        artifact.setScope(JavaScopes.COMPILE);
        return closure(session, repositories, new TransitiveDependencySelector(compile, optionals), !compile,
                artifact, exclusions);
    }

    private Collection<Artifact> closure(final RepositorySystemSession session,
            final List<RemoteRepository> repositories, final DependencySelector selector, final boolean runtime,
            final Artifact artifact, final Collection<Exclusion> exclusions) throws MojoExecutionException {
        final org.eclipse.aether.graph.Dependency root = aetherDependency(artifact, exclusions);

        // Guidelines for a possible upgrade path
        // http://labs.bsb.com/2012/10/using-aether-to-resolve-dependencies-in-a-maven-plugins/
        // http://git.eclipse.org/c/aether/aether-demo.git/tree/
        // Resolve one artifacto: http://git.eclipse.org/c/aether/aether-demo.git/tree/aether-demo-maven-plugin/src/main/java/org/eclipse/aether/examples/maven/ResolveArtifactMojo.java
        // Transitive dependencies: http://git.eclipse.org/c/aether/aether-demo.git/tree/aether-demo-snippets/src/main/java/org/eclipse/aether/examples/ResolveTransitiveDependencies.java

        /*
         * http://wiki.eclipse.org/Aether
         * http://wiki.eclipse.org/Aether/Using_Aether_in_Maven_Plugins
         * http://wiki.eclipse.org/Aether/Dependency_Graph
         * http://wiki.eclipse.org/Aether/Transitive_Dependency_Resolution
         * http://git.eclipse.org/c/aether/aether-demo.git/tree/aether-demo-snippets/src/main/java/org/eclipse/aether/examples
         */

        return Exceptions.wrap(Deferred.label("Finding transitive dependencies of %s", root),
                MojoExecutionException.class, () -> {
                    final DependencyFilterSession filter = new DependencyFilterSession(patch(session),
                            new DependencySelector() {
                                public boolean selectDependency(
                                        final org.eclipse.aether.graph.Dependency dependency) {
                                    return true; // always accept the root artifact
                                }

                                public DependencySelector deriveChildSelector(
                                        final DependencyCollectionContext context) {
                                    return root.isOptional() ? NO_SELECTOR : selector;
                                }
                            });

                    final CollectRequest request = new CollectRequest(root,
                            Collections.unmodifiableList(repositories));

                    final CollectResult result = system.collectDependencies(filter, request);
                    if (!result.getExceptions().isEmpty())
                        throw result.getExceptions().get(0);

                    final DependencyResult resolved = system.resolveDependencies(filter,
                            new DependencyRequest(result.getRoot(), null));
                    if (!resolved.getCollectExceptions().isEmpty())
                        throw resolved.getCollectExceptions().get(0);

                    final Collection<Artifact> dependencies = new HashSet<>();

                    result.getRoot().accept(new UniqueArtifacts(dependency -> {
                        if (!dependency.isResolved()) {
                            throw new IllegalArgumentException(String.format("Could not resolve %s", dependency));
                        } else if (!(POM_TYPE.equals(dependency.getType())
                                || (runtime && JavaScopes.PROVIDED.equals(dependency.getScope())))) {
                            dependencies.add(dependency);
                        }
                    }));

                    return dependencies;
                });
    }

    private static String[] projectId(final Class<?> type) throws MojoExecutionException {
        try {
            final String[] spec = Archives.attributes(Archives.containing(type), true, MANIFEST_MAVEN_GROUP_ID,
                    MANIFEST_MAVEN_ARTIFACT_ID);

            if (spec[0] == null || spec[1] == null) {
                throw new MojoExecutionException(String.format("Could not find Maven project for %s", type));
            }

            return spec;
        } catch (final IOException e) {
            throw new MojoExecutionException("Reading JAR manifest", e);
        }
    }

    @Override
    public Artifact dependencyArtifact(final Dependency dependency) {
        final DefaultArtifact artifact = new DefaultArtifact(dependency.getGroupId(), dependency.getArtifactId(),
                dependency.getVersion(), dependency.getScope(), dependency.getType(), dependency.getClassifier(),
                new DefaultArtifactHandler(dependency.getType()));
        artifact.setOptional(Boolean.valueOf(dependency.getOptional()));
        return artifact;
    }

    /**
     * Converts an Aether artifact to a Maven artifact.
     *
     * @param original the Aether artifact.
     *
     * @return the Maven artifact.
     */
    private static Artifact mavenArtifact(final org.eclipse.aether.artifact.Artifact original, final String scope) {
        final String setClassifier = original.getClassifier();
        final String classifier = (setClassifier == null || setClassifier.isEmpty()) ? null : setClassifier;

        final String type = original.getProperty(ArtifactProperties.TYPE, original.getExtension());
        final DefaultArtifactHandler handler = new DefaultArtifactHandler(type);

        handler.setExtension(original.getExtension());
        handler.setLanguage(original.getProperty(ArtifactProperties.LANGUAGE, null));

        final Artifact artifact = new DefaultArtifact(original.getGroupId(), original.getArtifactId(),
                original.getVersion(), null, type, classifier, handler);

        final File file = original.getFile();
        artifact.setFile(file);
        artifact.setResolved(file != null);

        artifact.setScope(scope);
        artifact.setDependencyTrail(Collections.singletonList(artifact.getId()));

        return artifact;
    }

    /**
     * Converts a Aether artifact to a Maven artifact.
     *
     * @param original the Maven artifact.
     *
     * @return the Aether artifact.
     */
    private static org.eclipse.aether.artifact.Artifact aetherArtifact(final Artifact original) {
        final String explicitVersion = original.getVersion();
        final VersionRange versionRange = original.getVersionRange();
        final String version = explicitVersion == null && versionRange != null ? versionRange.toString()
                : explicitVersion;

        final boolean isSystem = Artifact.SCOPE_SYSTEM.equals(original.getScope());
        final String path = (original.getFile() != null) ? original.getFile().getPath() : "";
        final Map<String, String> properties = isSystem
                ? Collections.singletonMap(ArtifactProperties.LOCAL_PATH, path)
                : null;

        final ArtifactHandler handler = original.getArtifactHandler();

        final String extension = handler.getExtension();
        final DefaultArtifactType type = new DefaultArtifactType(original.getType(), extension,
                handler.getClassifier(), handler.getLanguage(), handler.isAddedToClasspath(),
                handler.isIncludesDependencies());

        final org.eclipse.aether.artifact.Artifact artifact = new org.eclipse.aether.artifact.DefaultArtifact(
                original.getGroupId(), original.getArtifactId(), original.getClassifier(), extension, version,
                properties, type);
        return artifact.setFile(original.getFile());
    }

    /**
     * Converts a Maven dependency to an Aether dependency.
     *
     * @param original   the Aether dependency.
     * @param exclusions the list of dependency specifications to exclude.
     *
     * @return the Maven dependency.
     */
    private static org.eclipse.aether.graph.Dependency aetherDependency(final Artifact original,
            final Collection<Exclusion> exclusions) {
        final List<org.eclipse.aether.graph.Exclusion> exclusionList = new ArrayList<>();

        if (exclusions != null) {
            for (final Exclusion exclusion : exclusions) {
                exclusionList.add(new org.eclipse.aether.graph.Exclusion(exclusion.getGroupId(),
                        exclusion.getArtifactId(), "*", "*"));
            }
        }

        return new org.eclipse.aether.graph.Dependency(aetherArtifact(original), original.getScope(),
                original.isOptional(), exclusions == null ? null : exclusionList);
    }

    @Override
    public Collection<Artifact> runtimeDependencies(final RepositorySystemSession session,
            final List<RemoteRepository> repositories, final MavenProject project, final boolean optionals)
            throws MojoExecutionException {
        return dependencyClosure(session, repositories, project.getArtifact(), false, optionals, null);
    }

    @Override
    public Collection<Artifact> compileDependencies(final RepositorySystemSession session,
            final List<RemoteRepository> repositories, final MavenProject project, final boolean optionals)
            throws MojoExecutionException {
        return dependencyClosure(session, repositories, project.getArtifact(), true, optionals, null);
    }

    @Override
    public void saveArtifact(final MavenProject project, final File file, final String finalName,
            final String classifier, final String packaging, final Logger log) throws MojoExecutionException {
        final boolean unclassified = classifier == null || classifier.isEmpty();
        final String outputName = unclassified ? String.format("%s.%s", finalName, packaging)
                : String.format("%s-%s.%s", finalName, classifier, packaging);

        final File outputFile = new File(outputName);
        final String outputPath = outputFile.getAbsolutePath();

        final Artifact artifact = project.getArtifact();
        assert artifact != null;

        final File artifactFile = artifact.getFile();

        if (artifactFile != null && artifactFile.getAbsolutePath().equals(outputPath)) {
            log.info(String.format("Replacing %s: %s", packaging, artifactFile.getAbsolutePath()));

            if (!artifactFile.delete()) {
                throw new MojoExecutionException(String.format("Could not delete %s", artifactFile));
            }

            if (!file.renameTo(artifactFile)) {
                throw new MojoExecutionException(String.format("Could not create %s", artifactFile));
            }
        } else {
            boolean replacing = false;

            for (final Artifact attached : project.getAttachedArtifacts()) {
                if (attached.getFile().getAbsolutePath().equals(outputPath)) {
                    replacing = true;
                    break;
                }
            }

            log.info(String.format("%s %s: %s", replacing ? "Replacing" : "Saving", packaging, outputPath));

            if (outputFile.exists() && !outputFile.delete()) {
                throw new MojoExecutionException(String.format("Could not delete %s", outputFile));
            }

            if (!file.renameTo(outputFile)) {
                throw new MojoExecutionException(String.format("Could not create %s", outputFile));
            }

            if (!replacing) {
                final DefaultArtifact attachment = new DefaultArtifact(project.getGroupId(),
                        project.getArtifactId(), project.getVersion(), artifact.getScope(), packaging, classifier,
                        artifact.getArtifactHandler());
                attachment.setFile(outputFile);

                project.addAttachedArtifact(attachment);
            }
        }
    }

    @Override
    public Artifact resolve(final RepositorySystemSession session, final List<RemoteRepository> repositories,
            final Dependency dependency) throws MojoExecutionException {
        final List<Artifact> artifacts = new ArrayList<>();

        artifacts.addAll(closure(session, repositories, NO_SELECTOR, false, dependencyArtifact(dependency),
                dependency.getExclusions()));

        assert artifacts.size() == 1 : artifacts;
        return artifacts.get(0);
    }

    @Override
    public Collection<Artifact> resolve(final RepositorySystemSession session,
            final List<RemoteRepository> repositories, final List<Dependency> dependencies)
            throws MojoExecutionException {
        final DependencySelector selector = new TransitiveDependencySelector(false, false);
        final Collection<Artifact> artifacts = new LinkedHashSet<>();

        for (final Dependency dependency : dependencies) {
            artifacts.addAll(closure(session, repositories, selector, false, dependencyArtifact(dependency),
                    dependency.getExclusions()));
        }

        return artifacts;
    }

    @Override
    public void list(final Collection<Artifact> artifacts, final String prefix, final Logger log) {
        assert log.active();

        artifacts.stream().map(Artifact::getFile).map(File::getName).sorted().map(prefix::concat)
                .forEach(log::detail);
    }

    /*
     * This class applies a hack to undo the hack introduced to Maven to speed up POM processing:
     * https://github.com/apache/maven/commit/be3fb200326208ca4b8c41ebf16d5ae6b8049792#diff-1b94ec0f0a29b1dc6aae1f6ad855484bR307
     */
    // TODO: see if that hack has been removed: https://issues.apache.org/jira/browse/MNG-5899
    private static RepositorySystemSession patch(final RepositorySystemSession original) {
        final DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(original);

        // We simply provide a general workspace reader that does not cache parsed POMs,
        // thereby allowing property changes to affect profile activations.

        final WorkspaceReader reader = session.getWorkspaceReader();
        session.setWorkspaceReader(
                Proxies.create(WorkspaceReader.class, (proxy, method, args) -> method.invoke(reader, args)));

        return session;
    }

    /**
     * @author Tibor Varga
     */
    private static class DependencyFilterSession extends AbstractForwardingRepositorySystemSession {

        private final DependencySelector selector;
        private final RepositorySystemSession parent;

        DependencyFilterSession(final RepositorySystemSession parent, final DependencySelector selector) {
            this.parent = parent;
            this.selector = selector;
        }

        @Override
        protected RepositorySystemSession getSession() {
            return parent;
        }

        @Override
        public DependencySelector getDependencySelector() {
            return selector;
        }
    }

    /**
     * @author Tibor Varga
     */
    private static class TransitiveDependencySelector implements DependencySelector {

        private static final Set<String> RUNTIME_SCOPES = new HashSet<>(
                Arrays.asList(JavaScopes.COMPILE, JavaScopes.PROVIDED, JavaScopes.RUNTIME, JavaScopes.SYSTEM));
        private static final Set<String> COMPILE_SCOPES = new HashSet<>(
                Arrays.asList(JavaScopes.COMPILE, JavaScopes.PROVIDED, JavaScopes.SYSTEM));

        private final boolean compile;
        private final boolean optionals;

        private final Set<String> excludedArtifacts;
        private final Set<String> acceptedScopes;

        TransitiveDependencySelector(final boolean compile, boolean optionals, final String... exclusions) {
            this.compile = compile;
            this.optionals = optionals;
            this.excludedArtifacts = new HashSet<>(Arrays.asList(exclusions));
            this.acceptedScopes = compile ? COMPILE_SCOPES : RUNTIME_SCOPES;
        }

        public boolean selectDependency(final org.eclipse.aether.graph.Dependency dependency) {
            return dependency != null
                    && !excludedArtifacts.contains(artifactSpecification(dependency.getArtifact()))
                    && acceptedScopes.contains(dependency.getScope()) && (optionals || !dependency.isOptional());
        }

        public DependencySelector deriveChildSelector(final DependencyCollectionContext context) {
            final org.eclipse.aether.graph.Dependency dependency = context.getDependency();

            if (dependency == null || JavaScopes.PROVIDED.equals(dependency.getScope())
                    || (!compile && dependency.isOptional())) {
                return NO_SELECTOR;
            } else {
                final Collection<org.eclipse.aether.graph.Exclusion> exclusions = dependency.getExclusions();

                if (optionals || !exclusions.isEmpty()) {
                    final Set<String> excluded = new HashSet<>(exclusions.size() + excludedArtifacts.size());

                    excluded.addAll(excludedArtifacts);

                    for (final org.eclipse.aether.graph.Exclusion exclusion : exclusions) {
                        excluded.add(artifactSpecification(exclusion));
                    }

                    // next level optionals and those excluded by the current dependency will not be selected during descent
                    return new TransitiveDependencySelector(compile, false, Lists.asArray(String.class, excluded));
                } else {
                    return this;
                }
            }
        }
    }

    /**
     * Streams a single instance of the artifacts in the dependency tree.
     *
     * @author Tibor Varga
     */
    private static class UniqueArtifacts implements DependencyVisitor {

        private final Consumer<Artifact> collect;
        private final Set<String> visited = new HashSet<>();

        UniqueArtifacts(final Consumer<Artifact> collect) {
            this.collect = collect;
        }

        public boolean visitEnter(final DependencyNode node) {
            final org.eclipse.aether.graph.Dependency dependency = node.getDependency();
            final org.eclipse.aether.artifact.Artifact artifact = dependency.getArtifact();

            final boolean accepted = visited.add(identity(artifact));

            if (accepted) {
                this.collect.accept(mavenArtifact(artifact, dependency.getScope()));
            }

            return accepted;
        }

        public boolean visitLeave(final DependencyNode node) {
            return true;
        }

        private static String identity(final org.eclipse.aether.artifact.Artifact artifact) {
            return String.format("%s:%s:%s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(),
                    artifact.getClassifier(), artifact.getVersion(), artifact.getExtension());
        }
    }
}