001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.apache.tools.ant.module.run;
043:
044: import java.io.File;
045: import java.io.PrintWriter;
046: import java.io.StringWriter;
047: import java.net.MalformedURLException;
048: import java.net.URI;
049: import java.net.URISyntaxException;
050: import java.util.Stack;
051: import java.util.StringTokenizer;
052: import java.util.logging.Level;
053: import java.util.logging.Logger;
054: import java.util.regex.Matcher;
055: import java.util.regex.Pattern;
056: import org.apache.tools.ant.module.spi.AntEvent;
057: import org.apache.tools.ant.module.spi.AntLogger;
058: import org.apache.tools.ant.module.spi.AntSession;
059: import org.openide.awt.StatusDisplayer;
060: import org.openide.filesystems.FileUtil;
061: import org.openide.util.NbBundle;
062: import org.openide.windows.OutputListener;
063:
064: /**
065: * Standard logger for producing Ant output messages.
066: * @author Jesse Glick
067: */
068: public final class StandardLogger extends AntLogger {
069:
070: private static final Logger ERR = Logger
071: .getLogger(StandardLogger.class.getName());
072:
073: /**
074: * Regexp matching an output line that is a column marker from a compiler or similar.
075: * Captured groups:
076: * <ol>
077: * <li>spaces preceding caret; length indicates column number
078: * </ol>
079: * @see "#37358"
080: */
081: private static final Pattern CARET_SHOWING_COLUMN = Pattern
082: .compile("^( *)\\^$"); // NOI18N
083: /**
084: * Regexp matching an output line indicating a change into a current working directory, as e.g. from make.
085: * Captured groups:
086: * <ol>
087: * <li>new working dir
088: * </ol>
089: */
090: private static final Pattern CWD_ENTER = Pattern
091: .compile(".*Entering directory [`'\"]?([^`'\"]+)(['\"]|$|\\.\\.\\.$)"); // NOI18N
092: /**
093: * Regexp matching an output line indicating a change out of a current working directory.
094: * Captured groups:
095: * <ol>
096: * <li>previous working dir
097: * </ol>
098: */
099: private static final Pattern CWD_LEAVE = Pattern
100: .compile(".*Leaving directory [`'\"]?([^`'\"]+)(['\"]|$|\\.\\.\\.$)"); // NOI18N
101: /**
102: * Regexp matching an output line which should (perhaps) be hyperlinked to a file.
103: * Captured groups:
104: * <ol>
105: * <li>file name (rel/abs path or URL)
106: * <li>line1
107: * <li>col1
108: * <li>line2
109: * <li>col2
110: * <li>message
111: * </ol>
112: */
113: private static final Pattern HYPERLINK = Pattern
114: .compile("\"?(.+?)\"?(?::|, line )(?:(\\d+):(?:(\\d+):(?:(\\d+):(\\d+):)?)?)? +(.+)"); // NOI18N
115:
116: /**
117: * Data stored in the session.
118: */
119: private static final class SessionData {
120: /** Time build was started. */
121: public long startTime;
122: /** Last-created hyperlink, in case we need to adjust the column number. */
123: public Hyperlink lastHyperlink;
124: /** Current stack of working directories for which output is being displayed; top is current location. */
125: public Stack<File> currentDir = new Stack<File>();
126:
127: public SessionData() {
128: }
129: }
130:
131: /** used only for unit testing */
132: private final long mockTotalTime;
133:
134: /** Default constructor for lookup. */
135: public StandardLogger() {
136: mockTotalTime = 0L;
137: }
138:
139: /** used only for unit testing */
140: StandardLogger(long mockTotalTime) {
141: this .mockTotalTime = mockTotalTime;
142: }
143:
144: @Override
145: public boolean interestedInSession(AntSession session) {
146: return true;
147: }
148:
149: @Override
150: public boolean interestedInAllScripts(AntSession session) {
151: return true;
152: }
153:
154: @Override
155: public String[] interestedInTargets(AntSession session) {
156: return AntLogger.ALL_TARGETS;
157: }
158:
159: @Override
160: public String[] interestedInTasks(AntSession session) {
161: return AntLogger.ALL_TASKS;
162: }
163:
164: @Override
165: public int[] interestedInLogLevels(AntSession session) {
166: int verb = session.getVerbosity();
167: assert verb >= AntEvent.LOG_ERR && verb <= AntEvent.LOG_DEBUG : verb;
168: int[] levels = new int[verb + 1];
169: for (int i = 0; i <= verb; i++) {
170: levels[i] = i;
171: }
172: return levels;
173: }
174:
175: private SessionData getSessionData(AntSession session) {
176: SessionData data = (SessionData) session.getCustomData(this );
177: if (data == null) {
178: data = new SessionData();
179: session.putCustomData(this , data);
180: }
181: return data;
182: }
183:
184: @Override
185: public void buildInitializationFailed(AntEvent event) {
186: if (event.isConsumed()) {
187: return;
188: }
189: // Write errors to the output window, since
190: // a lot of errors could be annoying as dialogs
191: Throwable t = event.getException();
192: if (event.getSession().getVerbosity() >= AntEvent.LOG_VERBOSE) {
193: deliverStackTrace(t, event);
194: } else {
195: event.getSession().println(t.toString(), true, null);
196: }
197: StatusDisplayer.getDefault().setStatusText(
198: NbBundle.getMessage(StandardLogger.class,
199: "FMT_target_failed_status", event.getSession()
200: .getDisplayName()));
201: event.consume();
202: }
203:
204: private static void deliverBlockOfTextAsLines(String lines,
205: AntEvent originalEvent, int level) {
206: StringTokenizer tok = new StringTokenizer(lines, "\r\n"); // NOI18N
207: while (tok.hasMoreTokens()) {
208: String line = tok.nextToken();
209: originalEvent.getSession().deliverMessageLogged(
210: originalEvent, line, level);
211: }
212: }
213:
214: private static void deliverStackTrace(Throwable t,
215: AntEvent originalEvent) {
216: StringWriter sw = new StringWriter();
217: PrintWriter pw = new PrintWriter(sw);
218: t.printStackTrace(pw);
219: pw.flush();
220: deliverBlockOfTextAsLines(sw.toString(), originalEvent,
221: AntEvent.LOG_ERR);
222: }
223:
224: @Override
225: public void buildStarted(AntEvent event) {
226: if (event.isConsumed()) {
227: return;
228: }
229: getSessionData(event.getSession()).startTime = System
230: .currentTimeMillis();
231: StatusDisplayer.getDefault().setStatusText(
232: NbBundle.getMessage(StandardLogger.class,
233: "FMT_running_ant", event.getSession()
234: .getDisplayName()));
235: // no messages printed for now
236: event.consume();
237: }
238:
239: @Override
240: public void buildFinished(AntEvent event) {
241: if (event.isConsumed()) {
242: return;
243: }
244: AntSession session = event.getSession();
245: Throwable t = event.getException();
246: long time = System.currentTimeMillis()
247: - getSessionData(session).startTime; // #10305
248: if (mockTotalTime != 0L) {
249: time = mockTotalTime;
250: }
251: if (t == null) {
252: session.println(formatMessageWithTime(
253: "FMT_finished_target_printed", time), false, null);
254: StatusDisplayer.getDefault().setStatusText(
255: NbBundle.getMessage(StandardLogger.class,
256: "FMT_finished_target_status", session
257: .getDisplayName()));
258: } else {
259: if (isStopException(t.getCause())) {
260: // Sometimes wrapped, but we really want to know just that the thread was stopped.
261: t = t.getCause();
262: }
263: if (!session.isExceptionConsumed(t)) {
264: session.consumeException(t);
265: while (isBuildException(t)) { // http://issues.apache.org/bugzilla/show_bug.cgi?id=43398
266: Throwable cause = t.getCause();
267: if (cause != null
268: && cause.toString().equals(t.getMessage())) {
269: t = cause;
270: } else {
271: break;
272: }
273: }
274: if (isBuildException(t)
275: && session.getVerbosity() < AntEvent.LOG_VERBOSE) {
276: // Stack trace probably not required.
277: // Check for hyperlink to handle e.g. <fail>
278: // which produces a BE whose toString is the location + message.
279: // But send to other loggers since they may wish to suppress such an error.
280: String msg = t.toString();
281: deliverBlockOfTextAsLines(msg, event,
282: AntEvent.LOG_ERR);
283: } else if (!isStopException(t)
284: || event.getSession().getVerbosity() >= AntEvent.LOG_VERBOSE) {
285: // ThreadDeath can be thrown when killing an Ant process, so don't print it normally
286: deliverStackTrace(t, event);
287: }
288: }
289: if (isStopException(t)) {
290: event.getSession().println(
291: formatMessageWithTime(
292: "FMT_target_stopped_printed", time),
293: true, null);
294: StatusDisplayer.getDefault()
295: .setStatusText(
296: NbBundle.getMessage(
297: StandardLogger.class,
298: "FMT_target_stopped_status",
299: event.getSession()
300: .getDisplayName()));
301: } else {
302: event.getSession().println(
303: formatMessageWithTime(
304: "FMT_target_failed_printed", time),
305: true, null); // #10305
306: StatusDisplayer.getDefault()
307: .setStatusText(
308: NbBundle.getMessage(
309: StandardLogger.class,
310: "FMT_target_failed_status",
311: event.getSession()
312: .getDisplayName()));
313: }
314: }
315: event.consume();
316: }
317:
318: private static boolean isBuildException(Throwable t) {
319: Class c = t.getClass();
320: while (c != Throwable.class) {
321: if (c.getName().equals(
322: "org.apache.tools.ant.BuildException")) { // NOI18N
323: return true;
324: }
325: c = c.getSuperclass();
326: }
327: return false;
328: }
329:
330: private static boolean isStopException(Throwable t) {
331: return (t instanceof ThreadDeath)
332: || (t instanceof InterruptedException);
333: }
334:
335: /** Formats the millis in a human readable String.
336: * Total time: {0} minutes
337: * {1} seconds
338: */
339: private static String formatMessageWithTime(String key, long millis) {
340: int secs = (int) (millis / 1000);
341: int minutes = secs / 60;
342: int seconds = secs % 60;
343: return NbBundle.getMessage(StandardLogger.class, key, minutes,
344: seconds);
345: }
346:
347: @Override
348: public void targetStarted(AntEvent event) {
349: if (event.isConsumed()) {
350: return;
351: }
352: // XXX this could start indenting messages, perhaps
353: String name = event.getTargetName();
354: if (name != null) {
355: // Avoid printing internal targets normally:
356: int minlevel = (name.length() > 0 && name.charAt(0) == '-') ? AntEvent.LOG_VERBOSE
357: : AntEvent.LOG_INFO;
358: if (event.getSession().getVerbosity() >= minlevel) {
359: event.getSession().println(
360: NbBundle.getMessage(StandardLogger.class,
361: "MSG_target_started_printed", name),
362: false, null);
363: }
364: }
365: event.consume();
366: }
367:
368: @Override
369: public void messageLogged(AntEvent event) {
370: if (event.isConsumed()) {
371: return;
372: }
373: event.consume();
374: AntSession session = event.getSession();
375: String line = event.getMessage();
376: if (line
377: .equals("Trying to override old definition of task java")
378: && event.getLogLevel() == AntEvent.LOG_WARN) { // NOI18N
379: return; // #56341
380: }
381: ERR.log(Level.FINE, "Received message: {0}", line);
382: if (line.indexOf('\n') != -1) {
383: // Multiline message. Should be split into blocks and redelivered,
384: // to allow other loggers (e.g. JavaAntLogger) to process individual
385: // lines (e.g. stack traces). Note that other loggers are still capable
386: // of handling the original multiline message specially. Note also that
387: // only messages at or above the session verbosity will be split.
388: deliverBlockOfTextAsLines(line, event, event.getLogLevel());
389: return;
390: }
391: Matcher m = CARET_SHOWING_COLUMN.matcher(line);
392: if (m.matches()) {
393: // #37358: adjust the column number of the last hyperlink accordingly.
394: ERR.fine(" Looks like a special caret line");
395: SessionData data = getSessionData(session);
396: if (data.lastHyperlink != null) {
397: // For " ^", infer a column number of 3.
398: data.lastHyperlink.setColumn1(m.group(1).length() + 1);
399: data.lastHyperlink = null;
400: // Don't print the actual caret line, just noise.
401: return;
402: }
403: }
404: m = CWD_ENTER.matcher(line);
405: if (m.matches()) {
406: ERR.fine(" Looks like a change of CWD");
407: File d = new File(m.group(1));
408: if (d.isDirectory()) {
409: Stack<File> stack = getSessionData(session).currentDir;
410: stack.push(d);
411: ERR.log(Level.FINE,
412: " ...is a change of CWD; stack now: {0}",
413: stack);
414: }
415: }
416: m = CWD_LEAVE.matcher(line);
417: if (m.matches()) {
418: ERR.fine(" Looks like a change of CWD back out");
419: File d = new File(m.group(1));
420: Stack<File> stack = getSessionData(session).currentDir;
421: if (stack.empty()) {
422: ERR.log(Level.FINE,
423: " ...but there was nowhere to change out of");
424: } else {
425: File previous = stack.pop();
426: if (!previous.equals(d)) {
427: ERR.log(Level.FINE,
428: " ...stack mismatch: {0} vs. {1}",
429: new Object[] { previous, d });
430: }
431: }
432: }
433: OutputListener hyperlink = findHyperlink(session, line);
434: if (hyperlink instanceof Hyperlink) {
435: getSessionData(session).lastHyperlink = (Hyperlink) hyperlink;
436: }
437: // XXX should translate tabs to spaces here as a safety measure (esp. since output window messes it up...)
438: event.getSession().println(line,
439: event.getLogLevel() <= AntEvent.LOG_WARN, hyperlink);
440: }
441:
442: @Override
443: public void taskFinished(AntEvent event) {
444: // Do not consider hyperlinks from previous tasks.
445: getSessionData(event.getSession()).lastHyperlink = null;
446: }
447:
448: /**
449: * Possibly hyperlink a message logged event.
450: */
451: private OutputListener findHyperlink(AntSession session, String line) {
452: Stack<File> cwd = getSessionData(session).currentDir;
453: Matcher m = HYPERLINK.matcher(line);
454: if (!m.matches()) {
455: ERR.fine("does not look like a hyperlink");
456: return null;
457: }
458: String path = m.group(1);
459: File file;
460: if (path.startsWith("file:")) {
461: try {
462: file = new File(new URI(path));
463: } catch (URISyntaxException e) {
464: ERR.log(Level.FINE, "invalid URI, skipping", e);
465: return null;
466: } catch (IllegalArgumentException e) {
467: ERR.log(Level.FINE, "invalid URI, skipping", e);
468: return null;
469: }
470: } else {
471: file = new File(path);
472: if (!file.isAbsolute()) {
473: if (cwd.isEmpty()) {
474: ERR.fine("Non-absolute path with no CWD, skipping");
475: // don't waste time on File.exists!
476: return null;
477: } else {
478: file = new File(cwd.peek(), path);
479: }
480: }
481: }
482: if (!file.exists()) {
483: ERR.log(Level.FINE, "no such file {0}, skipping", file);
484: return null;
485: }
486:
487: int line1 = -1, col1 = -1, line2 = -1, col2 = -1;
488: String num = m.group(2);
489: try {
490: if (num != null) {
491: line1 = Integer.parseInt(num);
492: num = m.group(3);
493: if (num != null) {
494: col1 = Integer.parseInt(num);
495: num = m.group(4);
496: if (num != null) {
497: line2 = Integer.parseInt(num);
498: col2 = Integer.parseInt(m.group(5));
499: }
500: }
501: }
502: } catch (NumberFormatException e) {
503: ERR.log(Level.FINE, "bad line/col #", e);
504: return null;
505: }
506:
507: String message = m.group(6);
508:
509: file = FileUtil.normalizeFile(file); // do this late, after File.exists
510: ERR
511: .log(Level.FINE,
512: "Hyperlink: {0} [{1}:{2}:{3}:{4}]: {5}",
513: new Object[] { file, line1, col1, line2, col2,
514: message });
515: try {
516: return session.createStandardHyperlink(
517: file.toURI().toURL(), message, line1, col1, line2,
518: col2);
519: } catch (MalformedURLException e) {
520: assert false : e;
521: return null;
522: }
523: }
524:
525: }
|