001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed with the code. If you do not have a copy, 007 * see: 008 * 009 * http://www.gnu.org/copyleft/lesser.html 010 * 011 * Copyright for this code is held jointly by the individual 012 * authors. These should be listed in @author doc comments. 013 * 014 * For more information on the BioJava project and its aims, 015 * or to join the biojava-l mailing list, visit the home page 016 * at: 017 * 018 * http://www.biojava.org/ 019 * 020 */ 021 022package org.biojava.nbio.core.util; 023 024import java.io.IOException; 025import java.io.PrintWriter; 026import java.util.*; 027 028/** 029 * Implementation of XMLWriter which emits nicely formatted documents 030 * to a PrintWriter. 031 * 032 * @author Thomas Down 033 * @since 1.3 034 */ 035 036public class PrettyXMLWriter implements XMLWriter { 037 private int indentUnit = 2; 038 039 private PrintWriter writer; 040 private boolean isOpeningTag = false; 041 private boolean afterNewline = true; 042 private int indent = 0; 043 044 private Map<String, String> namespacePrefixes = new HashMap<String, String>(); 045 private int namespaceSeed = 0; 046 private LinkedList<List<String>> namespaceBindings = new LinkedList<List<String>>(); 047 private List<String> namespacesDeclared = new ArrayList<String>(); 048 049 public PrettyXMLWriter(PrintWriter writer) { 050 this.writer = writer; 051 } 052 053 /** 054 * Declare a namespace for current and following elements 055 * 'prefixHint' is ignored entirely in this implementation 056 * 057 */ 058 @Override 059 public void declareNamespace(String nsURI, String prefixHint) 060 throws IOException 061 { 062 if (!namespacePrefixes.containsKey(nsURI)) { 063 if (isOpeningTag) { 064 String prefix = allocPrefix(nsURI); 065 attribute("xmlns:" + prefix, nsURI); 066 } else { 067 namespacesDeclared.add(nsURI); 068 } 069 } 070 } 071 072 private void handleDeclaredNamespaces() 073 throws IOException 074 { 075 if (namespacesDeclared.size() == 0) { 076 for (Iterator<String> nsi = namespacesDeclared.iterator(); nsi.hasNext(); ) { 077 String nsURI = nsi.next(); 078 if (!namespacePrefixes.containsKey(nsURI)) { 079 String prefix = allocPrefix(nsURI); 080 attribute("xmlns:" + prefix, nsURI); 081 } 082 } 083 namespacesDeclared.clear(); 084 } 085 } 086 087 protected void writeIndent() 088 throws IOException 089 { 090 for (int i = 0; i < indent * indentUnit; ++i) { 091 writer.write(' '); 092 } 093 } 094 095 private void _openTag() 096 throws IOException 097 { 098 if (isOpeningTag) { 099 writer.println('>'); 100 afterNewline = true; 101 } 102 if (afterNewline) { 103 writeIndent(); 104 } 105 indent++; 106 isOpeningTag = true; 107 afterNewline = false; 108 namespaceBindings.add(null); 109 } 110 111 private String allocPrefix(String nsURI) { 112 String prefix = "ns" + (++namespaceSeed); 113 namespacePrefixes.put(nsURI, prefix); 114 List<String> bindings = namespaceBindings.getLast(); 115 if (bindings == null) { 116 bindings = new ArrayList<String>(); 117 namespaceBindings.removeLast(); 118 namespaceBindings.add(bindings); 119 } 120 bindings.add(nsURI); 121 return prefix; 122 } 123 124 @Override 125 public void openTag(String nsURI, String localName) 126 throws IOException 127 { 128 if (nsURI == null || nsURI.length() == 0) 129 { 130 throw new IOException("Invalid namespace URI: "+nsURI); 131 } 132 _openTag(); 133 boolean alloced = false; 134 String prefix = namespacePrefixes.get(nsURI); 135 if (prefix == null) { 136 prefix = allocPrefix(nsURI); 137 alloced = true; 138 } 139 writer.print('<'); 140 writer.print(prefix); 141 writer.print(':'); 142 writer.print(localName); 143 if (alloced) { 144 attribute("xmlns:" + prefix, nsURI); 145 } 146 handleDeclaredNamespaces(); 147 } 148 149 @Override 150 public void openTag(String qName) 151 throws IOException 152 { 153 _openTag(); 154 writer.print('<'); 155 writer.print(qName); 156 handleDeclaredNamespaces(); 157 } 158 159 @Override 160 public void attribute(String nsURI, String localName, String value) 161 throws IOException 162 { 163 if (! isOpeningTag) { 164 throw new IOException("attributes must follow an openTag"); 165 } 166 167 String prefix = namespacePrefixes.get(nsURI); 168 if (prefix == null) { 169 prefix = allocPrefix(nsURI); 170 attribute("xmlns:" + prefix, nsURI); 171 } 172 173 writer.print(' '); 174 writer.print(prefix); 175 writer.print(':'); 176 writer.print(localName); 177 writer.print("=\""); 178 printAttributeValue(value); 179 writer.print('"'); 180 } 181 182 @Override 183 public void attribute(String qName, String value) 184 throws IOException 185 { 186 if (! isOpeningTag) { 187 throw new IOException("attributes must follow an openTag"); 188 } 189 190 writer.print(' '); 191 writer.print(qName); 192 writer.print("=\""); 193 printAttributeValue(value); 194 writer.print('"'); 195 } 196 197 private void _closeTag() { 198 isOpeningTag = false; 199 afterNewline = true; 200 List<String> hereBindings = namespaceBindings.removeLast(); 201 if (hereBindings != null) { 202 for (Iterator<String> bi = hereBindings.iterator(); bi.hasNext(); ) { 203 namespacePrefixes.remove(bi.next()); 204 } 205 } 206 } 207 208 @Override 209 public void closeTag(String nsURI, String localName) 210 throws IOException 211 { 212 String prefix = namespacePrefixes.get(nsURI); 213 if (prefix == null) { 214 throw new IOException("Assertion failed: unknown namespace when closing tag"); 215 } 216 indent--; 217 218 if (isOpeningTag) { 219 writer.println(" />"); 220 } else { 221 if (afterNewline) { 222 writeIndent(); 223 } 224 writer.print("</"); 225 writer.print(prefix); 226 writer.print(':'); 227 writer.print(localName); 228 writer.println('>'); 229 } 230 _closeTag(); 231 } 232 233 @Override 234 public void closeTag(String qName) 235 throws IOException 236 { 237 indent--; 238 239 if (isOpeningTag) { 240 writer.println(" />"); 241 } else { 242 if (afterNewline) { 243 writeIndent(); 244 } 245 writer.print("</"); 246 writer.print(qName); 247 writer.println('>'); 248 } 249 _closeTag(); 250 } 251 252 @Override 253 public void println(String data) 254 throws IOException 255 { 256 if (isOpeningTag) { 257 writer.println('>'); 258 isOpeningTag = false; 259 } 260 printChars(data); 261 writer.println(); 262 afterNewline = true; 263 } 264 265 @Override 266 public void print(String data) 267 throws IOException 268 { 269 if (isOpeningTag) { 270 writer.print('>'); 271 isOpeningTag = false; 272 } 273 printChars(data); 274 afterNewline = false; 275 } 276 277 // does not work for adding literal XML elements. 278 @Override 279 public void printRaw(String data) 280 throws IOException 281 { 282 writer.println(data); 283 } 284 285 protected void printChars(String data) 286 throws IOException 287 { 288 if (data == null) { 289 printChars("null"); 290 return; 291 } 292 293 for (int pos = 0; pos < data.length(); ++pos) { 294 char c = data.charAt(pos); 295 if (c == '<' || c == '>' || c == '&') { 296 numericalEntity(c); 297 } else { 298 writer.write(c); 299 } 300 } 301 } 302 303 protected void printAttributeValue(String data) 304 throws IOException 305 { 306 if (data == null) { 307 printAttributeValue("null"); 308 return; 309 } 310 311 for (int pos = 0; pos < data.length(); ++pos) { 312 char c = data.charAt(pos); 313 if (c == '<' || c == '>' || c == '&' || c == '"') { 314 numericalEntity(c); 315 } else { 316 writer.write(c); 317 } 318 } 319 } 320 321 protected void numericalEntity(char c) 322 throws IOException 323 { 324 writer.print("&#"); 325 writer.print((int) c); 326 writer.print(';'); 327 } 328 329 @Override 330 public void close() 331 throws IOException 332 { 333 writer.close(); 334 } 335}