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

org.l2x6.cq.common.FlattenBomTask Maven / Gradle / Ivy

/*
 * Copyright (c) 2020 CQ Maven Plugin
 * project contributors as indicated by the @author tags.
 *
 * 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.l2x6.cq.common;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.XMLConstants;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.Exclusion;
import org.apache.maven.model.InputLocation;
import org.apache.maven.model.InputLocation.StringFormatter;
import org.apache.maven.model.InputSource;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.apache.maven.model.io.xpp3.MavenXpp3WriterEx;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.assertj.core.util.diff.Delta;
import org.assertj.core.util.diff.DiffUtils;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.DependencyVisitor;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.util.graph.transformer.ConflictResolver;
import org.jdom2.Document;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.l2x6.pom.tuner.ExpressionEvaluator;
import org.l2x6.pom.tuner.MavenSourceTree;
import org.l2x6.pom.tuner.PomTransformer;
import org.l2x6.pom.tuner.PomTransformer.ContainerElement;
import org.l2x6.pom.tuner.PomTransformer.NodeGavtcs;
import org.l2x6.pom.tuner.PomTransformer.SimpleElementWhitespace;
import org.l2x6.pom.tuner.PomTransformer.TransformationContext;
import org.l2x6.pom.tuner.model.Ga;
import org.l2x6.pom.tuner.model.Gav;
import org.l2x6.pom.tuner.model.GavPattern;
import org.l2x6.pom.tuner.model.GavSet;
import org.l2x6.pom.tuner.model.Gavtcs;
import org.l2x6.pom.tuner.model.Module;
import org.l2x6.pom.tuner.model.Profile;
import org.w3c.dom.Node;

import static java.util.stream.Collectors.joining;

public class FlattenBomTask {
    public static class BomEntryTransformation {
        private GavPattern gavPattern;
        private Set internalExclusions = new TreeSet<>();
        private Pattern versionPattern;
        private String versionReplace;

        public BomEntryTransformation() {
        }

        public BomEntryTransformation(String gavPattern, String versionReplacement, String exclusions, String addExclusions) {
            if (gavPattern != null) {
                setGavPattern(gavPattern);
            }
            if (versionReplacement != null) {
                setVersionReplacement(versionReplacement);
            }
            if (exclusions != null) {
                setExclusions(exclusions);
            }
            if (addExclusions != null) {
                setAddExclusions(addExclusions);
            }
        }

        public List getAddExclusions() {
            return internalExclusions.stream()
                    .map(ga -> {
                        final Exclusion excl = new Exclusion();
                        excl.setGroupId(ga.getGroupId());
                        excl.setArtifactId(ga.getArtifactId());
                        return excl;
                    })
                    .collect(Collectors.toList());
        }

        public void setAddExclusions(String exclusions) {
            for (String rawExcl : exclusions.split("[,\\s]+")) {
                this.internalExclusions.add(GavPattern.of(rawExcl).asWildcardGa());
            }
        }

        /**
         * An alias for {@link #setAddExclusions(String)}
         *
         * @param      exclusions items to exclude
         * @deprecated            use {@link #setAddExclusions(String)}
         */
        @Deprecated
        public void setExclusions(String exclusions) {
            setAddExclusions(exclusions);
        }

        public GavPattern getGavPattern() {
            return gavPattern;
        }

        public void setGavPattern(String gavPattern) {
            this.gavPattern = GavPattern.of(gavPattern);
        }

        public String replaceVersion(String version) {
            return versionPattern == null ? version : versionPattern.matcher(version).replaceAll(versionReplace);
        }

        public void setVersionReplacement(String versionReplacement) {
            final int slashPos = versionReplacement.indexOf('/');
            if (slashPos < 1) {
                throw new IllegalStateException(
                        "versionReplacement is expected to contain exactly one slash (/); found " + versionReplacement);
            }
            this.versionPattern = Pattern.compile(versionReplacement.substring(0, slashPos));
            this.versionReplace = versionReplacement.substring(slashPos + 1);
        }

        @Override
        public String toString() {
            return "BomEntryTransformation [gavPattern=" + gavPattern + ", internalExclusions=" + internalExclusions
                    + ", versionReplace=" + versionReplace + "]";
        }
    }

    static class DependencyCollector implements DependencyVisitor {
        private final Set allTransitives = new TreeSet<>();
        private final GavSet excludes;
        private final BiConsumer exclusionConsumer;
        private final Deque stack = new ArrayDeque<>();
        private final GavSet bannedDependencies;
        private final Predicate isCurrentBomEntry;
        private final Predicate isCurrentBomOrIncludedEntry;
        private final GavSet suspects;
        private final Consumer> suspectConsumer;
        private final Map> additionalBomConstraits;
        private final Log log;
        private final boolean format;

        public DependencyCollector(
                GavSet excludes,
                BiConsumer exclusionConsumer,
                GavSet bannedDependencies,
                Predicate isCurrentBomEntry,
                Predicate isCurrentBomIncludedEntry,
                Map> additionalBomConstraits,
                GavSet suspects,
                Consumer> suspectConsumer,
                Log log,
                boolean format) {
            this.excludes = excludes;
            this.exclusionConsumer = exclusionConsumer;
            this.bannedDependencies = bannedDependencies;
            this.isCurrentBomEntry = isCurrentBomEntry;
            this.isCurrentBomOrIncludedEntry = isCurrentBomIncludedEntry;
            this.additionalBomConstraits = additionalBomConstraits;
            this.suspects = suspects;
            this.suspectConsumer = suspectConsumer;
            this.log = log;
            this.format = format;
        }

        @Override
        public boolean visitLeave(DependencyNode node) {
            if (!format || node.getData().get(ConflictResolver.NODE_DATA_WINNER) == null) {
                /*
                 * We always push in non-format mode, so we have to always pop, thus saving some node.getData() map
                 * lookups
                 */
                stack.pop();
            }
            return true;
        }

        @Override
        public boolean visitEnter(DependencyNode node) {
            final Artifact a = node.getArtifact();
            final Ga ga = new Ga(a.getGroupId(), a.getArtifactId());
            DependencyNode winner;
            if (format && (winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER)) != null) {
                /* We use ConflictResolver.CONFIG_PROP_VERBOSE = true only when format is true */
                /* Recurse the winner instead of the current looser */
                if (!stack.contains(ga)) {
                    winner.accept(this);
                }
                return false; // should have empty children anyway as stated in class level JavaDoc of ConflictResolver
            }

            boolean result = true;
            if (!excludes.contains(a.getGroupId(), a.getArtifactId())) {
                if (bannedDependencies.contains(ga)) {
                    result = false;
                    /*
                     * Find the closest own managed dependent and register an exclusion there
                     * This is to make the enforcer happy when the BOM is taken from the reactor.
                     * The reactor BOM is not flattened and the bomEntryTransformations are thus not applied there.
                     * Hence adding an exclusion on our own entry may help
                     */
                    final Optional dependent = stack.stream()
                            .filter(isCurrentBomEntry)
                            .findFirst();
                    if (dependent.isPresent()) {
                        exclusionConsumer.accept(dependent.get(), ga);
                    }
                    /* Find the closest included managed dependent and register an exclusion there */
                    final Optional includedDependent = stack.stream()
                            .filter(isCurrentBomOrIncludedEntry)
                            .findFirst();
                    if (includedDependent.isPresent() && !Objects.equals(includedDependent.get(), dependent.orElse(null))) {
                        exclusionConsumer.accept(includedDependent.get(), ga);
                    } else if (!dependent.isPresent()
                            && !includedDependent.isPresent()) {
                        /* Look if this banned dependency comes via some additional BOM, such as Quarkus BOM */

                        final Map> missingAddionalBomExclusions = new TreeMap>();
                        stack.stream()
                                .forEach(stackEntry -> Optional.ofNullable(additionalBomConstraits.get(stackEntry))
                                        .ifPresent(additionalBomGavs -> additionalBomGavs.stream()
                                                .forEach(bomGav -> missingAddionalBomExclusions.put(bomGav,
                                                        new SimpleImmutableEntry<>(stackEntry, ga)))));
                        if (!missingAddionalBomExclusions.isEmpty()) {
                            missingAddionalBomExclusions.forEach((Gav additionalBomGav, Map.Entry entry) -> log.warn(
                                    additionalBomGav + " is possibly missing an exclusion on " + entry.getKey() + ":\n\n"
                                            + "    \n"
                                            + "        " + entry.getValue().getGroupId() + "\n"
                                            + "        " + entry.getValue().getArtifactId() + "\n"
                                            + "    \n"));
                        } else {
                            throw new IllegalStateException(
                                    "Cannot link banned dependency to any own or included BOM entry:\n    " + ga + "\n    -> "
                                            + stack.stream().map(Ga::toString).collect(Collectors.joining("\n    -> ")));
                        }

                    }
                }
                allTransitives.add(ga);

            }
            stack.push(ga);
            if (suspects.contains(ga)) {
                suspectConsumer.accept(stack);
            }
            return result;
        }

    }

    private static class InputLocationStringFormatter
            extends InputLocation.StringFormatter {

        private final String versionSuffix;

        public InputLocationStringFormatter(String version) {
            this.versionSuffix = ":" + version;
        }

        private static final String GAV_PREFIX = FlattenBomTask.ORG_APACHE_CAMEL_QUARKUS_GROUP_ID + ":";

        public String toString(InputLocation location) {
            InputSource source = location.getSource();

            String s = source.getModelId(); // by default, display modelId

            if (StringUtils.isBlank(s) || s.contains("[unknown-version]")) {
                // unless it is blank or does not provide version information
                s = source.toString();
            }

            if (s.startsWith(GAV_PREFIX)) {
                s = s.replace(versionSuffix, ":${project.version}");
            }

            return "#} " + s + " ";
        }

    }

    private static class BomEntryTransformationData {
        final BomEntryTransformation bomEntryTransformation;
        final ContainerElement containerElement;

        public BomEntryTransformationData(BomEntryTransformation bomEntryTransformation, ContainerElement containerElement) {
            this.bomEntryTransformation = bomEntryTransformation;
            this.containerElement = containerElement;
        }

        public void addExclusions(Set missingExclusions) {
            Set existingExclusions = bomEntryTransformation.internalExclusions;
            if (!existingExclusions.containsAll(missingExclusions)) {
                existingExclusions.addAll(missingExclusions);
                containerElement.addOrSetChildTextElement("addExclusions",
                        existingExclusions.stream().map(Ga::toString).collect(Collectors.joining(",")));
                final Optional exclusions = containerElement.getChildContainerElement("exclusions");
                if (exclusions.isPresent()) {
                    exclusions.get().remove(true, true);
                }
            }
        }

        public static BomEntryTransformationData create(GavPattern pattern, Set missingExclusions,
                ContainerElement parent) {
            final BomEntryTransformation transformation = new BomEntryTransformation();
            transformation.setGavPattern(pattern.toString());
            final String exclusions = missingExclusions.stream().map(Ga::toString).collect(Collectors.joining(","));
            transformation.setAddExclusions(exclusions);

            ContainerElement node = parent.addChildContainerElement("autogeneratedBomEntryTransformation");

            node.addChildTextElement("gavPattern", pattern.toString());
            node.addChildTextElement("addExclusions", exclusions);

            return new BomEntryTransformationData(transformation, node);
        }
    }

    private static class RequiredGas {

        private final Set gas;
        private final Map> expectedExclusions;

        public RequiredGas(Set gas, Map> expectedExclusions) {
            this.gas = gas;
            this.expectedExclusions = expectedExclusions;
        }

    }

    private static class ExpectedExclusions {
        final Map> expectedExclusions = new TreeMap<>();

        public void add(Ga bomEntry, Ga exclusion) {
            expectedExclusions.compute(bomEntry, (Ga k, Set v) -> {
                (v == null ? v = new TreeSet() : v).add(exclusion);
                return v;
            });
        }
    }

    public static enum InstallFlavor {
        FULL, REDUCED, REDUCED_VERBOSE, ORIGINAL
    }

    private final List resolutionEntryPointIncludes;
    private final List resolutionEntryPointExcludes;
    private final List resolutionSuspects;
    private final List originExcludes;
    private final List bomEntryTransformations;
    private final GavSet requiredBomEntries;
    private final OnFailure onCheckFailure;
    private final Model effectivePomModel;
    private final String version;
    private final Path basePath;
    private final Path rootModuleDirectory;
    private final Path fullPomPath;
    private final Path reducedVerbosePamPath;
    private final Path reducedPomPath;
    private final Charset charset;
    private final Log log;
    private final List repositories;
    private final RepositorySystem repoSystem;
    private final RepositorySystemSession repoSession;
    private final Predicate profiles;
    private final boolean format;
    private final SimpleElementWhitespace simpleElementWhitespace;
    private final MavenProject project;
    private final FlattenBomTask.InstallFlavor installFlavor;
    private final boolean quickly;
    private final GavSet bannedDependencies;
    private final List ownManagedDependencies;
    private final Path localRepositoryPath;
    private final List additionalBoms;
    private static final Pattern LOCATION_COMMENT_PATTERN = Pattern.compile("\\s*\\Q