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 22 package org.kxml2.io; 23 24 import java.io.*; 25 import java.util.Locale; 26 import org.xmlpull.v1.*; 27 28 public 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 allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 129 if (allowedInXml) { 130 if (unicode || c < 127) { 131 writer.write(c); 132 } else { 133 writer.write("&#" + ((int) c) + ";"); 134 } 135 } else if (Character.isHighSurrogate(c) && i < s.length() - 1) { 136 writeSurrogate(c, s.charAt(i + 1)); 137 ++i; 138 } else { 139 reportInvalidCharacter(c); 140 } 141 // END android-changed 142 } 143 } 144 } 145 146 // BEGIN android-added 147 private static void reportInvalidCharacter(char ch) { 148 throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")"); 149 } 150 // END android-added 151 152 /* 153 private final void writeIndent() throws IOException { 154 writer.write("\r\n"); 155 for (int i = 0; i < depth; i++) 156 writer.write(' '); 157 }*/ 158 159 public void docdecl(String dd) throws IOException { 160 writer.write("<!DOCTYPE"); 161 writer.write(dd); 162 writer.write(">"); 163 } 164 165 public void endDocument() throws IOException { 166 while (depth > 0) { 167 endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); 168 } 169 flush(); 170 } 171 172 public void entityRef(String name) throws IOException { 173 check(false); 174 writer.write('&'); 175 writer.write(name); 176 writer.write(';'); 177 } 178 179 public boolean getFeature(String name) { 180 //return false; 181 return ( 182 "http://xmlpull.org/v1/doc/features.html#indent-output" 183 .equals( 184 name)) 185 ? indent[depth] 186 : false; 187 } 188 189 public String getPrefix(String namespace, boolean create) { 190 try { 191 return getPrefix(namespace, false, create); 192 } 193 catch (IOException e) { 194 throw new RuntimeException(e.toString()); 195 } 196 } 197 198 private final String getPrefix( 199 String namespace, 200 boolean includeDefault, 201 boolean create) 202 throws IOException { 203 204 for (int i = nspCounts[depth + 1] * 2 - 2; 205 i >= 0; 206 i -= 2) { 207 if (nspStack[i + 1].equals(namespace) 208 && (includeDefault || !nspStack[i].isEmpty())) { 209 String cand = nspStack[i]; 210 for (int j = i + 2; 211 j < nspCounts[depth + 1] * 2; 212 j++) { 213 if (nspStack[j].equals(cand)) { 214 cand = null; 215 break; 216 } 217 } 218 if (cand != null) 219 return cand; 220 } 221 } 222 223 if (!create) 224 return null; 225 226 String prefix; 227 228 if (namespace.isEmpty()) 229 prefix = ""; 230 else { 231 do { 232 prefix = "n" + (auto++); 233 for (int i = nspCounts[depth + 1] * 2 - 2; 234 i >= 0; 235 i -= 2) { 236 if (prefix.equals(nspStack[i])) { 237 prefix = null; 238 break; 239 } 240 } 241 } 242 while (prefix == null); 243 } 244 245 boolean p = pending; 246 pending = false; 247 setPrefix(prefix, namespace); 248 pending = p; 249 return prefix; 250 } 251 252 public Object getProperty(String name) { 253 throw new RuntimeException("Unsupported property"); 254 } 255 256 public void ignorableWhitespace(String s) 257 throws IOException { 258 text(s); 259 } 260 261 public void setFeature(String name, boolean value) { 262 if ("http://xmlpull.org/v1/doc/features.html#indent-output" 263 .equals(name)) { 264 indent[depth] = value; 265 } 266 else 267 throw new RuntimeException("Unsupported Feature"); 268 } 269 270 public void setProperty(String name, Object value) { 271 throw new RuntimeException( 272 "Unsupported Property:" + value); 273 } 274 275 public void setPrefix(String prefix, String namespace) 276 throws IOException { 277 278 check(false); 279 if (prefix == null) 280 prefix = ""; 281 if (namespace == null) 282 namespace = ""; 283 284 String defined = getPrefix(namespace, true, false); 285 286 // boil out if already defined 287 288 if (prefix.equals(defined)) 289 return; 290 291 int pos = (nspCounts[depth + 1]++) << 1; 292 293 if (nspStack.length < pos + 1) { 294 String[] hlp = new String[nspStack.length + 16]; 295 System.arraycopy(nspStack, 0, hlp, 0, pos); 296 nspStack = hlp; 297 } 298 299 nspStack[pos++] = prefix; 300 nspStack[pos] = namespace; 301 } 302 303 public void setOutput(Writer writer) { 304 // BEGIN android-changed 305 // Guarantee that the writer is always buffered. 306 if (writer instanceof BufferedWriter) { 307 this.writer = (BufferedWriter) writer; 308 } else { 309 this.writer = new BufferedWriter(writer, WRITE_BUFFER_SIZE); 310 } 311 // END android-changed 312 313 // elementStack = new String[12]; //nsp/prefix/name 314 //nspCounts = new int[4]; 315 //nspStack = new String[8]; //prefix/nsp 316 //indent = new boolean[4]; 317 318 nspCounts[0] = 2; 319 nspCounts[1] = 2; 320 nspStack[0] = ""; 321 nspStack[1] = ""; 322 nspStack[2] = "xml"; 323 nspStack[3] = "http://www.w3.org/XML/1998/namespace"; 324 pending = false; 325 auto = 0; 326 depth = 0; 327 328 unicode = false; 329 } 330 331 public void setOutput(OutputStream os, String encoding) 332 throws IOException { 333 if (os == null) 334 throw new IllegalArgumentException("os == null"); 335 setOutput( 336 encoding == null 337 ? new OutputStreamWriter(os) 338 : new OutputStreamWriter(os, encoding)); 339 this.encoding = encoding; 340 if (encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")) { 341 unicode = true; 342 } 343 } 344 345 public void startDocument(String encoding, Boolean standalone) throws IOException { 346 writer.write("<?xml version='1.0' "); 347 348 if (encoding != null) { 349 this.encoding = encoding; 350 if (encoding.toLowerCase(Locale.US).startsWith("utf")) { 351 unicode = true; 352 } 353 } 354 355 if (this.encoding != null) { 356 writer.write("encoding='"); 357 writer.write(this.encoding); 358 writer.write("' "); 359 } 360 361 if (standalone != null) { 362 writer.write("standalone='"); 363 writer.write( 364 standalone.booleanValue() ? "yes" : "no"); 365 writer.write("' "); 366 } 367 writer.write("?>"); 368 } 369 370 public XmlSerializer startTag(String namespace, String name) 371 throws IOException { 372 check(false); 373 374 // if (namespace == null) 375 // namespace = ""; 376 377 if (indent[depth]) { 378 writer.write("\r\n"); 379 for (int i = 0; i < depth; i++) 380 writer.write(" "); 381 } 382 383 int esp = depth * 3; 384 385 if (elementStack.length < esp + 3) { 386 String[] hlp = new String[elementStack.length + 12]; 387 System.arraycopy(elementStack, 0, hlp, 0, esp); 388 elementStack = hlp; 389 } 390 391 String prefix = 392 namespace == null 393 ? "" 394 : getPrefix(namespace, true, true); 395 396 if (namespace != null && namespace.isEmpty()) { 397 for (int i = nspCounts[depth]; 398 i < nspCounts[depth + 1]; 399 i++) { 400 if (nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()) { 401 throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); 402 } 403 } 404 } 405 406 elementStack[esp++] = namespace; 407 elementStack[esp++] = prefix; 408 elementStack[esp] = name; 409 410 writer.write('<'); 411 if (!prefix.isEmpty()) { 412 writer.write(prefix); 413 writer.write(':'); 414 } 415 416 writer.write(name); 417 418 pending = true; 419 420 return this; 421 } 422 423 public XmlSerializer attribute( 424 String namespace, 425 String name, 426 String value) 427 throws IOException { 428 if (!pending) 429 throw new IllegalStateException("illegal position for attribute"); 430 431 // int cnt = nspCounts[depth]; 432 433 if (namespace == null) 434 namespace = ""; 435 436 // depth--; 437 // pending = false; 438 439 String prefix = 440 namespace.isEmpty() 441 ? "" 442 : getPrefix(namespace, false, true); 443 444 // pending = true; 445 // depth++; 446 447 /* if (cnt != nspCounts[depth]) { 448 writer.write(' '); 449 writer.write("xmlns"); 450 if (nspStack[cnt * 2] != null) { 451 writer.write(':'); 452 writer.write(nspStack[cnt * 2]); 453 } 454 writer.write("=\""); 455 writeEscaped(nspStack[cnt * 2 + 1], '"'); 456 writer.write('"'); 457 } 458 */ 459 460 writer.write(' '); 461 if (!prefix.isEmpty()) { 462 writer.write(prefix); 463 writer.write(':'); 464 } 465 writer.write(name); 466 writer.write('='); 467 char q = value.indexOf('"') == -1 ? '"' : '\''; 468 writer.write(q); 469 writeEscaped(value, q); 470 writer.write(q); 471 472 return this; 473 } 474 475 public void flush() throws IOException { 476 check(false); 477 writer.flush(); 478 } 479 /* 480 public void close() throws IOException { 481 check(); 482 writer.close(); 483 } 484 */ 485 public XmlSerializer endTag(String namespace, String name) 486 throws IOException { 487 488 if (!pending) 489 depth--; 490 // if (namespace == null) 491 // namespace = ""; 492 493 if ((namespace == null 494 && elementStack[depth * 3] != null) 495 || (namespace != null 496 && !namespace.equals(elementStack[depth * 3])) 497 || !elementStack[depth * 3 + 2].equals(name)) 498 throw new IllegalArgumentException("</{"+namespace+"}"+name+"> does not match start"); 499 500 if (pending) { 501 check(true); 502 depth--; 503 } 504 else { 505 if (indent[depth + 1]) { 506 writer.write("\r\n"); 507 for (int i = 0; i < depth; i++) 508 writer.write(" "); 509 } 510 511 writer.write("</"); 512 String prefix = elementStack[depth * 3 + 1]; 513 if (!prefix.isEmpty()) { 514 writer.write(prefix); 515 writer.write(':'); 516 } 517 writer.write(name); 518 writer.write('>'); 519 } 520 521 nspCounts[depth + 1] = nspCounts[depth]; 522 return this; 523 } 524 525 public String getNamespace() { 526 return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; 527 } 528 529 public String getName() { 530 return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; 531 } 532 533 public int getDepth() { 534 return pending ? depth + 1 : depth; 535 } 536 537 public XmlSerializer text(String text) throws IOException { 538 check(false); 539 indent[depth] = false; 540 writeEscaped(text, -1); 541 return this; 542 } 543 544 public XmlSerializer text(char[] text, int start, int len) 545 throws IOException { 546 text(new String(text, start, len)); 547 return this; 548 } 549 550 public void cdsect(String data) throws IOException { 551 check(false); 552 // BEGIN android-changed: ]]> is not allowed within a CDATA, 553 // so break and start a new one when necessary. 554 data = data.replace("]]>", "]]]]><![CDATA[>"); 555 writer.write("<![CDATA["); 556 for (int i = 0; i < data.length(); ++i) { 557 char ch = data.charAt(i); 558 boolean allowedInCdata = (ch >= 0x20 && ch <= 0xd7ff) || 559 (ch == '\t' || ch == '\n' || ch == '\r') || 560 (ch >= 0xe000 && ch <= 0xfffd); 561 if (allowedInCdata) { 562 writer.write(ch); 563 } else if (Character.isHighSurrogate(ch) && i < data.length() - 1) { 564 // Character entities aren't valid in CDATA, so break out for this. 565 writer.write("]]>"); 566 writeSurrogate(ch, data.charAt(++i)); 567 writer.write("<![CDATA["); 568 } else { 569 reportInvalidCharacter(ch); 570 } 571 } 572 writer.write("]]>"); 573 // END android-changed 574 } 575 576 // BEGIN android-added 577 private void writeSurrogate(char high, char low) throws IOException { 578 if (!Character.isLowSurrogate(low)) { 579 throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) + 580 " U+" + Integer.toHexString((int) low) + ")"); 581 } 582 // Java-style surrogate pairs aren't allowed in XML. We could use the > 3-byte encodings, but that 583 // seems likely to upset anything expecting modified UTF-8 rather than "real" UTF-8. It seems more 584 // conservative in a Java environment to use an entity reference instead. 585 int codePoint = Character.toCodePoint(high, low); 586 writer.write("&#" + codePoint + ";"); 587 } 588 // END android-added 589 590 public void comment(String comment) throws IOException { 591 check(false); 592 writer.write("<!--"); 593 writer.write(comment); 594 writer.write("-->"); 595 } 596 597 public void processingInstruction(String pi) 598 throws IOException { 599 check(false); 600 writer.write("<?"); 601 writer.write(pi); 602 writer.write("?>"); 603 } 604 } 605