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         try {
101             return directory.relativize(Path.of(file)).toString();
102         } catch (IllegalArgumentException e) {
103             // Ignore, keep the absolute path.
104             return file;
105         }
106     }
107 
108     /**
109      * Invoked when the compiler emitted a warning.
110      *
111      * @param diagnostic the warning emitted by the Java compiler
112      */
113     @Override
114     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
115         String message = diagnostic.getMessage(locale);
116         if (message == null || message.isBlank()) {
117             return;
118         }
119         MessageBuilder record = messageBuilderFactory.builder();
120         record.a(message);
121         JavaFileObject source = diagnostic.getSource();
122         Diagnostic.Kind kind = diagnostic.getKind();
123         String style;
124         switch (kind) {
125             case ERROR:
126                 style = ".error:-bold,f:red";
127                 break;
128             case MANDATORY_WARNING:
129             case WARNING:
130                 style = ".warning:-bold,f:yellow";
131                 break;
132             default:
133                 style = ".info:-bold,f:blue";
134                 if (diagnostic.getLineNumber() == Diagnostic.NOPOS) {
135                     source = null; // Some messages are generic, e.g. "Recompile with -Xlint:deprecation".
136                 }
137                 break;
138         }
139         if (source != null) {
140             record.newline().a("    at ").a(relativize(source.getName()));
141             long line = diagnostic.getLineNumber();
142             long column = diagnostic.getColumnNumber();
143             if (line != Diagnostic.NOPOS || column != Diagnostic.NOPOS) {
144                 record.style(style).a('[');
145                 if (line != Diagnostic.NOPOS) {
146                     record.a(line);
147                 }
148                 if (column != Diagnostic.NOPOS) {
149                     record.a(',').a(column);
150                 }
151                 record.a(']').resetStyle();
152             }
153         }
154         String log = record.toString();
155         switch (kind) {
156             case ERROR:
157                 if (firstError == null) {
158                     firstError = message;
159                 }
160                 logger.error(log);
161                 numErrors++;
162                 break;
163             case MANDATORY_WARNING:
164             case WARNING:
165                 logger.warn(log);
166                 numWarnings++;
167                 break;
168             default:
169                 logger.info(log);
170                 break;
171         }
172         // Statistics
173         String code = diagnostic.getCode();
174         if (code != null) {
175             codeCount.merge(code, 1, (old, initial) -> old + 1);
176         }
177     }
178 
179     /**
180      * Returns the first error, if any.
181      *
182      * @param cause if compilation failed with an exception, the cause
183      */
184     Optional<String> firstError(Exception cause) {
185         return Optional.ofNullable(cause != null && firstError == null ? cause.getMessage() : firstError);
186     }
187 
188     /**
189      * Reports summary after the compilation finished.
190      */
191     void logSummary() {
192         MessageBuilder message = messageBuilderFactory.builder();
193         final String patternForCount;
194         if (!codeCount.isEmpty()) {
195             @SuppressWarnings("unchecked")
196             Map.Entry<String, Integer>[] entries = codeCount.entrySet().toArray(Map.Entry[]::new);
197             Arrays.sort(entries, (a, b) -> Integer.compare(b.getValue(), a.getValue()));
198             patternForCount = patternForCount(Math.max(entries[0].getValue(), Math.max(numWarnings, numErrors)));
199             message.strong("Summary of compiler messages:").newline();
200             for (Map.Entry<String, Integer> entry : entries) {
201                 int count = entry.getValue();
202                 message.format(patternForCount, count, entry.getKey()).newline();
203             }
204         } else {
205             patternForCount = patternForCount(Math.max(numWarnings, numErrors));
206         }
207         if ((numWarnings | numErrors) != 0) {
208             message.strong("Total:");
209         }
210         if (numWarnings != 0) {
211             writeCount(message, patternForCount, numWarnings, "warning");
212         }
213         if (numErrors != 0) {
214             writeCount(message, patternForCount, numErrors, "error");
215         }
216         logger.info(message.toString());
217     }
218 
219     /**
220      * {@return the pattern for formatting the specified number followed by a label}
221      * The given number should be the widest number to format.
222      * A margin of 4 spaces is added at the beginning of the line.
223      */
224     private static String patternForCount(int n) {
225         return "    %" + Integer.toString(n).length() + "d %s";
226     }
227 
228     /**
229      * Appends the count of warnings or errors, making them plural if needed.
230      */
231     private static void writeCount(MessageBuilder message, String patternForCount, int count, String name) {
232         message.newline();
233         message.format(patternForCount, count, name);
234         if (count > 1) {
235             message.append('s');
236         }
237     }
238 }