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