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