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