From 2e36b19261bc24648ce3cc6688b1ffcd89fa6d99 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Mon, 8 Sep 2014 22:12:13 +0200 Subject: [PATCH] SONAR-5604 Design plugin should no more rely on Maven APIs --- plugins/sonar-design-plugin/pom.xml | 10 +- .../design/batch/MavenDependenciesSensor.java | 184 +++++++++++++++--- .../batch/MavenDependenciesSensorTest.java | 74 +++++++ sonar-maven-plugin/pom.xml | 4 + .../main/java/org/sonar/maven/SonarMojo.java | 138 ++++++++++++- 5 files changed, 383 insertions(+), 27 deletions(-) create mode 100644 plugins/sonar-design-plugin/src/test/java/org/sonar/plugins/design/batch/MavenDependenciesSensorTest.java diff --git a/plugins/sonar-design-plugin/pom.xml b/plugins/sonar-design-plugin/pom.xml index d7aef6c0223..f04d435231f 100644 --- a/plugins/sonar-design-plugin/pom.xml +++ b/plugins/sonar-design-plugin/pom.xml @@ -28,13 +28,21 @@ maven-dependency-tree provided - + + com.google.code.gson + gson + + org.codehaus.sonar sonar-testing-harness test + + org.mockito + mockito-core + diff --git a/plugins/sonar-design-plugin/src/main/java/org/sonar/plugins/design/batch/MavenDependenciesSensor.java b/plugins/sonar-design-plugin/src/main/java/org/sonar/plugins/design/batch/MavenDependenciesSensor.java index 088b3d182b7..9afa425a0e4 100644 --- a/plugins/sonar-design-plugin/src/main/java/org/sonar/plugins/design/batch/MavenDependenciesSensor.java +++ b/plugins/sonar-design-plugin/src/main/java/org/sonar/plugins/design/batch/MavenDependenciesSensor.java @@ -19,6 +19,15 @@ */ package org.sonar.plugins.design.batch; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.metadata.ArtifactMetadataSource; @@ -34,28 +43,42 @@ import org.apache.maven.shared.dependency.tree.traversal.BuildingDependencyNodeV import org.apache.maven.shared.dependency.tree.traversal.CollectingDependencyNodeVisitor; import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor; import org.apache.maven.shared.dependency.tree.traversal.FilteringDependencyNodeVisitor; -import org.sonar.api.batch.SupportedEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.batch.SonarIndex; +import org.sonar.api.batch.SupportedEnvironment; +import org.sonar.api.config.Settings; import org.sonar.api.design.Dependency; import org.sonar.api.resources.Library; import org.sonar.api.resources.Project; import org.sonar.api.resources.Resource; import org.sonar.api.utils.SonarException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + @SupportedEnvironment("maven") public class MavenDependenciesSensor implements Sensor { + private static final String SONAR_MAVEN_PROJECT_DEPENDENCY = "sonar.maven.projectDependencies"; + + private static final Logger LOG = LoggerFactory.getLogger(MavenDependenciesSensor.class); + private ArtifactRepository localRepository; private ArtifactFactory artifactFactory; private ArtifactMetadataSource artifactMetadataSource; private ArtifactCollector artifactCollector; private DependencyTreeBuilder treeBuilder; private SonarIndex index; + private Settings settings; - public MavenDependenciesSensor(ArtifactRepository localRepository, ArtifactFactory artifactFactory, ArtifactMetadataSource artifactMetadataSource, - ArtifactCollector artifactCollector, DependencyTreeBuilder treeBuilder, SonarIndex index) { + public MavenDependenciesSensor(Settings settings, ArtifactRepository localRepository, ArtifactFactory artifactFactory, ArtifactMetadataSource artifactMetadataSource, + ArtifactCollector artifactCollector, DependencyTreeBuilder treeBuilder, SonarIndex index) { + this.settings = settings; this.localRepository = localRepository; this.artifactFactory = artifactFactory; this.artifactMetadataSource = artifactMetadataSource; @@ -64,42 +87,153 @@ public class MavenDependenciesSensor implements Sensor { this.treeBuilder = treeBuilder; } + /** + * Used with SQ Maven plugin 2.5+ + */ + public MavenDependenciesSensor(Settings settings, SonarIndex index) { + this.settings = settings; + this.index = index; + } + public boolean shouldExecuteOnProject(Project project) { return true; } - public void analyse(final Project project, final SensorContext context) { - try { - DependencyNode root = treeBuilder.buildDependencyTree(project.getPom(), localRepository, artifactFactory, artifactMetadataSource, null, artifactCollector); + private static class InputDependency { + + private final String key; + + private final String version; + + private String scope; + + List dependencies = new ArrayList(); + + public InputDependency(String key, String version) { + this.key = key; + this.version = version; + } + + public String key() { + return key; + } + + public String version() { + return version; + } + + public String scope() { + return scope; + } + + public InputDependency setScope(String scope) { + this.scope = scope; + return this; + } + + public List dependencies() { + return dependencies; + } + } + + private static class DependencyDeserializer implements JsonDeserializer { - DependencyNodeVisitor visitor = new BuildingDependencyNodeVisitor(new DependencyNodeVisitor() { - public boolean visit(DependencyNode node) { - return true; + @Override + public InputDependency deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + + JsonObject dep = json.getAsJsonObject(); + String key = dep.get("k").getAsString(); + String version = dep.get("v").getAsString(); + InputDependency result = new InputDependency(key, version); + result.setScope(dep.get("s").getAsString()); + JsonElement subDeps = dep.get("d"); + if (subDeps != null) { + JsonArray arrayOfSubDeps = subDeps.getAsJsonArray(); + for (JsonElement e : arrayOfSubDeps) { + result.dependencies().add(deserialize(e, typeOfT, context)); } + } + return result; + } + + } + + public void analyse(final Project project, final SensorContext context) { + if (settings.hasKey(SONAR_MAVEN_PROJECT_DEPENDENCY)) { + LOG.debug("Using dependency provided by property " + SONAR_MAVEN_PROJECT_DEPENDENCY); + String depsAsJson = settings.getString(SONAR_MAVEN_PROJECT_DEPENDENCY); + Collection deps; + try { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(InputDependency.class, new DependencyDeserializer()); + Gson gson = gsonBuilder.create(); - public boolean endVisit(DependencyNode node) { - if (node.getParent() != null && node.getParent() != node) { - saveDependency(node, context); + Type collectionType = new TypeToken>() { + }.getType(); + deps = gson.fromJson(depsAsJson, collectionType); + saveDependencies(project, deps, context); + } catch (Exception e) { + throw new IllegalStateException("Unable to deserialize dependency information: " + depsAsJson, e); + } + } + else if (treeBuilder != null) { + LOG.warn("Computation of Maven dependencies by SonarQube is deprecated. Please update the version of SonarQube Maven plugin to 2.5+"); + try { + DependencyNode root = treeBuilder.buildDependencyTree(project.getPom(), localRepository, artifactFactory, artifactMetadataSource, null, artifactCollector); + + DependencyNodeVisitor visitor = new BuildingDependencyNodeVisitor(new DependencyNodeVisitor() { + public boolean visit(DependencyNode node) { + return true; } - return true; - } - }); - // mode verbose OFF : do not show the same lib many times - DependencyNodeFilter filter = StateDependencyNodeFilter.INCLUDED; + public boolean endVisit(DependencyNode node) { + if (node.getParent() != null && node.getParent() != node) { + saveDependency(node, context); + } + return true; + } + }); + + // mode verbose OFF : do not show the same lib many times + DependencyNodeFilter filter = StateDependencyNodeFilter.INCLUDED; + + CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor(); + DependencyNodeVisitor firstPassVisitor = new FilteringDependencyNodeVisitor(collectingVisitor, filter); + root.accept(firstPassVisitor); - CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor(); - DependencyNodeVisitor firstPassVisitor = new FilteringDependencyNodeVisitor(collectingVisitor, filter); - root.accept(firstPassVisitor); + DependencyNodeFilter secondPassFilter = new AncestorOrSelfDependencyNodeFilter(collectingVisitor.getNodes()); + visitor = new FilteringDependencyNodeVisitor(visitor, secondPassFilter); - DependencyNodeFilter secondPassFilter = new AncestorOrSelfDependencyNodeFilter(collectingVisitor.getNodes()); - visitor = new FilteringDependencyNodeVisitor(visitor, secondPassFilter); + root.accept(visitor); + + } catch (DependencyTreeBuilderException e) { + throw new SonarException("Can not load the graph of dependencies of the project " + project.getKey(), e); + } + } + } - root.accept(visitor); + private void saveDependencies(Resource from, Collection deps, SensorContext context) { + for (InputDependency inputDep : deps) { + Resource to = toResource(inputDep, context); + Dependency dependency = new Dependency(from, to); + dependency.setUsage(inputDep.scope()); + dependency.setWeight(1); + context.saveDependency(dependency); + if (!inputDep.dependencies().isEmpty()) { + saveDependencies(to, inputDep.dependencies(), context); + } + } + } - } catch (DependencyTreeBuilderException e) { - throw new SonarException("Can not load the graph of dependencies of the project " + project.getKey(), e); + private Resource toResource(InputDependency dependency, SensorContext context) { + Project project = new Project(dependency.key()); + Resource result = context.getResource(project); + if (result == null || !((Project) result).getAnalysisVersion().equals(dependency.version())) { + Library lib = new Library(project.getKey(), dependency.version()); + context.saveResource(lib); + result = context.getResource(lib); } + return result; } protected void saveDependency(DependencyNode node, SensorContext context) { diff --git a/plugins/sonar-design-plugin/src/test/java/org/sonar/plugins/design/batch/MavenDependenciesSensorTest.java b/plugins/sonar-design-plugin/src/test/java/org/sonar/plugins/design/batch/MavenDependenciesSensorTest.java new file mode 100644 index 00000000000..a0adb1a03fb --- /dev/null +++ b/plugins/sonar-design-plugin/src/test/java/org/sonar/plugins/design/batch/MavenDependenciesSensorTest.java @@ -0,0 +1,74 @@ +package org.sonar.plugins.design.batch; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.batch.SonarIndex; +import org.sonar.api.config.Settings; +import org.sonar.api.design.Dependency; +import org.sonar.api.resources.Library; +import org.sonar.api.resources.Project; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MavenDependenciesSensorTest { + + private MavenDependenciesSensor sensor; + private Settings settings; + private SonarIndex sonarIndex; + private SensorContext sensorContext; + + @Before + public void prepare() { + settings = new Settings(); + sonarIndex = mock(SonarIndex.class); + sensor = new MavenDependenciesSensor(settings, sonarIndex); + sensorContext = mock(SensorContext.class); + when(sensorContext.getResource(any(Library.class))).thenAnswer(new Answer() { + public Library answer(InvocationOnMock invocation) throws Throwable { + return (invocation.getArguments()[0] instanceof Library) ? (Library) invocation.getArguments()[0] : null; + } + }); + + } + + @Test + public void testDependenciesProvidedAsProperties() { + settings + .setProperty( + "sonar.maven.projectDependencies", + "[{\"k\":\"antlr:antlr\",\"v\":\"2.7.2\",\"s\":\"compile\",\"d\":[]}," + + "{\"k\":\"commons-beanutils:commons-beanutils\",\"v\":\"1.7.0\",\"s\":\"compile\",\"d\":[]}," + + "{\"k\":\"commons-chain:commons-chain\",\"v\":\"1.1\",\"s\":\"compile\",\"d\":[]}," + + "{\"k\":\"commons-digester:commons-digester\",\"v\":\"1.8\",\"s\":\"compile\",\"d\":[]}," + + "{\"k\":\"commons-fileupload:commons-fileupload\",\"v\":\"1.1.1\",\"s\":\"compile\",\"d\":[{\"k\":\"commons-io:commons-io\",\"v\":\"1.1\",\"s\":\"compile\",\"d\":[]}]}," + + "{\"k\":\"commons-logging:commons-logging\",\"v\":\"1.0.4\",\"s\":\"compile\",\"d\":[]}," + + "{\"k\":\"commons-validator:commons-validator\",\"v\":\"1.3.1\",\"s\":\"compile\",\"d\":[]}," + + "{\"k\":\"javax.servlet:servlet-api\",\"v\":\"2.3\",\"s\":\"provided\",\"d\":[]}," + + "{\"k\":\"junit:junit\",\"v\":\"3.8.1\",\"s\":\"test\",\"d\":[]}," + + "{\"k\":\"oro:oro\",\"v\":\"2.0.8\",\"s\":\"compile\",\"d\":[]}]"); + + Project project = new Project("foo"); + sensor.analyse(project, sensorContext); + + Library antlr = new Library("antlr:antlr", "2.7.2"); + verify(sensorContext).saveResource(eq(antlr)); + Library commonsFU = new Library("commons-fileupload:commons-fileupload", "1.1.1"); + verify(sensorContext).saveResource(eq(commonsFU)); + Library commonsIo = new Library("commons-io:commons-io", "1.1"); + verify(sensorContext).saveResource(eq(commonsIo)); + Library junit = new Library("junit:junit", "3.8.1"); + verify(sensorContext).saveResource(eq(junit)); + + verify(sensorContext).saveDependency(new Dependency(project, antlr).setUsage("compile").setWeight(1)); + verify(sensorContext).saveDependency(new Dependency(commonsFU, commonsIo).setUsage("compile").setWeight(1)); + verify(sensorContext).saveDependency(new Dependency(project, junit).setUsage("test").setWeight(1)); + } + +} diff --git a/sonar-maven-plugin/pom.xml b/sonar-maven-plugin/pom.xml index d59510fd17b..5a93cf227ad 100644 --- a/sonar-maven-plugin/pom.xml +++ b/sonar-maven-plugin/pom.xml @@ -19,6 +19,10 @@ org.codehaus.sonar.runner sonar-runner-api + + com.google.code.gson + gson + org.apache.maven maven-plugin-api diff --git a/sonar-maven-plugin/src/main/java/org/sonar/maven/SonarMojo.java b/sonar-maven-plugin/src/main/java/org/sonar/maven/SonarMojo.java index dfc313844a2..b3bbc9bb70b 100644 --- a/sonar-maven-plugin/src/main/java/org/sonar/maven/SonarMojo.java +++ b/sonar-maven-plugin/src/main/java/org/sonar/maven/SonarMojo.java @@ -31,19 +31,34 @@ import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectBuilder; +import org.apache.maven.shared.dependency.tree.DependencyNode; import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder; +import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException; +import org.apache.maven.shared.dependency.tree.filter.AncestorOrSelfDependencyNodeFilter; +import org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter; +import org.apache.maven.shared.dependency.tree.filter.StateDependencyNodeFilter; +import org.apache.maven.shared.dependency.tree.traversal.BuildingDependencyNodeVisitor; +import org.apache.maven.shared.dependency.tree.traversal.CollectingDependencyNodeVisitor; +import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor; +import org.apache.maven.shared.dependency.tree.traversal.FilteringDependencyNodeVisitor; import org.sonar.runner.api.EmbeddedRunner; import org.sonar.runner.api.RunnerProperties; import org.sonar.runner.api.ScanProperties; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; /** * @goal sonar * @aggregator * @requiresDependencyResolution test + * @deprecated Only kept for backward compatibility with old version of SQ Maven plugin */ +@Deprecated public final class SonarMojo extends AbstractMojo { /** @@ -151,7 +166,8 @@ public final class SonarMojo extends AbstractMojo { .setProperty(ScanProperties.PROJECT_VERSION, toString(project.getVersion())) .setProperty(ScanProperties.PROJECT_NAME, toString(project.getName())) .setProperty(ScanProperties.PROJECT_DESCRIPTION, toString(project.getDescription())) - .setProperty(ScanProperties.PROJECT_SOURCE_DIRS, "."); + .setProperty(ScanProperties.PROJECT_SOURCE_DIRS, ".") + .setProperty("sonar.maven.projectDependencies", dependenciesToJson(collectProjectDependencies())); // Exclude log implementation to not conflict with Maven 3.1 logging impl runner.mask("org.slf4j.LoggerFactory") // Include slf4j Logger that is exposed by some Sonar components @@ -176,6 +192,126 @@ public final class SonarMojo extends AbstractMojo { } } + private static class Dependency { + + private final String key; + private final String version; + private String scope; + List dependencies = new ArrayList(); + + public Dependency(String key, String version) { + this.key = key; + this.version = version; + } + + public String key() { + return key; + } + + public String version() { + return version; + } + + public String scope() { + return scope; + } + + public Dependency setScope(String scope) { + this.scope = scope; + return this; + } + + public List dependencies() { + return dependencies; + } + } + + private List collectProjectDependencies() { + final List result = new ArrayList(); + try { + DependencyNode root = dependencyTreeBuilder.buildDependencyTree(project, localRepository, artifactFactory, artifactMetadataSource, null, artifactCollector); + + DependencyNodeVisitor visitor = new BuildingDependencyNodeVisitor(new DependencyNodeVisitor() { + + private Stack stack = new Stack(); + + public boolean visit(DependencyNode node) { + if (node.getParent() != null && node.getParent() != node) { + Dependency dependency = toDependency(node); + if (stack.isEmpty()) { + result.add(dependency); + } + else { + stack.peek().dependencies().add(dependency); + } + stack.push(dependency); + } + return true; + } + + public boolean endVisit(DependencyNode node) { + if (!stack.isEmpty()) { + stack.pop(); + } + return true; + } + }); + + // mode verbose OFF : do not show the same lib many times + DependencyNodeFilter filter = StateDependencyNodeFilter.INCLUDED; + + CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor(); + DependencyNodeVisitor firstPassVisitor = new FilteringDependencyNodeVisitor(collectingVisitor, filter); + root.accept(firstPassVisitor); + + DependencyNodeFilter secondPassFilter = new AncestorOrSelfDependencyNodeFilter(collectingVisitor.getNodes()); + visitor = new FilteringDependencyNodeVisitor(visitor, secondPassFilter); + + root.accept(visitor); + + } catch (DependencyTreeBuilderException e) { + throw new IllegalStateException("Can not load the graph of dependencies of the project " + getSonarKey(project), e); + } + return result; + } + + private Dependency toDependency(DependencyNode node) { + String key = String.format("%s:%s", node.getArtifact().getGroupId(), node.getArtifact().getArtifactId()); + String version = node.getArtifact().getBaseVersion(); + return new Dependency(key, version).setScope(node.getArtifact().getScope()); + } + + private String dependenciesToJson(List deps) { + StringBuilder json = new StringBuilder(); + json.append('['); + serializeDeps(json, deps); + json.append(']'); + return json.toString(); + } + + private void serializeDeps(StringBuilder json, List deps) { + for (Iterator dependencyIt = deps.iterator(); dependencyIt.hasNext();) { + serializeDep(json, dependencyIt.next()); + if (dependencyIt.hasNext()) { + json.append(','); + } + } + } + + private void serializeDep(StringBuilder json, Dependency dependency) { + json.append("{"); + json.append("\"k\":"); + json.append(dependency.key()); + json.append(",\"v\":\""); + json.append(dependency.version()); + json.append("\",\"s\":\""); + json.append(dependency.scope()); + json.append("\",\"d\":["); + serializeDeps(json, dependency.dependencies()); + json.append("]"); + json.append("}"); + } + private ArtifactVersion getMavenVersion() { return runtimeInformation.getApplicationVersion(); } -- 2.39.5