Skip to content

Commit

Permalink
Configure RecommendationStrategy
Browse files Browse the repository at this point in the history
  • Loading branch information
rbrindl committed Nov 30, 2015
1 parent bc91e4d commit 4ce98d2
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
==================

Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Project> {
@Override
public void apply(final Project project) {
Expand All @@ -30,14 +27,12 @@ public void execute(JavaPlugin javaPlugin) {
project.getConfigurations().all(new Action<Configuration>() {
@Override
public void execute(final Configuration conf) {
final List<String> firstOrderDepsWithoutVersions = new ArrayList<>();

final RecommendationStrategyFactory rsFactory = new RecommendationStrategyFactory(project);
conf.getIncoming().beforeResolve(new Action<ResolvableDependencies>() {
@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);
}
}
});
Expand All @@ -46,19 +41,15 @@ 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()) {
if (requested.getGroup().equals(force.getGroup()) && requested.getName().equals(force.getName())) {
return;
}
}

String version = getRecommendedVersionRecursive(project, requested);
if (version != null && firstOrderDepsWithoutVersions.contains(coord)) {
details.useVersion(version);
}
rsFactory.getRecommendationStrategy().recommendVersion(details, version);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 "<group>:<name>", taken from details.requested.
*/
protected String getCoord(DependencyResolveDetails details) {
ModuleVersionSelector requested = details.getRequested();
return requested.getGroup() + ":" + requested.getName();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,7 +15,13 @@
import java.util.Map;

public class RecommendationProviderContainer extends DefaultNamedDomainObjectList<RecommendationProvider> {

private Project project;
private Class<? extends RecommendationStrategy> 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<? super RecommendationProvider> addLastAction = new Action<RecommendationProvider>() {
public void execute(RecommendationProvider r) {
Expand Down Expand Up @@ -110,4 +119,12 @@ public String getRecommendedVersion(String group, String name) {
}
return null;
}

public Class<? extends RecommendationStrategy> getRecommendationStrategy() {
return recommendationStrategy;
}

public void setRecommendationStrategy(Class<? extends RecommendationStrategy> recommendationStrategy) {
this.recommendationStrategy = recommendationStrategy;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 4ce98d2

Please sign in to comment.