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 com.android.org.kxml2.io; 23 24 import java.io.*; 25 import java.util.Arrays; 26 import java.util.Locale; 27 import org.xmlpull.v1.*; 28 29 public class KXmlSerializer implements XmlSerializer { 30 31 private static final int BUFFER_LEN = 8192; 32 private final char[] mText = new char[BUFFER_LEN]; 33 private int mPos; 34 35 // static final String UNDEFINED = ":"; 36 37 private Writer writer; 38 39 private boolean pending; 40 private int auto; 41 private int depth; 42 43 private String[] elementStack = new String[12]; 44 //nsp/prefix/name 45 private int[] nspCounts = new int[4]; 46 private String[] nspStack = new String[8]; 47 //prefix/nsp; both empty are "" 48 private boolean[] indent = new boolean[4]; 49 private boolean unicode; 50 private String encoding; 51 append(char c)52 private void append(char c) throws IOException { 53 if (mPos >= BUFFER_LEN) { 54 flushBuffer(); 55 } 56 mText[mPos++] = c; 57 } 58 append(String str, int i, int length)59 private void append(String str, int i, int length) throws IOException { 60 while (length > 0) { 61 if (mPos == BUFFER_LEN) { 62 flushBuffer(); 63 } 64 int batch = BUFFER_LEN - mPos; 65 if (batch > length) { 66 batch = length; 67 } 68 str.getChars(i, i + batch, mText, mPos); 69 i += batch; 70 length -= batch; 71 mPos += batch; 72 } 73 } 74 75 // BEGIN Android-added: Speed-up indentation. http://b/230007772 appendSpace(int length)76 private void appendSpace(int length) throws IOException { 77 while (length > 0) { 78 if (mPos == BUFFER_LEN) { 79 flushBuffer(); 80 } 81 int batch = BUFFER_LEN - mPos; 82 if (batch > length) { 83 batch = length; 84 } 85 Arrays.fill(mText, mPos, mPos + batch, ' '); 86 length -= batch; 87 mPos += batch; 88 } 89 } 90 // END Android-added: Speed-up indentation. http://b/230007772 91 append(String str)92 private void append(String str) throws IOException { 93 append(str, 0, str.length()); 94 } 95 flushBuffer()96 private final void flushBuffer() throws IOException { 97 if(mPos > 0) { 98 writer.write(mText, 0, mPos); 99 writer.flush(); 100 mPos = 0; 101 } 102 } 103 check(boolean close)104 private final void check(boolean close) throws IOException { 105 if (!pending) 106 return; 107 108 depth++; 109 pending = false; 110 111 if (indent.length <= depth) { 112 boolean[] hlp = new boolean[depth + 4]; 113 System.arraycopy(indent, 0, hlp, 0, depth); 114 indent = hlp; 115 } 116 indent[depth] = indent[depth - 1]; 117 118 for (int i = nspCounts[depth - 1]; i < nspCounts[depth]; i++) { 119 append(" xmlns"); 120 if (!nspStack[i * 2].isEmpty()) { 121 append(':'); 122 append(nspStack[i * 2]); 123 } 124 else if (getNamespace().isEmpty() && !nspStack[i * 2 + 1].isEmpty()) 125 throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); 126 append("=\""); 127 writeEscaped(nspStack[i * 2 + 1], '"'); 128 append('"'); 129 } 130 131 if (nspCounts.length <= depth + 1) { 132 int[] hlp = new int[depth + 8]; 133 System.arraycopy(nspCounts, 0, hlp, 0, depth + 1); 134 nspCounts = hlp; 135 } 136 137 nspCounts[depth + 1] = nspCounts[depth]; 138 // nspCounts[depth + 2] = nspCounts[depth]; 139 140 if (close) { 141 append(" />"); 142 } else { 143 append('>'); 144 } 145 } 146 writeEscaped(String s, int quot)147 private final void writeEscaped(String s, int quot) throws IOException { 148 for (int i = 0; i < s.length(); i++) { 149 char c = s.charAt(i); 150 switch (c) { 151 case '\n': 152 case '\r': 153 case '\t': 154 if(quot == -1) 155 append(c); 156 else 157 append("&#"+((int) c)+';'); 158 break; 159 case '&' : 160 append("&"); 161 break; 162 case '>' : 163 append(">"); 164 break; 165 case '<' : 166 append("<"); 167 break; 168 default: 169 if (c == quot) { 170 append(c == '"' ? """ : "'"); 171 break; 172 } 173 // BEGIN Android-changed: refuse to output invalid characters 174 // See http://www.w3.org/TR/REC-xml/#charsets for definition. 175 // No other Java XML writer we know of does this, but no Java 176 // XML reader we know of is able to parse the bad output we'd 177 // otherwise generate. 178 // Note: tab, newline, and carriage return have already been 179 // handled above. 180 boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 181 if (allowedInXml) { 182 if (unicode || c < 127) { 183 append(c); 184 } else { 185 append("&#" + ((int) c) + ";"); 186 } 187 } else if (Character.isHighSurrogate(c) && i < s.length() - 1) { 188 writeSurrogate(c, s.charAt(i + 1)); 189 ++i; 190 } else { 191 reportInvalidCharacter(c); 192 } 193 // END Android-changed 194 } 195 } 196 } 197 198 // BEGIN Android-added reportInvalidCharacter(char ch)199 private static void reportInvalidCharacter(char ch) { 200 throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")"); 201 } 202 // END Android-added 203 204 /* 205 private final void writeIndent() throws IOException { 206 writer.write("\r\n"); 207 for (int i = 0; i < depth; i++) 208 writer.write(' '); 209 }*/ 210 docdecl(String dd)211 public void docdecl(String dd) throws IOException { 212 append("<!DOCTYPE"); 213 append(dd); 214 append('>'); 215 } 216 endDocument()217 public void endDocument() throws IOException { 218 while (depth > 0) { 219 endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]); 220 } 221 flush(); 222 } 223 entityRef(String name)224 public void entityRef(String name) throws IOException { 225 check(false); 226 append('&'); 227 append(name); 228 append(';'); 229 } 230 getFeature(String name)231 public boolean getFeature(String name) { 232 //return false; 233 return ( 234 "http://xmlpull.org/v1/doc/features.html#indent-output" 235 .equals( 236 name)) 237 ? indent[depth] 238 : false; 239 } 240 getPrefix(String namespace, boolean create)241 public String getPrefix(String namespace, boolean create) { 242 try { 243 return getPrefix(namespace, false, create); 244 } 245 catch (IOException e) { 246 throw new RuntimeException(e.toString()); 247 } 248 } 249 getPrefix( String namespace, boolean includeDefault, boolean create)250 private final String getPrefix( 251 String namespace, 252 boolean includeDefault, 253 boolean create) 254 throws IOException { 255 256 for (int i = nspCounts[depth + 1] * 2 - 2; 257 i >= 0; 258 i -= 2) { 259 if (nspStack[i + 1].equals(namespace) 260 && (includeDefault || !nspStack[i].isEmpty())) { 261 String cand = nspStack[i]; 262 for (int j = i + 2; 263 j < nspCounts[depth + 1] * 2; 264 j++) { 265 if (nspStack[j].equals(cand)) { 266 cand = null; 267 break; 268 } 269 } 270 if (cand != null) 271 return cand; 272 } 273 } 274 275 if (!create) 276 return null; 277 278 String prefix; 279 280 if (namespace.isEmpty()) 281 prefix = ""; 282 else { 283 do { 284 prefix = "n" + (auto++); 285 for (int i = nspCounts[depth + 1] * 2 - 2; 286 i >= 0; 287 i -= 2) { 288 if (prefix.equals(nspStack[i])) { 289 prefix = null; 290 break; 291 } 292 } 293 } 294 while (prefix == null); 295 } 296 297 boolean p = pending; 298 pending = false; 299 setPrefix(prefix, namespace); 300 pending = p; 301 return prefix; 302 } 303 getProperty(String name)304 public Object getProperty(String name) { 305 throw new RuntimeException("Unsupported property"); 306 } 307 ignorableWhitespace(String s)308 public void ignorableWhitespace(String s) 309 throws IOException { 310 text(s); 311 } 312 setFeature(String name, boolean value)313 public void setFeature(String name, boolean value) { 314 if ("http://xmlpull.org/v1/doc/features.html#indent-output" 315 .equals(name)) { 316 indent[depth] = value; 317 } 318 else 319 throw new RuntimeException("Unsupported Feature"); 320 } 321 setProperty(String name, Object value)322 public void setProperty(String name, Object value) { 323 throw new RuntimeException( 324 "Unsupported Property:" + value); 325 } 326 setPrefix(String prefix, String namespace)327 public void setPrefix(String prefix, String namespace) 328 throws IOException { 329 330 check(false); 331 if (prefix == null) 332 prefix = ""; 333 if (namespace == null) 334 namespace = ""; 335 336 String defined = getPrefix(namespace, true, false); 337 338 // boil out if already defined 339 340 if (prefix.equals(defined)) 341 return; 342 343 int pos = (nspCounts[depth + 1]++) << 1; 344 345 if (nspStack.length < pos + 1) { 346 String[] hlp = new String[nspStack.length + 16]; 347 System.arraycopy(nspStack, 0, hlp, 0, pos); 348 nspStack = hlp; 349 } 350 351 nspStack[pos++] = prefix; 352 nspStack[pos] = namespace; 353 } 354 setOutput(Writer writer)355 public void setOutput(Writer writer) { 356 this.writer = writer; 357 358 // elementStack = new String[12]; //nsp/prefix/name 359 //nspCounts = new int[4]; 360 //nspStack = new String[8]; //prefix/nsp 361 //indent = new boolean[4]; 362 363 nspCounts[0] = 2; 364 nspCounts[1] = 2; 365 nspStack[0] = ""; 366 nspStack[1] = ""; 367 nspStack[2] = "xml"; 368 nspStack[3] = "http://www.w3.org/XML/1998/namespace"; 369 pending = false; 370 auto = 0; 371 depth = 0; 372 373 unicode = false; 374 } 375 setOutput(OutputStream os, String encoding)376 public void setOutput(OutputStream os, String encoding) 377 throws IOException { 378 if (os == null) 379 throw new IllegalArgumentException("os == null"); 380 setOutput( 381 encoding == null 382 ? new OutputStreamWriter(os) 383 : new OutputStreamWriter(os, encoding)); 384 this.encoding = encoding; 385 if (encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")) { 386 unicode = true; 387 } 388 } 389 startDocument(String encoding, Boolean standalone)390 public void startDocument(String encoding, Boolean standalone) throws IOException { 391 append("<?xml version='1.0' "); 392 393 if (encoding != null) { 394 this.encoding = encoding; 395 if (encoding.toLowerCase(Locale.US).startsWith("utf")) { 396 unicode = true; 397 } 398 } 399 400 if (this.encoding != null) { 401 append("encoding='"); 402 append(this.encoding); 403 append("' "); 404 } 405 406 if (standalone != null) { 407 append("standalone='"); 408 append(standalone.booleanValue() ? "yes" : "no"); 409 append("' "); 410 } 411 append("?>"); 412 } 413 startTag(String namespace, String name)414 public XmlSerializer startTag(String namespace, String name) 415 throws IOException { 416 check(false); 417 418 // if (namespace == null) 419 // namespace = ""; 420 421 if (indent[depth]) { 422 // Android-changed: Speed-up indentation. http://b/230007772 423 // append("\r\n"); 424 // for (int i = 0; i < depth; i++) 425 // append(" "); 426 append('\r'); 427 append('\n'); 428 appendSpace(2 * depth); 429 430 } 431 432 int esp = depth * 3; 433 434 if (elementStack.length < esp + 3) { 435 String[] hlp = new String[elementStack.length + 12]; 436 System.arraycopy(elementStack, 0, hlp, 0, esp); 437 elementStack = hlp; 438 } 439 440 String prefix = 441 namespace == null 442 ? "" 443 : getPrefix(namespace, true, true); 444 445 if (namespace != null && namespace.isEmpty()) { 446 for (int i = nspCounts[depth]; 447 i < nspCounts[depth + 1]; 448 i++) { 449 if (nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()) { 450 throw new IllegalStateException("Cannot set default namespace for elements in no namespace"); 451 } 452 } 453 } 454 455 elementStack[esp++] = namespace; 456 elementStack[esp++] = prefix; 457 elementStack[esp] = name; 458 459 append('<'); 460 if (!prefix.isEmpty()) { 461 append(prefix); 462 append(':'); 463 } 464 465 append(name); 466 467 pending = true; 468 469 return this; 470 } 471 attribute( String namespace, String name, String value)472 public XmlSerializer attribute( 473 String namespace, 474 String name, 475 String value) 476 throws IOException { 477 if (!pending) 478 throw new IllegalStateException("illegal position for attribute"); 479 480 // int cnt = nspCounts[depth]; 481 482 if (namespace == null) 483 namespace = ""; 484 485 // depth--; 486 // pending = false; 487 488 String prefix = 489 namespace.isEmpty() 490 ? "" 491 : getPrefix(namespace, false, true); 492 493 // pending = true; 494 // depth++; 495 496 /* if (cnt != nspCounts[depth]) { 497 writer.write(' '); 498 writer.write("xmlns"); 499 if (nspStack[cnt * 2] != null) { 500 writer.write(':'); 501 writer.write(nspStack[cnt * 2]); 502 } 503 writer.write("=\""); 504 writeEscaped(nspStack[cnt * 2 + 1], '"'); 505 writer.write('"'); 506 } 507 */ 508 509 append(' '); 510 if (!prefix.isEmpty()) { 511 append(prefix); 512 append(':'); 513 } 514 append(name); 515 append('='); 516 char q = value.indexOf('"') == -1 ? '"' : '\''; 517 append(q); 518 writeEscaped(value, q); 519 append(q); 520 521 return this; 522 } 523 flush()524 public void flush() throws IOException { 525 check(false); 526 flushBuffer(); 527 } 528 /* 529 public void close() throws IOException { 530 check(); 531 writer.close(); 532 } 533 */ endTag(String namespace, String name)534 public XmlSerializer endTag(String namespace, String name) 535 throws IOException { 536 537 if (!pending) 538 depth--; 539 // if (namespace == null) 540 // namespace = ""; 541 542 if ((namespace == null 543 && elementStack[depth * 3] != null) 544 || (namespace != null 545 && !namespace.equals(elementStack[depth * 3])) 546 || !elementStack[depth * 3 + 2].equals(name)) 547 throw new IllegalArgumentException("</{"+namespace+"}"+name+"> does not match start"); 548 549 if (pending) { 550 check(true); 551 depth--; 552 } 553 else { 554 if (indent[depth + 1]) { 555 // Android-changed: Speed-up indentation. http://b/230007772 556 // append("\r\n"); 557 // for (int i = 0; i < depth; i++) 558 // append(" "); 559 append('\r'); 560 append('\n'); 561 appendSpace(2 * depth); 562 } 563 564 append("</"); 565 String prefix = elementStack[depth * 3 + 1]; 566 if (!prefix.isEmpty()) { 567 append(prefix); 568 append(':'); 569 } 570 append(name); 571 append('>'); 572 } 573 574 nspCounts[depth + 1] = nspCounts[depth]; 575 return this; 576 } 577 getNamespace()578 public String getNamespace() { 579 return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3]; 580 } 581 getName()582 public String getName() { 583 return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1]; 584 } 585 getDepth()586 public int getDepth() { 587 return pending ? depth + 1 : depth; 588 } 589 text(String text)590 public XmlSerializer text(String text) throws IOException { 591 check(false); 592 indent[depth] = false; 593 writeEscaped(text, -1); 594 return this; 595 } 596 text(char[] text, int start, int len)597 public XmlSerializer text(char[] text, int start, int len) 598 throws IOException { 599 text(new String(text, start, len)); 600 return this; 601 } 602 cdsect(String data)603 public void cdsect(String data) throws IOException { 604 check(false); 605 // BEGIN Android-changed: ]]> is not allowed within a CDATA, 606 // so break and start a new one when necessary. 607 data = data.replace("]]>", "]]]]><![CDATA[>"); 608 append("<![CDATA["); 609 for (int i = 0; i < data.length(); ++i) { 610 char ch = data.charAt(i); 611 boolean allowedInCdata = (ch >= 0x20 && ch <= 0xd7ff) || 612 (ch == '\t' || ch == '\n' || ch == '\r') || 613 (ch >= 0xe000 && ch <= 0xfffd); 614 if (allowedInCdata) { 615 append(ch); 616 } else if (Character.isHighSurrogate(ch) && i < data.length() - 1) { 617 // Character entities aren't valid in CDATA, so break out for this. 618 append("]]>"); 619 writeSurrogate(ch, data.charAt(++i)); 620 append("<![CDATA["); 621 } else { 622 reportInvalidCharacter(ch); 623 } 624 } 625 append("]]>"); 626 // END Android-changed 627 } 628 629 // BEGIN Android-added writeSurrogate(char high, char low)630 private void writeSurrogate(char high, char low) throws IOException { 631 if (!Character.isLowSurrogate(low)) { 632 throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) + 633 " U+" + Integer.toHexString((int) low) + ")"); 634 } 635 // Java-style surrogate pairs aren't allowed in XML. We could use the > 3-byte encodings, but that 636 // seems likely to upset anything expecting modified UTF-8 rather than "real" UTF-8. It seems more 637 // conservative in a Java environment to use an entity reference instead. 638 int codePoint = Character.toCodePoint(high, low); 639 append("&#" + codePoint + ";"); 640 } 641 // END Android-added 642 comment(String comment)643 public void comment(String comment) throws IOException { 644 check(false); 645 append("<!--"); 646 append(comment); 647 append("-->"); 648 } 649 processingInstruction(String pi)650 public void processingInstruction(String pi) 651 throws IOException { 652 check(false); 653 append("<?"); 654 append(pi); 655 append("?>"); 656 } 657 } 658