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