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