Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fba398d
[MDEP-799] tree: add optional output type json
MartinWitt May 22, 2023
f708cdf
add testcase
MartinWitt May 22, 2023
a988792
replace System.lineSeparator with constant
MartinWitt May 22, 2023
acbb5b4
change according to feedback
MartinWitt Jun 18, 2023
8b95bb8
Use PrintWriter from AbstractSerializingVisitor
LogFlames May 16, 2024
d1397ca
enable test and fix typo
LogFlames May 16, 2024
f5b595d
Test circular dependency
LogFlames May 16, 2024
68357de
add visited set to stop infinite recursion
LogFlames May 16, 2024
7184b7b
use repeat instead of commons
LogFlames May 20, 2024
c8c50ed
add blank lines before @params
LogFlames May 20, 2024
7e679ee
remove dep on apache commons StringUtils
LogFlames May 20, 2024
48743b3
blank line before @param
LogFlames May 20, 2024
fe34bd3
let printwriter create output file
LogFlames May 20, 2024
ce41f60
create file before json node visiter to verify macos file not found i…
LogFlames May 22, 2024
07c3dc7
create new file explicitly
LogFlames May 22, 2024
e96cd5c
create dirs above file
LogFlames May 22, 2024
bcdb729
add test: parse json and verify key-value pairs
LogFlames May 22, 2024
0e45124
fix testcase
LogFlames May 22, 2024
500cdb2
add missing " in tests
LogFlames May 22, 2024
6c35efd
remove redundant test - replaced by testTreeJsonParsing
LogFlames May 22, 2024
de7e7f9
replace printwriter with outputstreamwriter
LogFlames May 22, 2024
f2ef84c
throw ioexception instead of exception
LogFlames May 22, 2024
83fc3d6
use try-with-resource
LogFlames May 22, 2024
1b7425c
remove temporary json showcase file
LogFlames May 22, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ under the License.
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
<version>1.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- support for JUnit 4 -->
<groupId>org.junit.vintage</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* 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.Writer;
import java.util.HashSet;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;

/**
* A dependency node visitor that serializes visited nodes to a writer using the JSON format.
*/
public class JsonDependencyNodeVisitor extends AbstractSerializingVisitor implements DependencyNodeVisitor {

private String indentChar = " ";

/**
* Creates a new instance of {@link JsonDependencyNodeVisitor}. The writer will be used to write the output.
*
* @param writer the writer to write to
*/
public JsonDependencyNodeVisitor(Writer writer) {
super(writer);
}

@Override
public boolean visit(DependencyNode node) {
if (node.getParent() == null || node.getParent() == node) {
writeRootNode(node);
}
return true;
}

/**
* Writes the node to the writer. This method is recursive and will write all children nodes.
*
* @param node the node to write
*/
private void writeRootNode(DependencyNode node) {
Set<DependencyNode> visited = new HashSet<DependencyNode>();
int indent = 2;
StringBuilder sb = new StringBuilder();
sb.append("{").append("\n");
writeNode(indent, node, sb, visited);
sb.append("}").append("\n");
writer.write(sb.toString());
}
/**
* Appends the node and its children to the string builder.
*
* @param indent the current indent level
* @param node the node to write
* @param sb the string builder to append to
*/
private void writeNode(int indent, DependencyNode node, StringBuilder sb, Set<DependencyNode> visited) {
if (visited.contains(node)) {
// Circular dependency detected
// Should an exception be thrown?
return;
}
visited.add(node);
appendNodeValues(sb, indent, node.getArtifact(), !node.getChildren().isEmpty());
if (!node.getChildren().isEmpty()) {
writeChildren(indent, node, sb, visited);
}
}
/**
* Writes the children of the node to the string builder. And each children of each node will be written recursively.
*
* @param indent the current indent level
* @param node the node to write
* @param sb the string builder to append to
*/
private void writeChildren(int indent, DependencyNode node, StringBuilder sb, Set<DependencyNode> visited) {
sb.append(indent(indent)).append("\"children\": [").append("\n");
indent += 2;
for (int i = 0; i < node.getChildren().size(); i++) {
DependencyNode child = node.getChildren().get(i);
sb.append(indent(indent));
sb.append("{").append("\n");
writeNode(indent + 2, child, sb, visited);
sb.append(indent(indent)).append("}");
// we skip the comma for the last child
if (i != node.getChildren().size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append(indent(indent)).append("]").append("\n");
}

@Override
public boolean endVisit(DependencyNode node) {
return true;
}
/**
* Appends the artifact values to the string builder.
*
* @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
*/
private void appendNodeValues(StringBuilder sb, int indent, Artifact artifact, boolean hasChildren) {
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) {
appendKeyValue(sb, indent, "optional", String.valueOf(artifact.isOptional()));
} else {
appendKeyWithoutComma(sb, indent, "optional", String.valueOf(artifact.isOptional()));
}
}
/**
* Appends a key value pair to the string builder.
*
* @param sb the string builder to append to
* @param indent the current indent level
* @param key the key used as json key
* @param value the value used as json value
*/
private void appendKeyValue(StringBuilder sb, int indent, String key, String value) {
if (value == null) {
value = "";
}

sb.append(indent(indent))
.append("\"")
.append(key)
.append("\"")
.append(":")
.append(indentChar)
.append("\"")
.append(value)
.append("\"")
.append(",")
.append("\n");
}
/**
* Appends a key value pair to the string builder without a comma at the end. This is used for the last children of a node.
*
* @param sb the string builder to append to
* @param indent the current indent level
* @param key the key used as json key
* @param value the value used as json value
*/
private void appendKeyWithoutComma(StringBuilder sb, int indent, String key, String value) {
if (value == null) {
value = "";
}

sb.append(indent(indent))
.append("\"")
.append(key)
.append("\"")
.append(":")
.append(indentChar)
.append("\"")
.append(value)
.append("\"")
.append("\n");
}

/**
* Returns a string of {@link #indentChar} for the indent level.
*
* @param indent the number of indent levels
* @return the string of indent characters
*/
private String indent(int indent) {
if (indent < 1) {
return "";
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < indent; i++) {
sb.append(indentChar);
}

return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ public DependencyNodeVisitor getSerializingDependencyNodeVisitor(Writer writer)
return new TGFDependencyNodeVisitor(writer);
} else if ("dot".equals(outputType)) {
return new DOTDependencyNodeVisitor(writer);
} else if ("json".equals(outputType)) {
return new JsonDependencyNodeVisitor(writer);
} else {
return new SerializingDependencyNodeVisitor(writer, toGraphTokens(tokens));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,19 @@
*/
package org.apache.maven.plugins.dependency.tree;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
Expand All @@ -32,6 +42,7 @@
import org.apache.maven.plugins.dependency.AbstractDependencyMojoTestCase;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.apache.maven.shared.dependency.graph.internal.DefaultDependencyNode;

/**
* Tests <code>TreeMojo</code>.
Expand Down Expand Up @@ -137,6 +148,74 @@ public void testTreeTGFSerializing() throws Exception {
assertTrue(findString(contents, "testGroupId:release:jar:1.0:compile"));
}

/**
* Test the JSON format serialization on DependencyNodes with circular dependence
*/
public void testTreeJsonCircularDependency() throws IOException {
String outputFileName = testDir.getAbsolutePath() + "tree1.json";
File outputFile = new File(outputFileName);
Files.createDirectories(outputFile.getParentFile().toPath());
outputFile.createNewFile();

Artifact artifact1 = this.stubFactory.createArtifact("testGroupId", "project1", "1.0");
Artifact artifact2 = this.stubFactory.createArtifact("testGroupId", "project2", "1.0");
DefaultDependencyNode node1 = new DefaultDependencyNode(artifact1);
DefaultDependencyNode node2 = new DefaultDependencyNode(artifact2);

node1.setChildren(new ArrayList<DependencyNode>());
node2.setChildren(new ArrayList<DependencyNode>());

node1.getChildren().add(node2);
node2.getChildren().add(node1);

JsonDependencyNodeVisitor jsonDependencyNodeVisitor =
new JsonDependencyNodeVisitor(new OutputStreamWriter(new FileOutputStream(outputFile)));

jsonDependencyNodeVisitor.visit(node1);
}

/*
* Test parsing of Json output and verify all key-value pairs
*/
public void testTreeJsonParsing() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you declare a more specific exception?

Copy link
Contributor Author

@LogFlames LogFlames May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses the prebuilt function runTreeMojo that is also used by most other tests in the file, which throws an Exception. I don't think this can be more specific without major changes to runTreeMojo.
Within runTreeMojo there are function that throws the following exceptions:
lookupMojo: Exception
setVariableValueToObject: IllegalAccessException
this.stubFactory.createArtifact: IOException
mojo.execute(): MojoFailureException and MojoExecutionException

List<String> contents = runTreeMojo("tree2.json", "json");

try (JsonReader reader = Json.createReader(new StringReader(String.join("\n", contents)))) {
JsonObject root = reader.readObject();

assertEquals(root.getString("groupId"), "testGroupId");
assertEquals(root.getString("artifactId"), "project");
assertEquals(root.getString("version"), "1.0");
assertEquals(root.getString("type"), "jar");
assertEquals(root.getString("scope"), "compile");
assertEquals(root.getString("classifier"), "");
assertEquals(root.getString("optional"), "false");

JsonArray children = root.getJsonArray("children");
assertEquals(children.size(), 2);

JsonObject child0 = children.getJsonObject(0);

assertEquals(child0.getString("groupId"), "testGroupId");
assertEquals(child0.getString("artifactId"), "release");
assertEquals(child0.getString("version"), "1.0");
assertEquals(child0.getString("type"), "jar");
assertEquals(child0.getString("scope"), "compile");
assertEquals(child0.getString("classifier"), "");
assertEquals(child0.getString("optional"), "false");

JsonObject child1 = children.getJsonObject(1);

assertEquals(child1.getString("groupId"), "testGroupId");
assertEquals(child1.getString("artifactId"), "snapshot");
assertEquals(child1.getString("version"), "2.0-SNAPSHOT");
assertEquals(child1.getString("type"), "jar");
assertEquals(child1.getString("scope"), "compile");
assertEquals(child1.getString("classifier"), "");
assertEquals(child1.getString("optional"), "false");
}
}

/**
* Help finding content in the given list of string
*
Expand Down