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
22package org.kxml2.io;
23
24import java.io.*;
25import java.util.Locale;
26import org.xmlpull.v1.*;
27
28public 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("&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
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