001/** 002 * jshred - Shred's Toolbox 003 * 004 * Copyright (C) 2009 Richard "Shred" Körber 005 * http://jshred.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU General Public License / GNU Lesser 009 * General Public License as published by the Free Software Foundation, 010 * either version 3 of the License, or (at your option) any later version. 011 * 012 * Licensed under the Apache License, Version 2.0 (the "License"); 013 * you may not use this file except in compliance with the License. 014 * 015 * This program is distributed in the hope that it will be useful, 016 * but WITHOUT ANY WARRANTY; without even the implied warranty of 017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 018 * 019 */ 020package net.shredzone.jshred.io; 021 022import java.io.BufferedWriter; 023import java.io.IOException; 024import java.io.OutputStream; 025import java.io.OutputStreamWriter; 026import java.io.UnsupportedEncodingException; 027import java.io.Writer; 028import java.util.Map; 029import java.util.Stack; 030 031import org.xml.sax.Attributes; 032 033/** 034 * A very simple writer for cleanly formatted XML output. 035 * <p> 036 * This writer will do for creating simple XML files, but please note that it is not 037 * perfect at all. It does not support namespace, DTDs and it is unable to read DOM 038 * structures as in the {@code org.w3c.dom} package. And last but not least it does 039 * not validate your XML against a DTD. 040 * <p> 041 * What it <em>does</em> is indention of the text, proper escaping of XML special chars 042 * and correct charset encoding. 043 * 044 * @author Richard "Shred" Körber 045 */ 046public class XMLWriter extends BufferedWriter { 047 private String indent = " "; 048 private String charset = "UTF-8"; 049 private int level = 0; 050 private Stack<String> sElements = new Stack<>(); 051 private StringBuilder bTag = new StringBuilder(); 052 private String store = null; 053 054 /** 055 * Creates a new XMLWriter basing on a {@link Writer}. 056 * 057 * @param out 058 * {@link Writer} to send the XML data to 059 */ 060 public XMLWriter(Writer out) { 061 super(out); 062 if (out instanceof OutputStreamWriter) { 063 charset = ((OutputStreamWriter) out).getEncoding(); 064 } 065 } 066 067 /** 068 * Creates a new XMLWriter basing on an {@link OutputStream}. The {@link OutputStream} 069 * will receive UTF-8 encoded data. 070 * 071 * @param out 072 * {@link OutputStream} to send the XML data to 073 * @throws UnsupportedEncodingException 074 * if this VM does not support UTF-8, which should never happen... 075 */ 076 public XMLWriter(OutputStream out) throws UnsupportedEncodingException { 077 this(new OutputStreamWriter(out, "UTF-8")); 078 } 079 080 /** 081 * Sets the indention string. This string is used to indent lines according to their 082 * nesting. Defaults to two spaces, but you can also set several spaces, tabs or an 083 * empty string. 084 * <p> 085 * If you want to change the indention string, you must do so before invoking 086 * {@link #startDocument()}. 087 * 088 * @param indent 089 * New Indention String 090 */ 091 public void setIndent(String indent) { 092 this.indent = indent; 093 } 094 095 /** 096 * Sets the encoding used by this writer. The XMLWriter tries to find out the proper 097 * encoding itself. If an @{link OutputStream} is used, encoding will always be UTF-8. 098 * If an @{link OutputStreamWriter} was passed in, its current encoding will be used. 099 * In any other case UTF-8 is assumed for encoding, which can be changed by using this 100 * method. 101 * <p> 102 * If you want to change the encoding string, you must do so before invoking 103 * {@link #startDocument()}. 104 * 105 * @param encoding 106 * New encoding 107 */ 108 public void setEncoding(String encoding) { 109 this.charset = encoding; 110 } 111 112 /** 113 * Indents by one level 114 */ 115 protected void writeIndent() throws IOException { 116 for (int cnt = 0; cnt < level; cnt++) { 117 write(indent); 118 } 119 } 120 121 /** 122 * Starts an XML document. The XML header will be written. 123 */ 124 public void startDocument() throws IOException { 125 if (charset.equalsIgnoreCase("UTF8")) charset = "UTF-8"; 126 write("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>"); 127 newLine(); 128 newLine(); 129 } 130 131 /** 132 * Finishes an XML document. 133 */ 134 public void endDocument() throws IOException { 135 flushTag(); 136 if (!sElements.isEmpty()) { 137 throw new IOException("Still open elements"); 138 } 139 } 140 141 /** 142 * Starts a new XML element. There are no attributes added to this element. 143 * 144 * @param element 145 * Element name 146 */ 147 public void startElement(String element) throws IOException { 148 startElement(element, (Attributes) null); 149 } 150 151 /** 152 * Starts a new XML element with attributes. This method will take proper care for 153 * excaping all chars within the attribute values. 154 * 155 * @param element 156 * Element name 157 * @param attr 158 * Attribute name/value pairs 159 * @since R6 160 */ 161 public void startElement(String element, String... attr) throws IOException { 162 if (attr.length % 2 == 1) 163 throw new IllegalArgumentException("Number of attr/value pairs must be even!"); 164 165 org.xml.sax.helpers.AttributesImpl xmla = new org.xml.sax.helpers.AttributesImpl(); 166 for (int ix = 0; ix < attr.length; ix += 2) { 167 xmla.addAttribute("", "", attr[ix], "CDATA", attr[ix + 1]); 168 } 169 startElement(element, xmla); 170 } 171 172 /** 173 * Starts a new XML element with attributes. This method will take proper care for 174 * excaping all chars within the attribute values. 175 * <p> 176 * Note that since R15, the map is required to have {@link String} keys and values. 177 * 178 * @param element 179 * Element name 180 * @param attrs 181 * Map with attributes and corresponding values 182 * @since R6 183 */ 184 public void startElement(String element, Map<String, String> attrs) 185 throws IOException { 186 org.xml.sax.helpers.AttributesImpl xmla = new org.xml.sax.helpers.AttributesImpl(); 187 for (String key : attrs.keySet()) { 188 xmla.addAttribute("", "", key, "CDATA", attrs.get(key)); 189 } 190 startElement(element, xmla); 191 } 192 193 /** 194 * Starts a new XML element with attributes. This method will take proper care for 195 * excaping all chars within the attribute values. 196 * 197 * @param element 198 * Element name 199 * @param attr 200 * Attributes 201 */ 202 public void startElement(String element, Attributes attr) throws IOException { 203 flushTag(); 204 sElements.push(element); 205 writeIndent(); 206 bTag.append(element); 207 208 // --- Append attributes --- 209 if (attr != null && attr.getLength() > 0) { 210 int cnt = attr.getLength(); 211 for (int ix = 0; ix < cnt; ix++) { 212 bTag.append(' '); 213 bTag.append(attr.getQName(ix)); 214 bTag.append("=\""); 215 bTag.append(escape(attr.getValue(ix))); 216 bTag.append('"'); 217 } 218 } 219 level++; 220 } 221 222 /** 223 * Closes the most recent XML element. The XMLWriter recognizes empty elements and will 224 * send a shortcut to the output. 225 */ 226 public void endElement() throws IOException { 227 String element = sElements.pop(); 228 if (element == null) { 229 throw new IOException("Too many elements closed"); 230 } 231 232 if (store != null && bTag.length() > 0) { 233 level--; 234 write('<'); 235 write(bTag.toString()); 236 write('>'); 237 write(escape(store)); 238 write("</"); 239 write(element); 240 write('>'); 241 store = null; 242 bTag = new StringBuilder(); 243 } else { 244 if (store != null) { 245 writeIndent(); 246 write(escape(store)); 247 newLine(); 248 store = null; 249 } 250 level--; 251 if (bTag.length() > 0) { 252 write('<'); 253 write(bTag.toString()); 254 write("/>"); 255 bTag = new StringBuilder(); 256 } else { 257 writeIndent(); 258 write("</" + element + '>'); 259 } 260 } 261 newLine(); 262 } 263 264 /** 265 * Writes the content of an XML container. Leading and trailing spaces will be trimmed. 266 * Empty contents will be ignored. You can invoke this method several times for a 267 * container. Each content will then be written into a separate line. Special chars 268 * will automatically be escaped. 269 * 270 * @param content 271 * Content of the current XML container 272 */ 273 public void writeContent(String content) throws IOException { 274 // --- Ignore empty container --- 275 content = content.trim(); 276 if (content.equals("")) return; 277 278 if (store == null) { 279 // --- Temporary store it --- 280 store = content; 281 } else { 282 // --- Write it --- 283 flushTag(); 284 writeIndent(); 285 write(escape(content)); 286 newLine(); 287 } 288 } 289 290 /** 291 * Writes a comment. Content will be escaped properly. 292 * 293 * @param comment 294 * The comment's content. 295 */ 296 public void writeComment(String comment) throws IOException { 297 // --- Ignore comment --- 298 comment = comment.trim(); 299 if (comment.equals("")) return; 300 301 // --- Write it --- 302 flushTag(); 303 writeIndent(); 304 write("<!-- " + escape(comment) + " -->"); 305 newLine(); 306 } 307 308 /** 309 * Escapes a String so it can be used in XML context. All &, <, > and " 310 * will be converted into their respective entity. 311 * 312 * @param text 313 * Text to be escaped 314 * @return Escapted text 315 */ 316 public static String escape(String text) { 317 text = text.replaceAll("\\&", "&"); 318 text = text.replaceAll("\\<", "<"); 319 text = text.replaceAll("\\>", ">"); 320 text = text.replaceAll("\\\"", """); 321 return text; 322 } 323 324 /** 325 * Flushes the tag buffer, which is used to write empty containers in their short 326 * form. 327 */ 328 protected void flushTag() throws IOException { 329 if (bTag.length() > 0) { 330 write("<"); 331 write(bTag.toString()); 332 write(">"); 333 newLine(); 334 bTag = new StringBuilder(); 335 } 336 if (store != null) { 337 writeIndent(); 338 write(escape(store)); 339 newLine(); 340 store = null; 341 } 342 } 343}