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.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 }