/*******************************************************************************
* Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/
package org.jacoco.report.internal.xml;
import static java.lang.String.format;
import java.io.IOException;
import java.io.Writer;
/**
* Simple API to create well formed XML streams. A {@link XMLElement} instance
* represents a single element in a XML document.
*
* @see XMLDocument
*/
public class XMLElement {
private static final char SPACE = ' ';
private static final char EQ = '=';
private static final char LT = '<';
private static final char GT = '>';
private static final char QUOT = '"';
private static final char AMP = '&';
private static final char SLASH = '/';
/** Writer for content output */
protected final Writer writer;
private final String name;
private boolean openTagDone;
private boolean closed;
private XMLElement lastchild;
/**
* Creates a new element for a XML document.
*
* @param writer
* all output will be written directly to this
* @param name
* element name
*/
protected XMLElement(final Writer writer, final String name) {
this.writer = writer;
this.name = name;
this.openTagDone = false;
this.closed = false;
this.lastchild = null;
}
/**
* Emits the beginning of the open tag. This method has to be called before
* other other methods are called on this element.
*
* @throws IOException
* in case of problems with the writer
*/
protected void beginOpenTag() throws IOException {
writer.write(LT);
writer.write(name);
}
private void finishOpenTag() throws IOException {
if (!openTagDone) {
writer.append(GT);
openTagDone = true;
}
}
/**
* Adds the given child to this element. This will close all previous child
* elements.
*
* @param child
* child element to add
* @throws IOException
* in case of invalid nesting or problems with the writer
*/
protected void addChildElement(final XMLElement child) throws IOException {
if (closed) {
throw new IOException(format("Element %s already closed.", name));
}
finishOpenTag();
if (lastchild != null) {
lastchild.close();
}
child.beginOpenTag();
lastchild = child;
}
private void quote(final String text) throws IOException {
final int len = text.length();
for (int i = 0; i < len; i++) {
final char c = text.charAt(i);
switch (c) {
case LT:
writer.write("<");
break;
case GT:
writer.write(">");
break;
case QUOT:
writer.write(""");
break;
case AMP:
writer.write("&");
break;
default:
writer.write(c);
break;
}
}
}
/**
* Adds an attribute to this element. May only be called before an child
* element is added or this element has been closed. The attribute value
* will be quoted. If the value is <code>null</code> the attribute will not
* be added.
*
* @param name
* attribute name
* @param value
* attribute value or <code>null</code>
*
* @return this element
* @throws IOException
* in case of problems with the writer
*/
public XMLElement attr(final String name, final String value)
throws IOException {
if (value == null) {
return this;
}
if (closed || openTagDone) {
throw new IOException(format("Element %s already closed.",
this.name));
}
writer.write(SPACE);
writer.write(name);
writer.write(EQ);
writer.write(QUOT);
quote(value);
writer.write(QUOT);
return this;
}
/**
* Adds an attribute to this element. May only be called before an child
* element is added or this element has been closed. The attribute value is
* the decimal representation of the given int value.
*
* @param name
* attribute name
* @param value
* attribute value
*
* @return this element
* @throws IOException
* in case of problems with the writer
*/
public XMLElement attr(final String name, final int value)
throws IOException {
return attr(name, String.valueOf(value));
}
/**
* Adds an attribute to this element. May only be called before an child
* element is added or this element has been closed. The attribute value is
* the decimal representation of the given long value.
*
* @param name
* attribute name
* @param value
* attribute value
*
* @return this element
* @throws IOException
* in case of problems with the writer
*/
public XMLElement attr(final String name, final long value)
throws IOException {
return attr(name, String.valueOf(value));
}
/**
* Adds the given text as a child to this node. The text will be quoted.
*
* @param text
* text to add
* @return this element
* @throws IOException
* in case of problems with the writer
*/
public XMLElement text(final String text) throws IOException {
if (closed) {
throw new IOException(format("Element %s already closed.", name));
}
finishOpenTag();
if (lastchild != null) {
lastchild.close();
}
quote(text);
return this;
}
/**
* Creates a new child element for this element,
*
* @param name
* name of the child element
* @return child element instance
* @throws IOException
* in case of problems with the writer
*/
public XMLElement element(final String name) throws IOException {
final XMLElement element = new XMLElement(writer, name);
addChildElement(element);
return element;
}
/**
* Closes this element if it has not been closed before.
*
* @throws IOException
* in case of problems with the writer
*/
public void close() throws IOException {
if (!closed) {
if (lastchild != null) {
lastchild.close();
}
if (openTagDone) {
writer.write(LT);
writer.write(SLASH);
writer.write(name);
} else {
writer.write(SLASH);
}
writer.write(GT);
closed = true;
openTagDone = true;
}
}
}