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("&amp;");
161                     break;
162                 case '>' :
163                     append("&gt;");
164                     break;
165                 case '<' :
166                     append("&lt;");
167                     break;
168                 default:
169                     if (c == quot) {
170                         append(c == '"' ? "&quot;" : "&apos;");
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