Skip to content

Commit 25754e6

Browse files
oheger-boschsschuberth
authored andcommittedFeb 25, 2025
feat(Maven): Add functionality to parse JSON-based dependency trees
The Tycho implementation is going to invoke the `dependency:tree` goal of Maven. The newly introduced functions are able to parse the output format of this goal and to collect the corresponding dependency information. In addition, add a `DependencyTreeLogger` class to make sure that the output generated by the dependency plugin is separated from the other output produced by the Maven build. Signed-off-by: Oliver Heger <oliver.heger@bosch.io>
1 parent dbc2e7b commit 25754e6

File tree

4 files changed

+541
-0
lines changed

4 files changed

+541
-0
lines changed
 

‎plugins/package-managers/maven/build.gradle.kts

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
plugins {
2121
// Apply precompiled plugins.
2222
id("ort-library-conventions")
23+
24+
// Apply third-party plugins.
25+
alias(libs.plugins.kotlinSerialization)
2326
}
2427

2528
dependencies {
@@ -32,6 +35,9 @@ dependencies {
3235
implementation(projects.downloader)
3336
implementation(projects.utils.commonUtils)
3437

38+
implementation(libs.kotlinx.serialization.core)
39+
implementation(libs.kotlinx.serialization.json)
40+
3541
// The classes from the maven-resolver dependencies are not used directly but initialized by the Plexus IoC
3642
// container automatically. They are required on the classpath for Maven dependency resolution to work.
3743
runtimeOnly(libs.bundles.mavenResolver)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.packagemanagers.maven.utils
21+
22+
import java.io.PrintStream
23+
24+
import org.apache.logging.log4j.kotlin.logger
25+
26+
import org.codehaus.plexus.logging.AbstractLogger
27+
28+
/**
29+
* A special logger implementation that can redirect the output of the Maven dependency tree plugin to a separate file.
30+
*
31+
* When running a Maven build programmatically, the format of the generated log output is determined by the logging
32+
* configuration of the client application. This also affects the log level and the log format. Because of this, it can
33+
* be problematic to detect the output of the Maven dependency tree plugin in the log output. This logger
34+
* implementation solves this problem by printing the log messages verbatim into a configured output stream.
35+
*/
36+
internal class DependencyTreeLogger(
37+
/** The stream where to direct log output to. */
38+
private val outputStream: PrintStream
39+
) : AbstractLogger(LEVEL_DEBUG, "DependencyTreeLogger") {
40+
override fun getChildLogger(name: String?) = this
41+
42+
override fun debug(message: String, throwable: Throwable?) = log(message, throwable)
43+
44+
override fun error(message: String, throwable: Throwable?) = log(message, throwable)
45+
46+
override fun fatalError(message: String, throwable: Throwable?) = log(message, throwable)
47+
48+
override fun info(message: String, throwable: Throwable?) = log(message, throwable)
49+
50+
override fun warn(message: String, throwable: Throwable?) = log(message, throwable)
51+
52+
/**
53+
* Log the given [message] and optional [throwable] in the standard format defined by this logger.
54+
*/
55+
private fun log(message: String, throwable: Throwable?) {
56+
outputStream.println(message)
57+
58+
logger.info { "[DEPENDENCY TREE] $message" }
59+
throwable?.also {
60+
logger.error("[DEPENDENCY TREE ERROR]", it)
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.packagemanagers.maven.utils
21+
22+
import java.io.InputStream
23+
24+
import kotlinx.serialization.Serializable
25+
import kotlinx.serialization.json.Json
26+
import kotlinx.serialization.json.decodeToSequence
27+
28+
import org.apache.maven.project.MavenProject
29+
30+
import org.eclipse.aether.artifact.DefaultArtifact
31+
import org.eclipse.aether.graph.DefaultDependencyNode
32+
import org.eclipse.aether.graph.Dependency
33+
import org.eclipse.aether.graph.DependencyNode
34+
import org.eclipse.aether.repository.RemoteRepository
35+
36+
/** The [Json] instance to use for parsing of dependency trees in JSON format. */
37+
internal val JSON = Json { ignoreUnknownKeys = true }
38+
39+
/**
40+
* A data class to represent a node in the JSON output generated by the Maven Dependency Plugin.
41+
*/
42+
@Serializable
43+
internal data class DependencyTreeMojoNode(
44+
val groupId: String,
45+
val artifactId: String,
46+
val version: String,
47+
val type: String,
48+
val scope: String,
49+
val classifier: String,
50+
val children: List<DependencyTreeMojoNode> = emptyList()
51+
) {
52+
/** An ID that matches the internal IDs generated for Maven projects. */
53+
val projectId: String = "$groupId:$artifactId:$type:$version"
54+
}
55+
56+
/**
57+
* Parse the file with the aggregated output of the Maven Dependency Plugin from the given [inputStream] to a sequence
58+
* of [DependencyNode]s for all the projects encountered during the build. Use the given [projects] that were
59+
* encountered during the build to enrich the dependencies with further information.
60+
*/
61+
internal fun parseDependencyTree(
62+
inputStream: InputStream,
63+
projects: Collection<MavenProject>
64+
): Sequence<DependencyNode> {
65+
val projectsById = projects.associateBy(MavenProject::getId)
66+
67+
return JSON.decodeToSequence<DependencyTreeMojoNode>(inputStream)
68+
.mapNotNull { node ->
69+
projectsById[node.projectId]?.let { project ->
70+
node.toDependencyNode(project.remoteProjectRepositories.orEmpty())
71+
}
72+
}
73+
}
74+
75+
/**
76+
* Convert this [DependencyTreeMojoNode] and all its children to a [DependencyNode]. Set the given [repositories] for
77+
* all created [DependencyNode]s.
78+
*/
79+
private fun DependencyTreeMojoNode.toDependencyNode(repositories: List<RemoteRepository>): DependencyNode {
80+
val artifact = DefaultArtifact(groupId, artifactId, classifier, type, version)
81+
val dependency = Dependency(artifact, scope)
82+
val childNodes = children.map { it.toDependencyNode(repositories) }
83+
84+
return DefaultDependencyNode(dependency).apply {
85+
children = childNodes
86+
setRepositories(repositories)
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.packagemanagers.maven.utils
21+
22+
import io.kotest.core.spec.style.WordSpec
23+
import io.kotest.inspectors.forAll
24+
import io.kotest.matchers.collections.shouldBeSingleton
25+
import io.kotest.matchers.collections.shouldContainExactly
26+
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
27+
import io.kotest.matchers.collections.shouldHaveSize
28+
import io.kotest.matchers.shouldBe
29+
30+
import io.mockk.every
31+
import io.mockk.mockk
32+
import io.mockk.spyk
33+
34+
import java.io.InputStream
35+
36+
import org.apache.maven.project.MavenProject
37+
38+
import org.eclipse.aether.graph.DependencyNode
39+
import org.eclipse.aether.repository.RemoteRepository
40+
41+
private const val DEPENDENCY_TREE_JSON = """
42+
{
43+
"groupId": "org.ossreviewtoolkit",
44+
"artifactId": "tycho-test-bundle",
45+
"version": "1.0.0-SNAPSHOT",
46+
"type": "pom",
47+
"scope": "",
48+
"classifier": "",
49+
"optional": "false",
50+
"children": [
51+
{
52+
"groupId": "org.apache.commons",
53+
"artifactId": "commons-configuration2",
54+
"version": "2.11.0",
55+
"type": "jar",
56+
"scope": "compile",
57+
"classifier": "",
58+
"optional": "false",
59+
"children": [
60+
{
61+
"groupId": "org.apache.commons",
62+
"artifactId": "commons-lang3",
63+
"version": "3.14.0",
64+
"type": "jar",
65+
"scope": "compile",
66+
"classifier": "",
67+
"optional": "false"
68+
},
69+
{
70+
"groupId": "org.apache.commons",
71+
"artifactId": "commons-text",
72+
"version": "1.12.0",
73+
"type": "bundle",
74+
"scope": "test",
75+
"classifier": "foo",
76+
"optional": "false"
77+
},
78+
{
79+
"groupId": "commons-logging",
80+
"artifactId": "commons-logging",
81+
"version": "1.3.2",
82+
"type": "jar",
83+
"scope": "compile",
84+
"classifier": "",
85+
"optional": "false"
86+
}
87+
]
88+
}
89+
]
90+
}
91+
"""
92+
93+
class DependencyTreeParserTest : WordSpec({
94+
"JSON serialization" should {
95+
"deserialize a hierarchical structure" {
96+
val node = JSON.decodeFromString<DependencyTreeMojoNode>(DEPENDENCY_TREE_JSON)
97+
98+
with(node) {
99+
groupId shouldBe "org.ossreviewtoolkit"
100+
artifactId shouldBe "tycho-test-bundle"
101+
version shouldBe "1.0.0-SNAPSHOT"
102+
type shouldBe "pom"
103+
scope shouldBe ""
104+
classifier shouldBe ""
105+
106+
with(children.single()) {
107+
groupId shouldBe "org.apache.commons"
108+
artifactId shouldBe "commons-configuration2"
109+
version shouldBe "2.11.0"
110+
type shouldBe "jar"
111+
scope shouldBe "compile"
112+
classifier shouldBe ""
113+
114+
children shouldContainExactlyInAnyOrder listOf(
115+
DependencyTreeMojoNode(
116+
"org.apache.commons",
117+
"commons-lang3",
118+
"3.14.0",
119+
"jar",
120+
"compile",
121+
""
122+
),
123+
DependencyTreeMojoNode(
124+
"org.apache.commons",
125+
"commons-text",
126+
"1.12.0",
127+
"bundle",
128+
"test",
129+
"foo"
130+
),
131+
DependencyTreeMojoNode(
132+
"commons-logging",
133+
"commons-logging",
134+
"1.3.2",
135+
"jar",
136+
"compile",
137+
""
138+
)
139+
)
140+
}
141+
}
142+
}
143+
}
144+
145+
"parseDependencyTree()" should {
146+
"parse the dependency tree of a single project" {
147+
val projectNode = DependencyTreeMojoNode(
148+
"org.ossreviewtoolkit",
149+
"ort",
150+
"1.2.3-SNAPSHOT",
151+
"pom",
152+
"",
153+
"",
154+
listOf(
155+
DependencyTreeMojoNode(
156+
"org.apache.commons",
157+
"commons-configuration2",
158+
"2.11.0",
159+
"jar",
160+
"compile",
161+
"",
162+
listOf(
163+
DependencyTreeMojoNode(
164+
"org.apache.commons",
165+
"commons-lang3",
166+
"3.14.0",
167+
"jar",
168+
"compile",
169+
""
170+
),
171+
DependencyTreeMojoNode(
172+
"org.apache.commons",
173+
"commons-text",
174+
"1.12.0",
175+
"bundle",
176+
"test",
177+
"foo"
178+
),
179+
DependencyTreeMojoNode(
180+
"commons-logging",
181+
"commons-logging",
182+
"1.3.2",
183+
"jar",
184+
"compile",
185+
""
186+
)
187+
)
188+
)
189+
)
190+
)
191+
192+
val projectDependencies = parseDependencyTree(
193+
inputStreamFor(projectNode),
194+
listOf(createProject("ort"))
195+
).toList()
196+
197+
projectDependencies.shouldBeSingleton {
198+
compareNodes(projectNode, it)
199+
}
200+
}
201+
202+
"parse the dependency trees of multiple projects" {
203+
val projectNode1 = DependencyTreeMojoNode(
204+
"org.ossreviewtoolkit",
205+
"module1",
206+
"1.2.3-SNAPSHOT",
207+
"pom",
208+
"",
209+
"",
210+
listOf(
211+
DependencyTreeMojoNode(
212+
"org.apache.commons",
213+
"commons-configuration2",
214+
"2.11.0",
215+
"jar",
216+
"compile",
217+
""
218+
)
219+
)
220+
)
221+
val projectNode2 = DependencyTreeMojoNode(
222+
"org.ossreviewtoolkit",
223+
"module2",
224+
"1.2.3-SNAPSHOT",
225+
"eclipse-plugin",
226+
"",
227+
"",
228+
listOf(
229+
DependencyTreeMojoNode(
230+
"org.apache.commons",
231+
"commons-lang3",
232+
"3.14.0",
233+
"jar",
234+
"compile",
235+
""
236+
)
237+
)
238+
)
239+
240+
val projectDependencies = parseDependencyTree(
241+
inputStreamFor(projectNode1, projectNode2),
242+
listOf(createProject("module1"), createProject("module2", "eclipse-plugin"))
243+
).toList()
244+
245+
projectDependencies shouldHaveSize 2
246+
compareNodes(projectNode1, projectDependencies.first())
247+
compareNodes(projectNode2, projectDependencies.last())
248+
}
249+
250+
"use the available repositories" {
251+
val remoteRepo1 = mockk<RemoteRepository>()
252+
val remoteRepo2 = mockk<RemoteRepository>()
253+
val remoteRepositories = listOf(remoteRepo1, remoteRepo2)
254+
val project = spyk(createProject("module1", "eclipse-plugin")) {
255+
every { remoteProjectRepositories } returns remoteRepositories
256+
}
257+
258+
val projectNode = DependencyTreeMojoNode(
259+
"org.ossreviewtoolkit",
260+
"module1",
261+
"1.2.3-SNAPSHOT",
262+
"eclipse-plugin",
263+
"",
264+
"",
265+
listOf(
266+
DependencyTreeMojoNode(
267+
"org.apache.commons",
268+
"commons-configuration2",
269+
"2.11.0",
270+
"jar",
271+
"compile",
272+
""
273+
),
274+
DependencyTreeMojoNode(
275+
"org.apache.commons",
276+
"commons-lang3",
277+
"3.14.0",
278+
"jar",
279+
"compile",
280+
""
281+
)
282+
)
283+
)
284+
285+
val dependencies = parseDependencyTree(
286+
inputStreamFor(projectNode),
287+
listOf(project)
288+
).single()
289+
290+
dependencies.children.forAll { node ->
291+
node.repositories shouldContainExactly remoteRepositories
292+
}
293+
}
294+
295+
"skip unknown projects" {
296+
val projectNode1 = DependencyTreeMojoNode(
297+
"org.ossreviewtoolkit",
298+
"module1",
299+
"1.2.3-SNAPSHOT",
300+
"pom",
301+
"",
302+
"",
303+
listOf(
304+
DependencyTreeMojoNode(
305+
"org.apache.commons",
306+
"commons-configuration2",
307+
"2.11.0",
308+
"jar",
309+
"compile",
310+
""
311+
)
312+
)
313+
)
314+
val projectNode2 = DependencyTreeMojoNode(
315+
"org.ossreviewtoolkit",
316+
"module2",
317+
"1.2.3-SNAPSHOT",
318+
"eclipse-plugin",
319+
"",
320+
"",
321+
listOf(
322+
DependencyTreeMojoNode(
323+
"org.apache.commons",
324+
"commons-lang3",
325+
"3.14.0",
326+
"jar",
327+
"compile",
328+
""
329+
)
330+
)
331+
)
332+
333+
val projectDependencies = parseDependencyTree(
334+
inputStreamFor(projectNode1, projectNode2),
335+
listOf(createProject("module1"))
336+
).toList()
337+
338+
projectDependencies.shouldBeSingleton {
339+
compareNodes(projectNode1, it)
340+
}
341+
}
342+
}
343+
})
344+
345+
/**
346+
* Return an [InputStream] with the JSON representation of the given [projects] in the same way as this would be done
347+
* by the dependency tree plugin.
348+
*/
349+
private fun inputStreamFor(vararg projects: DependencyTreeMojoNode): InputStream =
350+
projects.joinToString("\n") { JSON.encodeToString(it) }.byteInputStream()
351+
352+
/**
353+
* Compare a hierarchy of [DependencyTreeMojoNode]s with a hierarchy of [DependencyNode]s given the root nodes
354+
* [mojoNode] and [dependencyNode].
355+
*/
356+
private fun compareNodes(mojoNode: DependencyTreeMojoNode, dependencyNode: DependencyNode) {
357+
with(dependencyNode) {
358+
with(artifact) {
359+
groupId shouldBe mojoNode.groupId
360+
artifactId shouldBe mojoNode.artifactId
361+
version shouldBe mojoNode.version
362+
extension shouldBe mojoNode.type
363+
classifier shouldBe mojoNode.classifier
364+
}
365+
366+
dependency.scope shouldBe mojoNode.scope
367+
368+
mojoNode.children.size shouldBe children.orEmpty().size
369+
mojoNode.children.zip(children.orEmpty()).forAll { (mojoChild, child) ->
370+
compareNodes(mojoChild, child)
371+
}
372+
}
373+
}
374+
375+
/**
376+
* Create a [MavenProject] with the given [name] and optional [packaging].
377+
*/
378+
private fun createProject(name: String, packaging: String = "pom"): MavenProject =
379+
MavenProject().apply {
380+
artifactId = name
381+
groupId = "org.ossreviewtoolkit"
382+
version = "1.2.3-SNAPSHOT"
383+
this.packaging = packaging
384+
}

0 commit comments

Comments
 (0)
Please sign in to comment.