LangtoolsIdeaAntLogger.java revision 4075:b398971f7b6f
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
26package idea;
27
28import org.apache.tools.ant.BuildEvent;
29import org.apache.tools.ant.BuildListener;
30import org.apache.tools.ant.DefaultLogger;
31import org.apache.tools.ant.Project;
32
33import java.util.EnumSet;
34import java.util.Stack;
35
36import 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 */
52public 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 {
60        CONTAINS() {
61            @Override
62            boolean apply(String s1, String s2) {
63                return s1.contains(s2);
64            }
65        },
66        STARTS_WITH {
67            @Override
68            boolean apply(String s1, String s2) {
69                return s1.startsWith(s2);
70            }
71        };
72
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
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         */
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
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
144            boolean matches(String s) {
145                return true;
146            }
147        };
148
149        String taskName;
150        MessageKind[] msgs;
151
152        Task(String taskName, MessageKind... msgs) {
153            this.taskName = taskName;
154            this.msgs = msgs;
155        }
156
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
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
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
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
191            String getDisplayMessage(BuildEvent e) {
192                return "Executing Ant target(s): " + e.getProject().getProperty("ant.project.invoked-targets");
193            }
194            @Override
195            boolean matches(String msg) {
196                return true;
197            }
198        };
199
200        String targetName;
201
202        Target(String targetName) {
203            this.targetName = targetName;
204        }
205
206        boolean matches(String msg) {
207            return msg.equals(targetName);
208        }
209
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
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 {
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
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
271    public void buildStarted(BuildEvent event) {
272        //do nothing
273    }
274
275    @Override
276    public void buildFinished(BuildEvent event) {
277        //do nothing
278    }
279
280    @Override
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
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
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
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
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