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.plugin.compiler;
20  
21  import javax.tools.Diagnostic;
22  import javax.tools.DiagnosticListener;
23  import javax.tools.JavaFileObject;
24  
25  import java.nio.file.Path;
26  import java.util.Arrays;
27  import java.util.LinkedHashMap;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Optional;
31  
32  import org.apache.maven.api.plugin.Log;
33  import org.apache.maven.api.services.MessageBuilder;
34  import org.apache.maven.api.services.MessageBuilderFactory;
35  
36  /**
37   * A Java compiler diagnostic listener which send the messages to the Maven logger.
38   *
39   * @author Martin Desruisseaux
40   */
41  final class DiagnosticLogger implements DiagnosticListener<JavaFileObject> {
42      /**
43       * The logger where to send diagnostics.
44       */
45      private final Log logger;
46  
47      /**
48       * The factory for creating message builders.
49       */
50      private final MessageBuilderFactory messageBuilderFactory;
51  
52      /**
53       * The locale for compiler message.
54       */
55      private final Locale locale;
56  
57      /**
58       * The base directory with which to relativize the paths to source files.
59       */
60      private final Path directory;
61  
62      /**
63       * Number of errors or warnings.
64       */
65      private int numErrors, numWarnings;
66  
67      /**
68       * Number of messages received for each code.
69       */
70      private final Map<String, Integer> codeCount;
71  
72      /**
73       * The first error, or {@code null} if none.
74       */
75      private String firstError;
76  
77      /**
78       * Creates a listener which will send the diagnostics to the given logger.
79       *
80       * @param logger the logger where to send diagnostics
81       * @param messageBuilderFactory the factory for creating message builders
82       * @param locale the locale for compiler message
83       * @param directory the base directory with which to relativize the paths to source files
84       */
85      DiagnosticLogger(Log logger, MessageBuilderFactory messageBuilderFactory, Locale locale, Path directory) {
86          this.logger = logger;
87          this.messageBuilderFactory = messageBuilderFactory;
88          this.locale = locale;
89          this.directory = directory;
90          codeCount = new LinkedHashMap<>();
91      }
92  
93      /**
94       * Makes the given file relative to the base directory.
95       *
96       * @param  file  the path to make relative to the base directory
97       * @return the given path, potentially relative to the base directory
98       */
99      private String relativize(String file) {
100         if (directory != null) {
101             try {
102                 return directory.relativize(Path.of(file)).toString();
103             } catch (IllegalArgumentException e) {
104                 // Ignore, keep the absolute path.
105             }
106         }
107         return file;
108     }
109 
110     /**
111      * Invoked when the compiler emitted a warning.
112      *
113      * @param diagnostic the warning emitted by the Java compiler
114      */
115     @Override
116     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
117         String message = diagnostic.getMessage(locale);
118         if (message == null || message.isBlank()) {
119             return;
120         }
121         MessageBuilder record = messageBuilderFactory.builder();
122         record.a(message);
123         JavaFileObject source = diagnostic.getSource();
124         Diagnostic.Kind kind = diagnostic.getKind();
125         String style;
126         switch (kind) {
127             case ERROR:
128                 style = ".error:-bold,f:red";
129                 break;
130             case MANDATORY_WARNING:
131             case WARNING:
132                 style = ".warning:-bold,f:yellow";
133                 break;
134             default:
135                 style = ".info:-bold,f:blue";
136                 if (diagnostic.getLineNumber() == Diagnostic.NOPOS) {
137                     source = null; // Some messages are generic, e.g. "Recompile with -Xlint:deprecation".
138                 }
139                 break;
140         }
141         if (source != null) {
142             record.newline().a("    at ").a(relativize(source.getName()));
143             long line = diagnostic.getLineNumber();
144             long column = diagnostic.getColumnNumber();
145             if (line != Diagnostic.NOPOS || column != Diagnostic.NOPOS) {
146                 record.style(style).a('[');
147                 if (line != Diagnostic.NOPOS) {
148                     record.a(line);
149                 }
150                 if (column != Diagnostic.NOPOS) {
151                     record.a(',').a(column);
152                 }
153                 record.a(']').resetStyle();
154             }
155         }
156         String log = record.toString();
157         switch (kind) {
158             case ERROR:
159                 if (firstError == null) {
160                     firstError = message;
161                 }
162                 logger.error(log);
163                 numErrors++;
164                 break;
165             case MANDATORY_WARNING:
166             case WARNING:
167                 logger.warn(log);
168                 numWarnings++;
169                 break;
170             default:
171                 logger.info(log);
172                 break;
173         }
174         // Statistics
175         String code = diagnostic.getCode();
176         if (code != null) {
177             codeCount.merge(code, 1, (old, initial) -> old + 1);
178         }
179     }
180 
181     /**
182      * Returns the first error, if any.
183      *
184      * @param cause if compilation failed with an exception, the cause
185      */
186     Optional<String> firstError(Throwable cause) {
187         return Optional.ofNullable(cause != null && firstError == null ? cause.getMessage() : firstError);
188     }
189 
190     /**
191      * Reports summary after the compilation finished.
192      */
193     void logSummary() {
194         MessageBuilder message = messageBuilderFactory.builder();
195         final String patternForCount;
196         if (!codeCount.isEmpty()) {
197             @SuppressWarnings("unchecked")
198             Map.Entry<String, Integer>[] entries = codeCount.entrySet().toArray(Map.Entry[]::new);
199             Arrays.sort(entries, (a, b) -> Integer.compare(b.getValue(), a.getValue()));
200             patternForCount = patternForCount(Math.max(entries[0].getValue(), Math.max(numWarnings, numErrors)));
201             message.strong("Summary of compiler messages:").newline();
202             for (Map.Entry<String, Integer> entry : entries) {
203                 int count = entry.getValue();
204                 message.format(patternForCount, count, entry.getKey()).newline();
205             }
206         } else {
207             patternForCount = patternForCount(Math.max(numWarnings, numErrors));
208         }
209         if ((numWarnings | numErrors) != 0) {
210             message.strong("Total:");
211         }
212         if (numWarnings != 0) {
213             writeCount(message, patternForCount, numWarnings, "warning");
214         }
215         if (numErrors != 0) {
216             writeCount(message, patternForCount, numErrors, "error");
217         }
218         logger.info(message.toString());
219     }
220 
221     /**
222      * {@return the pattern for formatting the specified number followed by a label}
223      * The given number should be the widest number to format.
224      * A margin of 4 spaces is added at the beginning of the line.
225      */
226     private static String patternForCount(int n) {
227         return "    %" + Integer.toString(n).length() + "d %s";
228     }
229 
230     /**
231      * Appends the count of warnings or errors, making them plural if needed.
232      */
233     private static void writeCount(MessageBuilder message, String patternForCount, int count, String name) {
234         message.newline();
235         message.format(patternForCount, count, name);
236         if (count > 1) {
237             message.append('s');
238         }
239     }
240 }