/* * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package idea; import org.apache.tools.ant.BuildEvent; import org.apache.tools.ant.BuildListener; import org.apache.tools.ant.DefaultLogger; import org.apache.tools.ant.Project; import java.util.EnumSet; import java.util.Stack; import static org.apache.tools.ant.Project.*; /** * This class is used to wrap the IntelliJ ant logger in order to provide more meaningful * output when building langtools. The basic ant output in IntelliJ can be quite cumbersome to * work with, as it provides two separate views: (i) a tree view, which is good to display build task * in a hierarchical fashion as they are processed; and a (ii) plain text view, which gives you * the full ant output. The main problem is that javac-related messages are buried into the * ant output (which is made very verbose by IntelliJ in order to support the tree view). It is * not easy to figure out which node to expand in order to see the error message; switching * to plain text doesn't help either, as now the output is totally flat. * * This logger class removes a lot of verbosity from the IntelliJ ant logger by not propagating * all the events to the IntelliJ's logger. In addition, certain events are handled in a custom * fashion, to generate better output during the build. */ public final class LangtoolsIdeaAntLogger extends DefaultLogger { /** * This is just a way to pass in customized binary string predicates; * * TODO: replace with @code{BiPredicate} and method reference when moving to 8 */ enum StringBinaryPredicate { CONTAINS() { @Override boolean apply(String s1, String s2) { return s1.contains(s2); } }, STARTS_WITH { @Override boolean apply(String s1, String s2) { return s1.startsWith(s2); } }; abstract boolean apply(String s1, String s2); } /** * Various kinds of ant messages that we shall intercept */ enum MessageKind { /** a javac error */ JAVAC_ERROR(StringBinaryPredicate.CONTAINS, MSG_ERR, "error:", "compiler.err"), /** a javac warning */ JAVAC_WARNING(StringBinaryPredicate.CONTAINS, MSG_WARN, "warning:", "compiler.warn"), /** a javac note */ JAVAC_NOTE(StringBinaryPredicate.CONTAINS, MSG_INFO, "note:", "compiler.note"), /** a javac raw error (these typically come from a build misconfiguration - such as a bad javac flag) */ JAVAC_RAW_ERROR(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "javac: "), /** continuation of some javac error message */ JAVAC_NESTED_DIAG(StringBinaryPredicate.STARTS_WITH, MSG_INFO, " "), /** a javac crash */ JAVAC_CRASH(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "An exception has occurred in the compiler"), /** jtreg test success */ JTREG_TEST_PASSED(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "Passed: "), /** jtreg test failure */ JTREG_TEST_FAILED(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "FAILED: "), /** jtreg test error */ JTREG_TEST_ERROR(StringBinaryPredicate.STARTS_WITH, MSG_ERR, "Error: "), /** jtreg report */ JTREG_TEST_REPORT(StringBinaryPredicate.STARTS_WITH, MSG_INFO, "Report written"); StringBinaryPredicate sbp; int priority; String[] keys; MessageKind(StringBinaryPredicate sbp, int priority, String... keys) { this.sbp = sbp; this.priority = priority; this.keys = keys; } /** * Does a given message string matches this kind? */ boolean matches(String s) { for (String key : keys) { if (sbp.apply(s, key)) { return true; } } return false; } } /** * This enum is used to represent the list of tasks we need to keep track of during logging. */ enum Task { /** exec task - invoked during compilation */ JAVAC("exec", MessageKind.JAVAC_ERROR, MessageKind.JAVAC_WARNING, MessageKind.JAVAC_NOTE, MessageKind.JAVAC_RAW_ERROR, MessageKind.JAVAC_NESTED_DIAG, MessageKind.JAVAC_CRASH), /** jtreg task - invoked during test execution */ JTREG("jtreg", MessageKind.JTREG_TEST_PASSED, MessageKind.JTREG_TEST_FAILED, MessageKind.JTREG_TEST_ERROR, MessageKind.JTREG_TEST_REPORT), /** initial synthetic task when the logger is created */ ROOT("") { @Override boolean matches(String s) { return false; } }, /** synthetic task catching any other tasks not in this list */ ANY("") { @Override boolean matches(String s) { return true; } }; String taskName; MessageKind[] msgs; Task(String taskName, MessageKind... msgs) { this.taskName = taskName; this.msgs = msgs; } boolean matches(String s) { return s.equals(taskName); } } /** * This enum is used to represent the list of targets we need to keep track of during logging. * A regular expression is used to match a given target name. */ enum Target { /** jtreg target - executed when launching tests */ JTREG("jtreg") { @Override String getDisplayMessage(BuildEvent e) { return "Running jtreg tests: " + e.getProject().getProperty("jtreg.tests"); } }, /** build bootstrap tool target - executed when bootstrapping javac */ BUILD_BOOTSTRAP_JAVAC("build-bootstrap-javac-classes") { @Override String getDisplayMessage(BuildEvent e) { return "Building bootstrap javac..."; } }, /** build classes target - executed when building classes of given tool */ BUILD_ALL_CLASSES("build-all-classes") { @Override String getDisplayMessage(BuildEvent e) { return "Building all classes..."; } }, /** synthetic target catching any other target not in this list */ ANY("") { @Override String getDisplayMessage(BuildEvent e) { return "Executing Ant target(s): " + e.getProject().getProperty("ant.project.invoked-targets"); } @Override boolean matches(String msg) { return true; } }; String targetName; Target(String targetName) { this.targetName = targetName; } boolean matches(String msg) { return msg.equals(targetName); } abstract String getDisplayMessage(BuildEvent e); } /** * A custom build event used to represent status changes which should be notified inside * Intellij */ static class StatusEvent extends BuildEvent { /** the target to which the status update refers */ Target target; StatusEvent(BuildEvent e, Target target) { super(new StatusTask(e, target.getDisplayMessage(e))); this.target = target; setMessage(getTask().getTaskName(), 2); } /** * A custom task used to channel info regarding a status change */ static class StatusTask extends org.apache.tools.ant.Task { StatusTask(BuildEvent event, String msg) { setProject(event.getProject()); setOwningTarget(event.getTarget()); setTaskName(msg); } } } /** wrapped ant logger (IntelliJ's own logger) */ DefaultLogger logger; /** flag - is this the first target we encounter? */ boolean firstTarget = true; /** flag - should subsequenet failures be suppressed ? */ boolean suppressTaskFailures = false; /** flag - have we ran into a javac crash ? */ boolean crashFound = false; /** stack of status changes associated with pending targets */ Stack statusEvents = new Stack<>(); /** stack of pending tasks */ Stack tasks = new Stack<>(); public LangtoolsIdeaAntLogger(Project project) { for (Object o : project.getBuildListeners()) { if (o instanceof DefaultLogger) { this.logger = (DefaultLogger)o; project.removeBuildListener((BuildListener)o); project.addBuildListener(this); } } logger.setMessageOutputLevel(3); tasks.push(Task.ROOT); } @Override public void buildStarted(BuildEvent event) { //do nothing } @Override public void buildFinished(BuildEvent event) { //do nothing } @Override public void targetStarted(BuildEvent event) { EnumSet statusKinds = firstTarget ? EnumSet.allOf(Target.class) : EnumSet.complementOf(EnumSet.of(Target.ANY)); String targetName = event.getTarget().getName(); for (Target statusKind : statusKinds) { if (statusKind.matches(targetName)) { StatusEvent statusEvent = new StatusEvent(event, statusKind); statusEvents.push(statusEvent); logger.taskStarted(statusEvent); firstTarget = false; return; } } } @Override public void targetFinished(BuildEvent event) { if (!statusEvents.isEmpty()) { StatusEvent lastEvent = statusEvents.pop(); if (lastEvent.target.matches(event.getTarget().getName())) { logger.taskFinished(lastEvent); } } } @Override public void taskStarted(BuildEvent event) { String taskName = event.getTask().getTaskName(); for (Task task : Task.values()) { if (task.matches(taskName)) { tasks.push(task); return; } } } @Override public void taskFinished(BuildEvent event) { if (tasks.peek() == Task.ROOT) { //we need to 'close' the root task to get nicer output logger.taskFinished(event); } else if (!suppressTaskFailures && event.getException() != null) { //the first (innermost) task failure should always be logged event.setMessage(event.getException().toString(), 0); event.setException(null); //note: we turn this into a plain message to avoid stack trace being logged by Idea logger.messageLogged(event); suppressTaskFailures = true; } tasks.pop(); } @Override public void messageLogged(BuildEvent event) { String msg = event.getMessage(); boolean processed = false; if (!tasks.isEmpty()) { Task task = tasks.peek(); for (MessageKind messageKind : task.msgs) { if (messageKind.matches(msg)) { event.setMessage(msg, messageKind.priority); processed = true; if (messageKind == MessageKind.JAVAC_CRASH) { crashFound = true; } break; } } } if (event.getPriority() == MSG_ERR || crashFound) { //we log errors regardless of owning task logger.messageLogged(event); suppressTaskFailures = true; } else if (processed) { logger.messageLogged(event); } } }