001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.tools.ant;
020:
021: import java.io.FileOutputStream;
022: import java.io.IOException;
023: import java.io.OutputStream;
024: import java.io.OutputStreamWriter;
025: import java.io.PrintStream;
026: import java.io.Writer;
027: import java.util.Hashtable;
028: import java.util.Stack;
029: import java.util.Enumeration;
030: import javax.xml.parsers.DocumentBuilder;
031: import javax.xml.parsers.DocumentBuilderFactory;
032: import org.apache.tools.ant.util.DOMElementWriter;
033: import org.apache.tools.ant.util.StringUtils;
034: import org.w3c.dom.Document;
035: import org.w3c.dom.Element;
036: import org.w3c.dom.Text;
037:
038: /**
039: * Generates a file in the current directory with
040: * an XML description of what happened during a build.
041: * The default filename is "log.xml", but this can be overridden
042: * with the property <code>XmlLogger.file</code>.
043: *
044: * This implementation assumes in its sanity checking that only one
045: * thread runs a particular target/task at a time. This is enforced
046: * by the way that parallel builds and antcalls are done - and
047: * indeed all but the simplest of tasks could run into problems
048: * if executed in parallel.
049: *
050: * @see Project#addBuildListener(BuildListener)
051: */
052: public class XmlLogger implements BuildLogger {
053:
054: private int msgOutputLevel = Project.MSG_DEBUG;
055: private PrintStream outStream;
056:
057: /** DocumentBuilder to use when creating the document to start with. */
058: private static DocumentBuilder builder = getDocumentBuilder();
059:
060: /**
061: * Returns a default DocumentBuilder instance or throws an
062: * ExceptionInInitializerError if it can't be created.
063: *
064: * @return a default DocumentBuilder instance.
065: */
066: private static DocumentBuilder getDocumentBuilder() {
067: try {
068: return DocumentBuilderFactory.newInstance()
069: .newDocumentBuilder();
070: } catch (Exception exc) {
071: throw new ExceptionInInitializerError(exc);
072: }
073: }
074:
075: /** XML element name for a build. */
076: private static final String BUILD_TAG = "build";
077: /** XML element name for a target. */
078: private static final String TARGET_TAG = "target";
079: /** XML element name for a task. */
080: private static final String TASK_TAG = "task";
081: /** XML element name for a message. */
082: private static final String MESSAGE_TAG = "message";
083: /** XML attribute name for a name. */
084: private static final String NAME_ATTR = "name";
085: /** XML attribute name for a time. */
086: private static final String TIME_ATTR = "time";
087: /** XML attribute name for a message priority. */
088: private static final String PRIORITY_ATTR = "priority";
089: /** XML attribute name for a file location. */
090: private static final String LOCATION_ATTR = "location";
091: /** XML attribute name for an error description. */
092: private static final String ERROR_ATTR = "error";
093: /** XML element name for a stack trace. */
094: private static final String STACKTRACE_TAG = "stacktrace";
095:
096: /** The complete log document for this build. */
097: private Document doc = builder.newDocument();
098: /** Mapping for when tasks started (Task to TimedElement). */
099: private Hashtable tasks = new Hashtable();
100: /** Mapping for when targets started (Task to TimedElement). */
101: private Hashtable targets = new Hashtable();
102: /**
103: * Mapping of threads to stacks of elements
104: * (Thread to Stack of TimedElement).
105: */
106: private Hashtable threadStacks = new Hashtable();
107: /**
108: * When the build started.
109: */
110: private TimedElement buildElement = null;
111:
112: /** Utility class representing the time an element started. */
113: private static class TimedElement {
114: /**
115: * Start time in milliseconds
116: * (as returned by <code>System.currentTimeMillis()</code>).
117: */
118: private long startTime;
119: /** Element created at the start time. */
120: private Element element;
121:
122: public String toString() {
123: return element.getTagName() + ":"
124: + element.getAttribute("name");
125: }
126: }
127:
128: /**
129: * Constructs a new BuildListener that logs build events to an XML file.
130: */
131: public XmlLogger() {
132: }
133:
134: /**
135: * Fired when the build starts, this builds the top-level element for the
136: * document and remembers the time of the start of the build.
137: *
138: * @param event Ignored.
139: */
140: public void buildStarted(BuildEvent event) {
141: buildElement = new TimedElement();
142: buildElement.startTime = System.currentTimeMillis();
143: buildElement.element = doc.createElement(BUILD_TAG);
144: }
145:
146: /**
147: * Fired when the build finishes, this adds the time taken and any
148: * error stacktrace to the build element and writes the document to disk.
149: *
150: * @param event An event with any relevant extra information.
151: * Will not be <code>null</code>.
152: */
153: public void buildFinished(BuildEvent event) {
154: long totalTime = System.currentTimeMillis()
155: - buildElement.startTime;
156: buildElement.element.setAttribute(TIME_ATTR, DefaultLogger
157: .formatTime(totalTime));
158:
159: if (event.getException() != null) {
160: buildElement.element.setAttribute(ERROR_ATTR, event
161: .getException().toString());
162: // print the stacktrace in the build file it is always useful...
163: // better have too much info than not enough.
164: Throwable t = event.getException();
165: Text errText = doc.createCDATASection(StringUtils
166: .getStackTrace(t));
167: Element stacktrace = doc.createElement(STACKTRACE_TAG);
168: stacktrace.appendChild(errText);
169: buildElement.element.appendChild(stacktrace);
170: }
171:
172: String outFilename = event.getProject().getProperty(
173: "XmlLogger.file");
174: if (outFilename == null) {
175: outFilename = "log.xml";
176: }
177: String xslUri = event.getProject().getProperty(
178: "ant.XmlLogger.stylesheet.uri");
179: if (xslUri == null) {
180: xslUri = "log.xsl";
181: }
182: Writer out = null;
183: try {
184: // specify output in UTF8 otherwise accented characters will blow
185: // up everything
186: OutputStream stream = outStream;
187: if (stream == null) {
188: stream = new FileOutputStream(outFilename);
189: }
190: out = new OutputStreamWriter(stream, "UTF8");
191: out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
192: if (xslUri.length() > 0) {
193: out.write("<?xml-stylesheet type=\"text/xsl\" href=\""
194: + xslUri + "\"?>\n\n");
195: }
196: (new DOMElementWriter()).write(buildElement.element, out,
197: 0, "\t");
198: out.flush();
199: } catch (IOException exc) {
200: throw new BuildException("Unable to write log file", exc);
201: } finally {
202: if (out != null) {
203: try {
204: out.close();
205: } catch (IOException e) {
206: // ignore
207: }
208: }
209: }
210: buildElement = null;
211: }
212:
213: /**
214: * Returns the stack of timed elements for the current thread.
215: * @return the stack of timed elements for the current thread
216: */
217: private Stack getStack() {
218: Stack threadStack = (Stack) threadStacks.get(Thread
219: .currentThread());
220: if (threadStack == null) {
221: threadStack = new Stack();
222: threadStacks.put(Thread.currentThread(), threadStack);
223: }
224: /* For debugging purposes uncomment:
225: org.w3c.dom.Comment s = doc.createComment("stack=" + threadStack);
226: buildElement.element.appendChild(s);
227: */
228: return threadStack;
229: }
230:
231: /**
232: * Fired when a target starts building, this pushes a timed element
233: * for the target onto the stack of elements for the current thread,
234: * remembering the current time and the name of the target.
235: *
236: * @param event An event with any relevant extra information.
237: * Will not be <code>null</code>.
238: */
239: public void targetStarted(BuildEvent event) {
240: Target target = event.getTarget();
241: TimedElement targetElement = new TimedElement();
242: targetElement.startTime = System.currentTimeMillis();
243: targetElement.element = doc.createElement(TARGET_TAG);
244: targetElement.element.setAttribute(NAME_ATTR, target.getName());
245: targets.put(target, targetElement);
246: getStack().push(targetElement);
247: }
248:
249: /**
250: * Fired when a target finishes building, this adds the time taken
251: * and any error stacktrace to the appropriate target element in the log.
252: *
253: * @param event An event with any relevant extra information.
254: * Will not be <code>null</code>.
255: */
256: public void targetFinished(BuildEvent event) {
257: Target target = event.getTarget();
258: TimedElement targetElement = (TimedElement) targets.get(target);
259: if (targetElement != null) {
260: long totalTime = System.currentTimeMillis()
261: - targetElement.startTime;
262: targetElement.element.setAttribute(TIME_ATTR, DefaultLogger
263: .formatTime(totalTime));
264:
265: TimedElement parentElement = null;
266: Stack threadStack = getStack();
267: if (!threadStack.empty()) {
268: TimedElement poppedStack = (TimedElement) threadStack
269: .pop();
270: if (poppedStack != targetElement) {
271: throw new RuntimeException(
272: "Mismatch - popped element = "
273: + poppedStack
274: + " finished target element = "
275: + targetElement);
276: }
277: if (!threadStack.empty()) {
278: parentElement = (TimedElement) threadStack.peek();
279: }
280: }
281: if (parentElement == null) {
282: buildElement.element.appendChild(targetElement.element);
283: } else {
284: parentElement.element
285: .appendChild(targetElement.element);
286: }
287: }
288: targets.remove(target);
289: }
290:
291: /**
292: * Fired when a task starts building, this pushes a timed element
293: * for the task onto the stack of elements for the current thread,
294: * remembering the current time and the name of the task.
295: *
296: * @param event An event with any relevant extra information.
297: * Will not be <code>null</code>.
298: */
299: public void taskStarted(BuildEvent event) {
300: TimedElement taskElement = new TimedElement();
301: taskElement.startTime = System.currentTimeMillis();
302: taskElement.element = doc.createElement(TASK_TAG);
303:
304: Task task = event.getTask();
305: String name = event.getTask().getTaskName();
306: if (name == null) {
307: name = "";
308: }
309: taskElement.element.setAttribute(NAME_ATTR, name);
310: taskElement.element.setAttribute(LOCATION_ATTR, event.getTask()
311: .getLocation().toString());
312: tasks.put(task, taskElement);
313: getStack().push(taskElement);
314: }
315:
316: /**
317: * Fired when a task finishes building, this adds the time taken
318: * and any error stacktrace to the appropriate task element in the log.
319: *
320: * @param event An event with any relevant extra information.
321: * Will not be <code>null</code>.
322: */
323: public void taskFinished(BuildEvent event) {
324: Task task = event.getTask();
325: TimedElement taskElement = (TimedElement) tasks.get(task);
326: if (taskElement != null) {
327: long totalTime = System.currentTimeMillis()
328: - taskElement.startTime;
329: taskElement.element.setAttribute(TIME_ATTR, DefaultLogger
330: .formatTime(totalTime));
331: Target target = task.getOwningTarget();
332: TimedElement targetElement = null;
333: if (target != null) {
334: targetElement = (TimedElement) targets.get(target);
335: }
336: if (targetElement == null) {
337: buildElement.element.appendChild(taskElement.element);
338: } else {
339: targetElement.element.appendChild(taskElement.element);
340: }
341: Stack threadStack = getStack();
342: if (!threadStack.empty()) {
343: TimedElement poppedStack = (TimedElement) threadStack
344: .pop();
345: if (poppedStack != taskElement) {
346: throw new RuntimeException(
347: "Mismatch - popped element = "
348: + poppedStack
349: + " finished task element = "
350: + taskElement);
351: }
352: }
353: tasks.remove(task);
354: } else {
355: throw new RuntimeException("Unknown task " + task
356: + " not in " + tasks);
357: }
358: }
359:
360: /**
361: * Get the TimedElement associated with a task.
362: *
363: * Where the task is not found directly, search for unknown elements which
364: * may be hiding the real task
365: */
366: private TimedElement getTaskElement(Task task) {
367: TimedElement element = (TimedElement) tasks.get(task);
368: if (element != null) {
369: return element;
370: }
371:
372: for (Enumeration e = tasks.keys(); e.hasMoreElements();) {
373: Task key = (Task) e.nextElement();
374: if (key instanceof UnknownElement) {
375: if (((UnknownElement) key).getTask() == task) {
376: return (TimedElement) tasks.get(key);
377: }
378: }
379: }
380:
381: return null;
382: }
383:
384: /**
385: * Fired when a message is logged, this adds a message element to the
386: * most appropriate parent element (task, target or build) and records
387: * the priority and text of the message.
388: *
389: * @param event An event with any relevant extra information.
390: * Will not be <code>null</code>.
391: */
392: public void messageLogged(BuildEvent event) {
393: int priority = event.getPriority();
394: if (priority > msgOutputLevel) {
395: return;
396: }
397: Element messageElement = doc.createElement(MESSAGE_TAG);
398:
399: String name = "debug";
400: switch (event.getPriority()) {
401: case Project.MSG_ERR:
402: name = "error";
403: break;
404: case Project.MSG_WARN:
405: name = "warn";
406: break;
407: case Project.MSG_INFO:
408: name = "info";
409: break;
410: default:
411: name = "debug";
412: break;
413: }
414: messageElement.setAttribute(PRIORITY_ATTR, name);
415:
416: Throwable ex = event.getException();
417: if (Project.MSG_DEBUG <= msgOutputLevel && ex != null) {
418: Text errText = doc.createCDATASection(StringUtils
419: .getStackTrace(ex));
420: Element stacktrace = doc.createElement(STACKTRACE_TAG);
421: stacktrace.appendChild(errText);
422: buildElement.element.appendChild(stacktrace);
423: }
424: Text messageText = doc.createCDATASection(event.getMessage());
425: messageElement.appendChild(messageText);
426:
427: TimedElement parentElement = null;
428:
429: Task task = event.getTask();
430:
431: Target target = event.getTarget();
432: if (task != null) {
433: parentElement = getTaskElement(task);
434: }
435: if (parentElement == null && target != null) {
436: parentElement = (TimedElement) targets.get(target);
437: }
438:
439: /*
440: if (parentElement == null) {
441: Stack threadStack
442: = (Stack) threadStacks.get(Thread.currentThread());
443: if (threadStack != null) {
444: if (!threadStack.empty()) {
445: parentElement = (TimedElement) threadStack.peek();
446: }
447: }
448: }
449: */
450:
451: if (parentElement != null) {
452: parentElement.element.appendChild(messageElement);
453: } else {
454: buildElement.element.appendChild(messageElement);
455: }
456: }
457:
458: // -------------------------------------------------- BuildLogger interface
459:
460: /**
461: * Set the logging level when using this as a Logger
462: *
463: * @param level the logging level -
464: * see {@link org.apache.tools.ant.Project#MSG_ERR Project}
465: * class for level definitions
466: */
467: public void setMessageOutputLevel(int level) {
468: msgOutputLevel = level;
469: }
470:
471: /**
472: * Set the output stream to which logging output is sent when operating
473: * as a logger.
474: *
475: * @param output the output PrintStream.
476: */
477: public void setOutputPrintStream(PrintStream output) {
478: this .outStream = new PrintStream(output, true);
479: }
480:
481: /**
482: * Ignore emacs mode, as it has no meaning in XML format
483: *
484: * @param emacsMode true if logger should produce emacs compatible
485: * output
486: */
487: public void setEmacsMode(boolean emacsMode) {
488: }
489:
490: /**
491: * Ignore error print stream. All output will be written to
492: * either the XML log file or the PrintStream provided to
493: * setOutputPrintStream
494: *
495: * @param err the stream we are going to ignore.
496: */
497: public void setErrorPrintStream(PrintStream err) {
498: }
499:
500: }
|