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 allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
    129                     if (allowedInXml) {
    130                         if (unicode || c < 127) {
    131                             writer.write(c);
    132                         } else {
    133                             writer.write("&#" + ((int) c) + ";");
    134                         }
    135                     } else if (Character.isHighSurrogate(c) && i < s.length() - 1) {
    136                         writeSurrogate(c, s.charAt(i + 1));
    137                         ++i;
    138                     } else {
    139                         reportInvalidCharacter(c);
    140                     }
    141                     // END android-changed
    142             }
    143         }
    144     }
    145 
    146     // BEGIN android-added
    147     private static void reportInvalidCharacter(char ch) {
    148         throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")");
    149     }
    150     // END android-added
    151 
    152     /*
    153         private final void writeIndent() throws IOException {
    154             writer.write("\r\n");
    155             for (int i = 0; i < depth; i++)
    156                 writer.write(' ');
    157         }*/
    158 
    159     public void docdecl(String dd) throws IOException {
    160         writer.write("<!DOCTYPE");
    161         writer.write(dd);
    162         writer.write(">");
    163     }
    164 
    165     public void endDocument() throws IOException {
    166         while (depth > 0) {
    167             endTag(elementStack[depth * 3 - 3], elementStack[depth * 3 - 1]);
    168         }
    169         flush();
    170     }
    171 
    172     public void entityRef(String name) throws IOException {
    173         check(false);
    174         writer.write('&');
    175         writer.write(name);
    176         writer.write(';');
    177     }
    178 
    179     public boolean getFeature(String name) {
    180         //return false;
    181         return (
    182             "http://xmlpull.org/v1/doc/features.html#indent-output"
    183                 .equals(
    184                 name))
    185             ? indent[depth]
    186             : false;
    187     }
    188 
    189     public String getPrefix(String namespace, boolean create) {
    190         try {
    191             return getPrefix(namespace, false, create);
    192         }
    193         catch (IOException e) {
    194             throw new RuntimeException(e.toString());
    195         }
    196     }
    197 
    198     private final String getPrefix(
    199         String namespace,
    200         boolean includeDefault,
    201         boolean create)
    202         throws IOException {
    203 
    204         for (int i = nspCounts[depth + 1] * 2 - 2;
    205             i >= 0;
    206             i -= 2) {
    207             if (nspStack[i + 1].equals(namespace)
    208                 && (includeDefault || !nspStack[i].isEmpty())) {
    209                 String cand = nspStack[i];
    210                 for (int j = i + 2;
    211                     j < nspCounts[depth + 1] * 2;
    212                     j++) {
    213                     if (nspStack[j].equals(cand)) {
    214                         cand = null;
    215                         break;
    216                     }
    217                 }
    218                 if (cand != null)
    219                     return cand;
    220             }
    221         }
    222 
    223         if (!create)
    224             return null;
    225 
    226         String prefix;
    227 
    228         if (namespace.isEmpty())
    229             prefix = "";
    230         else {
    231             do {
    232                 prefix = "n" + (auto++);
    233                 for (int i = nspCounts[depth + 1] * 2 - 2;
    234                     i >= 0;
    235                     i -= 2) {
    236                     if (prefix.equals(nspStack[i])) {
    237                         prefix = null;
    238                         break;
    239                     }
    240                 }
    241             }
    242             while (prefix == null);
    243         }
    244 
    245         boolean p = pending;
    246         pending = false;
    247         setPrefix(prefix, namespace);
    248         pending = p;
    249         return prefix;
    250     }
    251 
    252     public Object getProperty(String name) {
    253         throw new RuntimeException("Unsupported property");
    254     }
    255 
    256     public void ignorableWhitespace(String s)
    257         throws IOException {
    258         text(s);
    259     }
    260 
    261     public void setFeature(String name, boolean value) {
    262         if ("http://xmlpull.org/v1/doc/features.html#indent-output"
    263             .equals(name)) {
    264             indent[depth] = value;
    265         }
    266         else
    267             throw new RuntimeException("Unsupported Feature");
    268     }
    269 
    270     public void setProperty(String name, Object value) {
    271         throw new RuntimeException(
    272             "Unsupported Property:" + value);
    273     }
    274 
    275     public void setPrefix(String prefix, String namespace)
    276         throws IOException {
    277 
    278         check(false);
    279         if (prefix == null)
    280             prefix = "";
    281         if (namespace == null)
    282             namespace = "";
    283 
    284         String defined = getPrefix(namespace, true, false);
    285 
    286         // boil out if already defined
    287 
    288         if (prefix.equals(defined))
    289             return;
    290 
    291         int pos = (nspCounts[depth + 1]++) << 1;
    292 
    293         if (nspStack.length < pos + 1) {
    294             String[] hlp = new String[nspStack.length + 16];
    295             System.arraycopy(nspStack, 0, hlp, 0, pos);
    296             nspStack = hlp;
    297         }
    298 
    299         nspStack[pos++] = prefix;
    300         nspStack[pos] = namespace;
    301     }
    302 
    303     public void setOutput(Writer writer) {
    304         // BEGIN android-changed
    305         // Guarantee that the writer is always buffered.
    306         if (writer instanceof BufferedWriter) {
    307             this.writer = (BufferedWriter) writer;
    308         } else {
    309             this.writer = new BufferedWriter(writer, WRITE_BUFFER_SIZE);
    310         }
    311         // END android-changed
    312 
    313         // elementStack = new String[12]; //nsp/prefix/name
    314         //nspCounts = new int[4];
    315         //nspStack = new String[8]; //prefix/nsp
    316         //indent = new boolean[4];
    317 
    318         nspCounts[0] = 2;
    319         nspCounts[1] = 2;
    320         nspStack[0] = "";
    321         nspStack[1] = "";
    322         nspStack[2] = "xml";
    323         nspStack[3] = "http://www.w3.org/XML/1998/namespace";
    324         pending = false;
    325         auto = 0;
    326         depth = 0;
    327 
    328         unicode = false;
    329     }
    330 
    331     public void setOutput(OutputStream os, String encoding)
    332         throws IOException {
    333         if (os == null)
    334             throw new IllegalArgumentException("os == null");
    335         setOutput(
    336             encoding == null
    337                 ? new OutputStreamWriter(os)
    338                 : new OutputStreamWriter(os, encoding));
    339         this.encoding = encoding;
    340         if (encoding != null && encoding.toLowerCase(Locale.US).startsWith("utf")) {
    341             unicode = true;
    342         }
    343     }
    344 
    345     public void startDocument(String encoding, Boolean standalone) throws IOException {
    346         writer.write("<?xml version='1.0' ");
    347 
    348         if (encoding != null) {
    349             this.encoding = encoding;
    350             if (encoding.toLowerCase(Locale.US).startsWith("utf")) {
    351                 unicode = true;
    352             }
    353         }
    354 
    355         if (this.encoding != null) {
    356             writer.write("encoding='");
    357             writer.write(this.encoding);
    358             writer.write("' ");
    359         }
    360 
    361         if (standalone != null) {
    362             writer.write("standalone='");
    363             writer.write(
    364                 standalone.booleanValue() ? "yes" : "no");
    365             writer.write("' ");
    366         }
    367         writer.write("?>");
    368     }
    369 
    370     public XmlSerializer startTag(String namespace, String name)
    371         throws IOException {
    372         check(false);
    373 
    374         //        if (namespace == null)
    375         //            namespace = "";
    376 
    377         if (indent[depth]) {
    378             writer.write("\r\n");
    379             for (int i = 0; i < depth; i++)
    380                 writer.write("  ");
    381         }
    382 
    383         int esp = depth * 3;
    384 
    385         if (elementStack.length < esp + 3) {
    386             String[] hlp = new String[elementStack.length + 12];
    387             System.arraycopy(elementStack, 0, hlp, 0, esp);
    388             elementStack = hlp;
    389         }
    390 
    391         String prefix =
    392             namespace == null
    393                 ? ""
    394                 : getPrefix(namespace, true, true);
    395 
    396         if (namespace != null && namespace.isEmpty()) {
    397             for (int i = nspCounts[depth];
    398                 i < nspCounts[depth + 1];
    399                 i++) {
    400                 if (nspStack[i * 2].isEmpty() && !nspStack[i * 2 + 1].isEmpty()) {
    401                     throw new IllegalStateException("Cannot set default namespace for elements in no namespace");
    402                 }
    403             }
    404         }
    405 
    406         elementStack[esp++] = namespace;
    407         elementStack[esp++] = prefix;
    408         elementStack[esp] = name;
    409 
    410         writer.write('<');
    411         if (!prefix.isEmpty()) {
    412             writer.write(prefix);
    413             writer.write(':');
    414         }
    415 
    416         writer.write(name);
    417 
    418         pending = true;
    419 
    420         return this;
    421     }
    422 
    423     public XmlSerializer attribute(
    424         String namespace,
    425         String name,
    426         String value)
    427         throws IOException {
    428         if (!pending)
    429             throw new IllegalStateException("illegal position for attribute");
    430 
    431         //        int cnt = nspCounts[depth];
    432 
    433         if (namespace == null)
    434             namespace = "";
    435 
    436         //        depth--;
    437         //        pending = false;
    438 
    439         String prefix =
    440             namespace.isEmpty()
    441                 ? ""
    442                 : getPrefix(namespace, false, true);
    443 
    444         //        pending = true;
    445         //        depth++;
    446 
    447         /*        if (cnt != nspCounts[depth]) {
    448                     writer.write(' ');
    449                     writer.write("xmlns");
    450                     if (nspStack[cnt * 2] != null) {
    451                         writer.write(':');
    452                         writer.write(nspStack[cnt * 2]);
    453                     }
    454                     writer.write("=\"");
    455                     writeEscaped(nspStack[cnt * 2 + 1], '"');
    456                     writer.write('"');
    457                 }
    458                 */
    459 
    460         writer.write(' ');
    461         if (!prefix.isEmpty()) {
    462             writer.write(prefix);
    463             writer.write(':');
    464         }
    465         writer.write(name);
    466         writer.write('=');
    467         char q = value.indexOf('"') == -1 ? '"' : '\'';
    468         writer.write(q);
    469         writeEscaped(value, q);
    470         writer.write(q);
    471 
    472         return this;
    473     }
    474 
    475     public void flush() throws IOException {
    476         check(false);
    477         writer.flush();
    478     }
    479     /*
    480         public void close() throws IOException {
    481             check();
    482             writer.close();
    483         }
    484     */
    485     public XmlSerializer endTag(String namespace, String name)
    486         throws IOException {
    487 
    488         if (!pending)
    489             depth--;
    490         //        if (namespace == null)
    491         //          namespace = "";
    492 
    493         if ((namespace == null
    494             && elementStack[depth * 3] != null)
    495             || (namespace != null
    496                 && !namespace.equals(elementStack[depth * 3]))
    497             || !elementStack[depth * 3 + 2].equals(name))
    498             throw new IllegalArgumentException("</{"+namespace+"}"+name+"> does not match start");
    499 
    500         if (pending) {
    501             check(true);
    502             depth--;
    503         }
    504         else {
    505             if (indent[depth + 1]) {
    506                 writer.write("\r\n");
    507                 for (int i = 0; i < depth; i++)
    508                     writer.write("  ");
    509             }
    510 
    511             writer.write("</");
    512             String prefix = elementStack[depth * 3 + 1];
    513             if (!prefix.isEmpty()) {
    514                 writer.write(prefix);
    515                 writer.write(':');
    516             }
    517             writer.write(name);
    518             writer.write('>');
    519         }
    520 
    521         nspCounts[depth + 1] = nspCounts[depth];
    522         return this;
    523     }
    524 
    525     public String getNamespace() {
    526         return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3];
    527     }
    528 
    529     public String getName() {
    530         return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1];
    531     }
    532 
    533     public int getDepth() {
    534         return pending ? depth + 1 : depth;
    535     }
    536 
    537     public XmlSerializer text(String text) throws IOException {
    538         check(false);
    539         indent[depth] = false;
    540         writeEscaped(text, -1);
    541         return this;
    542     }
    543 
    544     public XmlSerializer text(char[] text, int start, int len)
    545         throws IOException {
    546         text(new String(text, start, len));
    547         return this;
    548     }
    549 
    550     public void cdsect(String data) throws IOException {
    551         check(false);
    552         // BEGIN android-changed: ]]> is not allowed within a CDATA,
    553         // so break and start a new one when necessary.
    554         data = data.replace("]]>", "]]]]><![CDATA[>");
    555         writer.write("<![CDATA[");
    556         for (int i = 0; i < data.length(); ++i) {
    557             char ch = data.charAt(i);
    558             boolean allowedInCdata = (ch >= 0x20 && ch <= 0xd7ff) ||
    559                     (ch == '\t' || ch == '\n' || ch == '\r') ||
    560                     (ch >= 0xe000 && ch <= 0xfffd);
    561             if (allowedInCdata) {
    562                 writer.write(ch);
    563             } else if (Character.isHighSurrogate(ch) && i < data.length() - 1) {
    564                 // Character entities aren't valid in CDATA, so break out for this.
    565                 writer.write("]]>");
    566                 writeSurrogate(ch, data.charAt(++i));
    567                 writer.write("<![CDATA[");
    568             } else {
    569                 reportInvalidCharacter(ch);
    570             }
    571         }
    572         writer.write("]]>");
    573         // END android-changed
    574     }
    575 
    576     // BEGIN android-added
    577     private void writeSurrogate(char high, char low) throws IOException {
    578         if (!Character.isLowSurrogate(low)) {
    579             throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) +
    580                                                " U+" + Integer.toHexString((int) low) + ")");
    581         }
    582         // Java-style surrogate pairs aren't allowed in XML. We could use the > 3-byte encodings, but that
    583         // seems likely to upset anything expecting modified UTF-8 rather than "real" UTF-8. It seems more
    584         // conservative in a Java environment to use an entity reference instead.
    585         int codePoint = Character.toCodePoint(high, low);
    586         writer.write("&#" + codePoint + ";");
    587     }
    588     // END android-added
    589 
    590     public void comment(String comment) throws IOException {
    591         check(false);
    592         writer.write("<!--");
    593         writer.write(comment);
    594         writer.write("-->");
    595     }
    596 
    597     public void processingInstruction(String pi)
    598         throws IOException {
    599         check(false);
    600         writer.write("<?");
    601         writer.write(pi);
    602         writer.write("?>");
    603     }
    604 }
    605