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