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