/*
 *  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
 *
 *      https://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.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Hashtable;
import java.util.Map;
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 XmlLogger.file. 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 Also:
  • addBuildListener.addBuildListener(BuildListener)
/** * 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.
/** 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.
Returns:a default DocumentBuilder instance.
/** * 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.
/** XML element name for a build. */
private static final String BUILD_TAG = "build";
XML element name for a target.
/** XML element name for a target. */
private static final String TARGET_TAG = "target";
XML element name for a task.
/** XML element name for a task. */
private static final String TASK_TAG = "task";
XML element name for a message.
/** XML element name for a message. */
private static final String MESSAGE_TAG = "message";
XML attribute name for a name.
/** XML attribute name for a name. */
private static final String NAME_ATTR = "name";
XML attribute name for a time.
/** XML attribute name for a time. */
private static final String TIME_ATTR = "time";
XML attribute name for a message priority.
/** XML attribute name for a message priority. */
private static final String PRIORITY_ATTR = "priority";
XML attribute name for a file location.
/** XML attribute name for a file location. */
private static final String LOCATION_ATTR = "location";
XML attribute name for an error description.
/** XML attribute name for an error description. */
private static final String ERROR_ATTR = "error";
XML element name for a stack trace.
/** XML element name for a stack trace. */
private static final String STACKTRACE_TAG = "stacktrace";
The complete log document for this build.
/** The complete log document for this build. */
private Document doc = builder.newDocument();
Mapping for when tasks started (Task to TimedElement).
/** Mapping for when tasks started (Task to TimedElement). */
private Map<Task, TimedElement> tasks = new Hashtable<>();
Mapping for when targets started (Target to TimedElement).
/** Mapping for when targets started (Target to TimedElement). */
private Map<Target, TimedElement> targets = new Hashtable<>();
Mapping of threads to stacks of elements (Thread to Stack of TimedElement).
/** * Mapping of threads to stacks of elements * (Thread to Stack of TimedElement). */
private Map<Thread, Stack<TimedElement>> threadStacks = new Hashtable<>();
When the build started.
/** * When the build started. */
private TimedElement buildElement = null;
Utility class representing the time an element started.
/** Utility class representing the time an element started. */
private static class TimedElement {
Start time in milliseconds (as returned by System.currentTimeMillis()).
/** * Start time in milliseconds * (as returned by <code>System.currentTimeMillis()</code>). */
private long startTime;
Element created at the start time.
/** 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.
Params:
  • event – Ignored.
/** * 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.
Params:
  • event – An event with any relevant extra information. Will not be null.
/** * 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, StandardCharsets.UTF_8)) { out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); if (!xslUri.isEmpty()) { 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.
Returns:the stack of timed elements for the current thread
/** * Returns the stack of timed elements for the current thread. * @return the stack of timed elements for the current thread */
private Stack<TimedElement> getStack() { /* For debugging purposes uncomment: if (threadStacks.containsKey(Thread.currentThread())) { org.w3c.dom.Comment s = doc.createComment("stack=" + threadStacks(Thread.currentThread())); buildElement.element.appendChild(s); } */ return threadStacks.computeIfAbsent(Thread.currentThread(), k -> new Stack<>()); }
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.
Params:
  • event – An event with any relevant extra information. Will not be null.
/** * 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.
Params:
  • event – An event with any relevant extra information. Will not be null.
/** * 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.
Params:
  • event – An event with any relevant extra information. Will not be null.
/** * 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.
Params:
  • event – An event with any relevant extra information. Will not be null.
/** * 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
/** * 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; } return tasks.keySet().stream().filter(UnknownElement.class::isInstance) .filter(key -> ((UnknownElement) key).getTask() == task).findFirst() .map(key -> tasks.get(key)).orElse(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.
Params:
  • event – An event with any relevant extra information. Will not be 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
Params:
  • level – the logging level - see Project class for level definitions
/** * 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.
Params:
  • output – the output PrintStream.
/** * 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
Params:
  • emacsMode – true if logger should produce emacs compatible output
/** * 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
Params:
  • err – the stream we are going to ignore.
/** * 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); } } }