1 /*
2  * Copyright (c) 2014, 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 JdkIdeaAntLogger 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         MATCHES {
73             @Override
apply(String s1, String s2)74             boolean apply(String s1, String s2) {
75                 return s1.matches(s2);
76             }
77         };
78 
apply(String s1, String s2)79         abstract boolean apply(String s1, String s2);
80     }
81 
82     /**
83      * Various kinds of ant messages that we shall intercept
84      */
85     enum MessageKind {
86 
87         /** a make error */
88         MAKE_ERROR(StringBinaryPredicate.CONTAINS, MSG_ERR, "error:", "compiler.err"),
89         /** a make warning */
90         MAKE_WARNING(StringBinaryPredicate.CONTAINS, MSG_WARN, "warning:", "compiler.warn"),
91         /** a make note */
92         MAKE_NOTE(StringBinaryPredicate.CONTAINS, MSG_INFO, "note:", "compiler.note"),
93         /** std make output */
94         MAKE_OTHER(StringBinaryPredicate.MATCHES, MSG_INFO, ".*"),
95         /** a javac crash */
96         JAVAC_CRASH(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "An exception has occurred in the compiler"),
97         /** jtreg test success */
98         JTREG_TEST_PASSED(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "Passed: "),
99         /** jtreg test failure */
100         JTREG_TEST_FAILED(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "FAILED: "),
101         /** jtreg test error */
102         JTREG_TEST_ERROR(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "Error: "),
103         /** jtreg report */
104         JTREG_TEST_REPORT(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "Report written");
105 
106         StringBinaryPredicate sbp;
107         int priority;
108         String[] keys;
109 
MessageKind(StringBinaryPredicate sbp, int priority, String... keys)110         MessageKind(StringBinaryPredicate sbp, int priority, String... keys) {
111             this.sbp = sbp;
112             this.priority = priority;
113             this.keys = keys;
114         }
115 
116         /**
117          * Does a given message string matches this kind?
118          */
matches(String s)119         boolean matches(String s) {
120             for (String key : keys) {
121                 if (sbp.apply(s, key)) {
122                     return true;
123                 }
124             }
125             return false;
126         }
127     }
128 
129     /**
130      * This enum is used to represent the list of tasks we need to keep track of during logging.
131      */
132     enum Task {
133         /** javac task - invoked during compilation */
134         MAKE("exec", MessageKind.MAKE_ERROR, MessageKind.MAKE_WARNING, MessageKind.MAKE_NOTE,
135                        MessageKind.MAKE_OTHER, MessageKind.JAVAC_CRASH),
136         /** jtreg task - invoked during test execution */
137         JTREG("jtreg", MessageKind.JTREG_TEST_PASSED, MessageKind.JTREG_TEST_FAILED, MessageKind.JTREG_TEST_ERROR, MessageKind.JTREG_TEST_REPORT),
138         /** initial synthetic task when the logger is created */
139         ROOT("") {
140             @Override
matches(String s)141             boolean matches(String s) {
142                 return false;
143             }
144         },
145         /** synthetic task catching any other tasks not in this list */
146         ANY("") {
147             @Override
matches(String s)148             boolean matches(String s) {
149                 return true;
150             }
151         };
152 
153         String taskName;
154         MessageKind[] msgs;
155 
Task(String taskName, MessageKind... msgs)156         Task(String taskName, MessageKind... msgs) {
157             this.taskName = taskName;
158             this.msgs = msgs;
159         }
160 
matches(String s)161         boolean matches(String s) {
162             return s.equals(taskName);
163         }
164     }
165 
166     /**
167      * This enum is used to represent the list of targets we need to keep track of during logging.
168      * A regular expression is used to match a given target name.
169      */
170     enum Target {
171         /** jtreg target - executed when launching tests */
172         JTREG("jtreg") {
173             @Override
getDisplayMessage(BuildEvent e)174             String getDisplayMessage(BuildEvent e) {
175                 return "Running jtreg tests: " + e.getProject().getProperty("jtreg.tests");
176             }
177         },
178         /** build selected modules */
179         BUILD_MODULE("build-module") {
180             @Override
getDisplayMessage(BuildEvent e)181             String getDisplayMessage(BuildEvent e) {
182                 return "Building modules: " + e.getProject().getProperty("module.name") + "...";
183             }
184         },
185         /** build images */
186         BUILD_IMAGES("images") {
187             @Override
getDisplayMessage(BuildEvent e)188             String getDisplayMessage(BuildEvent e) {
189                 return "Building images...";
190             }
191         },
192         /** build images */
193         CONFIGURE("-do-configure") {
194             @Override
getDisplayMessage(BuildEvent e)195             String getDisplayMessage(BuildEvent e) {
196                 return "Configuring build...";
197             }
198         },
199         /** synthetic target catching any other target not in this list */
200         ANY("") {
201             @Override
getDisplayMessage(BuildEvent e)202             String getDisplayMessage(BuildEvent e) {
203                 return "Executing Ant target(s): " + e.getProject().getProperty("ant.project.invoked-targets");
204             }
205             @Override
matches(String msg)206             boolean matches(String msg) {
207                 return true;
208             }
209         };
210 
211         String targetRegex;
212 
Target(String targetRegex)213         Target(String targetRegex) {
214             this.targetRegex = targetRegex;
215         }
216 
matches(String msg)217         boolean matches(String msg) {
218             return msg.matches(targetRegex);
219         }
220 
getDisplayMessage(BuildEvent e)221         abstract String getDisplayMessage(BuildEvent e);
222     }
223 
224     /**
225      * A custom build event used to represent status changes which should be notified inside
226      * Intellij
227      */
228     static class StatusEvent extends BuildEvent {
229 
230         /** the target to which the status update refers */
231         Target target;
232 
StatusEvent(BuildEvent e, Target target)233         StatusEvent(BuildEvent e, Target target) {
234             super(new StatusTask(e, target.getDisplayMessage(e)));
235             this.target = target;
236             setMessage(getTask().getTaskName(), 2);
237         }
238 
239         /**
240          * A custom task used to channel info regarding a status change
241          */
242         static class StatusTask extends org.apache.tools.ant.Task {
StatusTask(BuildEvent event, String msg)243             StatusTask(BuildEvent event, String msg) {
244                 setProject(event.getProject());
245                 setOwningTarget(event.getTarget());
246                 setTaskName(msg);
247             }
248         }
249     }
250 
251     /** wrapped ant logger (IntelliJ's own logger) */
252     DefaultLogger logger;
253 
254     /** flag - is this the first target we encounter? */
255     boolean firstTarget = true;
256 
257     /** flag - should subsequenet failures be suppressed ? */
258     boolean suppressTaskFailures = false;
259 
260     /** flag - have we ran into a javac crash ? */
261     boolean crashFound = false;
262 
263     /** stack of status changes associated with pending targets */
264     Stack<StatusEvent> statusEvents = new Stack<>();
265 
266     /** stack of pending tasks */
267     Stack<Task> tasks = new Stack<>();
268 
JdkIdeaAntLogger(Project project)269     public JdkIdeaAntLogger(Project project) {
270         for (Object o : project.getBuildListeners()) {
271             if (o instanceof DefaultLogger) {
272                 this.logger = (DefaultLogger)o;
273                 project.removeBuildListener((BuildListener)o);
274                 project.addBuildListener(this);
275             }
276         }
277         tasks.push(Task.ROOT);
278     }
279 
280     @Override
buildStarted(BuildEvent event)281     public void buildStarted(BuildEvent event) {
282         //do nothing
283     }
284 
285     @Override
buildFinished(BuildEvent event)286     public void buildFinished(BuildEvent event) {
287         //do nothing
288     }
289 
290     @Override
targetStarted(BuildEvent event)291     public void targetStarted(BuildEvent event) {
292         EnumSet<Target> statusKinds = firstTarget ?
293                 EnumSet.allOf(Target.class) :
294                 EnumSet.complementOf(EnumSet.of(Target.ANY));
295 
296         String targetName = event.getTarget().getName();
297 
298         for (Target statusKind : statusKinds) {
299             if (statusKind.matches(targetName)) {
300                 StatusEvent statusEvent = new StatusEvent(event, statusKind);
301                 statusEvents.push(statusEvent);
302                 logger.taskStarted(statusEvent);
303                 firstTarget = false;
304                 return;
305             }
306         }
307     }
308 
309     @Override
targetFinished(BuildEvent event)310     public void targetFinished(BuildEvent event) {
311         if (!statusEvents.isEmpty()) {
312             StatusEvent lastEvent = statusEvents.pop();
313             if (lastEvent.target.matches(event.getTarget().getName())) {
314                 logger.taskFinished(lastEvent);
315             }
316         }
317     }
318 
319     @Override
taskStarted(BuildEvent event)320     public void taskStarted(BuildEvent event) {
321         String taskName = event.getTask().getTaskName();
322         System.err.println("task started " + taskName);
323         for (Task task : Task.values()) {
324             if (task.matches(taskName)) {
325                 tasks.push(task);
326                 return;
327             }
328         }
329     }
330 
331     @Override
taskFinished(BuildEvent event)332     public void taskFinished(BuildEvent event) {
333         if (tasks.peek() == Task.ROOT) {
334             //we need to 'close' the root task to get nicer output
335             logger.taskFinished(event);
336         } else if (!suppressTaskFailures && event.getException() != null) {
337             //the first (innermost) task failure should always be logged
338             event.setMessage(event.getException().toString(), 0);
339             event.setException(null);
340             //note: we turn this into a plain message to avoid stack trace being logged by Idea
341             logger.messageLogged(event);
342             suppressTaskFailures = true;
343         }
344         tasks.pop();
345     }
346 
347     @Override
messageLogged(BuildEvent event)348     public void messageLogged(BuildEvent event) {
349         String msg = event.getMessage();
350 
351         boolean processed = false;
352 
353         if (!tasks.isEmpty()) {
354             Task task = tasks.peek();
355             for (MessageKind messageKind : task.msgs) {
356                 if (messageKind.matches(msg)) {
357                     event.setMessage(msg, messageKind.priority);
358                     processed = true;
359                     if (messageKind == MessageKind.JAVAC_CRASH) {
360                         crashFound = true;
361                     }
362                     break;
363                 }
364             }
365         }
366 
367         if (event.getPriority() == MSG_ERR || crashFound) {
368             //we log errors regardless of owning task
369             logger.messageLogged(event);
370             suppressTaskFailures = true;
371         } else if (processed) {
372             logger.messageLogged(event);
373         }
374     }
375 }
376