View Javadoc
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.shared.utils.logging;
20  
21  import org.fusesource.jansi.Ansi;
22  import org.fusesource.jansi.AnsiConsole;
23  import org.fusesource.jansi.AnsiMode;
24  
25  /**
26   * Colored message utils, to manage colors consistently across plugins (only if Maven version is at least 3.5.0).
27   * For Maven version before 3.5.0, message built with this util will never add color.
28   * <p>
29   * Internally, <a href="http://fusesource.github.io/jansi/">Jansi</a> is used to render
30   * <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI colors</a> on any platform.
31   * @since 3.1.0
32   */
33  public class MessageUtils {
34      private static final boolean JANSI;
35  
36      /** Reference to the JVM shutdown hook, if registered */
37      private static Thread shutdownHook;
38  
39      /** Synchronization monitor for the "uninstall" */
40      private static final Object STARTUP_SHUTDOWN_MONITOR = new Object();
41  
42      static {
43          boolean jansi = true;
44          try {
45              // Jansi is provided by Maven core since 3.5.0
46              Class.forName("org.fusesource.jansi.Ansi");
47          } catch (ClassNotFoundException cnfe) {
48              jansi = false;
49          }
50          JANSI = jansi;
51      }
52  
53      /**
54       * Install color support.
55       * This method is called by Maven core, and calling it is not necessary in plugins.
56       */
57      public static void systemInstall() {
58          if (JANSI) {
59              AnsiConsole.systemInstall();
60          }
61      }
62  
63      /**
64       * Undo a previous {@link #systemInstall()}.  If {@link #systemInstall()} was called
65       * multiple times, {@link #systemUninstall()} must be called call the same number of times before
66       * it is actually uninstalled.
67       */
68      public static void systemUninstall() {
69          synchronized (STARTUP_SHUTDOWN_MONITOR) {
70              doSystemUninstall();
71  
72              // hook can only set when Jansi is true
73              if (shutdownHook != null) {
74                  try {
75                      Runtime.getRuntime().removeShutdownHook(shutdownHook);
76                  } catch (IllegalStateException ex) {
77                      // ignore - VM is already shutting down
78                  }
79              }
80          }
81      }
82  
83      private static void doSystemUninstall() {
84          if (JANSI) {
85              AnsiConsole.systemUninstall();
86          }
87      }
88  
89      /**
90       * Enables message color (if Jansi is available).
91       * @param flag to enable Jansi
92       */
93      public static void setColorEnabled(boolean flag) {
94          if (JANSI) {
95              AnsiConsole.out().setMode(flag ? AnsiMode.Force : AnsiMode.Strip);
96              Ansi.setEnabled(flag);
97              System.setProperty(
98                      AnsiConsole.JANSI_MODE, flag ? AnsiConsole.JANSI_MODE_FORCE : AnsiConsole.JANSI_MODE_STRIP);
99              boolean installed = AnsiConsole.isInstalled();
100             while (AnsiConsole.isInstalled()) {
101                 AnsiConsole.systemUninstall();
102             }
103             if (installed) {
104                 AnsiConsole.systemInstall();
105             }
106         }
107     }
108 
109     /**
110      * Is message color enabled: requires Jansi available (through Maven) and the color has not been disabled.
111      * @return whether colored messages are enabled
112      */
113     public static boolean isColorEnabled() {
114         return JANSI ? Ansi.isEnabled() : false;
115     }
116 
117     /**
118      * Create a default message buffer.
119      * @return a new buffer
120      */
121     public static MessageBuilder buffer() {
122         return JANSI ? new AnsiMessageBuilder() : new PlainMessageBuilder();
123     }
124 
125     /**
126      * Create a message buffer with defined String builder.
127      * @param builder initial content of the message buffer
128      * @return a new buffer
129      */
130     public static MessageBuilder buffer(StringBuilder builder) {
131         return JANSI ? new AnsiMessageBuilder(builder) : new PlainMessageBuilder(builder);
132     }
133 
134     /**
135      * Create a message buffer with an internal buffer of defined size.
136      * @param size size of the buffer
137      * @return a new buffer
138      */
139     public static MessageBuilder buffer(int size) {
140         return JANSI ? new AnsiMessageBuilder(size) : new PlainMessageBuilder(size);
141     }
142 
143     /**
144      * Create a logger level renderer.
145      * @return a logger level renderer
146      * @since 3.2.0
147      */
148     @SuppressWarnings("checkstyle:magicnumber")
149     public static LoggerLevelRenderer level() {
150         return JANSI ? new AnsiMessageBuilder(20) : new PlainMessageBuilder(7);
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 }