1/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany 2 * 3 * Permission is hereby granted, free of charge, to any person obtaining a copy 4 * of this software and associated documentation files (the "Software"), to deal 5 * in the Software without restriction, including without limitation the rights 6 * to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 * sell copies of the Software, and to permit persons to whom the Software is 8 * furnished to do so, subject to the following conditions: 9 * 10 * The above copyright notice and this permission notice shall be included in 11 * all copies or substantial portions of the Software. 12 * 13 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 * IN THE SOFTWARE. */ 20 21 22package org.kxml2.io; 23 24import java.io.*; 25import java.util.Locale; 26import org.xmlpull.v1.*; 27 28public class KXmlSerializer implements XmlSerializer { 29 30 // static final String UNDEFINED = ":"; 31 32 // BEGIN android-added 33 /** size (in characters) for the write buffer */ 34 private static final int WRITE_BUFFER_SIZE = 500; 35 // END android-added 36 37 // BEGIN android-changed 38 // (Guarantee that the writer is always buffered.) 39 private BufferedWriter writer; 40 // END android-changed 41 42 private boolean pending; 43 private int auto; 44 private int depth; 45 46 private String[] elementStack = new String[12]; 47 //nsp/prefix/name 48 private int[] nspCounts = new int[4]; 49 private String[] nspStack = new String[8]; 50 //prefix/nsp; both empty are "" 51 private boolean[] indent = new boolean[4]; 52 private boolean unicode; 53 private String encoding; 54 55 private final void check(boolean close) throws IOException { 56 if (!pending) 57 return; 58 59 depth++; 60 pending = false; 61 62 if (indent.length <= depth) { 63 boolean[] hlp = new boolean[depth + 4]; 64 System.arraycopy(indent, 0, hlp, 0, depth); 65 indent = hlp; 66 } 67 indent[depth] = indent[depth - 1]; 68 69 for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++) { 70 writer.write(' '); 71 writer.write("xmlns"); 72 if (!nspStack[i * 2].isEmpty()) { 73 writer.write(':'); 74 writer.write(nspStack[i * 2]); 75 } 76 else if (getNamespace().isEmpty() && !nspStack[i * 2 + 1].isEmpty()) 77 throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); 78 writer.write("=\""); 79 writeEscaped(nspStack[i * 2 + 1], '"'); 80 writer.write('"'); 81 } 82 83 if (nspCounts.length <= depth + 1) { 84 int[] hlp = new int[depth + 8]; 85 System.arraycopy(nspCounts, 0, hlp, 0, depth + 1); 86 nspCounts = hlp; 87 } 88 89 nspCounts[depth + 1] = nspCounts[depth]; 90 // nspCounts[depth + 2] = nspCounts[depth]; 91 92 writer.write(close ? " />" : ">"); 93 } 94 95 private final void writeEscaped(String s, int quot) throws IOException { 96 for (int i = 0; i < s.length(); i++) { 97 char c = s.charAt(i); 98 switch (c) { 99 case '\n': 100 case '\r': 101 case '\t': 102 if(quot == -1) 103 writer.write(c); 104 else 105 writer.write("&#"+((int) c)+';'); 106 break; 107 case '&' : 108 writer.write("&"); 109 break; 110 case '>' : 111 writer.write(">"); 112 break; 113 case '<' : 114 writer.write("<"); 115 break; 116 default: 117 if (c == quot) { 118 writer.write(c == '"' ? """ : "'"); 119 break; 120 } 121 // BEGIN android-changed: refuse to output invalid characters 122 // See http://www.w3.org/TR/REC-xml/#charsets for definition. 123 // No other Java XML writer we know of does this, but no Java 124 // XML reader we know of is able to parse the bad output we'd 125 // otherwise generate. 126 // Note: tab, newline, and carriage return have already been 127 // handled above. 128 boolean valid = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 129 if (!valid) { 130 reportInvalidCharacter(c); 131 } 132 if (unicode || c < 127) { 133 writer.write(c); 134 } else { 135 writer.write("&#" + ((int) c) + ";"); 136 } 137 // END android-changed 138 } 139 } 140 } 141 142 // BEGIN android-added 143 private static void reportInvalidCharacter(char ch) { 144 throw new IllegalArgumentException("Illegal character (" + Integer.toHexString((int) ch) + ")"); 145 } 146 // END android-added 147 148 /* 149 private final void writeIndent() throws IOException { 150 writer.write("\r\n"); 151 for (int i = 0; i < depth; i++) 152 writer.write(' '); 153 }*/ 154 155 public void docdecl(String dd) throws IOException { 156 writer.write("<!DOCTYPE"); 157 writer.write(dd); 158 writer.write(">"); 159 } 160 161 public void endDocument() throws IOException { 162 while (depth > 0) { 163 endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); 164 } 165 flush(); 166 } 167 168 public void entityRef(String name) throws IOException { 169 check(false); 170 writer.write('&'); 171 writer.write(name); 172 writer.write(';'); 173 } 174 175 public boolean getFeature(String name) { 176 //return false; 177 return ( 178 "http://xmlpull.org/v1/doc/features.html#indent-output" 179 .equals( 180 name)) 181 ? indent[depth] 182 : false; 183 } 184 185 public String getPrefix(String namespace, boolean create) { 186 try { 187 return getPrefix(namespace, false, create); 188 } 189 catch (IOException e) { 190 throw new RuntimeException(e.toString()); 191 } 192 } 193 194 private final String getPrefix( 195 String namespace, 196 boolean includeDefault, 197 boolean create) 198 throws IOException { 199 200 for (int i = nspCounts[depth + 1] * 2 - 2; 201 i >= 0; 202 i -= 2) { 203 if (nspStack[i + 1].equals(namespace) 204 && (includeDefault || !nspStack[i].isEmpty())) { 205 String cand = nspStack[i]; 206 for (int j = i + 2; 207 j < nspCounts[depth + 1] * 2; 208 j++) { 209 if (nspStack[j].equals(cand)) { 210 cand = null; 211 break; 212 } 213 } 214 if (cand != null) 215 return cand; 216 } 217 } 218 219 if (!create) 220 return null; 221 222 String prefix; 223 224 if (namespace.isEmpty()) 225 prefix = ""; 226 else { 227 do { 228 prefix = "n" + (auto++); 229 for (int i = nspCounts[depth + 1] * 2 - 2; 230 i >= 0; 231 i -= 2) { 232 if (prefix.equals(nspStack[i])) { 233 prefix = null; 234 break; 235 } 236 } 237 } 238 while (prefix == null); 239 } 240 241 boolean p = pending; 242 pending = false; 243 setPrefix(prefix, namespace); 244 pending = p; 245 return prefix; 246 } 247 248 public Object getProperty(String name) { 249 throw new RuntimeException("Unsupported property"); 250 } 251 252 public void ignorableWhitespace(String s) 253 throws IOException { 254 text(s); 255 } 256 257 public void setFeature(String name, boolean value) { 258 if ("http://xmlpull.org/v1/doc/features.html#indent-output" 259 .equals(name)) { 260 indent[depth] = value; 261 } 262 else 263 throw new RuntimeException("Unsupported Feature"); 264 } 265 266 public void setProperty(String name, Object value) { 267 throw new RuntimeException( 268 "Unsupported Property:" + value); 269 } 270 271 public void setPrefix(String prefix, String namespace) 272 throws IOException { 273 274 check(false); 275 if (prefix == null) 276 prefix = ""; 277 if (namespace == null) 278 namespace = ""; 279 280 String defined = getPrefix(namespace, true, false); 281 282 // boil out if already defined 283 284 if (prefix.equals(defined)) 285 return; 286 287 int pos = (nspCounts[depth + 1]++) << 1; 288 289 if (nspStack.length < pos + 1) { 290 String[] hlp = new String[nspStack.length + 16]; 291 System.arraycopy(nspStack, 0, hlp, 0, pos); 292 nspStack = hlp; 293 } 294 295 nspStack[pos++] = prefix; 296 nspStack[pos] = namespace; 297 } 298 299 public void setOutput(Writer writer) { 300 // BEGIN android-changed 301 // Guarantee that the writer is always buffered. 302 if (writer instanceof BufferedWriter) { 303 this.writer = (BufferedWriter) writer; 304 } else { 305 this.writer = new BufferedWriter(writer, WRITE_BUFFER_SIZE); 306 } 307 // END android-changed 308 309 // elementStack = new String[12]; //nsp/prefix/name 310 //nspCounts = new int[4]; 311 //nspStack = new String[8]; //prefix/nsp 312 //indent = new boolean[4]; 313 314 nspCounts[0] = 2; 315 nspCounts[1] = 2; 316 nspStack[0] = ""; 317 nspStack[1] = ""; 318 nspStack[2] = "xml"; 319 nspStack[3] = "http://www.w3.org/XML/1998/namespace"; 320 pending = false; 321 auto = 0; 322 depth = 0; 323 324 unicode = false; 325 } 326 327 public void setOutput(OutputStream os, String encoding) 328 throws IOException { 329 if (os == null) 330 throw new IllegalArgumentException(); 331 setOutput( 332 encoding == null 333 ? new OutputStreamWriter(os) 334 : new OutputStreamWriter(os, encoding)); 335 this.encoding = encoding; 336 if (encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")) { 337 unicode = true; 338 } 339 } 340 341 public void startDocument(String encoding, Boolean standalone) throws IOException { 342 writer.write("<?xml version='1.0' "); 343 344 if (encoding != null) { 345 this.encoding = encoding; 346 if (encoding.toLowerCase(Locale.US).startsWith("utf")) { 347 unicode = true; 348 } 349 } 350 351 if (this.encoding != null) { 352 writer.write("encoding='"); 353 writer.write(this.encoding); 354 writer.write("' "); 355 } 356 357 if (standalone != null) { 358 writer.write("standalone='"); 359 writer.write( 360 standalone.booleanValue() ? "yes" : "no"); 361 writer.write("' "); 362 } 363 writer.write("?>"); 364 } 365 366 public XmlSerializer startTag(String namespace, String name) 367 throws IOException { 368 check(false); 369 370 // if (namespace == null) 371 // namespace = ""; 372 373 if (indent[depth]) { 374 writer.write("\r\n"); 375 for (int i = 0; i < depth; i++) 376 writer.write(" "); 377 } 378 379 int esp = depth * 3; 380 381 if (elementStack.length < esp + 3) { 382 String[] hlp = new String[elementStack.length + 12]; 383 System.arraycopy(elementStack, 0, hlp, 0, esp); 384 elementStack = hlp; 385 } 386 387 String prefix = 388 namespace == null 389 ? "" 390 : getPrefix(namespace, true, true); 391 392 if (namespace != null && namespace.isEmpty()) { 393 for (int i = nspCounts[depth]; 394 i < nspCounts[depth + 1]; 395 i++) { 396 if (nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()) { 397 throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); 398 } 399 } 400 } 401 402 elementStack[esp++] = namespace; 403 elementStack[esp++] = prefix; 404 elementStack[esp] = name; 405 406 writer.write('<'); 407 if (!prefix.isEmpty()) { 408 writer.write(prefix); 409 writer.write(':'); 410 } 411 412 writer.write(name); 413 414 pending = true; 415 416 return this; 417 } 418 419 public XmlSerializer attribute( 420 String namespace, 421 String name, 422 String value) 423 throws IOException { 424 if (!pending) 425 throw new IllegalStateException("illegal position for attribute"); 426 427 // int cnt = nspCounts[depth]; 428 429 if (namespace == null) 430 namespace = ""; 431 432 // depth--; 433 // pending = false; 434 435 String prefix = 436 namespace.isEmpty() 437 ? "" 438 : getPrefix(namespace, false, true); 439 440 // pending = true; 441 // depth++; 442 443 /* if (cnt != nspCounts[depth]) { 444 writer.write(' '); 445 writer.write("xmlns"); 446 if (nspStack[cnt * 2] != null) { 447 writer.write(':'); 448 writer.write(nspStack[cnt * 2]); 449 } 450 writer.write("=\""); 451 writeEscaped(nspStack[cnt * 2 + 1], '"'); 452 writer.write('"'); 453 } 454 */ 455 456 writer.write(' '); 457 if (!prefix.isEmpty()) { 458 writer.write(prefix); 459 writer.write(':'); 460 } 461 writer.write(name); 462 writer.write('='); 463 char q = value.indexOf('"') == -1 ? '"' : '\''; 464 writer.write(q); 465 writeEscaped(value, q); 466 writer.write(q); 467 468 return this; 469 } 470 471 public void flush() throws IOException { 472 check(false); 473 writer.flush(); 474 } 475 /* 476 public void close() throws IOException { 477 check(); 478 writer.close(); 479 } 480 */ 481 public XmlSerializer endTag(String namespace, String name) 482 throws IOException { 483 484 if (!pending) 485 depth--; 486 // if (namespace == null) 487 // namespace = ""; 488 489 if ((namespace == null 490 && elementStack[depth * 3] != null) 491 || (namespace != null 492 && !namespace.equals(elementStack[depth * 3])) 493 || !elementStack[depth * 3 + 2].equals(name)) 494 throw new IllegalArgumentException("</{"+namespace+"}"+name+"> does not match start"); 495 496 if (pending) { 497 check(true); 498 depth--; 499 } 500 else { 501 if (indent[depth + 1]) { 502 writer.write("\r\n"); 503 for (int i = 0; i < depth; i++) 504 writer.write(" "); 505 } 506 507 writer.write("</"); 508 String prefix = elementStack[depth * 3 + 1]; 509 if (!prefix.isEmpty()) { 510 writer.write(prefix); 511 writer.write(':'); 512 } 513 writer.write(name); 514 writer.write('>'); 515 } 516 517 nspCounts[depth + 1] = nspCounts[depth]; 518 return this; 519 } 520 521 public String getNamespace() { 522 return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; 523 } 524 525 public String getName() { 526 return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; 527 } 528 529 public int getDepth() { 530 return pending ? depth + 1 : depth; 531 } 532 533 public XmlSerializer text(String text) throws IOException { 534 check(false); 535 indent[depth] = false; 536 writeEscaped(text, -1); 537 return this; 538 } 539 540 public XmlSerializer text(char[] text, int start, int len) 541 throws IOException { 542 text(new String(text, start, len)); 543 return this; 544 } 545 546 public void cdsect(String data) throws IOException { 547 check(false); 548 // BEGIN android-changed: ]]> is not allowed within a CDATA, 549 // so break and start a new one when necessary. 550 data = data.replace("]]>", "]]]]><![CDATA[>"); 551 char[] chars = data.toCharArray(); 552 // We also aren't allowed any invalid characters. 553 for (char ch : chars) { 554 boolean valid = (ch >= 0x20 && ch <= 0xd7ff) || 555 (ch == '\t' || ch == '\n' || ch == '\r') || 556 (ch >= 0xe000 && ch <= 0xfffd); 557 if (!valid) { 558 reportInvalidCharacter(ch); 559 } 560 } 561 writer.write("<![CDATA["); 562 writer.write(chars, 0, chars.length); 563 writer.write("]]>"); 564 // END android-changed 565 } 566 567 public void comment(String comment) throws IOException { 568 check(false); 569 writer.write("<!--"); 570 writer.write(comment); 571 writer.write("-->"); 572 } 573 574 public void processingInstruction(String pi) 575 throws IOException { 576 check(false); 577 writer.write("<?"); 578 writer.write(pi); 579 writer.write("?>"); 580 } 581} 582