001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package org.basepom.mojo.dvc.mojo;
016
017import static com.google.common.base.Preconditions.checkState;
018import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
019
020import org.basepom.mojo.dvc.AbstractDependencyVersionsMojo;
021import org.basepom.mojo.dvc.QualifiedName;
022import org.basepom.mojo.dvc.dependency.DependencyMap;
023import org.basepom.mojo.dvc.strategy.Strategy;
024import org.basepom.mojo.dvc.version.VersionResolutionCollection;
025import org.basepom.mojo.dvc.version.VersionResolutionElement;
026
027import java.util.function.Function;
028
029import com.google.common.base.Strings;
030import com.google.common.collect.ImmutableMap;
031import com.google.common.collect.ImmutableSetMultimap;
032import com.google.common.collect.Maps;
033import org.apache.maven.artifact.versioning.ComparableVersion;
034import org.apache.maven.plugin.MojoFailureException;
035import org.apache.maven.plugins.annotations.LifecyclePhase;
036import org.apache.maven.plugins.annotations.Mojo;
037import org.apache.maven.plugins.annotations.Parameter;
038import org.apache.maven.plugins.annotations.ResolutionScope;
039import org.apache.maven.shared.utils.logging.MessageBuilder;
040import org.apache.maven.shared.utils.logging.MessageUtils;
041import org.eclipse.aether.graph.DependencyNode;
042import org.eclipse.aether.version.Version;
043
044/**
045 * Resolves all dependencies of a project and reports version conflicts.
046 */
047@Mojo(name = "check", requiresProject = true, threadSafe = true, defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.NONE)
048public class DependencyVersionsCheckMojo
049        extends AbstractDependencyVersionsMojo {
050
051    /**
052     * List only dependencies in conflict or all dependencies.
053     *
054     * @since 3.0.0
055     */
056    @Parameter(defaultValue = "true", property = "dvc.conflicts-only")
057    public boolean conflictsOnly = true;
058
059    /**
060     * Fail the build if a conflict is detected. Any conflict (direct and transitive) will cause a failure.
061     *
062     * @since 3.0.0
063     */
064    @Parameter(defaultValue = "false", alias = "failBuildInCaseOfConflict", property = "dvc.conflicts-fail-build")
065    protected boolean conflictsFailBuild = false;
066
067    /**
068     * Fail the build only if a version conflict involves one or more direct dependencies. Direct dependency versions are controlled by the project itself so
069     * any conflict here can be fixed by changing the version in the project.
070     * <br>
071     * It is strongly recommended to review and fix these conflicts.
072     *
073     * @since 3.0.0
074     */
075    @Parameter(defaultValue = "false", property = "dvc.direct-conflicts-fail-build")
076    protected boolean directConflictsFailBuild = false;
077
078    @Override
079    protected void doExecute(final ImmutableSetMultimap<QualifiedName, VersionResolutionCollection> resolutionMap, final DependencyMap rootDependencyMap)
080            throws Exception {
081        // filter out what to display.
082        final var filteredMap = ImmutableMap.copyOf(Maps.filterValues(
083                resolutionMap.asMap(),
084                v -> {
085                    // report if no condition is set.
086                    boolean report = true;
087
088                    if (conflictsOnly) {
089                        // do not report if conflicts are requested but none exists
090                        report &= v.stream().anyMatch(VersionResolutionCollection::hasConflict);
091                    }
092                    if (directOnly) {
093                        // do not report if only directs are requested but it is not direct
094                        report &= v.stream().anyMatch(VersionResolutionCollection::hasDirectDependencies);
095                    }
096
097                    if (managedOnly) {
098                        report &= v.stream().anyMatch(VersionResolutionCollection::hasManagedDependencies);
099                    }
100
101                    return report;
102                }));
103
104        log.report(quiet, "Checking %s%s dependencies%s for '%s' scope%s",
105                (directOnly ? "direct" : "all"),
106                (managedOnly ? ", managed" : ""),
107                (deepScan ? " using deep scan" : ""),
108                scope,
109                (conflictsOnly ? ", reporting only conflicts" : ""));
110
111        if (filteredMap.isEmpty()) {
112            return;
113        }
114
115        final var rootDependencies = rootDependencyMap.getAllDependencies();
116
117        boolean directConflicts = false;
118        boolean transitiveConflicts = false;
119
120        for (final var entry : filteredMap.entrySet()) {
121            final var versionMap = entry.getValue().stream()
122                    .collect(toImmutableSetMultimap(VersionResolutionCollection::getExpectedVersion, Function.identity()));
123
124            boolean willWarn = false;
125            boolean willFail = false;
126
127            final boolean isDirect = entry.getValue().stream().anyMatch(VersionResolutionCollection::hasDirectDependencies);
128            final QualifiedName dependencyName = entry.getKey();
129            final DependencyNode currentDependency = rootDependencies.get(dependencyName);
130            assert currentDependency != null;
131
132            final boolean isManaged = (currentDependency.getManagedBits() & DependencyNode.MANAGED_VERSION) != 0;
133
134            final Version dependencyVersion = currentDependency.getVersion();
135            checkState(dependencyVersion != null, "Dependency Version for %s is null! File a bug!", currentDependency);
136            final ComparableVersion resolvedVersion = new ComparableVersion(dependencyVersion.toString());
137
138            final Strategy strategy = strategyCache.forQualifiedName(dependencyName);
139
140            final MessageBuilder mb = MessageUtils.buffer();
141
142            mb.strong(dependencyName.getShortName())
143                    .a(": ")
144                    .strong(resolvedVersion)
145                    .format(" (%s%s) - scope: %s - strategy: %s",
146                            isDirect ? "direct" : "transitive",
147                            isManaged ? ", managed" : "",
148                            currentDependency.getDependency().getScope(),
149                            strategy.getName()
150                    )
151                    .newline();
152
153            final int versionPadding = versionMap.keySet().stream().map(v -> v.toString().length()).reduce(0, Math::max);
154            for (final var versionEntry : versionMap.asMap().entrySet()) {
155                final boolean hasConflictVersion = versionEntry.getValue().stream().anyMatch(VersionResolutionCollection::hasConflict);
156                final boolean perfectMatch = versionEntry.getValue().stream().anyMatch(v -> v.isMatchFor(resolvedVersion));
157                final String paddedVersion = Strings.padEnd(versionEntry.getKey().toString(), versionPadding + 1, ' ');
158
159                mb.a("       ");
160
161                if (hasConflictVersion) {
162                    mb.failure(paddedVersion);
163                } else if (perfectMatch) {
164                    mb.success(paddedVersion);
165                } else {
166                    mb.a(paddedVersion);
167                }
168
169                mb.a("expected by ");
170
171                for (var it = versionEntry.getValue().stream()
172                        .flatMap(v -> v.getRequestingDependencies().stream())
173                        .iterator(); it.hasNext(); ) {
174                    final VersionResolutionElement versionResolutionElement = it.next();
175                    final String name = versionResolutionElement.getRequestingDependency().getShortName();
176
177                    if (versionResolutionElement.isDirectDependency()) {
178                        mb.strong("*" + name + "*");
179                    } else {
180                        mb.a(name);
181                    }
182                    if (it.hasNext()) {
183                        mb.a(", ");
184                    }
185                }
186
187                mb.newline();
188
189                if (hasConflictVersion) {
190                    willWarn = true;
191                    willFail |= conflictsFailBuild; // any conflict fails build.
192                    willFail |= isDirect && directConflictsFailBuild;
193
194                    directConflicts |= isDirect;       // any direct dependency in conflict
195                    transitiveConflicts |= !isDirect;  // any transitive dependency in conflict
196                }
197            }
198
199            if (willFail) {
200                log.error("%s", mb);
201            } else if (willWarn) {
202                log.warn("%s", mb);
203            } else {
204                log.info("%s", mb);
205            }
206        }
207
208        if (directConflicts && (conflictsFailBuild || directConflictsFailBuild)) {
209            throw new MojoFailureException("Version conflict in direct dependencies detected!");
210        }
211
212        if (transitiveConflicts && conflictsFailBuild) {
213            throw new MojoFailureException("Version conflict in transitive dependencies detected!");
214        }
215    }
216}