From b57151657e85608b20bba732344d9ebfe3e1face Mon Sep 17 00:00:00 2001 From: Andrzej Jarmoniuk Date: Mon, 24 Oct 2022 13:54:46 +0200 Subject: [PATCH] Resolves #387: Provide an enforcer rule to specify a maximum number of allowed dependency updates --- pom.xml | 7 + .../invoker.properties | 2 + src/it/it-max-dependency-updates-001/pom.xml | 44 +++ .../invoker.properties | 2 + src/it/it-max-dependency-updates-002/pom.xml | 44 +++ .../invoker.properties | 2 + src/it/it-max-dependency-updates-003/pom.xml | 47 +++ .../invoker.properties | 2 + src/it/it-max-dependency-updates-004/pom.xml | 45 +++ .../enforcer/MaxDependencyUpdates.java | 366 ++++++++++++++++++ .../DisplayDependencyUpdatesMojo.java | 253 ++---------- .../versions/api/AbstractVersionDetails.java | 48 ++- .../mojo/versions/api/VersionDetails.java | 24 ++ .../versions/filtering/DependencyFilter.java | 49 ++- .../ordering/AbstractVersionComparator.java | 10 +- .../versions/ordering/VersionComparator.java | 33 -- .../versions/utils/MavenProjectUtils.java | 152 ++++++++ .../enforcer/max-dependency-updates.md | 109 ++++++ src/site/markdown/index.md | 6 + .../enforcer/MaxDependencyUpdatesTest.java | 314 +++++++++++++++ .../versions/api/ArtifactVersionsTest.java | 63 +++ .../mojo/max-dependency-upgrades/pom.xml | 44 +++ 22 files changed, 1403 insertions(+), 263 deletions(-) create mode 100644 src/it/it-max-dependency-updates-001/invoker.properties create mode 100644 src/it/it-max-dependency-updates-001/pom.xml create mode 100644 src/it/it-max-dependency-updates-002/invoker.properties create mode 100644 src/it/it-max-dependency-updates-002/pom.xml create mode 100644 src/it/it-max-dependency-updates-003/invoker.properties create mode 100644 src/it/it-max-dependency-updates-003/pom.xml create mode 100644 src/it/it-max-dependency-updates-004/invoker.properties create mode 100644 src/it/it-max-dependency-updates-004/pom.xml create mode 100644 src/main/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdates.java create mode 100644 src/main/java/org/codehaus/mojo/versions/utils/MavenProjectUtils.java create mode 100644 src/site/markdown/enforcer/max-dependency-updates.md create mode 100644 src/test/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdatesTest.java create mode 100644 src/test/resources/org/codehaus/mojo/max-dependency-upgrades/pom.xml diff --git a/pom.xml b/pom.xml index 4fc1fcde21..3db1a192dd 100644 --- a/pom.xml +++ b/pom.xml @@ -200,6 +200,13 @@ ${wagonVersion} + + + org.apache.maven.enforcer + enforcer-api + 3.1.0 + + org.apache.maven.doxia diff --git a/src/it/it-max-dependency-updates-001/invoker.properties b/src/it/it-max-dependency-updates-001/invoker.properties new file mode 100644 index 0000000000..38b185e579 --- /dev/null +++ b/src/it/it-max-dependency-updates-001/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = enforcer:enforce +invoker.buildResult = failure diff --git a/src/it/it-max-dependency-updates-001/pom.xml b/src/it/it-max-dependency-updates-001/pom.xml new file mode 100644 index 0000000000..feded7f7a8 --- /dev/null +++ b/src/it/it-max-dependency-updates-001/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + localhost + it-max-dependency-upgrades + 1.0 + pom + + + + localhost + dummy-api + 1.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + enforce + + + + + 0 + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + + + diff --git a/src/it/it-max-dependency-updates-002/invoker.properties b/src/it/it-max-dependency-updates-002/invoker.properties new file mode 100644 index 0000000000..a30e3767bb --- /dev/null +++ b/src/it/it-max-dependency-updates-002/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = enforcer:enforce +invoker.buildResult = success diff --git a/src/it/it-max-dependency-updates-002/pom.xml b/src/it/it-max-dependency-updates-002/pom.xml new file mode 100644 index 0000000000..c748a8293b --- /dev/null +++ b/src/it/it-max-dependency-updates-002/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + localhost + it-max-dependency-upgrades + 1.0 + pom + + + + localhost + dummy-api + 1.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + enforce + + + + + 1 + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + + + diff --git a/src/it/it-max-dependency-updates-003/invoker.properties b/src/it/it-max-dependency-updates-003/invoker.properties new file mode 100644 index 0000000000..a30e3767bb --- /dev/null +++ b/src/it/it-max-dependency-updates-003/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = enforcer:enforce +invoker.buildResult = success diff --git a/src/it/it-max-dependency-updates-003/pom.xml b/src/it/it-max-dependency-updates-003/pom.xml new file mode 100644 index 0000000000..3fdec699b0 --- /dev/null +++ b/src/it/it-max-dependency-updates-003/pom.xml @@ -0,0 +1,47 @@ + + 4.0.0 + localhost + it-max-dependency-upgrades + 1.0 + pom + + + + localhost + dummy-api + 1.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + enforce + + + + + 0 + + localhost:* + + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + + + diff --git a/src/it/it-max-dependency-updates-004/invoker.properties b/src/it/it-max-dependency-updates-004/invoker.properties new file mode 100644 index 0000000000..a30e3767bb --- /dev/null +++ b/src/it/it-max-dependency-updates-004/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = enforcer:enforce +invoker.buildResult = success diff --git a/src/it/it-max-dependency-updates-004/pom.xml b/src/it/it-max-dependency-updates-004/pom.xml new file mode 100644 index 0000000000..3fcfb07e0c --- /dev/null +++ b/src/it/it-max-dependency-updates-004/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + localhost + it-max-dependency-upgrades + 1.0 + pom + + + + localhost + dummy-impl + 2.0 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + enforce + + + + + 0 + true + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + + + diff --git a/src/main/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdates.java b/src/main/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdates.java new file mode 100644 index 0000000000..af1593a41c --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdates.java @@ -0,0 +1,366 @@ +package org.apache.maven.plugins.enforcer; +/* + * 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. + */ + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.apache.maven.artifact.manager.WagonManager; +import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException; +import org.apache.maven.artifact.metadata.ArtifactMetadataSource; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.resolver.ArtifactResolver; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.enforcer.rule.api.EnforcerRule; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.apache.maven.repository.RepositorySystem; +import org.apache.maven.settings.Settings; +import org.codehaus.mojo.versions.api.ArtifactVersions; +import org.codehaus.mojo.versions.api.DefaultVersionsHelper; +import org.codehaus.mojo.versions.api.Segment; +import org.codehaus.mojo.versions.api.VersionsHelper; +import org.codehaus.mojo.versions.utils.DependencyComparator; +import org.codehaus.mojo.versions.utils.MavenProjectUtils; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.codehaus.mojo.versions.api.Segment.INCREMENTAL; +import static org.codehaus.mojo.versions.api.Segment.MINOR; +import static org.codehaus.mojo.versions.api.Segment.SUBINCREMENTAL; +import static org.codehaus.mojo.versions.filtering.DependencyFilter.filterDependencies; +import static org.codehaus.mojo.versions.filtering.WildcardMatcher.WILDCARD; +import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractDependenciesFromDependencyManagement; +import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractDependenciesFromPlugins; +import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractPluginDependenciesFromPluginsInPluginManagement; + +public class MaxDependencyUpdates implements EnforcerRule +{ + /** + * Maximum allowed number of updates. + * + * @since 2.14.0 + */ + protected int maxUpdates = 0; + + /** + * Whether to process the dependencies section of the project. + * + * @since 2.14.0 + */ + protected boolean processDependencies = true; + + /** + * Whether to process the dependencyManagement section of the project. + * + * @since 1.2 + */ + protected boolean processDependencyManagement = true; + + /** + * Whether to process the dependencyManagement part transitive or not. + * In case of type {@code pom} and scope {@code import}, this means + * by default to report also the imported dependencies. + * If the parameter is set to {@code false}, the report will only show + * updates of the imported pom itself. + * + * @since 2.14.0 + */ + protected boolean processDependencyManagementTransitive = true; + + /** + * Whether to process the dependencies sections of plugins. + * + * @since 2.14.0 + */ + protected boolean processPluginDependencies = true; + + /** + * Whether to process the dependencies sections of plugins which are defined in pluginManagement. + * + * @since 2.14.0 + */ + protected boolean processPluginDependenciesInPluginManagement = true; + + /** + * Whether minor updates should be ignored. Default {@code false}. + * + *

Note: when {@code true}, will also assume that {@link #ignoreIncrementalUpdates} + * and {@link #ignoreSubIncrementalUpdates} are {@code true}.

+ * + * @since 2.14.0 + */ + protected boolean ignoreMinorUpdates = false; + + /** + * Whether incremental updates should be ignored. Default {@code false}. + * + *

Note: when {@code true}, will also assume that + * {@link #ignoreSubIncrementalUpdates} is {@code true}.

+ * + * @since 2.14.0 + */ + protected boolean ignoreIncrementalUpdates = false; + + /** + * Whether sub-incremental updates should be ignored. Default {@code false}. + * + * @since 2.14.0 + */ + protected boolean ignoreSubIncrementalUpdates = false; + + /** + * List of dependency inclusion patterns. + * Only dependencies matching all the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List dependencyIncludes = singletonList( WILDCARD ); + + /** + * List of dependency exclusion patterns. + * Only dependencies matching none of the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List dependencyExcludes = emptyList(); + + /** + * List of dependency management inclusion patterns. + * Only dependencies matching all the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List dependencyManagementIncludes = singletonList( WILDCARD ); + + /** + * List of dependency management exclusion patterns. + * Only dependencies matching none of the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List dependencyManagementExcludes = emptyList(); + + /** + * List of plugin dependency inclusion patterns. + * Only dependencies matching all the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List pluginDependencyIncludes = singletonList( WILDCARD ); + + /** + * List of plugin dependency exclusion patterns. + * Only dependencies matching none of the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List pluginDependencyExcludes = emptyList(); + + /** + * List of plugin management dependency inclusion patterns. + * Only dependencies matching all the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.14.0 + */ + protected List pluginManagementDependencyIncludes = singletonList( WILDCARD ); + + /** + * List of plugin dependency management exclusion patterns. + * Only dependencies matching none of the patterns will be considered. + * The wildcard "*" can be used as the only, first, last or both characters in each token. + * The version token does support version ranges. + * + * @since 2.12.0 + */ + protected List pluginManagementDependencyExcludes = emptyList(); + + /** + * Retrieves the maven project from metadata + * @param ruleHelper EnforcerRuleHelper object + * @return maven project + */ + private static MavenProject getMavenProject( EnforcerRuleHelper ruleHelper ) + { + try + { + return (MavenProject) ruleHelper.evaluate( "${project}" ); + } + catch ( ExpressionEvaluationException e ) + { + throw new RuntimeException( "Cannot evaluate project metadata", e ); + } + } + + /** + * Creates the VersionsHelper object + * @param ruleHelper EnforcerRuleHelper object + * @return VersionsHelper object + */ + @SuppressWarnings( "unchecked" ) + private static VersionsHelper createVersionsHelper( EnforcerRuleHelper ruleHelper ) + { + try + { + return new DefaultVersionsHelper.Builder() + .withRepositorySystem( ruleHelper.getComponent( RepositorySystem.class ) ) + .withArtifactResolver( ruleHelper.getComponent( ArtifactResolver.class ) ) + .withArtifactMetadataSource( ruleHelper.getComponent( ArtifactMetadataSource.class ) ) + .withRemoteArtifactRepositories( (List) + ruleHelper.evaluate( "${project.remoteArtifactRepositories}" ) ) + .withRemotePluginRepositories( (List) + ruleHelper.evaluate( "${project.remotePluginRepositories}" ) ) + .withLocalRepository( (ArtifactRepository) ruleHelper.evaluate( "${localRepository}" ) ) + .withWagonManager( ruleHelper.getComponent( WagonManager.class ) ) + .withSettings( (Settings) ruleHelper.evaluate( "${settings}" ) ) + .withServerId( (String) ruleHelper.evaluate( "${maven.version.rules.serverId}" ) ) + .withRulesUri( null ) + .withRuleSet( null ) + .withIgnoredVersions( null ) + .withLog( ruleHelper.getLog() ) + .withMavenSession( (MavenSession) ruleHelper.evaluate( "${session}" ) ) + .withMojoExecution( (MojoExecution) ruleHelper.evaluate( "${mojoExecution}" ) ) + .build(); + } + catch ( ExpressionEvaluationException e ) + { + throw new RuntimeException( "Cannot evaluate project metadata", e ); + } + catch ( ComponentLookupException | MojoExecutionException e ) + { + throw new RuntimeException( "Cannot resolve dependency", e ); + } + } + @Override + public boolean isCacheable() + { + return false; + } + + @Override + public boolean isResultValid( EnforcerRule enforcerRule ) + { + return false; + } + + @Override + public String getCacheId() + { + return "Does not matter as not cacheable"; + } + + @Override + public void execute( EnforcerRuleHelper ruleHelper ) throws EnforcerRuleException + { + VersionsHelper versionsHelper = createVersionsHelper( ruleHelper ); + MavenProject project = getMavenProject( ruleHelper ); + Set dependencies = new TreeSet<>( DependencyComparator.INSTANCE ); + if ( processDependencyManagement ) + { + try + { + dependencies.addAll( filterDependencies( extractDependenciesFromDependencyManagement( project, + processDependencyManagementTransitive, ruleHelper.getLog() ), + dependencyManagementIncludes, dependencyManagementExcludes, "Dependency Management", + ruleHelper.getLog() ) ); + } + catch ( MavenProjectUtils.VersionRetrievalException e ) + { + throw new EnforcerRuleException( e.getMessage() ); + } + } + if ( processPluginDependencies ) + { + dependencies.addAll( filterDependencies( extractDependenciesFromPlugins( project ), + pluginDependencyIncludes, pluginDependencyExcludes, + "Plugin Dependencies", ruleHelper.getLog() ) ); + } + if ( processPluginDependenciesInPluginManagement ) + { + dependencies.addAll( filterDependencies( + extractPluginDependenciesFromPluginsInPluginManagement( project ), + pluginManagementDependencyIncludes, pluginManagementDependencyExcludes, + "Plugin Management Dependencies", ruleHelper.getLog() ) ); + } + if ( processDependencies ) + { + dependencies.addAll( filterDependencies( project.getDependencies(), + dependencyIncludes, dependencyExcludes, "Dependencies", ruleHelper.getLog() ) ); + } + try + { + Optional ignoredSegment = ignoreSubIncrementalUpdates + ? of( SUBINCREMENTAL ) + : ignoreIncrementalUpdates + ? of( INCREMENTAL ) + : ignoreMinorUpdates + ? of( MINOR ) + : empty(); + List upgradable = versionsHelper + .lookupDependenciesUpdates( dependencies, false ) + .values() + .parallelStream() + .filter( v -> + v.getVersions( v.restrictionForIgnoreScope( ignoredSegment ), true ).length > 0 + ) + .collect( Collectors.toList() ); + if ( upgradable.size() > maxUpdates ) + { + throw new EnforcerRuleException( "More than " + maxUpdates + " upgradable artifacts detected: " + + upgradable.stream().map( av -> av.getArtifact() + " -> [" + + Arrays.stream( av.getVersions() ) + .map( ArtifactVersion::toString ) + .collect( Collectors.joining( ", " ) ) + + "]" ) + .collect( Collectors.joining( ", " ) ) ); + } + } + catch ( ArtifactMetadataRetrievalException e ) + { + throw new RuntimeException( e ); + } + } +} diff --git a/src/main/java/org/codehaus/mojo/versions/DisplayDependencyUpdatesMojo.java b/src/main/java/org/codehaus/mojo/versions/DisplayDependencyUpdatesMojo.java index fa246271b1..dfdf9c2e02 100644 --- a/src/main/java/org/codehaus/mojo/versions/DisplayDependencyUpdatesMojo.java +++ b/src/main/java/org/codehaus/mojo/versions/DisplayDependencyUpdatesMojo.java @@ -29,7 +29,6 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; -import java.util.stream.Collectors; import org.apache.maven.artifact.ArtifactUtils; import org.apache.maven.artifact.manager.WagonManager; @@ -37,30 +36,31 @@ import org.apache.maven.artifact.metadata.ArtifactMetadataSource; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.artifact.versioning.ArtifactVersion; -import org.apache.maven.model.Build; import org.apache.maven.model.Dependency; -import org.apache.maven.model.DependencyManagement; -import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectBuilder; import org.apache.maven.repository.RepositorySystem; import org.codehaus.mojo.versions.api.ArtifactVersions; import org.codehaus.mojo.versions.api.Segment; -import org.codehaus.mojo.versions.filtering.DependencyFilter; import org.codehaus.mojo.versions.filtering.WildcardMatcher; import org.codehaus.mojo.versions.rewriting.ModifiedPomXMLEventReader; import org.codehaus.mojo.versions.utils.DependencyComparator; +import org.codehaus.mojo.versions.utils.MavenProjectUtils; import org.codehaus.mojo.versions.utils.SegmentUtils; import org.codehaus.plexus.util.StringUtils; +import static java.util.Collections.emptySet; import static java.util.Optional.empty; import static java.util.Optional.of; import static org.apache.commons.lang3.StringUtils.countMatches; import static org.codehaus.mojo.versions.api.Segment.MAJOR; +import static org.codehaus.mojo.versions.filtering.DependencyFilter.filterDependencies; +import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractDependenciesFromDependencyManagement; +import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractDependenciesFromPlugins; +import static org.codehaus.mojo.versions.utils.MavenProjectUtils.extractPluginDependenciesFromPluginsInPluginManagement; /** * Displays all dependencies that have newer versions available. @@ -356,68 +356,6 @@ public DisplayDependencyUpdatesMojo( RepositorySystem repositorySystem, super( repositorySystem, projectBuilder, artifactMetadataSource, wagonManager, artifactResolver ); } - private static Set extractPluginDependenciesFromPluginsInPluginManagement( Build build ) - { - Set result = new TreeSet<>( DependencyComparator.INSTANCE ); - if ( build.getPluginManagement() != null ) - { - for ( Plugin plugin : build.getPluginManagement().getPlugins() ) - { - if ( plugin.getDependencies() != null && !plugin.getDependencies().isEmpty() ) - { - result.addAll( plugin.getDependencies() ); - } - } - } - return result; - } - - private static Set extractDependenciesFromPlugins( List plugins ) - { - Set result = new TreeSet<>( DependencyComparator.INSTANCE ); - for ( Plugin plugin : plugins ) - { - if ( plugin.getDependencies() != null && !plugin.getDependencies().isEmpty() ) - { - result.addAll( plugin.getDependencies() ); - } - } - return result; - } - - /** - * Returns a set of dependencies where the dependencies which are defined in the dependency management section have - * been filtered out. - * - * @param dependencies The set of dependencies. - * @param dependencyManagement The set of dependencies from the dependency management section. - * @return A new set of dependencies which are from the set of dependencies but not from the set of dependency - * management dependencies. - * @since 1.0-beta-1 - */ - private static Set removeDependencyManagment( Set dependencies, - Set dependencyManagement ) - { - Set result = new TreeSet<>( DependencyComparator.INSTANCE ); - for ( Dependency dependency : dependencies ) - { - boolean matched = false; - for ( Dependency managedDependency : dependencyManagement ) - { - if ( dependenciesMatch( dependency, managedDependency ) ) - { - matched = true; - break; - } - } - if ( !matched ) - { - result.add( dependency ); - } - } - return result; - } - // open for tests protected static boolean dependenciesMatch( Dependency dependency, Dependency managedDependency ) { @@ -491,108 +429,51 @@ public void execute() validateInput(); - Set dependencyManagement = new TreeSet<>( DependencyComparator.INSTANCE ); - DependencyManagement projectDependencyManagement = getProjectDependencyManagement( getProject() ); - if ( projectDependencyManagement != null ) - { - - List dependenciesFromPom = projectDependencyManagement.getDependencies(); - for ( Dependency dependency : dependenciesFromPom ) - { - getLog().debug( "dependency from pom: " + dependency.getGroupId() + ":" + dependency.getArtifactId() - + ":" + dependency.getVersion() + ":" + dependency.getScope() ); - if ( dependency.getVersion() == null ) - { - // get parent and get the information from there. - if ( getProject().hasParent() ) - { - getLog().debug( "Reading parent dependencyManagement information" ); - DependencyManagement parentProjectDependencyManagement = - getProjectDependencyManagement( getProject().getParent() ); - if ( parentProjectDependencyManagement != null ) - { - List parentDeps = parentProjectDependencyManagement.getDependencies(); - for ( Dependency parentDep : parentDeps ) - { - // only groupId && artifactId needed cause version is null - if ( dependency.getGroupId().equals( parentDep.getGroupId() ) - && dependency.getArtifactId().equals( parentDep.getArtifactId() ) - && dependency.getType().equals( parentDep.getType() ) ) - { - dependencyManagement.add( parentDep ); - } - } - } - } - else - { - String message = "We can't get the version for the dependency " + dependency.getGroupId() + ":" - + dependency.getArtifactId() + " cause there does not exist a parent."; - getLog().error( message ); - // Throw error cause we will not able to get a version for a dependency. - throw new MojoExecutionException( message ); - } - } - else - { - dependencyManagement.add( dependency ); - } - } - } - - Set dependencies = new TreeSet<>( DependencyComparator.INSTANCE ); - dependencies.addAll( getProject().getDependencies() ); - - if ( isProcessingDependencyManagement() ) - { - dependencies = removeDependencyManagment( dependencies, dependencyManagement ); - } - - Set pluginDependencies = new TreeSet<>( DependencyComparator.INSTANCE ); - - if ( isProcessingPluginDependencies() ) - { - pluginDependencies = extractDependenciesFromPlugins( getProject().getBuildPlugins() ); - } - - Set pluginDependenciesInPluginManagement = new TreeSet<>( DependencyComparator.INSTANCE ); - if ( isProcessPluginDependenciesInDependencyManagement() ) - { - pluginDependenciesInPluginManagement = - extractPluginDependenciesFromPluginsInPluginManagement( getProject().getBuild() ); - } + Set dependencyManagement = emptySet(); try { if ( isProcessingDependencyManagement() ) { - dependencyManagement = filterDependencyManagementIncludes( dependencyManagement ); + dependencyManagement = filterDependencies( extractDependenciesFromDependencyManagement( getProject(), + processDependencyManagementTransitive, getLog() ), + dependencyManagementIncludes, dependencyManagementExcludes, "Dependecy Management", + getLog() ); - logUpdates( getHelper().lookupDependenciesUpdates( dependencyManagement, false ), - "Dependency Management" ); + logUpdates( getHelper().lookupDependenciesUpdates( dependencyManagement, + false ), "Dependency Management" ); } if ( isProcessingDependencies() ) { - dependencies = filterDependencyIncludes( dependencies ); - - logUpdates( getHelper().lookupDependenciesUpdates( dependencies, false ), "Dependencies" ); + Set finalDependencyManagement = dependencyManagement; + logUpdates( getHelper().lookupDependenciesUpdates( + filterDependencies( getProject().getDependencies() + .parallelStream() + .filter( dep -> finalDependencyManagement.parallelStream() + .noneMatch( depMan -> dependenciesMatch( dep, depMan ) ) ) + .collect( () -> new TreeSet<>( DependencyComparator.INSTANCE ), Set::add, Set::addAll ), + dependencyIncludes, dependencyExcludes, "Dependencies", getLog() ), + false ), + "Dependencies" ); } if ( isProcessPluginDependenciesInDependencyManagement() ) { - pluginDependenciesInPluginManagement = - filterPluginManagementIncludes( pluginDependenciesInPluginManagement ); - - logUpdates( getHelper().lookupDependenciesUpdates( pluginDependenciesInPluginManagement, false ), - "pluginManagement of plugins" ); + logUpdates( getHelper().lookupDependenciesUpdates( filterDependencies( + extractPluginDependenciesFromPluginsInPluginManagement( getProject() ), + pluginManagementDependencyIncludes, pluginManagementDependencyExcludes, + "Plugin Management Dependencies", getLog() ), false ), + "pluginManagement of plugins" ); } if ( isProcessingPluginDependencies() ) { - pluginDependencies = filterPluginDependencyIncludes( pluginDependencies ); - - logUpdates( getHelper().lookupDependenciesUpdates( pluginDependencies, false ), "Plugin Dependencies" ); + logUpdates( getHelper().lookupDependenciesUpdates( filterDependencies( + extractDependenciesFromPlugins( getProject() ), + pluginDependencyIncludes, pluginDependencyExcludes, "Plugin Dependencies", + getLog() ), false ), + "Plugin Dependencies" ); } } - catch ( ArtifactMetadataRetrievalException e ) + catch ( ArtifactMetadataRetrievalException | MavenProjectUtils.VersionRetrievalException e ) { throw new MojoExecutionException( e.getMessage(), e ); } @@ -626,72 +507,6 @@ static void validateGAVList( List gavList, int numSections, String argum } } - private Set filterDependencyIncludes( Set dependencies ) - { - return filterDependencies( dependencies, dependencyIncludes, dependencyExcludes, "Dependencies" ); - } - - private Set filterDependencyManagementIncludes( Set dependencyManagement ) - { - return filterDependencies( dependencyManagement, - dependencyManagementIncludes, dependencyManagementExcludes, "Dependecy Management" ); - } - - private Set filterPluginDependencyIncludes( Set dependencies ) - { - return filterDependencies( dependencies, pluginDependencyIncludes, pluginDependencyExcludes, - "Plugin Dependencies" ); - } - - private Set filterPluginManagementIncludes( Set dependencyManagement ) - { - return filterDependencies( dependencyManagement, - pluginManagementDependencyIncludes, pluginManagementDependencyExcludes, - "Plugin Management Dependencies" ); - } - - private Set filterDependencies( - Set dependencies, - List includes, - List excludes, - String section - ) - { - DependencyFilter includeDeps = DependencyFilter.parseFrom( includes ); - DependencyFilter excludeDeps = DependencyFilter.parseFrom( excludes ); - - Set filtered = includeDeps.retainingIn( dependencies ); - filtered = excludeDeps.removingFrom( filtered ); - - if ( getLog().isDebugEnabled() ) - { - getLog().debug( String.format( "parsed includes in %s: %s -> %s", section, includes, includeDeps ) ); - getLog().debug( String.format( "parsed excludes in %s: %s -> %s", section, excludes, excludeDeps ) ); - getLog().debug( String.format( "Unfiltered %s: ", section ) + output( dependencies ) ); - getLog().debug( String.format( "Filtered %s: ", section ) + output( filtered ) ); - } - - return filtered; - } - - private String output( Set dependencies ) - { - return dependencies.stream() - .map( d -> String.format( "%s:%s:%s", d.getGroupId(), d.getArtifactId(), d.getVersion() ) ) - .collect( Collectors.joining( ", " ) ); - } - private DependencyManagement getProjectDependencyManagement( MavenProject project ) - { - if ( processDependencyManagementTransitive ) - { - return project.getDependencyManagement(); - } - else - { - return project.getOriginalModel().getDependencyManagement(); - } - } - private Optional calculateUpdateScope() { return allowAnyUpdates diff --git a/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java b/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java index 1d3f9665fd..9b43da9a85 100644 --- a/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java +++ b/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java @@ -34,6 +34,7 @@ import static java.util.Collections.reverseOrder; import static java.util.Optional.empty; import static java.util.Optional.of; +import static org.codehaus.mojo.versions.api.Segment.MAJOR; import static org.codehaus.mojo.versions.api.Segment.SUBINCREMENTAL; /** @@ -72,6 +73,49 @@ protected AbstractVersionDetails() { } + /** + *

Returns a {@linkplain Restriction} object for computing version upgrades + * with the given segment allowing updates, with all more major segments locked in place.

+ *

The resulting restriction could be thought of as one + * retaining the versions on positions up to the held position, + * the position right after the position held in place will be incremented by one, + * and on all positions which are more minor than that, the range would contain -∞ + * for the bottom bound and +∞ for the above bound.

+ *

This will allow matching the required versions while not matching versions which are considered + * inferior than the zeroth version, i.e. versions with a qualifier.

+ * + * @param scope most major segment where updates are allowed Optional.empty() for no restriction + * @return {@linkplain Restriction} object based on the arguments + */ + @Override + public Restriction restrictionFor( Optional scope ) + throws InvalidSegmentException + { + ArtifactVersion nextVersion = scope + .filter( s -> s.isMajorTo( SUBINCREMENTAL ) ) + .map( s -> (ArtifactVersion) + new BoundArtifactVersion( currentVersion, Segment.of( s.value() + 1 ) ) ) + .orElse( currentVersion ); + return new Restriction( nextVersion, false, scope.filter( MAJOR::isMajorTo ) + .map( s -> (ArtifactVersion) new BoundArtifactVersion( currentVersion, s ) ).orElse( null ), + false ); + } + + /** + * Returns the {@link Restriction} objects for a segemnt scope which is to be ignored. + * + * @param ignored most major segment where updates are to be ignored; Optional.empty() for no ignored segments + * @return {@linkplain Restriction} object based on the arguments + */ + @Override + public Restriction restrictionForIgnoreScope( Optional ignored ) + { + ArtifactVersion nextVersion = ignored + .map( s -> (ArtifactVersion) new BoundArtifactVersion( currentVersion, s ) ) + .orElse( currentVersion ); + return new Restriction( nextVersion, false, null, false ); + } + @Override public final boolean isCurrentVersionDefined() { @@ -281,7 +325,7 @@ public final ArtifactVersion getNewestUpdate( ArtifactVersion currentVersion, Op { try { - return getNewestVersion( getVersionComparator().restrictionFor( currentVersion, updateScope ), + return getNewestVersion( restrictionFor( updateScope ), includeSnapshots ); } catch ( InvalidSegmentException e ) @@ -296,7 +340,7 @@ public final ArtifactVersion[] getAllUpdates( ArtifactVersion currentVersion, Op { try { - return getVersions( getVersionComparator().restrictionFor( currentVersion, updateScope ), + return getVersions( restrictionFor( updateScope ), includeSnapshots ); } catch ( InvalidSegmentException e ) diff --git a/src/main/java/org/codehaus/mojo/versions/api/VersionDetails.java b/src/main/java/org/codehaus/mojo/versions/api/VersionDetails.java index 265b4fcfaa..aed62eac93 100644 --- a/src/main/java/org/codehaus/mojo/versions/api/VersionDetails.java +++ b/src/main/java/org/codehaus/mojo/versions/api/VersionDetails.java @@ -375,4 +375,28 @@ ArtifactVersion[] getAllUpdates( Optional updateScope, boolean includeS */ ArtifactVersion[] getAllUpdates( VersionRange versionRange, boolean includeSnapshots ); + /** + *

Returns a {@linkplain Restriction} object for computing version upgrades + * with the given segment allowing updates, with all more major segments locked in place.

+ *

The resulting restriction could be thought of as one + * retaining the versions on positions up to the held position, + * the position right after the position held in place will be incremented by one, + * and on all positions which are more minor than that, the range would contain -∞ + * for the bottom bound and +∞ for the above bound.

+ *

This will allow matching the required versions while not matching versions which are considered + * inferior than the zeroth version, i.e. versions with a qualifier.

+ * + * @param scope most major segment where updates are allowed Optional.empty() for no restriction + * @return {@linkplain Restriction} object based on the arguments + */ + Restriction restrictionFor( Optional scope ) throws InvalidSegmentException; + + + /** + * Returns the {@link Restriction} objects for a segemnt scope which is to be ignored. + * + * @param ignored most major segment where updates are to be ignored; Optional.empty() for no ignored segments + * @return {@linkplain Restriction} object based on the arguments + */ + Restriction restrictionForIgnoreScope( Optional ignored ); } diff --git a/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java b/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java index 07bf61617e..6f9b2874e5 100644 --- a/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java +++ b/src/main/java/org/codehaus/mojo/versions/filtering/DependencyFilter.java @@ -1,5 +1,6 @@ package org.codehaus.mojo.versions.filtering; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -7,6 +8,7 @@ import java.util.stream.Collectors; import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.logging.Log; import org.codehaus.mojo.versions.utils.DependencyComparator; public class DependencyFilter @@ -43,12 +45,12 @@ public String toString() return String.format( "%s{%s}", getClass().getSimpleName(), pattern ); } - public Set retainingIn( Set dependencies ) + public Set retainingIn( Collection dependencies ) { return filterBy( dependencies, this::matchersMatch ); } - public Set removingFrom( Set dependencies ) + public Set removingFrom( Collection dependencies ) { return filterBy( dependencies, not( this::matchersMatch ) ); } @@ -58,10 +60,51 @@ private boolean matchersMatch( Dependency dependency ) return matchers.stream().anyMatch( m -> m.test( dependency ) ); } - private TreeSet filterBy( Set dependencies, Predicate predicate ) + private TreeSet filterBy( Collection dependencies, Predicate predicate ) { return dependencies.stream() .filter( predicate ) .collect( Collectors.toCollection( () -> new TreeSet<>( DependencyComparator.INSTANCE ) ) ); } + + /** + * Returns a set of dependencies filtered by the given include- and exclude filters. + * @param dependencies collection of dependencies to filter + * @param includes a list of dependency includes + * @param excludes a list of dependency excludes + * @param section if log is not null, dependency section name for the debug log + * @param log null or log to which debug information will be logged + * @return filtered set of dependencies + */ + public static Set filterDependencies( + Collection dependencies, + List includes, + List excludes, + String section, + Log log + ) + { + DependencyFilter includeDeps = DependencyFilter.parseFrom( includes ); + DependencyFilter excludeDeps = DependencyFilter.parseFrom( excludes ); + + Set filtered = includeDeps.retainingIn( dependencies ); + filtered = excludeDeps.removingFrom( filtered ); + + if ( log != null && log.isDebugEnabled() ) + { + log.debug( String.format( "parsed includes in %s: %s -> %s", section, includes, includeDeps ) ); + log.debug( String.format( "parsed excludes in %s: %s -> %s", section, excludes, excludeDeps ) ); + log.debug( String.format( "Unfiltered %s: ", section ) + output( dependencies ) ); + log.debug( String.format( "Filtered %s: ", section ) + output( filtered ) ); + } + + return filtered; + } + + private static String output( Collection dependencies ) + { + return dependencies.stream() + .map( d -> String.format( "%s:%s:%s", d.getGroupId(), d.getArtifactId(), d.getVersion() ) ) + .collect( Collectors.joining( ", " ) ); + } } diff --git a/src/main/java/org/codehaus/mojo/versions/ordering/AbstractVersionComparator.java b/src/main/java/org/codehaus/mojo/versions/ordering/AbstractVersionComparator.java index 30a4835026..75b659e047 100644 --- a/src/main/java/org/codehaus/mojo/versions/ordering/AbstractVersionComparator.java +++ b/src/main/java/org/codehaus/mojo/versions/ordering/AbstractVersionComparator.java @@ -29,14 +29,10 @@ public abstract class AbstractVersionComparator implements VersionComparator { - /** - * {@inheritDoc} - */ + @Override public abstract int compare( ArtifactVersion o1, ArtifactVersion o2 ); - /** - * {@inheritDoc} - */ + @Override public final int getSegmentCount( ArtifactVersion v ) { if ( v == null ) @@ -58,6 +54,7 @@ public final int getSegmentCount( ArtifactVersion v ) * * @return the hash code. */ + @Override public int hashCode() { return getClass().hashCode(); @@ -71,6 +68,7 @@ public int hashCode() * @see #hashCode() * @see java.util.Hashtable */ + @Override public boolean equals( Object obj ) { return obj == this || ( obj != null && getClass().equals( obj.getClass() ) ); diff --git a/src/main/java/org/codehaus/mojo/versions/ordering/VersionComparator.java b/src/main/java/org/codehaus/mojo/versions/ordering/VersionComparator.java index 91a40848c8..1ca3e6b075 100644 --- a/src/main/java/org/codehaus/mojo/versions/ordering/VersionComparator.java +++ b/src/main/java/org/codehaus/mojo/versions/ordering/VersionComparator.java @@ -20,14 +20,8 @@ */ import java.util.Comparator; -import java.util.Optional; import org.apache.maven.artifact.versioning.ArtifactVersion; -import org.apache.maven.artifact.versioning.Restriction; -import org.codehaus.mojo.versions.api.Segment; - -import static org.codehaus.mojo.versions.api.Segment.MAJOR; -import static org.codehaus.mojo.versions.api.Segment.SUBINCREMENTAL; /** * A rule for comparing and manipulating versions. @@ -43,31 +37,4 @@ public interface VersionComparator * @since 1.0-beta-1 */ int getSegmentCount( ArtifactVersion artifactVersion ); - - /** - *

Returns a {@linkplain Restriction} object for computing version upgrades - * with the given segment allowing updates, with all more major segments locked in place.

- *

The resulting restriction could be thought of as one - * retaining the versions on positions up to the held position, - * the position right after the position held in place will be incremented by one, - * and on all positions which are more minor than that, the range would contain -∞ - * for the bottom bound and +∞ for the above bound.

- *

This will allow matching the required versions while not matching versions which are considered - * inferior than the zeroth version, i.e. versions with a qualifier.

- * - * @param currentVersion The current version. - * @param scope most major segment where updates are allowed Optional.empty() for no restriction - * @return {@linkplain Restriction} object based on the arguments - */ - default Restriction restrictionFor( ArtifactVersion currentVersion, Optional scope ) - throws InvalidSegmentException - { - ArtifactVersion nextVersion = scope.filter( s -> s.isMajorTo( SUBINCREMENTAL ) ) - .map( s -> (ArtifactVersion) - new BoundArtifactVersion( currentVersion, Segment.of( s.value() + 1 ) ) ) - .orElse( currentVersion ); - return new Restriction( nextVersion, false, scope.filter( MAJOR::isMajorTo ) - .map( s -> (ArtifactVersion) new BoundArtifactVersion( currentVersion, s ) ).orElse( null ), - false ); - } } diff --git a/src/main/java/org/codehaus/mojo/versions/utils/MavenProjectUtils.java b/src/main/java/org/codehaus/mojo/versions/utils/MavenProjectUtils.java new file mode 100644 index 0000000000..14878e721a --- /dev/null +++ b/src/main/java/org/codehaus/mojo/versions/utils/MavenProjectUtils.java @@ -0,0 +1,152 @@ +package org.codehaus.mojo.versions.utils; +/* + * 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. + */ + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; + +import static java.util.Collections.emptySet; + +/** + * Utility methods for extracting dependencies from a {@link org.apache.maven.project.MavenProject} + */ +public class MavenProjectUtils +{ + /** + * Retrieves dependencies from the plugins section + * @param project {@link MavenProject} instance + * @return set of {@link Dependency} objects + * or an empty set if none have been retrieveddependencies or an empty set if none have been retrieved + */ + public static Set extractPluginDependenciesFromPluginsInPluginManagement( MavenProject project ) + { + return project.getBuild() == null || project.getBuild().getPluginManagement() == null + || project.getBuild().getPluginManagement().getPlugins() == null + ? emptySet() + : project.getBuild().getPluginManagement().getPlugins().parallelStream() + .filter( plugin -> plugin.getDependencies() != null ) + .flatMap( plugin -> plugin.getDependencies().stream() ) + .collect( () -> new TreeSet<>( DependencyComparator.INSTANCE ), Set::add, Set::addAll ); + } + + /** + * Retrieves dependencies from plugin management + * @param project {@link MavenProject} instance + * @return set of {@link Dependency} objects + * or an empty set if none have been retrieveddependencies or an empty set if none have been retrieved + */ + public static Set extractDependenciesFromPlugins( MavenProject project ) + { + return project.getBuildPlugins().parallelStream() + .filter( plugin -> plugin.getDependencies() != null ) + .flatMap( plugin -> plugin.getDependencies().stream() ) + .collect( () -> new TreeSet<>( DependencyComparator.INSTANCE ), Set::add, Set::addAll ); + } + + /** + * Retrieves dependencies from the dependency management of the project + * as well as its immediate parent project. + * + * @param project {@link MavenProject} instance + * @param processDependencyManagementTransitive if {@code true}, the original model will be considered + * instead of the interpolated model, which does not contain + * imported dependencies + * @return set of {@link Dependency} objects + * or an empty set if none have been retrieveddependencies or an empty set if none have been retrieved + */ + public static Set extractDependenciesFromDependencyManagement( MavenProject project, + boolean processDependencyManagementTransitive, Log log ) + throws VersionRetrievalException + { + Set dependencyManagement = new TreeSet<>( DependencyComparator.INSTANCE ); + DependencyManagement projectDependencyManagement = processDependencyManagementTransitive + ? project.getDependencyManagement() + : project.getOriginalModel().getDependencyManagement(); + if ( projectDependencyManagement != null ) + { + + List dependenciesFromPom = projectDependencyManagement.getDependencies(); + for ( Dependency dependency : dependenciesFromPom ) + { + log.debug( "dependency from pom: " + dependency.getGroupId() + ":" + dependency.getArtifactId() + + ":" + dependency.getVersion() + ":" + dependency.getScope() ); + if ( dependency.getVersion() == null ) + { + // get parent and get the information from there. + if ( project.hasParent() ) + { + log.debug( "Reading parent dependencyManagement information" ); + DependencyManagement parentProjectDependencyManagement = + processDependencyManagementTransitive + ? project.getParent().getDependencyManagement() + : project.getParent().getOriginalModel().getDependencyManagement(); + if ( parentProjectDependencyManagement != null ) + { + List parentDeps = parentProjectDependencyManagement.getDependencies(); + for ( Dependency parentDep : parentDeps ) + { + // only groupId && artifactId needed cause version is null + if ( dependency.getGroupId().equals( parentDep.getGroupId() ) + && dependency.getArtifactId().equals( parentDep.getArtifactId() ) + && dependency.getType().equals( parentDep.getType() ) ) + { + dependencyManagement.add( parentDep ); + } + } + } + } + else + { + String message = "We can't get the version for the dependency " + dependency.getGroupId() + ":" + + dependency.getArtifactId() + " because there does not exist a parent."; + log.error( message ); + // Throw error because we will not able to get a version for a dependency. + throw new VersionRetrievalException( message ); + } + } + else + { + dependencyManagement.add( dependency ); + } + } + } + return dependencyManagement; + } + + /** + * Exception thrown if a version of a dependency cannot be found + */ + public static class VersionRetrievalException extends Exception + { + /** + * Creates a new instance of the exception object with the given message + * @param message exception message + */ + public VersionRetrievalException( String message ) + { + super( message ); + } + } +} diff --git a/src/site/markdown/enforcer/max-dependency-updates.md b/src/site/markdown/enforcer/max-dependency-updates.md new file mode 100644 index 0000000000..272f013017 --- /dev/null +++ b/src/site/markdown/enforcer/max-dependency-updates.md @@ -0,0 +1,109 @@ +title: Introduction +author: Andrzej Jarmoniuk +date: 2022-10-27 + + + +# Max Dependency Updates + +This Maven Enforcer rule checks if the number of dependency updates does not exceed the given threshold. + +The following parameters are supported by this rule: + +| Parameter | Default | Description | +|-----------------------------------------------|:-------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `maxUpdates` | `0` | The total maximum allowed number of dependency updates. | +| `processDependencies` | `true` | Whether to process the dependencies section of the project. | +| `processDependencyManagement` | `true` | Whether to process the dependencyManagement section of the project. | +| `processDependencyManagementTransitive` | `true` | Whether to process the dependencyManagement part transitive or not. In case of *type* `pom` and *scope* `import`, this means by default to report also the imported dependencies. If the parameter is set to false the report will only show updates of the imported pom itself. | +| `processPluginDependencies` | `true` | Whether to process the dependencies sections of plugins. | +| `processPluginDependenciesInPluginManagement` | `true` | Whether to process the dependencies sections of plugins which are defined in pluginManagement. | +| `ignoreMinorUpdates` | `false` | Whether minor updates should be ignored. Default `false`.
**Note:** when true, will also assume that `ignoreIncrementalUpdates` and `ignoreSubIncrementalUpdates` are also `true`. | +| `ignoreIncrementalUpdates` | `false` | Whether incremental updates should be ignored. Default `false`.
**Note:** when true, will also assume that `ignoreSubIncrementalUpdates` is also `true`. | +| `ignoreSubIncrementalUpdates` | `false` | Whether sub-incremental updates should be ignored. Default `false`. | +| `dependencyIncludes` | `*` | List of dependency inclusion patterns. Only dependencies matching all the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `dependencyExcludes` | (empty) | List of dependency exclusion patterns. Only dependencies matching none of the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `dependencyManagementIncludes` | `*` | List of dependency management inclusion patterns. Only dependencies matching all the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `dependencyManagementExcludes` | (empty) | List of dependency management exclusion patterns. Only dependencies matching none of the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `pluginDependencyIncludes` | `*` | List of plugin dependency inclusion patterns. Only dependencies matching all the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `pluginDependencyExcludes` | (empty) | List of plugin dependency exclusion patterns. Only dependencies matching none of the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `pluginManagementDependencyIncludes` | `*` | List of plugin management dependency inclusion patterns. Only dependencies matching all the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | +| `pluginManagementDependencyExcludes` | (empty) | List of plugin management dependency exclusion patterns. Only dependencies matching none of the patterns will be considered.
The wildcard (`*`) can be used as the only, first, last or both characters in each token. The version token does support version ranges. | + +**Note:** Inclusion/exclusion parameters like `dependencyIncludes`, `dependencyExcludes`, etc. work the same way as parameters +of the same name of the [versions:display-dependency-updates](../display-dependency-updates-mojo.html) goal +of the plugin. + +The parameters accept a list of *extended GAV* patterns, meaning patterns of: + +`groupId:artifactId:version:type:classifier:scope` + +of which only `groupId` is obligatory. On top of that, all of the components can be replaced with the asterisk (`*`) +character in which case it will match all values. + +So, e.g. both of the below patterns: +- `org.codehaus.mojo` +- `org.codehaus.mojo:*` + +will match all artifacts with groupId `org.codehaus.mojo`. + +### Sample Plugin Configuration + +Below a rundimentary example of using the enforcer rule. The example assumes that the versions of both +the enforcer plugin as well as the versions plugin are set elsewhere (using dependencyManagement). + +The below example specifies a rule which will not allow any updates except for updates of `localhost:dummy-api`. +It will also ignore all sub-incremental updates. + +```xml + + + ... + + + + org.apache.maven.plugins + maven-enforcer-plugin + + enforce + + + + + 0 + + localhost:dummy-api + + true + + + + + + org.codehaus.mojo + versions-maven-plugin + + + + + + + +``` \ No newline at end of file diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index cf780e9359..2d29145e99 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -87,6 +87,12 @@ The Versions Plugin has the following reporting goals. * [versions:parent-updates-report](./parent-updates-report-mojo.html) produces a report on possible parent artifact upgrades. +## Enforcer rules overview + +The Versions Plugin currently provides one Maven Enforcer Plugin rule: +* [maxDependencyUpdates](./enforcer/max-dependency-updates.html) allows the user to specify a maximum number of updates which, + if exceeded, will trigger the enforcer plugin to fail. + ## Usage General instructions on how to use the Versions Plugin can be found on the [usage page](./usage.html). Some more diff --git a/src/test/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdatesTest.java b/src/test/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdatesTest.java new file mode 100644 index 0000000000..b98b32b6e0 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/enforcer/MaxDependencyUpdatesTest.java @@ -0,0 +1,314 @@ +package org.apache.maven.plugins.enforcer; +/* + * 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. + */ + +import java.util.Arrays; +import java.util.HashMap; + +import org.apache.maven.artifact.metadata.ArtifactMetadataSource; +import org.apache.maven.artifact.resolver.ArtifactResolver; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.testing.stubs.StubArtifactRepository; +import org.apache.maven.project.MavenProject; +import org.apache.maven.repository.RepositorySystem; +import org.apache.maven.settings.Settings; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import static java.util.Collections.singletonList; +import static org.codehaus.mojo.versions.utils.DependencyBuilder.dependencyWith; +import static org.codehaus.mojo.versions.utils.MockUtils.mockArtifactMetadataSource; +import static org.codehaus.mojo.versions.utils.MockUtils.mockRepositorySystem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MaxDependencyUpdatesTest +{ + private static Settings createSettings() + { + return new Settings(); + } + + private static MavenSession createSession() + { + return mock( MavenSession.class ); + } + + private static MojoExecution createMojoExecution() + { + return mock( MojoExecution.class ); + } + + + private static EnforcerRuleHelper mockRuleHelper( MavenProject mavenProject, + ArtifactMetadataSource artifactMetadataSource ) + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mock( EnforcerRuleHelper.class ); + when( ruleHelper.evaluate( anyString() ) ) + .then( ( a ) -> "${project}".equals( a.getArgument( 0 ) ) + ? mavenProject + : "${localRepository}".equals( a.getArgument( 0 ) ) + ? new StubArtifactRepository( "" ) + : "${settings}".equals( a.getArgument( 0 ) ) + ? createSettings() + : "${session}".equals( a.getArgument( 0 ) ) + ? createSession() + : "${mojoExecution}".equals( a.getArgument( 0 ) ) + ? createMojoExecution() + : null ); + when( ruleHelper.getComponent( ArgumentMatchers.>any() ) ) + .then( ( a ) -> a.getArgument( 0 ) == RepositorySystem.class + ? mockRepositorySystem() + : a.getArgument( 0 ) == ArtifactResolver.class + ? mock( ArtifactResolver.class ) + : a.getArgument( 0 ) == ArtifactMetadataSource.class + ? artifactMetadataSource + : null ); + return ruleHelper; + } + + @Test + public void testRuleFailsByMaxUpdatesExceeded() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ), + dependencyWith( "group", "artifactB", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "2.0.0" } ); + put( "artifactB", new String[] { "1.0.0", "2.0.0" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + maxUpdates = 1; + }}.execute( ruleHelper ); + + fail( "EnforcerRuleException should have been thrown" ); + } + catch ( EnforcerRuleException e ) + { + assertThat( e.getMessage(), containsString( "More than 1 upgradable artifacts detected" ) ); + } + } + + @Test + public void testRulePassesByMaxUpdatesNotExceeded() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( singletonList( + dependencyWith( "group", "artifactA", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "2.0.0" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + maxUpdates = 1; + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } + + @Test + public void testRulePassesByMaxUpdatesNotExceededDependencyIncludes() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ), + dependencyWith( "group", "artifactB", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "2.0.0" } ); + put( "artifactB", new String[] { "1.0.0" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + dependencyIncludes = singletonList( "group:artifactB" ); + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } + + @Test + public void testRulePassesByMaxUpdatesNotExceededDependencyExcludes() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ), + dependencyWith( "group", "artifactB", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "2.0.0" } ); + put( "artifactB", new String[] { "1.0.0" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + dependencyExcludes = singletonList( "group:artifactA" ); + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } + + @Test + public void testRulePassesByMaxUpdatesNotExceededDependencyIncludesExcludes() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ), + dependencyWith( "group", "artifactB", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "2.0.0" } ); + put( "artifactB", new String[] { "1.0.0" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + dependencyIncludes = singletonList( "group:*" ); + dependencyExcludes = singletonList( "group:artifactA" ); + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } + + @Test + public void testIgnoreSubIncrementalUpdates() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "1.0.0-1" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + ignoreSubIncrementalUpdates = true; + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } + + @Test + public void testIgnoreIncrementalUpdates() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "1.0.0-1", "1.0.1" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + ignoreIncrementalUpdates = true; + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } + + @Test + public void testIgnoreMinorUpdates() + throws ExpressionEvaluationException, ComponentLookupException + { + EnforcerRuleHelper ruleHelper = mockRuleHelper( new MavenProject() + {{ + setDependencies( Arrays.asList( + dependencyWith( "group", "artifactA", "1.0.0" ) ) ); + }}, mockArtifactMetadataSource( new HashMap() + {{ + put( "artifactA", new String[] { "1.0.0", "1.0.0-1", "1.0.1", "1.1.0" } ); + }} ) ); + + try + { + new MaxDependencyUpdates() + {{ + ignoreMinorUpdates = true; + }}.execute( ruleHelper ); + } + catch ( EnforcerRuleException e ) + { + fail( "No EnforcerRuleException should have been thrown" ); + } + } +} diff --git a/src/test/java/org/codehaus/mojo/versions/api/ArtifactVersionsTest.java b/src/test/java/org/codehaus/mojo/versions/api/ArtifactVersionsTest.java index 1f4b3633e1..25913f0af8 100644 --- a/src/test/java/org/codehaus/mojo/versions/api/ArtifactVersionsTest.java +++ b/src/test/java/org/codehaus/mojo/versions/api/ArtifactVersionsTest.java @@ -25,6 +25,7 @@ import org.apache.maven.artifact.handler.DefaultArtifactHandler; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.Restriction; import org.apache.maven.artifact.versioning.VersionRange; import org.codehaus.mojo.versions.ordering.InvalidSegmentException; import org.codehaus.mojo.versions.ordering.MavenVersionComparator; @@ -33,9 +34,12 @@ import static java.util.Optional.of; import static org.codehaus.mojo.versions.api.Segment.INCREMENTAL; +import static org.codehaus.mojo.versions.api.Segment.MAJOR; +import static org.codehaus.mojo.versions.api.Segment.MINOR; import static org.codehaus.mojo.versions.api.Segment.SUBINCREMENTAL; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -141,4 +145,63 @@ public void testGetNewerVersionsWithSnapshot() throws InvalidSegmentException assertThat( instance.getNewerVersions( "1.0.0-SNAPSHOT", of( SUBINCREMENTAL ), false, false ), arrayContaining( new DefaultArtifactVersion( "1.0.0" ) ) ); } + + @Test + public void testAllVersionsForIgnoreScopeSubIncremental() + { + ArtifactVersion[] versions = versions( "1.0.0", "1.0.0-1", "1.0.1" ); + ArtifactVersions instance = + new ArtifactVersions( new DefaultArtifact( "default-group", "dummy-api", + "1.0.0", "foo", "bar", + "jar", null ), + Arrays.asList( versions ), new MavenVersionComparator() ); + Restriction restriction = instance.restrictionForIgnoreScope( of( SUBINCREMENTAL ) ); + ArtifactVersion[] filteredVersions = instance.getVersions( restriction, false ); + assertThat( filteredVersions, arrayWithSize( 1 ) ); + assertThat( filteredVersions, arrayContaining( new DefaultArtifactVersion( "1.0.1" ) ) ); + } + + @Test + public void testAllVersionsForIgnoreScopeIncremental() + { + ArtifactVersion[] versions = versions( "1.0.0", "1.0.0-1", "1.0.1", "1.1.0" ); + ArtifactVersions instance = + new ArtifactVersions( new DefaultArtifact( "default-group", "dummy-api", + "1.0.0", "foo", "bar", + "jar", null ), + Arrays.asList( versions ), new MavenVersionComparator() ); + Restriction restriction = instance.restrictionForIgnoreScope( of( INCREMENTAL ) ); + ArtifactVersion[] filteredVersions = instance.getVersions( restriction, false ); + assertThat( filteredVersions, arrayWithSize( 1 ) ); + assertThat( filteredVersions, arrayContaining( new DefaultArtifactVersion( "1.1.0" ) ) ); + } + + @Test + public void testAllVersionsForIgnoreScopeMinor() + { + ArtifactVersion[] versions = versions( "1.0.0", "1.0.0-1", "1.0.1", "1.1.0", "2.0.0" ); + ArtifactVersions instance = + new ArtifactVersions( new DefaultArtifact( "default-group", "dummy-api", + "1.0.0", "foo", "bar", + "jar", null ), + Arrays.asList( versions ), new MavenVersionComparator() ); + Restriction restriction = instance.restrictionForIgnoreScope( of( MINOR ) ); + ArtifactVersion[] filteredVersions = instance.getVersions( restriction, false ); + assertThat( filteredVersions, arrayWithSize( 1 ) ); + assertThat( filteredVersions, arrayContaining( new DefaultArtifactVersion( "2.0.0" ) ) ); + } + + @Test + public void testAllVersionsForIgnoreScopeMajor() + { + ArtifactVersion[] versions = versions( "1.0.0", "1.0.0-1", "1.0.1", "1.1.0", "2.0.0" ); + ArtifactVersions instance = + new ArtifactVersions( new DefaultArtifact( "default-group", "dummy-api", + "1.0.0", "foo", "bar", + "jar", null ), + Arrays.asList( versions ), new MavenVersionComparator() ); + Restriction restriction = instance.restrictionForIgnoreScope( of( MAJOR ) ); + ArtifactVersion[] filteredVersions = instance.getVersions( restriction, false ); + assertThat( filteredVersions, arrayWithSize( 0 ) ); + } } diff --git a/src/test/resources/org/codehaus/mojo/max-dependency-upgrades/pom.xml b/src/test/resources/org/codehaus/mojo/max-dependency-upgrades/pom.xml new file mode 100644 index 0000000000..c869d723f4 --- /dev/null +++ b/src/test/resources/org/codehaus/mojo/max-dependency-upgrades/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + default-group + default-artifact + 1.0 + pom + + + + default-group + artifactA + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + enforce + + + + + 0 + + + true + + + + org.codehaus.mojo + versions-maven-plugin + ${project.version} + + + + + +