001 /*--------------------------------------------------------------------------+
002 $Id: XMLWriter.java 26963 2010-03-17 22:28:50Z hummelb $
003 | |
004 | Copyright 2005-2010 Technische Universitaet Muenchen |
005 | |
006 | Licensed under the Apache License, Version 2.0 (the "License"); |
007 | you may not use this file except in compliance with the License. |
008 | You may obtain a copy of the License at |
009 | |
010 | http://www.apache.org/licenses/LICENSE-2.0 |
011 | |
012 | Unless required by applicable law or agreed to in writing, software |
013 | distributed under the License is distributed on an "AS IS" BASIS, |
014 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
015 | See the License for the specific language governing permissions and |
016 | limitations under the License. |
017 +--------------------------------------------------------------------------*/
018 package edu.tum.cs.commons.xml;
019
020 import static edu.tum.cs.commons.string.StringUtils.CR;
021 import static edu.tum.cs.commons.string.StringUtils.SPACE;
022
023 import java.io.OutputStream;
024 import java.io.OutputStreamWriter;
025 import java.io.PrintWriter;
026 import java.io.UnsupportedEncodingException;
027 import java.util.EmptyStackException;
028 import java.util.HashSet;
029 import java.util.Stack;
030
031 import edu.tum.cs.commons.filesystem.FileSystemUtils;
032 import edu.tum.cs.commons.string.StringUtils;
033
034 /**
035 * Utility class for creating XML documents. Please consult test case
036 * {@link XMLWriterTest} to see how this class is intended to be used.
037 *
038 * @author Florian Deissenboeck
039 * @author $Author: hummelb $
040 * @version $Rev: 26963 $
041 * @levd.rating GREEN Hash: E346D576888FEF33DA193173CEE8337B
042 */
043 public class XMLWriter<E extends Enum<E>, A extends Enum<A>> {
044
045 /** This enunmeration describes the states of the writer. */
046 private enum EState {
047
048 /** Indicates that we are at the beginning of the document. */
049 DOCUMENT_START,
050
051 /** Started a tag but did not close it yet. */
052 INSIDE_TAG,
053
054 /** Inside a text element. */
055 INSIDE_TEXT,
056
057 /** Indicates that we are between to tags but not within a text element. */
058 OUTSIDE_TAG
059 }
060
061 /** XML comment end symbol. */
062 private final static String COMMENT_END = " -->";
063
064 /** XML comment start symbol. */
065 private final static String COMMENT_START = "<!-- ";
066
067 /** Right angle bracket */
068 private final static String GT = ">";
069
070 /** Left angle bracket */
071 private final static String LT = "<";
072
073 /** Resolver used by the writer. */
074 protected final IXMLResolver<E, A> xmlResolver;
075
076 /**
077 * This set maintains the attributes added to an element to detect duplicate
078 * attributes.
079 * <p>
080 * We can not use {@link java.util.EnumSet} here as we would need a
081 * reference to the defining class.
082 */
083 private final HashSet<A> currentAttributes = new HashSet<A>();
084
085 /** The current nesting depth is used to calculate the ident. */
086 private int currentNestingDepth = -1;
087
088 /**
089 * This stack maintains the elements in order of creation. It is used to
090 * check if elements are closed in the correct order.
091 */
092 private final Stack<E> elementStack = new Stack<E>();
093
094 /** The current state of the writer. */
095 private EState state = EState.DOCUMENT_START;
096
097 /** The writer to write to. */
098 private final PrintWriter writer;
099
100 /** This flag indicates if line breaks should be generated or not. */
101 private boolean suppressLineBreaks = false;
102
103 /**
104 * Create a new writer.
105 *
106 * @param stream
107 * the stream to write to.
108 * @param xmlResolver
109 * resolvers used by this writer
110 */
111 public XMLWriter(OutputStream stream, IXMLResolver<E, A> xmlResolver) {
112 try {
113 this.writer = new PrintWriter(new OutputStreamWriter(stream,
114 FileSystemUtils.UTF8_ENCODING));
115 } catch (UnsupportedEncodingException e) {
116 throw new AssertionError("UTF-8 should always be supported!");
117 }
118 this.xmlResolver = xmlResolver;
119 }
120
121 /**
122 * Create a new writer.
123 *
124 * @param writer
125 * the writer to write to.
126 * @param xmlResolver
127 * resolvers used by this writer
128 */
129 public XMLWriter(PrintWriter writer, IXMLResolver<E, A> xmlResolver) {
130 this.writer = writer;
131 this.xmlResolver = xmlResolver;
132 }
133
134 /**
135 * Toogle line break behavior. If set to <code>true</code> the writer does
136 * not write line breaks. If set to <code>false</code> (default) line breaks
137 * are written.
138 * <p>
139 * This can, for example, be used for HTML where line breaks sometimes
140 * change the layout.
141 */
142 public void setSuppressLineBreaks(boolean supressLineBreaks) {
143 this.suppressLineBreaks = supressLineBreaks;
144 }
145
146 /**
147 * Add an XML header.
148 *
149 * @param version
150 * version string
151 * @param encoding
152 * encoding definition
153 */
154 public void addHeader(String version, String encoding) {
155 if (state != EState.DOCUMENT_START) {
156 throw new XMLWriterException(
157 "Can be called at the beginning of a document only.",
158 EXMLWriterExceptionType.HEADER_WITHIN_DOCUMENT);
159 }
160 print(LT);
161 print("?xml version=\"");
162 print(version);
163 print("\" encoding=\"");
164 print(encoding);
165 print("\"?");
166 print(GT);
167
168 state = EState.OUTSIDE_TAG;
169 }
170
171 /**
172 * Add public document type definiton
173 *
174 * @param rootElement
175 * root element
176 * @param publicId
177 * public id
178 * @param systemId
179 * sytem id
180 */
181 public void addPublicDocTypeDefintion(E rootElement, String publicId,
182 String systemId) {
183 print(LT);
184 print("!DOCTYPE ");
185 print(xmlResolver.resolveElementName(rootElement));
186 print(" PUBLIC \"");
187 print(publicId);
188 print("\" \"");
189 print(systemId);
190 print("\"");
191 print(GT);
192
193 state = EState.OUTSIDE_TAG;
194 }
195
196 /**
197 * Start a new element
198 *
199 * @param element
200 * the element to start.
201 */
202 public void openElement(E element) {
203 if (state == EState.INSIDE_TAG) {
204 println(GT);
205 } else if (state == EState.OUTSIDE_TAG) {
206 println();
207 }
208
209 currentNestingDepth++;
210 if (state != EState.INSIDE_TEXT) {
211 printIndent();
212 }
213 print(LT);
214 print(xmlResolver.resolveElementName(element));
215
216 state = EState.INSIDE_TAG;
217 elementStack.push(element);
218 currentAttributes.clear();
219 }
220
221 /**
222 * Add a attribute. This only works if a element was started but no other
223 * elements were added yet.
224 *
225 * @param attribute
226 * the attribute to create
227 * @param value
228 * its value
229 * @throws XMLWriterException
230 * if there's no element to add attributes to (
231 * {@link EXMLWriterExceptionType#ATTRIBUTE_OUTSIDE_ELEMENT}) or
232 * if an attribute is added twice (
233 * {@link EXMLWriterExceptionType#DUPLICATE_ATTRIBUTE}).
234 */
235 public void addAttribute(A attribute, Object value) {
236 if (state != EState.INSIDE_TAG) {
237 throw new XMLWriterException("Must be called for an open element.",
238 EXMLWriterExceptionType.ATTRIBUTE_OUTSIDE_ELEMENT);
239 }
240
241 if (currentAttributes.contains(attribute)) {
242 throw new XMLWriterException("Duplicate attribute.",
243 EXMLWriterExceptionType.DUPLICATE_ATTRIBUTE);
244 }
245
246 print(SPACE);
247 print(xmlResolver.resolveAttributeName(attribute));
248 print("=");
249 print("\"");
250 print(escape(value.toString()));
251 print("\"");
252
253 currentAttributes.add(attribute);
254 }
255
256 /**
257 * Convenience method for adding an element together with (some of) its
258 * attributes.
259 *
260 * @param element
261 * The element to be opened (using {@link #openElement(Enum)}).
262 * @param attributes
263 * the attributes to be added. The number of arguments must be
264 * even, where the first, third, etc. argument is an attribute
265 * enum.
266 */
267 public void openElement(E element, Object... attributes) {
268 if (attributes.length % 2 != 0) {
269 throw new XMLWriterException(
270 "Expected an even number of arguments!",
271 EXMLWriterExceptionType.ODD_NUMBER_OF_ARGUMENTS);
272 }
273 for (int i = 0; i < attributes.length; i += 2) {
274 if (!xmlResolver.getAttributeClass().isAssignableFrom(
275 attributes[i].getClass())) {
276 throw new XMLWriterException("Attribute name (index " + i
277 + ") must be of type "
278 + xmlResolver.getAttributeClass().getName(),
279 EXMLWriterExceptionType.ILLEGAL_ATTRIBUTE_TYPE);
280 }
281 }
282 openElement(element);
283 for (int i = 0; i < attributes.length; i += 2) {
284 // this is ok as we checked it above
285 @SuppressWarnings("unchecked")
286 A a = (A) attributes[i];
287 addAttribute(a, attributes[i + 1]);
288 }
289 }
290
291 /**
292 * Convenience method for adding an element together with (some of) its
293 * attributes. This is the same as {@link #openElement(Enum, Object[])}, but
294 * also closes the element.
295 */
296 public void addClosedElement(E element, Object... attributes) {
297 openElement(element, attributes);
298 closeElement(element);
299 }
300
301 /**
302 * Convenience method for adding an element together with (some of) its
303 * attributes and text inbetween. This is the same as
304 * {@link #openElement(Enum, Object[])}, but then adds the provided text and
305 * closes the element.
306 */
307 public void addClosedTextElement(E element, String text,
308 Object... attributes) {
309 openElement(element, attributes);
310 addText(text);
311 closeElement(element);
312 }
313
314 /**
315 * Close an element.
316 *
317 * @param element
318 * the element to close.
319 * @throws XMLWriterException
320 * on attempt to close the wrong element (
321 * {@link EXMLWriterExceptionType#UNCLOSED_ELEMENT}).
322 */
323 public void closeElement(E element) {
324 if (element != elementStack.peek()) {
325 throw new XMLWriterException("Must close element "
326 + elementStack.peek() + " first.",
327 EXMLWriterExceptionType.UNCLOSED_ELEMENT);
328 }
329
330 if (state == EState.INSIDE_TAG) {
331 // if inside a tag, just close the tag and done
332 print(" /");
333 print(GT);
334
335 } else {
336 // we're not inside a tag
337
338 if (state != EState.INSIDE_TEXT) {
339 // if not inside a text element, create new line and indent
340 println();
341 printIndent();
342 }
343
344 // create closing tag
345 print(LT);
346 print("/");
347 print(xmlResolver.resolveElementName(element));
348 print(GT);
349 }
350
351 // we're done with this element
352 elementStack.pop();
353 currentNestingDepth--;
354 state = EState.OUTSIDE_TAG;
355 }
356
357 /**
358 * Add a text element to an element.
359 *
360 * @param text
361 * the text to add.
362 */
363 public void addText(String text) {
364 if (state == EState.INSIDE_TAG) {
365 print(GT);
366 }
367 print(escape(text));
368
369 state = EState.INSIDE_TEXT;
370 }
371
372 /**
373 * Add CDATA section. Added text is not escaped.
374 *
375 * @throws XMLWriterException
376 * If the added text contains the CDATA closing tag
377 * <code>]]></code>. This is not automatically escaped as some
378 * parsers do not automatically unescape it when reading.
379 */
380 public void addCDataSection(String cdata) {
381 if (state == EState.INSIDE_TAG) {
382 print(GT);
383 }
384
385 if (cdata.contains("]]>")) {
386 throw new XMLWriterException("CDATA contains ']]>'",
387 EXMLWriterExceptionType.CDATA_CONTAINS_CDATA_CLOSING_TAG);
388 }
389
390 print("<![CDATA[");
391 print(cdata);
392 print("]]>");
393 state = EState.INSIDE_TEXT;
394 }
395
396 /**
397 * Add an XML comment.
398 *
399 * @param text
400 * comment text.
401 */
402 public void addComment(String text) {
403 ensureOutsideTag();
404
405 currentNestingDepth++;
406 printIndent();
407 print(COMMENT_START);
408 print(escape(text));
409 print(COMMENT_END);
410 currentNestingDepth--;
411
412 state = EState.OUTSIDE_TAG;
413 }
414
415 /** Add new line. */
416 public void addNewLine() {
417 ensureOutsideTag();
418 }
419
420 /**
421 * Close the writer.
422 *
423 * @throws XMLWriterException
424 * if there is a remaining open element.
425 */
426 public void close() {
427 if (!elementStack.isEmpty()) {
428 throw new XMLWriterException("Need to close element <"
429 + xmlResolver.resolveElementName(elementStack.peek())
430 + "> before closing writer.",
431 EXMLWriterExceptionType.UNCLOSED_ELEMENT);
432 }
433 writer.close();
434 }
435
436 /** Flushes the underlying writer. */
437 public void flush() {
438 writer.flush();
439 }
440
441 /**
442 * Adds the given text unprocessed to the writer. This is useful for adding
443 * chunks of generated XML to avoid having the brackets escaped.
444 */
445 protected void addRawString(String text) {
446 if (state == EState.INSIDE_TAG) {
447 print(GT);
448 }
449 print(text);
450 state = EState.INSIDE_TEXT;
451 }
452
453 /** Get writer this writer writes to. */
454 protected PrintWriter getWriter() {
455 return writer;
456 }
457
458 /**
459 * Returns the element we are currently in.
460 *
461 * @throws EmptyStackException
462 * if there is no unclosed element.
463 */
464 protected E getCurrentElement() {
465 return elementStack.peek();
466 }
467
468 /** Make sure the current tag is closed. */
469 private void ensureOutsideTag() {
470 if (state == EState.INSIDE_TAG) {
471 println(GT);
472 state = EState.OUTSIDE_TAG;
473 } else if (state == EState.OUTSIDE_TAG) {
474 println();
475 }
476 }
477
478 /**
479 * Escape text for XML. Creates empty string for <code>null</code> value.
480 */
481 public static String escape(String text) {
482 if (text == null) {
483 return StringUtils.EMPTY_STRING;
484 }
485
486 text = text.replaceAll("&", "&");
487 text = text.replaceAll(LT, "<");
488 text = text.replaceAll(GT, ">");
489 text = text.replaceAll("\"", """);
490
491 // normalize line breaks
492 text = StringUtils.replaceLineBreaks(text, CR);
493 return text;
494 }
495
496 /** Write to writer. */
497 private void print(String message) {
498 writer.print(message);
499 }
500
501 /** Write indent to writer. */
502 private void printIndent() {
503 if (!suppressLineBreaks) {
504 writer.print(StringUtils.fillString(currentNestingDepth * 2,
505 StringUtils.SPACE_CHAR));
506 }
507 }
508
509 /** Write to writer. */
510 private void println() {
511 if (!suppressLineBreaks) {
512 print(CR);
513 }
514 }
515
516 /** Write to writer. */
517 private void println(String text) {
518 print(text);
519 println();
520 }
521 }