Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/it/projects/tree-tokens/invoker.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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 -DoutputFile=auto.txt
invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:tree -Dtokens=extended -DoutputFile=extended.txt -DoutputEncoding=UTF-8
invoker.goals.3 = ${project.groupId}:${project.artifactId}:${project.version}:tree -Dtokens=whitespace -DoutputFile=whitespace.txt
invoker.goals.4 = ${project.groupId}:${project.artifactId}:${project.version}:tree -Dtokens=standard -DoutputFile=standard.txt
invoker.goals.5 = ${project.groupId}:${project.artifactId}:${project.version}:tree -Dtokens=invalid -DoutputFile=invalid.txt
invoker.goals.6 = ${project.groupId}:${project.artifactId}:${project.version}:tree -l tree.log -Dstyle.color=always
48 changes: 48 additions & 0 deletions src/it/projects/tree-tokens/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.apache.maven.its.dependency</groupId>
<artifactId>tree-tokens</artifactId>
<version>1.0-SNAPSHOT</version>

<name>Test</name>
<description>
Test automatic and explicitly selected dependency tree tokens
</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
<optional>true</optional>
</dependency>
</dependencies>

</project>
33 changes: 33 additions & 0 deletions src/it/projects/tree-tokens/verify.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat

String dependency = "org.apache.commons:commons-lang3:jar:3.18.0:compile"
String ascii = new File(basedir, "auto.txt").getText("UTF-8")
assertThat(ascii).contains("\\- " + dependency).doesNotContain("├", "└", "─", "│")
assertThat(new File(basedir, "standard.txt").getText("UTF-8")).isEqualTo(ascii)
assertThat(new File(basedir, "invalid.txt").getText("UTF-8")).isEqualTo(ascii)
assertThat(new File(basedir, "extended.txt").getText("UTF-8")).contains("└─ " + dependency)
assertThat(new File(basedir, "whitespace.txt").getText("UTF-8")).contains(" " + dependency)
.doesNotContain("\\-", "└")
assertThat(new File(basedir, "tree.log").getText("UTF-8")).contains("\\- " + dependency)
.doesNotContain("├", "└", "─", "│")

return true
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,14 @@ public class TreeMojo extends AbstractMojo {
/**
* The token set name to use when outputting the dependency tree. Possible values are <code>whitespace</code>,
* <code>standard</code> or <code>extended</code>, which use whitespace, standard (ie ASCII) or extended character
* sets respectively.
* sets respectively. When omitted, extended tokens are selected for an interactive console when
* Maven's output-capabilities metadata reports an encoding that supports the tree characters.
* Files, batch mode, missing metadata and incompatible encodings use standard tokens.
* An explicit value always takes precedence over detection.
*
* @since 2.0-alpha-6
*/
@Parameter(property = "tokens", defaultValue = "standard")
@Parameter(property = "tokens")
private String tokens;

/**
Expand Down Expand Up @@ -386,21 +389,7 @@ public DependencyNodeVisitor getSerializingDependencyNodeVisitor(Writer writer)
* @return the <code>GraphTokens</code> instance
*/
private GraphTokens toGraphTokens(String theTokens) {
GraphTokens graphTokens;

if ("whitespace".equals(theTokens)) {
getLog().debug("+ Using whitespace tree tokens");

graphTokens = SerializingDependencyNodeVisitor.WHITESPACE_TOKENS;
} else if ("extended".equals(theTokens)) {
getLog().debug("+ Using extended tree tokens");

graphTokens = SerializingDependencyNodeVisitor.EXTENDED_TOKENS;
} else {
graphTokens = SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}

return graphTokens;
return TreeTokens.select(theTokens, outputFile != null, session, getLog());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.nio.charset.Charset;
import java.util.Map;

import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.shared.dependency.graph.traversal.SerializingDependencyNodeVisitor;
import org.apache.maven.shared.dependency.graph.traversal.SerializingDependencyNodeVisitor.GraphTokens;

/** Selects text tree tokens using Maven's optional execution-request output metadata. */
final class TreeTokens {
private TreeTokens() {}

static GraphTokens select(String configured, boolean file, MavenSession session, Log log) {
if (configured != null) {
if ("extended".equals(configured)) {
log.debug("Using explicitly configured extended tree tokens");
return SerializingDependencyNodeVisitor.EXTENDED_TOKENS;
}
if ("whitespace".equals(configured)) {
log.debug("Using explicitly configured whitespace tree tokens");
return SerializingDependencyNodeVisitor.WHITESPACE_TOKENS;
}
// Preserve the historical fallback for unrecognized token names, including an empty name.
return SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}
if (file) {
log.debug("Using standard tree tokens for file output");
return SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}
MavenExecutionRequest request = session == null ? null : session.getRequest();
if (request == null || !request.isInteractiveMode()) {
log.debug("Using standard tree tokens: no interactive Maven request");
return SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}
// This string-based contract also works for plugins compiled against older Maven APIs.
Object value = request.getData().get("maven.logging.outputCapabilities");
if (!(value instanceof Map)) {
log.debug("Using standard tree tokens: Maven output capabilities are unavailable");
return SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}
Map<?, ?> capabilities = (Map<?, ?>) value;
if (!"CONSOLE".equals(capabilities.get("destination"))) {
log.debug("Using standard tree tokens: logging destination is not a known console");
return SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}
Object name = capabilities.get("encoding");
if (name instanceof String) {
try {
Charset encoding = Charset.forName((String) name);
if (encoding.canEncode() && encoding.newEncoder().canEncode("\u251c\u2514\u2500\u2502")) {
log.debug("Using extended tree tokens for console encoding " + encoding.name());
return SerializingDependencyNodeVisitor.EXTENDED_TOKENS;
}
} catch (IllegalArgumentException | UnsupportedOperationException e) {
log.debug("Cannot use Maven's logging encoding: " + e.getMessage());
}
}
log.debug("Using standard tree tokens: console encoding is unknown or cannot encode tree characters");
return SerializingDependencyNodeVisitor.STANDARD_TOKENS;
}
}
34 changes: 34 additions & 0 deletions src/site/markdown/examples/tree-mojo.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,40 @@ mvn dependency:tree -DoutputType=<format> -DoutputFile=<filename>

**Note**: Ensure you are using Maven Dependency Plugin version 3.7.0 or later (latest is 3.8.1 as of June 2025) to access these output formats.

## Text tree style

For text output, `tokens` selects the characters used for the branches:

- `standard`: ASCII, such as `+-` and `\-`.
- `extended`: box-drawing characters, such as `├─`, `└─` and `│`.
- `whitespace`: indentation without branch characters.

For example, to force box-drawing characters:

```shell
mvn dependency:tree -Dtokens=extended
```

When `tokens` is omitted, the plugin selects extended characters only for an
interactive console whose reported encoding can represent all the branch
characters. Compatible legacy encodings, such as CP437 and CP850, work as well
as UTF-8. The plugin reads Maven's `maven.logging.outputCapabilities` map from
execution-request data; it does not inspect terminal libraries or infer an
encoding from the operating system or JVM default.

Automatic selection uses ASCII for `outputFile`, Maven's `-l`/`--log-file`,
batch mode (`-B`), pipes and shell redirection, and missing or unusable output
metadata. Maven versions without the capability map, including unmodified
Maven 3.6.3 and 3.9.16, also use ASCII automatically. The minimum supported
Maven and Java versions are unchanged. Color settings do not control this
selection; the plugin follows the reported destination and encoding.

An explicit `tokens` value always takes precedence, including for files and
batch mode. Unrecognized values retain the historical ASCII fallback.
The `outputEncoding` parameter controls the plugin's `outputFile` encoding;
it does not change Maven's console or `-l` log encoding. Other output formats
are unaffected by `tokens`.

## Output Formats

# <a id="JSON_.28outputType.3Djson.29"></a>JSON (outputType\=json)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -52,12 +56,16 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.apache.maven.api.plugin.testing.MojoExtension.getVariableValueFromObject;
import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
Expand Down Expand Up @@ -90,6 +98,48 @@ void setUp() {

// tests ------------------------------------------------------------------

@Test
@InjectMojo(goal = "tree")
void usesRequestCapabilitiesForTextTree(TreeMojo mojo) throws Exception {
assertNull(getVariableValueFromObject(mojo, "tokens"));
session.getRequest().setInteractiveMode(true);
Map<String, String> capabilities = new HashMap<>();
capabilities.put("destination", "CONSOLE");
capabilities.put("encoding", "UTF-8");
session.getRequest().getData().put("maven.logging.outputCapabilities", capabilities);

DefaultDependencyNode root =
new DefaultDependencyNode(stubFactory.createArtifact("testGroupId", "project", "1.0"));
DefaultDependencyNode child =
new DefaultDependencyNode(stubFactory.createArtifact("testGroupId", "child", "1.0"));
child.setChildren(Collections.emptyList());
root.setChildren(Collections.singletonList(child));
StringWriter writer = new StringWriter();
root.accept(mojo.getSerializingDependencyNodeVisitor(writer));

assertTrue(writer.toString().contains("\u2514\u2500"), writer.toString());
}

@Test
void nonTextFormatsSkipCapabilityLookup() throws Exception {
MavenSession unusedSession = mock(MavenSession.class);
TreeMojo mojo = new TreeMojo(null, unusedSession, null, null);
String[] formats = {"dot", "graphml", "tgf", "json"};
Class<?>[] visitors = {
DOTDependencyNodeVisitor.class,
GraphmlDependencyNodeVisitor.class,
TGFDependencyNodeVisitor.class,
JsonDependencyNodeVisitor.class
};
for (int i = 0; i < formats.length; i++) {
setVariableValueToObject(mojo, "outputType", formats[i]);
assertEquals(
visitors[i],
mojo.getSerializingDependencyNodeVisitor(new StringWriter()).getClass());
}
verifyNoInteractions(unusedSession);
}

/**
* Tests the proper discovery and configuration of the mojo.
*
Expand Down
Loading
Loading