You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/10/13 22:14:59 UTC
(maven-dependency-plugin) 01/01: [MDEP-962] Report omitted reasons in verbose JSON tree output
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch agent/mdep-962-json-verbose
in repository https://gitbox.apache.org/repos/asf/maven-dependency-plugin.git
commit 57ea05673c49f3e83420eba9eb62d7d468a14e38
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Tue Oct 13 23:27:34 2026 +0200
[MDEP-962] Report omitted reasons in verbose JSON tree output
---
.../projects/tree-verbose-json/invoker.properties | 22 ++++
src/it/projects/tree-verbose-json/plain.properties | 19 ++++
src/it/projects/tree-verbose-json/pom.xml | 52 ++++++++++
.../projects/tree-verbose-json/verbose.properties | 20 ++++
src/it/projects/tree-verbose-json/verify.groovy | 53 ++++++++++
.../plugins/dependency/tree/DependencyNode.java | 92 +++++++++++++++++
.../dependency/tree/DependencyTreeBuilder.java | 40 ++++---
.../dependency/tree/JsonDependencyNodeVisitor.java | 53 +++++++++-
src/site/markdown/examples/tree-mojo.md | 1 +
.../tree/JsonDependencyNodeVisitorTest.java | 115 +++++++++++++++++++++
10 files changed, 449 insertions(+), 18 deletions(-)
diff --git a/src/it/projects/tree-verbose-json/invoker.properties b/src/it/projects/tree-verbose-json/invoker.properties
new file mode 100644
index 00000000..f7a40d29
--- /dev/null
+++ b/src/it/projects/tree-verbose-json/invoker.properties
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+invoker.goals.1 = ${project.groupId}:${project.artifactId}:${project.version}:tree
+invoker.userPropertiesFile.1 = verbose.properties
+
+invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:tree
+invoker.userPropertiesFile.2 = plain.properties
diff --git a/src/it/projects/tree-verbose-json/plain.properties b/src/it/projects/tree-verbose-json/plain.properties
new file mode 100644
index 00000000..4fbe19bb
--- /dev/null
+++ b/src/it/projects/tree-verbose-json/plain.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+outputType = json
+outputFile = target/plain.json
diff --git a/src/it/projects/tree-verbose-json/pom.xml b/src/it/projects/tree-verbose-json/pom.xml
new file mode 100644
index 00000000..7395ca17
--- /dev/null
+++ b/src/it/projects/tree-verbose-json/pom.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+
+<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.maven.plugins.dependency.its</groupId>
+ <artifactId>tree-verbose-json</artifactId>
+ <version>1.0-SNAPSHOT</version>
+
+ <description>
+ dependency:tree with outputType=json and verbose=true reports why a node was omitted.
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven.its.dependency</groupId>
+ <artifactId>scope-root</artifactId>
+ <version>1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.its.dependency</groupId>
+ <artifactId>scope-test-root</artifactId>
+ <version>1.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.its.dependency</groupId>
+ <artifactId>provided-root</artifactId>
+ <version>1.0</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/it/projects/tree-verbose-json/verbose.properties b/src/it/projects/tree-verbose-json/verbose.properties
new file mode 100644
index 00000000..2e10bb33
--- /dev/null
+++ b/src/it/projects/tree-verbose-json/verbose.properties
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+outputType = json
+verbose = true
+outputFile = target/verbose.json
diff --git a/src/it/projects/tree-verbose-json/verify.groovy b/src/it/projects/tree-verbose-json/verify.groovy
new file mode 100644
index 00000000..65b759a2
--- /dev/null
+++ b/src/it/projects/tree-verbose-json/verify.groovy
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import groovy.json.JsonSlurper
+
+import static org.assertj.core.api.Assertions.assertThat
+
+def verbose = new JsonSlurper().parse(new File(basedir, "target/verbose.json"))
+
+// scope-root -> scope-leaf:1.0 is the node Maven keeps
+def kept = verbose.children[0].children[0]
+assertThat(kept.version).isEqualTo("1.0")
+assertThat(kept.included).isEqualTo(true)
+assertThat(kept).doesNotContainKey("omittedReason")
+
+// scope-test-root -> scope-leaf:2.0 loses to 1.0
+def omitted = verbose.children[1].children[0]
+assertThat(omitted.version).isEqualTo("2.0")
+assertThat(omitted.included).isEqualTo(false)
+assertThat(omitted.omittedReason).isEqualTo("conflict")
+assertThat(omitted.winnerVersion).isEqualTo("1.0")
+
+// provided-root -> scope-leaf:1.0 repeats the kept node
+def duplicate = verbose.children[2].children[0]
+assertThat(duplicate.version).isEqualTo("1.0")
+assertThat(duplicate.included).isEqualTo(false)
+assertThat(duplicate.omittedReason).isEqualTo("duplicate")
+assertThat(duplicate).doesNotContainKey("winnerVersion")
+
+// the direct dependencies are included
+assertThat(verbose.children*.included).containsOnly(true)
+
+// without verbose the JSON has none of the additional fields
+def plain = new File(basedir, "target/plain.json").text
+assertThat(plain).doesNotContain("included").doesNotContain("omittedReason").doesNotContain("winnerVersion")
+
+return true
diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyNode.java b/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyNode.java
index 4fbaeca6..cae0a933 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyNode.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyNode.java
@@ -37,6 +37,8 @@ public class DependencyNode {
private List<DependencyNode> children = Collections.emptyList();
+ private VerboseInfo verboseInfo;
+
/**
* @param parent the parent node, or <code>null</code> for the root
* @param artifact the artifact of this node
@@ -64,6 +66,22 @@ public class DependencyNode {
this.children = children;
}
+ /**
+ * @return why the node is omitted and what was managed, or <code>null</code> if the tree was not built verbose
+ * @since 3.12.0
+ */
+ public VerboseInfo getVerboseInfo() {
+ return verboseInfo;
+ }
+
+ /**
+ * @param verboseInfo see {@link #getVerboseInfo()}
+ * @since 3.12.0
+ */
+ public void setVerboseInfo(VerboseInfo verboseInfo) {
+ this.verboseInfo = verboseInfo;
+ }
+
/**
* @return the label of this node
*/
@@ -104,6 +122,7 @@ public class DependencyNode {
}
DependencyNode copy = new DependencyNode(newParent, artifact, nodeString);
+ copy.setVerboseInfo(verboseInfo);
List<DependencyNode> copiedChildren = new ArrayList<>(children.size());
for (DependencyNode child : children) {
DependencyNode copiedChild = child.filter(copy, keep, include);
@@ -119,4 +138,77 @@ public class DependencyNode {
copy.setChildren(copiedChildren);
return copy;
}
+
+ /**
+ * What the verbose tree knows about a node besides its artifact: whether it is omitted and why, and the version and
+ * scope that dependency management or scope selection replaced.
+ *
+ * @since 3.12.0
+ */
+ public static final class VerboseInfo {
+ /**
+ * The node repeats an artifact of the same version that is kept elsewhere in the tree.
+ */
+ public static final String OMITTED_FOR_DUPLICATE = "duplicate";
+
+ /**
+ * The node loses to another version of the artifact.
+ */
+ public static final String OMITTED_FOR_CONFLICT = "conflict";
+
+ private final String omittedReason;
+
+ private final String winnerVersion;
+
+ private final String premanagedVersion;
+
+ private final String premanagedScope;
+
+ private final String scopeNotUpdatedTo;
+
+ /**
+ * @param omittedReason {@link #OMITTED_FOR_DUPLICATE}, {@link #OMITTED_FOR_CONFLICT} or <code>null</code> if
+ * the node is included
+ * @param winnerVersion the version kept instead of this node, <code>null</code> if the node is included
+ * @param premanagedVersion the version before dependency management, or <code>null</code>
+ * @param premanagedScope the scope before dependency management, or <code>null</code>
+ * @param scopeNotUpdatedTo the scope that scope selection wanted but did not apply, or <code>null</code>
+ */
+ public VerboseInfo(
+ String omittedReason,
+ String winnerVersion,
+ String premanagedVersion,
+ String premanagedScope,
+ String scopeNotUpdatedTo) {
+ this.omittedReason = omittedReason;
+ this.winnerVersion = winnerVersion;
+ this.premanagedVersion = premanagedVersion;
+ this.premanagedScope = premanagedScope;
+ this.scopeNotUpdatedTo = scopeNotUpdatedTo;
+ }
+
+ public boolean isIncluded() {
+ return omittedReason == null;
+ }
+
+ public String getOmittedReason() {
+ return omittedReason;
+ }
+
+ public String getWinnerVersion() {
+ return winnerVersion;
+ }
+
+ public String getPremanagedVersion() {
+ return premanagedVersion;
+ }
+
+ public String getPremanagedScope() {
+ return premanagedScope;
+ }
+
+ public String getScopeNotUpdatedTo() {
+ return scopeNotUpdatedTo;
+ }
+ }
}
diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyTreeBuilder.java b/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyTreeBuilder.java
index 7f3ec0e4..ccd771fa 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyTreeBuilder.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/tree/DependencyTreeBuilder.java
@@ -26,6 +26,7 @@ import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.model.Dependency;
+import org.apache.maven.plugins.dependency.tree.DependencyNode.VerboseInfo;
import org.apache.maven.project.DefaultDependencyResolutionRequest;
import org.apache.maven.project.DependencyResolutionException;
import org.apache.maven.project.DependencyResolutionRequest;
@@ -124,8 +125,10 @@ class DependencyTreeBuilder {
Artifact artifact,
ArtifactFilter filter,
boolean verbose) {
+ VerboseInfo info = verbose ? verboseInfo(node, artifact) : null;
DependencyNode current = new DependencyNode(
- parent, artifact, verbose ? verboseNodeString(node, artifact) : nodeString(artifact));
+ parent, artifact, verbose ? verboseNodeString(info, artifact) : nodeString(artifact));
+ current.setVerboseInfo(info);
List<DependencyNode> children = new ArrayList<>(node.getChildren().size());
for (org.eclipse.aether.graph.DependencyNode child : node.getChildren()) {
@@ -151,18 +154,31 @@ class DependencyTreeBuilder {
return artifact + (artifact.isOptional() ? " (optional)" : "");
}
- private static String verboseNodeString(org.eclipse.aether.graph.DependencyNode node, Artifact artifact) {
+ private static VerboseInfo verboseInfo(org.eclipse.aether.graph.DependencyNode node, Artifact artifact) {
org.eclipse.aether.graph.DependencyNode winner =
(org.eclipse.aether.graph.DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER);
String winnerVersion = null;
String ignoredScope = null;
+ String omittedReason = null;
if (winner != null) {
winnerVersion = winner.getArtifact().getBaseVersion();
+ omittedReason = winnerVersion.equals(artifact.getVersion())
+ ? VerboseInfo.OMITTED_FOR_DUPLICATE
+ : VerboseInfo.OMITTED_FOR_CONFLICT;
} else {
ignoredScope = (String) node.getData().get(VerboseJavaScopeSelector.REDUCED_SCOPE);
}
- boolean included = winnerVersion == null;
+ return new VerboseInfo(
+ omittedReason,
+ winnerVersion,
+ DependencyManagerUtils.getPremanagedVersion(node),
+ DependencyManagerUtils.getPremanagedScope(node),
+ ignoredScope);
+ }
+
+ private static String verboseNodeString(VerboseInfo info, Artifact artifact) {
+ boolean included = info.isIncluded();
StringBuilder buffer = new StringBuilder();
if (!included) {
@@ -171,22 +187,20 @@ class DependencyTreeBuilder {
buffer.append(artifact);
List<String> items = new ArrayList<>();
- String premanagedVersion = DependencyManagerUtils.getPremanagedVersion(node);
- if (premanagedVersion != null) {
- items.add("version managed from " + premanagedVersion);
+ if (info.getPremanagedVersion() != null) {
+ items.add("version managed from " + info.getPremanagedVersion());
}
- String premanagedScope = DependencyManagerUtils.getPremanagedScope(node);
- if (premanagedScope != null) {
- items.add("scope managed from " + premanagedScope);
+ if (info.getPremanagedScope() != null) {
+ items.add("scope managed from " + info.getPremanagedScope());
}
- if (ignoredScope != null) {
- items.add("scope not updated to " + ignoredScope);
+ if (info.getScopeNotUpdatedTo() != null) {
+ items.add("scope not updated to " + info.getScopeNotUpdatedTo());
}
if (!included) {
items.add(
- winnerVersion.equals(artifact.getVersion())
+ VerboseInfo.OMITTED_FOR_DUPLICATE.equals(info.getOmittedReason())
? "omitted for duplicate"
- : "omitted for conflict with " + winnerVersion);
+ : "omitted for conflict with " + info.getWinnerVersion());
}
if (!items.isEmpty()) {
diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitor.java b/src/main/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitor.java
index 630afcce..12f10a73 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitor.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitor.java
@@ -22,6 +22,9 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
@@ -84,7 +87,7 @@ public class JsonDependencyNodeVisitor extends AbstractSerializingVisitor implem
return;
}
visited.add(node);
- appendNodeValues(sb, indent, node.getArtifact(), !node.getChildren().isEmpty());
+ appendNodeValues(sb, indent, node, !node.getChildren().isEmpty());
if (!node.getChildren().isEmpty()) {
writeChildren(indent, node, sb, visited);
}
@@ -123,21 +126,61 @@ public class JsonDependencyNodeVisitor extends AbstractSerializingVisitor implem
*
* @param sb the string builder to append to
* @param indent the current indent level
- * @param artifact the artifact to write
- * @param hasChildren true if the artifact has children
+ * @param node the node whose artifact, and verbose information if present, is written
+ * @param hasChildren true if the node has children
*/
- private void appendNodeValues(StringBuilder sb, int indent, Artifact artifact, boolean hasChildren) {
+ private void appendNodeValues(StringBuilder sb, int indent, DependencyNode node, boolean hasChildren) {
+ Artifact artifact = node.getArtifact();
+ Map<String, String> verboseValues = verboseValues(node.getVerboseInfo());
appendKeyValue(sb, indent, "groupId", artifact.getGroupId());
appendKeyValue(sb, indent, "artifactId", artifact.getArtifactId());
appendKeyValue(sb, indent, "version", artifact.getVersion());
appendKeyValue(sb, indent, "type", artifact.getType());
appendKeyValue(sb, indent, "scope", artifact.getScope());
appendKeyValue(sb, indent, "classifier", artifact.getClassifier());
- if (hasChildren) {
+ if (hasChildren || !verboseValues.isEmpty()) {
appendKeyValue(sb, indent, "optional", String.valueOf(artifact.isOptional()));
} else {
appendKeyWithoutComma(sb, indent, "optional", String.valueOf(artifact.isOptional()));
}
+
+ Iterator<Map.Entry<String, String>> entries = verboseValues.entrySet().iterator();
+ while (entries.hasNext()) {
+ Map.Entry<String, String> entry = entries.next();
+ sb.append(indent(indent))
+ .append("\"")
+ .append(entry.getKey())
+ .append("\": ")
+ .append(entry.getValue())
+ .append(entries.hasNext() || hasChildren ? "," : "")
+ .append(System.lineSeparator());
+ }
+ }
+
+ /**
+ * The additional JSON values of a verbose node, in output order. The values are already JSON: <code>included</code>
+ * is a boolean, the others are strings.
+ */
+ private static Map<String, String> verboseValues(DependencyNode.VerboseInfo info) {
+ Map<String, String> values = new LinkedHashMap<>();
+ if (info == null) {
+ return values;
+ }
+ values.put("included", String.valueOf(info.isIncluded()));
+ putString(values, "omittedReason", info.getOmittedReason());
+ if (DependencyNode.VerboseInfo.OMITTED_FOR_CONFLICT.equals(info.getOmittedReason())) {
+ putString(values, "winnerVersion", info.getWinnerVersion());
+ }
+ putString(values, "versionManagedFrom", info.getPremanagedVersion());
+ putString(values, "scopeManagedFrom", info.getPremanagedScope());
+ putString(values, "scopeNotUpdatedTo", info.getScopeNotUpdatedTo());
+ return values;
+ }
+
+ private static void putString(Map<String, String> values, String key, String value) {
+ if (value != null) {
+ values.put(key, "\"" + value + "\"");
+ }
}
/**
* Appends a key value pair to the string builder.
diff --git a/src/site/markdown/examples/tree-mojo.md b/src/site/markdown/examples/tree-mojo.md
index fcaa3f69..92afe5a7 100644
--- a/src/site/markdown/examples/tree-mojo.md
+++ b/src/site/markdown/examples/tree-mojo.md
@@ -64,6 +64,7 @@ mvn dependency:tree -DoutputType=<format> -DoutputFile=<filename>
- `classifier`: The classifier, if any (e.g., empty string if none).
- `optional`: Whether the dependency is optional (`true` or `false`).
- `children`: An array of dependency objects with the same structure, representing transitive dependencies.
+ - With `-Dverbose`, every node also has `included` (`false` for a node Maven leaves out of the resolved tree), and, when they apply, `omittedReason` (`duplicate` or `conflict`), `winnerVersion` (the version kept instead, for `conflict`), `versionManagedFrom`, `scopeManagedFrom` and `scopeNotUpdatedTo`. These fields are not written without `-Dverbose`.
- **Nested Dependencies**: Each dependency in the `children` array follows the same structure, allowing for recursive representation of the dependency tree.
**Example**:
diff --git a/src/test/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitorTest.java b/src/test/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitorTest.java
new file mode 100644
index 00000000..5d8d48c8
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/dependency/tree/JsonDependencyNodeVisitorTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.maven.plugins.dependency.tree;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Collections;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonValue;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
+import org.apache.maven.plugins.dependency.tree.DependencyNode.VerboseInfo;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JsonDependencyNodeVisitorTest {
+
+ @Test
+ void nodeWithoutVerboseInfoHasNoVerboseFields() {
+ JsonObject json = toJson(new DependencyNode(null, artifact("a", "1.0"), "a"));
+
+ assertEquals(7, json.size());
+ assertEquals("false", json.getString("optional"));
+ assertFalse(json.containsKey("included"));
+ }
+
+ @Test
+ void includedNodeReportsIncludedAndManagement() {
+ DependencyNode node = new DependencyNode(null, artifact("a", "2.0"), "a");
+ node.setVerboseInfo(new VerboseInfo(null, null, "1.0", "test", "compile"));
+
+ JsonObject json = toJson(node);
+
+ assertEquals(JsonValue.TRUE, json.get("included"));
+ assertEquals("1.0", json.getString("versionManagedFrom"));
+ assertEquals("test", json.getString("scopeManagedFrom"));
+ assertEquals("compile", json.getString("scopeNotUpdatedTo"));
+ assertFalse(json.containsKey("omittedReason"));
+ assertFalse(json.containsKey("winnerVersion"));
+ }
+
+ @Test
+ void omittedForDuplicateHasReasonButNoWinnerVersion() {
+ DependencyNode node = new DependencyNode(null, artifact("a", "1.0"), "a");
+ node.setVerboseInfo(new VerboseInfo(VerboseInfo.OMITTED_FOR_DUPLICATE, "1.0", null, null, null));
+
+ JsonObject json = toJson(node);
+
+ assertEquals(JsonValue.FALSE, json.get("included"));
+ assertEquals("duplicate", json.getString("omittedReason"));
+ assertFalse(json.containsKey("winnerVersion"));
+ }
+
+ @Test
+ void omittedForConflictHasWinnerVersion() {
+ DependencyNode node = new DependencyNode(null, artifact("a", "1.0"), "a");
+ node.setVerboseInfo(new VerboseInfo(VerboseInfo.OMITTED_FOR_CONFLICT, "2.0", null, null, null));
+
+ JsonObject json = toJson(node);
+
+ assertEquals(JsonValue.FALSE, json.get("included"));
+ assertEquals("conflict", json.getString("omittedReason"));
+ assertEquals("2.0", json.getString("winnerVersion"));
+ }
+
+ @Test
+ void verboseFieldsOfChildrenAreNestedAndOutputStaysValidJson() {
+ DependencyNode root = new DependencyNode(null, artifact("root", "1.0"), "root");
+ DependencyNode child = new DependencyNode(root, artifact("child", "1.0"), "child");
+ child.setVerboseInfo(new VerboseInfo(VerboseInfo.OMITTED_FOR_DUPLICATE, "1.0", null, null, null));
+ root.setChildren(Collections.singletonList(child));
+
+ JsonObject json = toJson(root);
+
+ assertTrue(json.getJsonArray("children").getJsonObject(0).containsKey("omittedReason"));
+ assertFalse(json.containsKey("included"));
+ }
+
+ private static JsonObject toJson(DependencyNode node) {
+ StringWriter writer = new StringWriter();
+ node.accept(new JsonDependencyNodeVisitor(writer));
+ System.setProperty("jakarta.json.provider", "org.glassfish.json.JsonProviderImpl");
+ try (JsonReader reader = Json.createReader(new StringReader(writer.toString()))) {
+ return reader.readObject();
+ }
+ }
+
+ private static Artifact artifact(String artifactId, String version) {
+ return new DefaultArtifact(
+ "g", artifactId, version, Artifact.SCOPE_COMPILE, "jar", null, new DefaultArtifactHandler("jar"));
+ }
+}