diff --git a/src/it/projects/tree-tokens/invoker.properties b/src/it/projects/tree-tokens/invoker.properties
new file mode 100644
index 000000000..229c8b56b
--- /dev/null
+++ b/src/it/projects/tree-tokens/invoker.properties
@@ -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
diff --git a/src/it/projects/tree-tokens/pom.xml b/src/it/projects/tree-tokens/pom.xml
new file mode 100644
index 000000000..2a69a8595
--- /dev/null
+++ b/src/it/projects/tree-tokens/pom.xml
@@ -0,0 +1,48 @@
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.dependency
+ tree-tokens
+ 1.0-SNAPSHOT
+
+ Test
+
+ Test automatic and explicitly selected dependency tree tokens
+
+
+
+ UTF-8
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.18.0
+ true
+
+
+
+
diff --git a/src/it/projects/tree-tokens/verify.groovy b/src/it/projects/tree-tokens/verify.groovy
new file mode 100644
index 000000000..849aa1de0
--- /dev/null
+++ b/src/it/projects/tree-tokens/verify.groovy
@@ -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
diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java b/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java
index cbb0af336..46579ded8 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java
@@ -130,11 +130,14 @@ public class TreeMojo extends AbstractMojo {
/**
* The token set name to use when outputting the dependency tree. Possible values are whitespace,
* standard or extended, 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;
/**
@@ -386,21 +389,7 @@ public DependencyNodeVisitor getSerializingDependencyNodeVisitor(Writer writer)
* @return the GraphTokens 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());
}
/**
diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/TreeTokens.java b/src/main/java/org/apache/maven/plugins/dependency/tree/TreeTokens.java
new file mode 100644
index 000000000..ecc513ed5
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/dependency/tree/TreeTokens.java
@@ -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;
+ }
+}
diff --git a/src/site/markdown/examples/tree-mojo.md b/src/site/markdown/examples/tree-mojo.md
index fcaa3f690..4ef5f491b 100644
--- a/src/site/markdown/examples/tree-mojo.md
+++ b/src/site/markdown/examples/tree-mojo.md
@@ -47,6 +47,40 @@ mvn dependency:tree -DoutputType= -DoutputFile=
**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
# JSON (outputType\=json)
diff --git a/src/test/java/org/apache/maven/plugins/dependency/tree/TestTreeMojo.java b/src/test/java/org/apache/maven/plugins/dependency/tree/TestTreeMojo.java
index 487a1e847..a236bd554 100644
--- a/src/test/java/org/apache/maven/plugins/dependency/tree/TestTreeMojo.java
+++ b/src/test/java/org/apache/maven/plugins/dependency/tree/TestTreeMojo.java
@@ -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;
@@ -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;
/**
@@ -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 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.
*
diff --git a/src/test/java/org/apache/maven/plugins/dependency/tree/TreeTokensTest.java b/src/test/java/org/apache/maven/plugins/dependency/tree/TreeTokensTest.java
new file mode 100644
index 000000000..c6a14c9b9
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/dependency/tree/TreeTokensTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.apache.maven.execution.DefaultMavenExecutionRequest;
+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;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class TreeTokensTest {
+ @ParameterizedTest
+ @ValueSource(strings = {"standard", "extended", "whitespace", "", "invalid"})
+ void explicitStyleSkipsMetadataAndWinsForFileOutput(String configured) {
+ MavenSession session = mock(MavenSession.class);
+ GraphTokens expected = "extended".equals(configured)
+ ? SerializingDependencyNodeVisitor.EXTENDED_TOKENS
+ : "whitespace".equals(configured)
+ ? SerializingDependencyNodeVisitor.WHITESPACE_TOKENS
+ : SerializingDependencyNodeVisitor.STANDARD_TOKENS;
+ assertSame(expected, TreeTokens.select(configured, false, session, mock(Log.class)));
+ assertSame(expected, TreeTokens.select(configured, true, session, mock(Log.class)));
+ verifyNoInteractions(session);
+ }
+
+ @Test
+ void fileOutputSkipsMetadata() {
+ MavenSession session = mock(MavenSession.class);
+ assertSame(
+ SerializingDependencyNodeVisitor.STANDARD_TOKENS,
+ TreeTokens.select(null, true, session, mock(Log.class)));
+ verifyNoInteractions(session);
+ }
+
+ @Test
+ void batchModeSkipsMetadata() {
+ MavenExecutionRequest request = spy(new DefaultMavenExecutionRequest().setInteractiveMode(false));
+ MavenSession session = mock(MavenSession.class);
+ when(session.getRequest()).thenReturn(request);
+ assertSame(
+ SerializingDependencyNodeVisitor.STANDARD_TOKENS,
+ TreeTokens.select(null, false, session, mock(Log.class)));
+ verify(request, never()).getData();
+ }
+
+ @Test
+ void absentSessionOrRequestUsesStandardTokens() {
+ assertSame(
+ SerializingDependencyNodeVisitor.STANDARD_TOKENS,
+ TreeTokens.select(null, false, null, mock(Log.class)));
+ assertSame(
+ SerializingDependencyNodeVisitor.STANDARD_TOKENS,
+ TreeTokens.select(null, false, mock(MavenSession.class), mock(Log.class)));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"UTF-8", "IBM437", "IBM850"})
+ void compatibleConsoleEncodingUsesExtendedTokens(String encoding) {
+ Map