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 }