XMLElement.java revision 398ee59bebad6835dab57b60157eff16d511709e
1/*******************************************************************************
2 * Copyright (c) 2009, 2015 Mountainminds GmbH & Co. KG and Contributors
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *    Marc R. Hoffmann - initial API and implementation
10 *
11 *******************************************************************************/
12package org.jacoco.report.internal.xml;
13
14import static java.lang.String.format;
15
16import java.io.IOException;
17import java.io.Writer;
18
19/**
20 * Simple API to create well formed XML streams. A {@link XMLElement} instance
21 * represents a single element in a XML document.
22 *
23 * @see XMLDocument
24 */
25public class XMLElement {
26
27	private static final char SPACE = ' ';
28
29	private static final char EQ = '=';
30
31	private static final char LT = '<';
32
33	private static final char GT = '>';
34
35	private static final char QUOT = '"';
36
37	private static final char AMP = '&';
38
39	private static final char SLASH = '/';
40
41	/** Writer for content output */
42	protected final Writer writer;
43
44	private final String name;
45
46	private boolean openTagDone;
47
48	private boolean closed;
49
50	private XMLElement lastchild;
51
52	/**
53	 * Creates a new element for a XML document.
54	 *
55	 * @param writer
56	 *            all output will be written directly to this
57	 * @param name
58	 *            element name
59	 */
60	protected XMLElement(final Writer writer, final String name) {
61		this.writer = writer;
62		this.name = name;
63		this.openTagDone = false;
64		this.closed = false;
65		this.lastchild = null;
66	}
67
68	/**
69	 * Emits the beginning of the open tag. This method has to be called before
70	 * other other methods are called on this element.
71	 *
72	 * @throws IOException
73	 *             in case of problems with the writer
74	 */
75	protected void beginOpenTag() throws IOException {
76		writer.write(LT);
77		writer.write(name);
78	}
79
80	private void finishOpenTag() throws IOException {
81		if (!openTagDone) {
82			writer.append(GT);
83			openTagDone = true;
84		}
85	}
86
87	/**
88	 * Adds the given child to this element. This will close all previous child
89	 * elements.
90	 *
91	 * @param child
92	 *            child element to add
93	 * @throws IOException
94	 *             in case of invalid nesting or problems with the writer
95	 */
96	protected void addChildElement(final XMLElement child) throws IOException {
97		if (closed) {
98			throw new IOException(format("Element %s already closed.", name));
99		}
100		finishOpenTag();
101		if (lastchild != null) {
102			lastchild.close();
103		}
104		child.beginOpenTag();
105		lastchild = child;
106	}
107
108	private void quote(final String text) throws IOException {
109		final int len = text.length();
110		for (int i = 0; i < len; i++) {
111			final char c = text.charAt(i);
112			switch (c) {
113			case LT:
114				writer.write("&lt;");
115				break;
116			case GT:
117				writer.write("&gt;");
118				break;
119			case QUOT:
120				writer.write("&quot;");
121				break;
122			case AMP:
123				writer.write("&amp;");
124				break;
125			default:
126				writer.write(c);
127				break;
128			}
129		}
130	}
131
132	/**
133	 * Adds an attribute to this element. May only be called before an child
134	 * element is added or this element has been closed. The attribute value
135	 * will be quoted. If the value is <code>null</code> the attribute will not
136	 * be added.
137	 *
138	 * @param name
139	 *            attribute name
140	 * @param value
141	 *            attribute value or <code>null</code>
142	 *
143	 * @return this element
144	 * @throws IOException
145	 *             in case of problems with the writer
146	 */
147	public XMLElement attr(final String name, final String value)
148			throws IOException {
149		if (value == null) {
150			return this;
151		}
152		if (closed || openTagDone) {
153			throw new IOException(format("Element %s already closed.",
154					this.name));
155		}
156		writer.write(SPACE);
157		writer.write(name);
158		writer.write(EQ);
159		writer.write(QUOT);
160		quote(value);
161		writer.write(QUOT);
162		return this;
163	}
164
165	/**
166	 * Adds an attribute to this element. May only be called before an child
167	 * element is added or this element has been closed. The attribute value is
168	 * the decimal representation of the given int value.
169	 *
170	 * @param name
171	 *            attribute name
172	 * @param value
173	 *            attribute value
174	 *
175	 * @return this element
176	 * @throws IOException
177	 *             in case of problems with the writer
178	 */
179	public XMLElement attr(final String name, final int value)
180			throws IOException {
181		return attr(name, String.valueOf(value));
182	}
183
184	/**
185	 * Adds an attribute to this element. May only be called before an child
186	 * element is added or this element has been closed. The attribute value is
187	 * the decimal representation of the given long value.
188	 *
189	 * @param name
190	 *            attribute name
191	 * @param value
192	 *            attribute value
193	 *
194	 * @return this element
195	 * @throws IOException
196	 *             in case of problems with the writer
197	 */
198	public XMLElement attr(final String name, final long value)
199			throws IOException {
200		return attr(name, String.valueOf(value));
201	}
202
203	/**
204	 * Adds the given text as a child to this node. The text will be quoted.
205	 *
206	 * @param text
207	 *            text to add
208	 * @return this element
209	 * @throws IOException
210	 *             in case of problems with the writer
211	 */
212	public XMLElement text(final String text) throws IOException {
213		if (closed) {
214			throw new IOException(format("Element %s already closed.", name));
215		}
216		finishOpenTag();
217		if (lastchild != null) {
218			lastchild.close();
219		}
220		quote(text);
221		return this;
222	}
223
224	/**
225	 * Creates a new child element for this element,
226	 *
227	 * @param name
228	 *            name of the child element
229	 * @return child element instance
230	 * @throws IOException
231	 *             in case of problems with the writer
232	 */
233	public XMLElement element(final String name) throws IOException {
234		final XMLElement element = new XMLElement(writer, name);
235		addChildElement(element);
236		return element;
237	}
238
239	/**
240	 * Closes this element if it has not been closed before.
241	 *
242	 * @throws IOException
243	 *             in case of problems with the writer
244	 */
245	public void close() throws IOException {
246		if (!closed) {
247			if (lastchild != null) {
248				lastchild.close();
249			}
250			if (openTagDone) {
251				writer.write(LT);
252				writer.write(SLASH);
253				writer.write(name);
254			} else {
255				writer.write(SLASH);
256			}
257			writer.write(GT);
258			closed = true;
259			openTagDone = true;
260		}
261	}
262
263}
264