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