Support transitive excludes in dependency-tools

Update spring-boot-dependency-tools to support transitive excludes.
Transitive excludes are useful with Gradle which considers each
dependency independently (see GRADLE-3061).

Transitive excludes are supported by parsing the dependency-tree file
from spring-boot-versions.

See gh-1047
This commit is contained in:
Phillip Webb 2014-06-08 12:04:00 -07:00
parent 28090e8a5f
commit addc1f77bd
12 changed files with 1604 additions and 4 deletions

View File

@ -31,7 +31,7 @@
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-effective-pom</id>
<id>copy-resources</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
@ -47,6 +47,15 @@
<outputDirectory>${generated.pom.dir}</outputDirectory>
<destFileName>effective-pom.xml</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-versions</artifactId>
<version>${project.version}</version>
<type>dependency-tree</type>
<overWrite>true</overWrite>
<outputDirectory>${generated.pom.dir}</outputDirectory>
<destFileName>dependency-tree.txt</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>

View File

@ -0,0 +1,131 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.dependency.tools;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.dependency.tools.Dependency.Exclusion;
import org.springframework.boot.dependency.tools.Dependency.ExclusionType;
/**
* {@link Dependencies} to extend an existing {@link Dependencies} instance with
* transitive {@link Exclusion}s located from a {@link DependencyTree}.
*
* @author Phillip Webb
* @since 1.1.0
*/
class DependenciesWithTransitiveExclusions extends AbstractDependencies {
public DependenciesWithTransitiveExclusions(Dependencies dependencies,
DependencyTree tree) {
DependencyBuilder builder = new DependencyBuilder(dependencies);
builder.addTransitiveExcludes(tree);
builder.finish();
}
/**
* Builder used to collect the transitive exclusions.
*/
private class DependencyBuilder {
private Map<ArtifactAndGroupId, DependencyAndTransitiveExclusions> dependencies;
public DependencyBuilder(Dependencies dependencies) {
this.dependencies = new LinkedHashMap<ArtifactAndGroupId, DependencyAndTransitiveExclusions>();
for (Dependency dependency : dependencies) {
this.dependencies.put(new ArtifactAndGroupId(dependency),
new DependencyAndTransitiveExclusions(dependency));
}
}
public void addTransitiveExcludes(DependencyTree tree) {
for (DependencyNode node : tree) {
DependencyAndTransitiveExclusions dependency = this.dependencies
.get(asArtifactAndGroupId(node));
if (dependency != null) {
for (DependencyNode child : node) {
addTransitiveExcludes(dependency, child);
}
}
}
}
private void addTransitiveExcludes(DependencyAndTransitiveExclusions dependency,
DependencyNode node) {
DependencyAndTransitiveExclusions exclusions = this.dependencies
.get(asArtifactAndGroupId(node));
if (exclusions != null) {
dependency.addTransitiveExclusions(exclusions.getSourceDependency());
}
for (DependencyNode child : node) {
addTransitiveExcludes(dependency, child);
}
}
private ArtifactAndGroupId asArtifactAndGroupId(DependencyNode node) {
return new ArtifactAndGroupId(node.getGroupId(), node.getArtifactId());
}
public void finish() {
for (Map.Entry<ArtifactAndGroupId, DependencyAndTransitiveExclusions> entry : this.dependencies
.entrySet()) {
add(entry.getKey(), entry.getValue().createNewDependency());
}
}
}
/**
* Holds a {@link Dependency} with additional transitive {@link Exclusion}s.
*/
private static class DependencyAndTransitiveExclusions {
private Dependency dependency;
private Set<Exclusion> transitiveExclusions = new LinkedHashSet<Exclusion>();
public DependencyAndTransitiveExclusions(Dependency dependency) {
this.dependency = dependency;
}
public Dependency getSourceDependency() {
return this.dependency;
}
public void addTransitiveExclusions(Dependency dependency) {
for (Exclusion exclusion : dependency.getExclusions()) {
this.transitiveExclusions.add(new Exclusion(exclusion.getGroupId(),
exclusion.getArtifactId(), ExclusionType.TRANSITIVE));
}
}
public Dependency createNewDependency() {
Set<Exclusion> exclusions = new LinkedHashSet<Dependency.Exclusion>();
exclusions.addAll(this.dependency.getExclusions());
exclusions.addAll(this.transitiveExclusions);
return new Dependency(this.dependency.getGroupId(),
this.dependency.getArtifactId(), this.dependency.getVersion(),
new ArrayList<Exclusion>(exclusions));
}
}
}

View File

@ -137,11 +137,15 @@ public final class Dependency {
private final String artifactId;
Exclusion(String groupId, String artifactId) {
private final ExclusionType type;
Exclusion(String groupId, String artifactId, ExclusionType type) {
Assert.notNull(groupId, "GroupId must not be null");
Assert.notNull(groupId, "ArtifactId must not be null");
Assert.notNull(type, "Type must not be null");
this.groupId = groupId;
this.artifactId = artifactId;
this.type = type;
}
/**
@ -158,6 +162,10 @@ public final class Dependency {
return this.groupId;
}
public ExclusionType getType() {
return this.type;
}
@Override
public String toString() {
return this.groupId + ":" + this.artifactId;
@ -188,4 +196,22 @@ public final class Dependency {
}
public static enum ExclusionType {
/**
* An exclusion that was specified directly on the dependency.
*/
DIRECT,
/**
* An exclusion that is was specified on a dependency of this dependency. For
* example if {@literal commons-logging} is directly excluded from
* {@literal spring-core} then it is also transitive exclude on
* {@literal spring-context} (since {@literal spring-context} depends on
* {@literal spring-core}).
*/
TRANSITIVE
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.dependency.tools;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* A single node in a {@link DependencyTree}.
*
* @author Phillip Webb
* @see DependencyTree
* @since 1.1.0
*/
class DependencyNode implements Iterable<DependencyNode> {
private final String groupId;
private final String artifactId;
private final String version;
private List<DependencyNode> dependencies;
DependencyNode(String groupId, String artifactId, String version) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
this.dependencies = new ArrayList<DependencyNode>();
}
@Override
public Iterator<DependencyNode> iterator() {
return getDependencies().iterator();
}
public String getGroupId() {
return this.groupId;
}
public String getArtifactId() {
return this.artifactId;
}
public String getVersion() {
return this.version;
}
public List<DependencyNode> getDependencies() {
return Collections.unmodifiableList(this.dependencies);
}
@Override
public String toString() {
return this.groupId + ":" + this.artifactId + ":" + this.version;
}
void addDependency(DependencyNode node) {
this.dependencies.add(node);
}
DependencyNode getLastDependency() {
return this.dependencies.get(this.dependencies.size() - 1);
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.dependency.tools;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Dependency tree information that can be loaded from the output of
* {@literal mvn dependency:tree}.
*
* @author Phillip Webb
* @since 1.1.0
* @see DependencyNode
*/
class DependencyTree implements Iterable<DependencyNode> {
private final DependencyNode root;
/**
* Create a new {@link DependencyTree} instance for the given input stream.
* @param inputStream input stream containing content from
* {@literal mvn dependency:tree} (the stream will be closed).
*/
public DependencyTree(InputStream inputStream) {
try {
this.root = parse(inputStream);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private DependencyNode parse(InputStream inputStream) throws IOException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
Parser parser = new Parser();
String line;
while ((line = reader.readLine()) != null) {
parser.append(line);
}
return parser.getRoot();
}
finally {
inputStream.close();
}
}
@Override
public Iterator<DependencyNode> iterator() {
return getDependencies().iterator();
}
/**
* @return the root node for the tree.
*/
public DependencyNode getRoot() {
return this.root;
}
/**
* @return the dependencies of the root node.
*/
public List<DependencyNode> getDependencies() {
return this.root.getDependencies();
}
/**
* Return the node at the specified index.
* @param index the index (multiple indexes can be used to traverse the tree)
* @return the node at the specified index
*/
public DependencyNode get(int... index) {
DependencyNode rtn = this.root;
for (int i : index) {
rtn = rtn.getDependencies().get(i);
}
return rtn;
}
private static class Parser {
private static final int INDENT = 3;
private static final Set<Character> PREFIX_CHARS = new HashSet<Character>(
Arrays.asList(' ', '+', '-', '\\', '|'));
private static final Pattern LINE_PATTERN = Pattern
.compile("[(]?([^:]*):([^:]*):([^:]*):([^:\\s]*)");
private Deque<DependencyNode> stack = new ArrayDeque<DependencyNode>();
public void append(String line) {
int depth = getDepth(line);
String data = line.substring(depth * INDENT);
if (depth == 0) {
this.stack.push(createNode(data));
}
else {
while (depth < this.stack.size()) {
this.stack.pop();
}
if (depth > this.stack.size()) {
this.stack.push(this.stack.peek().getLastDependency());
}
this.stack.peek().addDependency(createNode(data));
}
}
private int getDepth(String line) {
for (int i = 0; i < line.length(); i++) {
if (!Parser.PREFIX_CHARS.contains(line.charAt(i))) {
return i / INDENT;
}
}
return 0;
}
private DependencyNode createNode(String line) {
Matcher matcher = LINE_PATTERN.matcher(line);
if (!matcher.find()) {
throw new IllegalStateException("Unable to parese line " + line);
}
return new DependencyNode(matcher.group(1), matcher.group(2),
matcher.group(4));
}
public DependencyNode getRoot() {
return this.stack.getLast();
}
}
}

View File

@ -57,7 +57,11 @@ class ManagedDependenciesDelegate extends AbstractDependencies {
private static Dependencies getSpringBootDependencies() {
if (springBootDependencies == null) {
springBootDependencies = new PomDependencies(getResource("effective-pom.xml"));
Dependencies dependencies = new PomDependencies(
getResource("effective-pom.xml"));
DependencyTree tree = new DependencyTree(getResource("dependency-tree.txt"));
dependencies = new DependenciesWithTransitiveExclusions(dependencies, tree);
springBootDependencies = dependencies;
}
return springBootDependencies;
}

View File

@ -25,6 +25,7 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.springframework.boot.dependency.tools.Dependency.Exclusion;
import org.springframework.boot.dependency.tools.Dependency.ExclusionType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@ -118,7 +119,7 @@ public class PomDependencies extends AbstractDependencies {
private Exclusion createExclusion(Element element) {
String groupId = getTextContent(element, "groupId");
String artifactId = getTextContent(element, "artifactId");
return new Exclusion(groupId, artifactId);
return new Exclusion(groupId, artifactId, ExclusionType.DIRECT);
}
private String getTextContent(Element element, String tagName) {

View File

@ -0,0 +1,50 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.dependency.tools;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DependenciesWithTransitiveExclusions}.
*
* @author Phillip Webb
*/
public class DependenciesWithTransitiveExclusionsTests {
@Test
public void findsTransitiveExclusions() throws Exception {
Dependencies source = new PomDependencies(getClass().getResourceAsStream(
"test-effective-pom.xml"));
DependencyTree tree = new DependencyTree(getClass().getResourceAsStream(
"test-effective-pom-dependency-tree.txt"));
DependenciesWithTransitiveExclusions dependencies = new DependenciesWithTransitiveExclusions(
source, tree);
assertExcludes(dependencies, "sample01", "[org.exclude:exclude01]");
assertExcludes(source, "sample02", "[]");
assertExcludes(dependencies, "sample02", "[org.exclude:exclude01]");
}
private void assertExcludes(Dependencies dependencies, String artifactId,
String expected) {
Dependency dependency = dependencies.find("org.sample", artifactId);
assertThat(dependency.getExclusions().toString(), equalTo(expected));
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.dependency.tools;
import org.junit.Test;
import org.springframework.boot.dependency.tools.DependencyTree;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DependencyTree}.
*
* @author Phillip Webb
*/
public class DependencyTreeTests {
@Test
public void parse() throws Exception {
DependencyTree tree = new DependencyTree(getClass().getResourceAsStream(
"sample-dependency-tree.txt"));
assertThat(tree.getRoot().toString(), equalTo("org.springframework.boot:"
+ "spring-boot-versions-dependency-tree:1.1.0.BUILD-SNAPSHOT"));
assertThat(tree.getDependencies().size(), equalTo(204));
assertThat(tree.get(0, 1).toString(), equalTo("org.slf4j:slf4j-api:1.7.6"));
assertThat(tree.get(203).toString(), equalTo("org.springframework.security:"
+ "spring-security-web:3.2.4.RELEASE"));
}
}

View File

@ -0,0 +1,5 @@
org.sample:sample:pom:1.0.0.BUILD-SNAPSHOT
+- org.sample:sample01:jar:1.0.0:compile
+- org.sample:sample02:jar:1.0.0:compile
| +- (org.sample:sample01:jar:1.0.0:compile - omitted for duplicate)
\- org.springframework.boot:spring-boot:jar:1.0.0.BUILD-SNAPSHOT:compile

View File

@ -2,6 +2,8 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<properties>
<sample.version>1.0.0</sample.version>