1 /*
2  * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package idea;
27 
28 import org.apache.tools.ant.BuildEvent;
29 import org.apache.tools.ant.BuildListener;
30 import org.apache.tools.ant.DefaultLogger;
31 import org.apache.tools.ant.Project;
32 
33 import java.util.EnumSet;
34 import java.util.Stack;
35 
36 import static org.apache.tools.ant.Project.*;
37 
38 /**
39  * This class is used to wrap the IntelliJ ant logger in order to provide more meaningful
40  * output when building langtools. The basic ant output in IntelliJ can be quite cumbersome to
41  * work with, as it provides two separate views: (i) a tree view, which is good to display build task
42  * in a hierarchical fashion as they are processed; and a (ii) plain text view, which gives you
43  * the full ant output. The main problem is that javac-related messages are buried into the
44  * ant output (which is made very verbose by IntelliJ in order to support the tree view). It is
45  * not easy to figure out which node to expand in order to see the error message; switching
46  * to plain text doesn't help either, as now the output is totally flat.
47  *
48  * This logger class removes a lot of verbosity from the IntelliJ ant logger by not propagating
49  * all the events to the IntelliJ's logger. In addition, certain events are handled in a custom
50  * fashion, to generate better output during the build.
51  */
52 public final class LangtoolsIdeaAntLogger extends DefaultLogger {
53 
54     /**
55      * This is just a way to pass in customized binary string predicates;
56      *
57      * TODO: replace with @code{BiPredicate<String, String>} and method reference when moving to 8
58      */
59     enum StringBinaryPredicate {
CONTAINS()60         CONTAINS() {
61             @Override
62             boolean apply(String s1, String s2) {
63                 return s1.contains(s2);
64             }
65         },
66         STARTS_WITH {
67             @Override
apply(String s1, String s2)68             boolean apply(String s1, String s2) {
69                 return s1.startsWith(s2);
70             }
71         };
72 
apply(String s1, String s2)73         abstract boolean apply(String s1, String s2);
74     }
75 
76     /**
77      * Various kinds of ant messages that we shall intercept
78      */
79     enum MessageKind {
80 
81         /** a javac error */
82         JAVAC_ERROR(StringBinaryPredicate.CONTAINS, MSG_ERR, "error:", "compiler.err"),
83         /** a javac warning */
84         JAVAC_WARNING(StringBinaryPredicate.CONTAINS, MSG_WARN, "warning:", "compiler.warn"),
85         /** a javac note */
86         JAVAC_NOTE(StringBinaryPredicate.CONTAINS, MSG_INFO, "note:", "compiler.note"),
87         /** a javac raw error (these typically come from a build misconfiguration - such as a bad javac flag) */
88         JAVAC_RAW_ERROR(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "javac: "),
89         /** continuation of some javac error message */
90         JAVAC_NESTED_DIAG(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "  "),
91         /** a javac crash */
92         JAVAC_CRASH(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "An exception has occurred in the compiler"),
93         /** jtreg test success */
94         JTREG_TEST_PASSED(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "Passed: "),
95         /** jtreg test failure */
96         JTREG_TEST_FAILED(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "FAILED: "),
97         /** jtreg test error */
98         JTREG_TEST_ERROR(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "Error: "),
99         /** jtreg report */
100         JTREG_TEST_REPORT(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "Report written");
101 
102         StringBinaryPredicate sbp;
103         int priority;
104         String[] keys;
105 
MessageKind(StringBinaryPredicate sbp, int priority, String... keys)106         MessageKind(StringBinaryPredicate sbp, int priority, String... keys) {
107             this.sbp = sbp;
108             this.priority = priority;
109             this.keys = keys;
110         }
111 
112         /**
113          * Does a given message string matches this kind?
114          */
matches(String s)115         boolean matches(String s) {
116             for (String key : keys) {
117                 if (sbp.apply(s, key)) {
118                     return true;
119                 }
120             }
121             return false;
122         }
123     }
124 
125     /**
126      * This enum is used to represent the list of tasks we need to keep track of during logging.
127      */
128     enum Task {
129         /** exec task - invoked during compilation */
130         JAVAC("exec", MessageKind.JAVAC_ERROR, MessageKind.JAVAC_WARNING, MessageKind.JAVAC_NOTE,
131                        MessageKind.JAVAC_RAW_ERROR, MessageKind.JAVAC_NESTED_DIAG, MessageKind.JAVAC_CRASH),
132         /** jtreg task - invoked during test execution */
133         JTREG("jtreg", MessageKind.JTREG_TEST_PASSED, MessageKind.JTREG_TEST_FAILED, MessageKind.JTREG_TEST_ERROR, MessageKind.JTREG_TEST_REPORT),
134         /** initial synthetic task when the logger is created */
135         ROOT("") {
136             @Override
matches(String s)137             boolean matches(String s) {
138                 return false;
139             }
140         },
141         /** synthetic task catching any other tasks not in this list */
142         ANY("") {
143             @Override
matches(String s)144             boolean matches(String s) {
145                 return true;
146             }
147         };
148 
149         String taskName;
150         MessageKind[] msgs;
151 
Task(String taskName, MessageKind... msgs)152         Task(String taskName, MessageKind... msgs) {
153             this.taskName = taskName;
154             this.msgs = msgs;
155         }
156 
matches(String s)157         boolean matches(String s) {
158             return s.equals(taskName);
159         }
160     }
161 
162     /**
163      * This enum is used to represent the list of targets we need to keep track of during logging.
164      * A regular expression is used to match a given target name.
165      */
166     enum Target {
167         /** jtreg target - executed when launching tests */
168         JTREG("jtreg") {
169             @Override
getDisplayMessage(BuildEvent e)170             String getDisplayMessage(BuildEvent e) {
171                 return "Running jtreg tests: " + e.getProject().getProperty("jtreg.tests");
172             }
173         },
174         /** build bootstrap tool target - executed when bootstrapping javac */
175         BUILD_BOOTSTRAP_JAVAC("build-bootstrap-javac-classes") {
176             @Override
getDisplayMessage(BuildEvent e)177             String getDisplayMessage(BuildEvent e) {
178                 return "Building bootstrap javac...";
179             }
180         },
181         /** build classes target - executed when building classes of given tool */
182         BUILD_ALL_CLASSES("build-all-classes") {
183             @Override
getDisplayMessage(BuildEvent e)184             String getDisplayMessage(BuildEvent e) {
185                 return "Building all classes...";
186             }
187         },
188         /** synthetic target catching any other target not in this list */
189         ANY("") {
190             @Override
getDisplayMessage(BuildEvent e)191             String getDisplayMessage(BuildEvent e) {
192                 return "Executing Ant target(s): " + e.getProject().getProperty("ant.project.invoked-targets");
193             }
194             @Override
matches(String msg)195             boolean matches(String msg) {
196                 return true;
197             }
198         };
199 
200         String targetName;
201 
Target(String targetName)202         Target(String targetName) {
203             this.targetName = targetName;
204         }
205 
matches(String msg)206         boolean matches(String msg) {
207             return msg.equals(targetName);
208         }
209 
getDisplayMessage(BuildEvent e)210         abstract String getDisplayMessage(BuildEvent e);
211     }
212 
213     /**
214      * A custom build event used to represent status changes which should be notified inside
215      * Intellij
216      */
217     static class StatusEvent extends BuildEvent {
218 
219         /** the target to which the status update refers */
220         Target target;
221 
StatusEvent(BuildEvent e, Target target)222         StatusEvent(BuildEvent e, Target target) {
223             super(new StatusTask(e, target.getDisplayMessage(e)));
224             this.target = target;
225             setMessage(getTask().getTaskName(), 2);
226         }
227 
228         /**
229          * A custom task used to channel info regarding a status change
230          */
231         static class StatusTask extends org.apache.tools.ant.Task {
StatusTask(BuildEvent event, String msg)232             StatusTask(BuildEvent event, String msg) {
233                 setProject(event.getProject());
234                 setOwningTarget(event.getTarget());
235                 setTaskName(msg);
236             }
237         }
238     }
239 
240     /** wrapped ant logger (IntelliJ's own logger) */
241     DefaultLogger logger;
242 
243     /** flag - is this the first target we encounter? */
244     boolean firstTarget = true;
245 
246     /** flag - should subsequenet failures be suppressed ? */
247     boolean suppressTaskFailures = false;
248 
249     /** flag - have we ran into a javac crash ? */
250     boolean crashFound = false;
251 
252     /** stack of status changes associated with pending targets */
253     Stack<StatusEvent> statusEvents = new Stack<>();
254 
255     /** stack of pending tasks */
256     Stack<Task> tasks = new Stack<>();
257 
LangtoolsIdeaAntLogger(Project project)258     public LangtoolsIdeaAntLogger(Project project) {
259         for (Object o : project.getBuildListeners()) {
260             if (o instanceof DefaultLogger) {
261                 this.logger = (DefaultLogger)o;
262                 project.removeBuildListener((BuildListener)o);
263                 project.addBuildListener(this);
264             }
265         }
266         logger.setMessageOutputLevel(3);
267         tasks.push(Task.ROOT);
268     }
269 
270     @Override
buildStarted(BuildEvent event)271     public void buildStarted(BuildEvent event) {
272         //do nothing
273     }
274 
275     @Override
buildFinished(BuildEvent event)276     public void buildFinished(BuildEvent event) {
277         //do nothing
278     }
279 
280     @Override
targetStarted(BuildEvent event)281     public void targetStarted(BuildEvent event) {
282         EnumSet<Target> statusKinds = firstTarget ?
283                 EnumSet.allOf(Target.class) :
284                 EnumSet.complementOf(EnumSet.of(Target.ANY));
285 
286         String targetName = event.getTarget().getName();
287 
288         for (Target statusKind : statusKinds) {
289             if (statusKind.matches(targetName)) {
290                 StatusEvent statusEvent = new StatusEvent(event, statusKind);
291                 statusEvents.push(statusEvent);
292                 logger.taskStarted(statusEvent);
293                 firstTarget = false;
294                 return;
295             }
296         }
297     }
298 
299     @Override
targetFinished(BuildEvent event)300     public void targetFinished(BuildEvent event) {
301         if (!statusEvents.isEmpty()) {
302             StatusEvent lastEvent = statusEvents.pop();
303             if (lastEvent.target.matches(event.getTarget().getName())) {
304                 logger.taskFinished(lastEvent);
305             }
306         }
307     }
308 
309     @Override
taskStarted(BuildEvent event)310     public void taskStarted(BuildEvent event) {
311         String taskName = event.getTask().getTaskName();
312         for (Task task : Task.values()) {
313             if (task.matches(taskName)) {
314                 tasks.push(task);
315                 return;
316             }
317         }
318     }
319 
320     @Override
taskFinished(BuildEvent event)321     public void taskFinished(BuildEvent event) {
322         if (tasks.peek() == Task.ROOT) {
323             //we need to 'close' the root task to get nicer output
324             logger.taskFinished(event);
325         } else if (!suppressTaskFailures && event.getException() != null) {
326             //the first (innermost) task failure should always be logged
327             event.setMessage(event.getException().toString(), 0);
328             event.setException(null);
329             //note: we turn this into a plain message to avoid stack trace being logged by Idea
330             logger.messageLogged(event);
331             suppressTaskFailures = true;
332         }
333         tasks.pop();
334     }
335 
336     @Override
messageLogged(BuildEvent event)337     public void messageLogged(BuildEvent event) {
338         String msg = event.getMessage();
339 
340         boolean processed = false;
341 
342         if (!tasks.isEmpty()) {
343             Task task = tasks.peek();
344             for (MessageKind messageKind : task.msgs) {
345                 if (messageKind.matches(msg)) {
346                     event.setMessage(msg, messageKind.priority);
347                     processed = true;
348                     if (messageKind == MessageKind.JAVAC_CRASH) {
349                         crashFound = true;
350                     }
351                     break;
352                 }
353             }
354         }
355 
356         if (event.getPriority() == MSG_ERR || crashFound) {
357             //we log errors regardless of owning task
358             logger.messageLogged(event);
359             suppressTaskFailures = true;
360         } else if (processed) {
361             logger.messageLogged(event);
362         }
363     }
364 }
365