/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Enumeration; import java.util.Hashtable; import java.util.Stack; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.tools.ant.util.DOMElementWriter; import org.apache.tools.ant.util.StringUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.Text; /** * Generates a file in the current directory with * an XML description of what happened during a build. * The default filename is "log.xml", but this can be overridden * with the property <code>XmlLogger.file</code>. * * This implementation assumes in its sanity checking that only one * thread runs a particular target/task at a time. This is enforced * by the way that parallel builds and antcalls are done - and * indeed all but the simplest of tasks could run into problems * if executed in parallel. * * @see Project#addBuildListener(BuildListener) */ public class XmlLogger implements BuildLogger { private int msgOutputLevel = Project.MSG_DEBUG; private PrintStream outStream; /** DocumentBuilder to use when creating the document to start with. */ private static DocumentBuilder builder = getDocumentBuilder(); /** * Returns a default DocumentBuilder instance or throws an * ExceptionInInitializerError if it can't be created. * * @return a default DocumentBuilder instance. */ private static DocumentBuilder getDocumentBuilder() { try { return DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (Exception exc) { throw new ExceptionInInitializerError(exc); } } /** XML element name for a build. */ private static final String BUILD_TAG = "build"; /** XML element name for a target. */ private static final String TARGET_TAG = "target"; /** XML element name for a task. */ private static final String TASK_TAG = "task"; /** XML element name for a message. */ private static final String MESSAGE_TAG = "message"; /** XML attribute name for a name. */ private static final String NAME_ATTR = "name"; /** XML attribute name for a time. */ private static final String TIME_ATTR = "time"; /** XML attribute name for a message priority. */ private static final String PRIORITY_ATTR = "priority"; /** XML attribute name for a file location. */ private static final String LOCATION_ATTR = "location"; /** XML attribute name for an error description. */ private static final String ERROR_ATTR = "error"; /** XML element name for a stack trace. */ private static final String STACKTRACE_TAG = "stacktrace"; /** The complete log document for this build. */ private Document doc = builder.newDocument(); /** Mapping for when tasks started (Task to TimedElement). */ private Hashtable<Task, TimedElement> tasks = new Hashtable<>(); /** Mapping for when targets started (Target to TimedElement). */ private Hashtable<Target, TimedElement> targets = new Hashtable<>(); /** * Mapping of threads to stacks of elements * (Thread to Stack of TimedElement). */ private Hashtable<Thread, Stack<TimedElement>> threadStacks = new Hashtable<>(); /** * When the build started. */ private TimedElement buildElement = null; /** Utility class representing the time an element started. */ private static class TimedElement { /** * Start time in milliseconds * (as returned by <code>System.currentTimeMillis()</code>). */ private long startTime; /** Element created at the start time. */ private Element element; @Override public String toString() { return element.getTagName() + ":" + element.getAttribute("name"); } } /** * Fired when the build starts, this builds the top-level element for the * document and remembers the time of the start of the build. * * @param event Ignored. */ @Override public void buildStarted(BuildEvent event) { buildElement = new TimedElement(); buildElement.startTime = System.currentTimeMillis(); buildElement.element = doc.createElement(BUILD_TAG); } /** * Fired when the build finishes, this adds the time taken and any * error stacktrace to the build element and writes the document to disk. * * @param event An event with any relevant extra information. * Will not be <code>null</code>. */ @Override public void buildFinished(BuildEvent event) { long totalTime = System.currentTimeMillis() - buildElement.startTime; buildElement.element.setAttribute(TIME_ATTR, DefaultLogger.formatTime(totalTime)); if (event.getException() != null) { buildElement.element.setAttribute(ERROR_ATTR, event.getException().toString()); // print the stacktrace in the build file it is always useful... // better have too much info than not enough. Throwable t = event.getException(); Text errText = doc.createCDATASection(StringUtils.getStackTrace(t)); Element stacktrace = doc.createElement(STACKTRACE_TAG); stacktrace.appendChild(errText); synchronizedAppend(buildElement.element, stacktrace); } String outFilename = getProperty(event, "XmlLogger.file", "log.xml"); String xslUri = getProperty(event, "ant.XmlLogger.stylesheet.uri", "log.xsl"); try (OutputStream stream = outStream == null ? Files.newOutputStream(Paths.get(outFilename)) : outStream; Writer out = new OutputStreamWriter(stream, "UTF8")) { out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); if (xslUri.length() > 0) { out.write("<?xml-stylesheet type=\"text/xsl\" href=\"" + xslUri + "\"?>\n\n"); } new DOMElementWriter().write(buildElement.element, out, 0, "\t"); out.flush(); } catch (IOException exc) { throw new BuildException("Unable to write log file", exc); } buildElement = null; } private String getProperty(BuildEvent event, String propertyName, String defaultValue) { String rv = defaultValue; if (event != null && event.getProject() != null && event.getProject().getProperty(propertyName) != null) { rv = event.getProject().getProperty(propertyName); } return rv; } /** * Returns the stack of timed elements for the current thread. * @return the stack of timed elements for the current thread */ private Stack<TimedElement> getStack() { Stack<TimedElement> threadStack = threadStacks.get(Thread.currentThread()); if (threadStack == null) { threadStack = new Stack<>(); threadStacks.put(Thread.currentThread(), threadStack); } /* For debugging purposes uncomment: org.w3c.dom.Comment s = doc.createComment("stack=" + threadStack); buildElement.element.appendChild(s); */ return threadStack; } /** * Fired when a target starts building, this pushes a timed element * for the target onto the stack of elements for the current thread, * remembering the current time and the name of the target. * * @param event An event with any relevant extra information. * Will not be <code>null</code>. */ @Override public void targetStarted(BuildEvent event) { Target target = event.getTarget(); TimedElement targetElement = new TimedElement(); targetElement.startTime = System.currentTimeMillis(); targetElement.element = doc.createElement(TARGET_TAG); targetElement.element.setAttribute(NAME_ATTR, target.getName()); targets.put(target, targetElement); getStack().push(targetElement); } /** * Fired when a target finishes building, this adds the time taken * and any error stacktrace to the appropriate target element in the log. * * @param event An event with any relevant extra information. * Will not be <code>null</code>. */ @Override public void targetFinished(BuildEvent event) { Target target = event.getTarget(); TimedElement targetElement = targets.get(target); if (targetElement != null) { long totalTime = System.currentTimeMillis() - targetElement.startTime; targetElement.element.setAttribute(TIME_ATTR, DefaultLogger.formatTime(totalTime)); TimedElement parentElement = null; Stack<TimedElement> threadStack = getStack(); if (!threadStack.empty()) { TimedElement poppedStack = threadStack.pop(); if (poppedStack != targetElement) { throw new RuntimeException("Mismatch - popped element = " + poppedStack //NOSONAR + " finished target element = " + targetElement); } if (!threadStack.empty()) { parentElement = threadStack.peek(); } } if (parentElement == null) { synchronizedAppend(buildElement.element, targetElement.element); } else { synchronizedAppend(parentElement.element, targetElement.element); } } targets.remove(target); } /** * Fired when a task starts building, this pushes a timed element * for the task onto the stack of elements for the current thread, * remembering the current time and the name of the task. * * @param event An event with any relevant extra information. * Will not be <code>null</code>. */ @Override public void taskStarted(BuildEvent event) { TimedElement taskElement = new TimedElement(); taskElement.startTime = System.currentTimeMillis(); taskElement.element = doc.createElement(TASK_TAG); Task task = event.getTask(); String name = event.getTask().getTaskName(); if (name == null) { name = ""; } taskElement.element.setAttribute(NAME_ATTR, name); taskElement.element.setAttribute(LOCATION_ATTR, event.getTask().getLocation().toString()); tasks.put(task, taskElement); getStack().push(taskElement); } /** * Fired when a task finishes building, this adds the time taken * and any error stacktrace to the appropriate task element in the log. * * @param event An event with any relevant extra information. * Will not be <code>null</code>. */ @Override public void taskFinished(BuildEvent event) { Task task = event.getTask(); TimedElement taskElement = tasks.get(task); if (taskElement == null) { throw new RuntimeException("Unknown task " + task + " not in " + tasks); //NOSONAR } long totalTime = System.currentTimeMillis() - taskElement.startTime; taskElement.element.setAttribute(TIME_ATTR, DefaultLogger.formatTime(totalTime)); Target target = task.getOwningTarget(); TimedElement targetElement = null; if (target != null) { targetElement = targets.get(target); } if (targetElement == null) { synchronizedAppend(buildElement.element, taskElement.element); } else { synchronizedAppend(targetElement.element, taskElement.element); } Stack<TimedElement> threadStack = getStack(); if (!threadStack.empty()) { TimedElement poppedStack = threadStack.pop(); if (poppedStack != taskElement) { throw new RuntimeException("Mismatch - popped element = " + poppedStack //NOSONAR + " finished task element = " + taskElement); } } tasks.remove(task); } /** * Get the TimedElement associated with a task. * * Where the task is not found directly, search for unknown elements which * may be hiding the real task */ private TimedElement getTaskElement(Task task) { TimedElement element = tasks.get(task); if (element != null) { return element; } for (Enumeration<Task> e = tasks.keys(); e.hasMoreElements();) { Task key = e.nextElement(); if (key instanceof UnknownElement && ((UnknownElement) key).getTask() == task) { return tasks.get(key); } } return null; } /** * Fired when a message is logged, this adds a message element to the * most appropriate parent element (task, target or build) and records * the priority and text of the message. * * @param event An event with any relevant extra information. * Will not be <code>null</code>. */ @Override public void messageLogged(BuildEvent event) { int priority = event.getPriority(); if (priority > msgOutputLevel) { return; } Element messageElement = doc.createElement(MESSAGE_TAG); String name; switch (priority) { case Project.MSG_ERR: name = "error"; break; case Project.MSG_WARN: name = "warn"; break; case Project.MSG_INFO: name = "info"; break; default: name = "debug"; break; } messageElement.setAttribute(PRIORITY_ATTR, name); Throwable ex = event.getException(); if (Project.MSG_DEBUG <= msgOutputLevel && ex != null) { Text errText = doc.createCDATASection(StringUtils.getStackTrace(ex)); Element stacktrace = doc.createElement(STACKTRACE_TAG); stacktrace.appendChild(errText); synchronizedAppend(buildElement.element, stacktrace); } Text messageText = doc.createCDATASection(event.getMessage()); messageElement.appendChild(messageText); TimedElement parentElement = null; Task task = event.getTask(); Target target = event.getTarget(); if (task != null) { parentElement = getTaskElement(task); } if (parentElement == null && target != null) { parentElement = targets.get(target); } if (parentElement != null) { synchronizedAppend(parentElement.element, messageElement); } else { synchronizedAppend(buildElement.element, messageElement); } } // -------------------------------------------------- BuildLogger interface /** * Set the logging level when using this as a Logger * * @param level the logging level - * see {@link org.apache.tools.ant.Project#MSG_ERR Project} * class for level definitions */ @Override public void setMessageOutputLevel(int level) { msgOutputLevel = level; } /** * Set the output stream to which logging output is sent when operating * as a logger. * * @param output the output PrintStream. */ @Override public void setOutputPrintStream(PrintStream output) { this.outStream = new PrintStream(output, true); } /** * Ignore emacs mode, as it has no meaning in XML format * * @param emacsMode true if logger should produce emacs compatible * output */ @Override public void setEmacsMode(boolean emacsMode) { } /** * Ignore error print stream. All output will be written to * either the XML log file or the PrintStream provided to * setOutputPrintStream * * @param err the stream we are going to ignore. */ @Override public void setErrorPrintStream(PrintStream err) { } private void synchronizedAppend(Node parent, Node child) { synchronized(parent) { parent.appendChild(child); } } }