From 4ce98d221db0953b325f3e0fea1900cfc8725d68 Mon Sep 17 00:00:00 2001 From: Brindl Ronald Date: Mon, 30 Nov 2015 12:12:05 +0100 Subject: [PATCH] Configure RecommendationStrategy --- CHANGELOG.md | 5 +++ README.md | 19 +++++++++-- .../DependencyRecommendationsPlugin.java | 15 ++------- ...omendIfFirstOrderWithoutVersionExists.java | 26 +++++++++++++++ ...comendIfNoFirstOrderWithVersionExists.java | 26 +++++++++++++++ .../recommender/RecommendationStrategy.java | 33 +++++++++++++++++++ .../RecommendationStrategyFactory.java | 29 ++++++++++++++++ .../RecommendationProviderContainer.java | 17 ++++++++++ ...RecommendationProviderContainerSpec.groovy | 20 +++++++++++ 9 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfFirstOrderWithoutVersionExists.java create mode 100644 src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfNoFirstOrderWithVersionExists.java create mode 100644 src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java create mode 100644 src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4107415..44f33a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +3.0.4 / 2015-11-30 +================== + +* Reintroduce old strategy to recommend for transitive dependencies if there are no first level dependencies with versions + 3.0.3 / 2015-11-04 ================== diff --git a/README.md b/README.md index 0657501..8c36119 100644 --- a/README.md +++ b/README.md @@ -226,9 +226,10 @@ dependencies { ### 4. Transitive dependencies -Whenever a recommendation provider can provide a version recommendation for a transitive dependency AND there is a first order dependency on that transitive that has no version specified, the recommendation overrides versions of the module that are provided by transitively. +Whenever a recommendation provider can provide a version recommendation for a transitive dependency AND one of below strategies applies, the recommendation overrides versions of the module that are provided by transitively. +* **OnlyReccomendIfFirstOrderWithoutVersionExists** (default): there is a first order dependency with the same group:artifactId on that configuration that has no version specified -Consider the following example with dependencies on `commons-configuration` and `commons-logging`. `commons-configuration:1.6` depends on `commons-logging:1.1.1`. Even though `commons-configuration` indicates that it prefers version `1.1.1`, `1.0` is selected because of the recommendation provider. + Consider the following example with dependencies on `commons-configuration` and `commons-logging`. `commons-configuration:1.6` depends on `commons-logging:1.1.1`. Even though `commons-configuration` indicates that it prefers version `1.1.1`, `1.0` is selected because of the recommendation provider. ```groovy dependencyRecommendations { @@ -240,6 +241,20 @@ dependencies { compile 'commons-logging:commons-logging' } ``` +* **OnlyReccomendIfNoFirstOrderWithVersionExists**: there is no first order dependency with the same group:artifactId on that configuration that has a version specified + + In the following example version `commons-logging:commons-logging:1.0` is selected even though `commons-logging` is not explicitely mentioned in dependencies. This would not work with the OnlyReccomendIfFirstOrderWithoutVersionExists strategy: + +```groovy +dependencyRecommendations { + map recommendations: ['commons-logging:commons-logging': '1.0'] +} + +dependencies { + compile 'commons-configuration:commons-configuration:1.6' +} +``` + Conversely, if no recommendation can be found for a dependency that has no version, but a version is provided by a transitive the version provided by the transitive is applied. In this scenario, if several transitives provide versions for the module, normal Gradle conflict resolution applies. diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java b/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java index 0c5ae91..aa5679b 100644 --- a/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java +++ b/src/main/groovy/netflix/nebula/dependency/recommender/DependencyRecommendationsPlugin.java @@ -11,9 +11,6 @@ import org.gradle.api.plugins.ExtraPropertiesExtension; import org.gradle.api.plugins.JavaPlugin; -import java.util.ArrayList; -import java.util.List; - public class DependencyRecommendationsPlugin implements Plugin { @Override public void apply(final Project project) { @@ -30,14 +27,12 @@ public void execute(JavaPlugin javaPlugin) { project.getConfigurations().all(new Action() { @Override public void execute(final Configuration conf) { - final List firstOrderDepsWithoutVersions = new ArrayList<>(); - + final RecommendationStrategyFactory rsFactory = new RecommendationStrategyFactory(project); conf.getIncoming().beforeResolve(new Action() { @Override public void execute(ResolvableDependencies resolvableDependencies) { for (Dependency dependency : resolvableDependencies.getDependencies()) { - if (dependency.getVersion() == null || dependency.getVersion().isEmpty()) - firstOrderDepsWithoutVersions.add(dependency.getGroup() + ":" + dependency.getName()); + rsFactory.getRecommendationStrategy().inspectDependency(dependency); } } }); @@ -46,7 +41,6 @@ public void execute(ResolvableDependencies resolvableDependencies) { @Override public void execute(DependencyResolveDetails details) { ModuleVersionSelector requested = details.getRequested(); - String coord = requested.getGroup() + ":" + requested.getName(); // don't interfere with the way forces trump everything for (ModuleVersionSelector force : conf.getResolutionStrategy().getForcedModules()) { @@ -54,11 +48,8 @@ public void execute(DependencyResolveDetails details) { return; } } - String version = getRecommendedVersionRecursive(project, requested); - if (version != null && firstOrderDepsWithoutVersions.contains(coord)) { - details.useVersion(version); - } + rsFactory.getRecommendationStrategy().recommendVersion(details, version); } }); } diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfFirstOrderWithoutVersionExists.java b/src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfFirstOrderWithoutVersionExists.java new file mode 100644 index 0000000..6356d35 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfFirstOrderWithoutVersionExists.java @@ -0,0 +1,26 @@ +package netflix.nebula.dependency.recommender; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.DependencyResolveDetails; + +import java.util.ArrayList; +import java.util.List; + +public class OnlyReccomendIfFirstOrderWithoutVersionExists extends RecommendationStrategy { + + private List firstOrderDepsWithoutVersions = new ArrayList<>(); + + @Override + public void inspectDependency(Dependency dependency) { + if (dependency.getVersion() == null || dependency.getVersion().isEmpty()) { + firstOrderDepsWithoutVersions.add(dependency.getGroup() + ":" + dependency.getName()); + } + } + + @Override + public void recommendVersion(DependencyResolveDetails details, String version) { + if (version != null && firstOrderDepsWithoutVersions.contains(getCoord(details))) { + details.useVersion(version); + } + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfNoFirstOrderWithVersionExists.java b/src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfNoFirstOrderWithVersionExists.java new file mode 100644 index 0000000..90bb3f5 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/OnlyReccomendIfNoFirstOrderWithVersionExists.java @@ -0,0 +1,26 @@ +package netflix.nebula.dependency.recommender; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.DependencyResolveDetails; + +import java.util.ArrayList; +import java.util.List; + +public class OnlyReccomendIfNoFirstOrderWithVersionExists extends RecommendationStrategy { + + private List firstOrderDepsWithVersions = new ArrayList<>(); + + @Override + public void inspectDependency(Dependency dependency) { + if (dependency.getVersion() != null && !dependency.getVersion().isEmpty()) { + firstOrderDepsWithVersions.add(dependency.getGroup() + ":" + dependency.getName()); + } + } + + @Override + public void recommendVersion(DependencyResolveDetails details, String version) { + if (version != null && !firstOrderDepsWithVersions.contains(getCoord(details))) { + details.useVersion(version); + } + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java new file mode 100644 index 0000000..ed4c24c --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategy.java @@ -0,0 +1,33 @@ +package netflix.nebula.dependency.recommender; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.DependencyResolveDetails; +import org.gradle.api.artifacts.ModuleVersionSelector; + +/** + * Defines in which cases recommendations are applied + */ +public abstract class RecommendationStrategy { + + /** + * This hook is called for each dependency in a project. It collects the dependencies we are interested in for determining if a recommendation should be applied. + * @param dependency the dependency to inspect. + */ + public abstract void inspectDependency(Dependency dependency); + + /** + * Puts the recommended version on details.useVersion depending on the strategy used. + * @param details the details to recommend a version for + * @param version the version to be potentially recommended for the requested artifact. + */ + public abstract void recommendVersion(DependencyResolveDetails details, String version); + + /** + * @param details the details to get coordinates from + * @return the coordinates in the form of ":", taken from details.requested. + */ + protected String getCoord(DependencyResolveDetails details) { + ModuleVersionSelector requested = details.getRequested(); + return requested.getGroup() + ":" + requested.getName(); + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java new file mode 100644 index 0000000..1b1e9e9 --- /dev/null +++ b/src/main/groovy/netflix/nebula/dependency/recommender/RecommendationStrategyFactory.java @@ -0,0 +1,29 @@ +package netflix.nebula.dependency.recommender; + +import netflix.nebula.dependency.recommender.provider.RecommendationProviderContainer; +import org.gradle.api.Project; + +/** + * Creates RecommendationStrategy lazily on demand and caches it. + * This is used to allow for scoped recommendationStrategies (e.g. per configuration as in DependencyRecommendationsPlugin) + */ +public class RecommendationStrategyFactory { + private final Project project; + private RecommendationStrategy recommendationStrategy; + + public RecommendationStrategyFactory(Project project) { + this.project = project; + } + + public RecommendationStrategy getRecommendationStrategy() { + if(recommendationStrategy == null) { + try { + RecommendationProviderContainer recommendationProviderContainer = project.getExtensions().getByType(RecommendationProviderContainer.class); + recommendationStrategy = recommendationProviderContainer.getRecommendationStrategy().newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + return recommendationStrategy; + } +} diff --git a/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java b/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java index 341527b..186ee5b 100644 --- a/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java +++ b/src/main/groovy/netflix/nebula/dependency/recommender/provider/RecommendationProviderContainer.java @@ -1,6 +1,9 @@ package netflix.nebula.dependency.recommender.provider; import groovy.lang.Closure; +import netflix.nebula.dependency.recommender.OnlyReccomendIfFirstOrderWithoutVersionExists; +import netflix.nebula.dependency.recommender.OnlyReccomendIfNoFirstOrderWithVersionExists; +import netflix.nebula.dependency.recommender.RecommendationStrategy; import org.gradle.api.Action; import org.gradle.api.Namer; import org.gradle.api.Project; @@ -12,7 +15,13 @@ import java.util.Map; public class RecommendationProviderContainer extends DefaultNamedDomainObjectList { + private Project project; + private Class recommendationStrategy = OnlyReccomendIfFirstOrderWithoutVersionExists.class; + + // Make classes available in buildscripts without import + public static final Class OnlyReccomendIfFirstOrderWithoutVersionExists = OnlyReccomendIfFirstOrderWithoutVersionExists.class; + public static final Class OnlyReccomendIfNoFirstOrderWithVersionExists = OnlyReccomendIfNoFirstOrderWithVersionExists.class; private final Action addLastAction = new Action() { public void execute(RecommendationProvider r) { @@ -110,4 +119,12 @@ public String getRecommendedVersion(String group, String name) { } return null; } + + public Class getRecommendationStrategy() { + return recommendationStrategy; + } + + public void setRecommendationStrategy(Class recommendationStrategy) { + this.recommendationStrategy = recommendationStrategy; + } } diff --git a/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy b/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy index b00d70a..6439912 100644 --- a/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy +++ b/src/test/groovy/netflix/nebula/dependency/recommender/RecommendationProviderContainerSpec.groovy @@ -161,6 +161,26 @@ class RecommendationProviderContainerSpec extends Specification { commonsLang.moduleVersion == '1.1.1' } + def 'transitive dependency versions are overriden by recommendations with OnlyReccomendIfNoFirstOrderWithVersionExists strategy'() { + setup: + project.dependencyRecommendations { + recommendationStrategy = OnlyReccomendIfNoFirstOrderWithVersionExists + map recommendations: ['commons-logging:commons-logging': '1.1'] + } + + when: + project.dependencies { + compile 'commons-configuration:commons-configuration:1.6' + // no first order dependency on commons-logging, but still recommend with ONLY_RECOMMNED_IF_NO_FIRST_ORDER_WITH_VERSION_EXISTS strategy + } + + def commonsConfig = project.configurations.compile.resolvedConfiguration.firstLevelModuleDependencies.iterator().next() + def commonsLang = commonsConfig.children.find { it.moduleName == 'commons-logging' } + + then: + commonsLang.moduleVersion == '1.1' + } + def 'transitive dependencies are used as a source of recommendations when no explicit recommendation is provided for a module'() { setup: project.dependencyRecommendations {