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     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