JdkIdeaAntLogger.java revision 2088:1817ea79cbbf
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 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 {
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        MATCHES {
73            @Override
74            boolean apply(String s1, String s2) {
75                return s1.matches(s2);
76            }
77        };
78
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
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         */
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
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
148            boolean matches(String s) {
149                return true;
150            }
151        };
152
153        String taskName;
154        MessageKind[] msgs;
155
156        Task(String taskName, MessageKind... msgs) {
157            this.taskName = taskName;
158            this.msgs = msgs;
159        }
160
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
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
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
188            String getDisplayMessage(BuildEvent e) {
189                return "Building images...";
190            }
191        },
192        /** build images */
193        CONFIGURE("-do-configure") {
194            @Override
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
202            String getDisplayMessage(BuildEvent e) {
203                return "Executing Ant target(s): " + e.getProject().getProperty("ant.project.invoked-targets");
204            }
205            @Override
206            boolean matches(String msg) {
207                return true;
208            }
209        };
210
211        String targetRegex;
212
213        Target(String targetRegex) {
214            this.targetRegex = targetRegex;
215        }
216
217        boolean matches(String msg) {
218            return msg.matches(targetRegex);
219        }
220
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
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 {
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
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
281    public void buildStarted(BuildEvent event) {
282        //do nothing
283    }
284
285    @Override
286    public void buildFinished(BuildEvent event) {
287        //do nothing
288    }
289
290    @Override
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
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
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
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
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