Home | History | Annotate | Download | only in io
      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     //    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