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