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 
append(char c)51     private void append(char c) throws IOException {
52         if (mPos >= BUFFER_LEN) {
53             flushBuffer();
54         }
55         mText[mPos++] = c;
56     }
57 
append(String str, int i, int length)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 
append(String str)74     private void append(String str) throws IOException {
75         append(str, 0, str.length());
76     }
77 
flushBuffer()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 
check(boolean close)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 
writeEscaped(String s, int quot)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("&amp;");
143                     break;
144                 case '>' :
145                     append("&gt;");
146                     break;
147                 case '<' :
148                     append("&lt;");
149                     break;
150                 default:
151                     if (c == quot) {
152                         append(c == '"' ? "&quot;" : "&apos;");
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
reportInvalidCharacter(char ch)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 
docdecl(String dd)193     public void docdecl(String dd) throws IOException {
194         append("<!DOCTYPE");
195         append(dd);
196         append('>');
197     }
198 
endDocument()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 
entityRef(String name)206     public void entityRef(String name) throws IOException {
207         check(false);
208         append('&');
209         append(name);
210         append(';');
211     }
212 
getFeature(String name)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 
getPrefix(String namespace, boolean create)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 
getPrefix( String namespace, boolean includeDefault, boolean create)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 
getProperty(String name)286     public Object getProperty(String name) {
287         throw new RuntimeException("Unsupported property");
288     }
289 
ignorableWhitespace(String s)290     public void ignorableWhitespace(String s)
291         throws IOException {
292         text(s);
293     }
294 
setFeature(String name, boolean value)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 
setProperty(String name, Object value)304     public void setProperty(String name, Object value) {
305         throw new RuntimeException(
306             "Unsupported Property:" + value);
307     }
308 
setPrefix(String prefix, String namespace)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 
setOutput(Writer writer)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 
setOutput(OutputStream os, String encoding)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 
startDocument(String encoding, Boolean standalone)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 
startTag(String namespace, String name)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 
attribute( String namespace, String name, String value)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 
flush()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     */
endTag(String namespace, String name)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 
getNamespace()551     public String getNamespace() {
552         return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3];
553     }
554 
getName()555     public String getName() {
556         return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1];
557     }
558 
getDepth()559     public int getDepth() {
560         return pending ? depth + 1 : depth;
561     }
562 
text(String text)563     public XmlSerializer text(String text) throws IOException {
564         check(false);
565         indent[depth] = false;
566         writeEscaped(text, -1);
567         return this;
568     }
569 
text(char[] text, int start, int len)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 
cdsect(String data)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
writeSurrogate(char high, char low)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 
comment(String comment)616     public void comment(String comment) throws IOException {
617         check(false);
618         append("<!--");
619         append(comment);
620         append("-->");
621     }
622 
processingInstruction(String pi)623     public void processingInstruction(String pi)
624         throws IOException {
625         check(false);
626         append("<?");
627         append(pi);
628         append("?>");
629     }
630 }
631