1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.cli.jansi;
20
21 import org.apache.maven.api.services.MessageBuilder;
22 import org.apache.maven.internal.impl.DefaultMessageBuilder;
23 import org.fusesource.jansi.Ansi;
24 import org.fusesource.jansi.AnsiConsole;
25 import org.fusesource.jansi.AnsiMode;
26
27 /**
28 * Colored message utils, to manage colors. This is the core implementation of the
29 * {@link JansiMessageBuilderFactory} and {@link JansiMessageBuilder} classes.
30 * This class should not be used outside of maven-embedder and the public
31 * {@link org.apache.maven.api.services.MessageBuilderFactory} should be used instead.
32 * <p>
33 * Internally, <a href="http://fusesource.github.io/jansi/">Jansi</a> is used to render
34 * <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI colors</a> on any platform.
35 * <p>
36 *
37 * @see MessageBuilder
38 * @see org.apache.maven.api.services.MessageBuilderFactory
39 * @see JansiMessageBuilderFactory
40 * @see JansiMessageBuilder
41 * @since 4.0.0
42 */
43 public class MessageUtils {
44 private static final boolean JANSI;
45
46 /** Reference to the JVM shutdown hook, if registered */
47 private static Thread shutdownHook;
48
49 /** Synchronization monitor for the "uninstall" */
50 private static final Object STARTUP_SHUTDOWN_MONITOR = new Object();
51
52 static {
53 boolean jansi = true;
54 try {
55 // Jansi is provided by Maven core since 3.5.0
56 Class.forName("org.fusesource.jansi.Ansi");
57 } catch (ClassNotFoundException cnfe) {
58 jansi = false;
59 }
60 JANSI = jansi;
61 }
62
63 /**
64 * Install color support.
65 * This method is called by Maven core, and calling it is not necessary in plugins.
66 */
67 public static void systemInstall() {
68 if (JANSI) {
69 AnsiConsole.systemInstall();
70 }
71 }
72
73 /**
74 * Undo a previous {@link #systemInstall()}. If {@link #systemInstall()} was called
75 * multiple times, {@link #systemUninstall()} must be called call the same number of times before
76 * it is actually uninstalled.
77 */
78 public static void systemUninstall() {
79 synchronized (STARTUP_SHUTDOWN_MONITOR) {
80 doSystemUninstall();
81
82 // hook can only set when Jansi is true
83 if (shutdownHook != null) {
84 try {
85 Runtime.getRuntime().removeShutdownHook(shutdownHook);
86 } catch (IllegalStateException ex) {
87 // ignore - VM is already shutting down
88 }
89 }
90 }
91 }
92
93 private static void doSystemUninstall() {
94 if (JANSI) {
95 AnsiConsole.systemUninstall();
96 }
97 }
98
99 /**
100 * Enables message color (if Jansi is available).
101 * @param flag to enable Jansi
102 */
103 public static void setColorEnabled(boolean flag) {
104 if (JANSI) {
105 AnsiConsole.out().setMode(flag ? AnsiMode.Force : AnsiMode.Strip);
106 Ansi.setEnabled(flag);
107 System.setProperty(
108 AnsiConsole.JANSI_MODE, flag ? AnsiConsole.JANSI_MODE_FORCE : AnsiConsole.JANSI_MODE_STRIP);
109 boolean installed = AnsiConsole.isInstalled();
110 while (AnsiConsole.isInstalled()) {
111 AnsiConsole.systemUninstall();
112 }
113 if (installed) {
114 AnsiConsole.systemInstall();
115 }
116 }
117 }
118
119 /**
120 * Is message color enabled: requires Jansi available (through Maven) and the color has not been disabled.
121 * @return whether colored messages are enabled
122 */
123 public static boolean isColorEnabled() {
124 return JANSI ? Ansi.isEnabled() : false;
125 }
126
127 /**
128 * Create a default message buffer.
129 * @return a new buffer
130 */
131 public static MessageBuilder builder() {
132 return builder(new StringBuilder());
133 }
134
135 /**
136 * Create a message buffer with an internal buffer of defined size.
137 * @param size size of the buffer
138 * @return a new buffer
139 */
140 public static MessageBuilder builder(int size) {
141 return builder(new StringBuilder(size));
142 }
143
144 /**
145 * Create a message buffer with defined String builder.
146 * @param builder initial content of the message buffer
147 * @return a new buffer
148 */
149 public static MessageBuilder builder(StringBuilder builder) {
150 return JANSI && isColorEnabled() ? new JansiMessageBuilder(builder) : new DefaultMessageBuilder(builder);
151 }
152
153 /**
154 * Remove any ANSI code from a message (colors or other escape sequences).
155 * @param msg message eventually containing ANSI codes
156 * @return the message with ANSI codes removed
157 */
158 public static String stripAnsiCodes(String msg) {
159 return msg.replaceAll("\u001B\\[[;\\d]*[ -/]*[@-~]", "");
160 }
161
162 /**
163 * Register a shutdown hook with the JVM runtime, uninstalling Ansi support on
164 * JVM shutdown unless is has already been uninstalled at that time.
165 * <p>Delegates to {@link #doSystemUninstall()} for the actual uninstall procedure
166 *
167 * @see Runtime#addShutdownHook(Thread)
168 * @see MessageUtils#systemUninstall()
169 * @see #doSystemUninstall()
170 */
171 public static void registerShutdownHook() {
172 if (JANSI && shutdownHook == null) {
173 // No shutdown hook registered yet.
174 shutdownHook = new Thread() {
175 @Override
176 public void run() {
177 synchronized (STARTUP_SHUTDOWN_MONITOR) {
178 while (AnsiConsole.isInstalled()) {
179 doSystemUninstall();
180 }
181 }
182 }
183 };
184 Runtime.getRuntime().addShutdownHook(shutdownHook);
185 }
186 }
187
188 /**
189 * Get the terminal width or -1 if the width cannot be determined.
190 *
191 * @return the terminal width
192 */
193 public static int getTerminalWidth() {
194 if (JANSI) {
195 int width = AnsiConsole.getTerminalWidth();
196 return width > 0 ? width : -1;
197 } else {
198 return -1;
199 }
200 }
201 }