    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.io.input;
    018
    019    import java.io.BufferedInputStream;
    020    import java.io.BufferedReader;
    021    import java.io.File;
    022    import java.io.FileInputStream;
    023    import java.io.IOException;
    024    import java.io.InputStream;
    025    import java.io.InputStreamReader;
    026    import java.io.Reader;
    027    import java.io.StringReader;
    028    import java.net.HttpURLConnection;
    029    import java.net.URL;
    030    import java.net.URLConnection;
    031    import java.text.MessageFormat;
    032    import java.util.regex.Matcher;
    033    import java.util.regex.Pattern;
    034
    035    import org.apache.commons.io.ByteOrderMark;
    036
    037    /**
    038     * Character stream that handles all the necessary Voodo to figure out the
    039     * charset encoding of the XML document within the stream.
    040     * <p>
    041     * IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader.
    042     * This one IS a character stream.
    043     * <p>
    044     * All this has to be done without consuming characters from the stream, if not
    045     * the XML parser will not recognized the document as a valid XML. This is not
    046     * 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers
    047     * right now, XmlStreamReader handles it and things work in all parsers).
    048     * <p>
    049     * The XmlStreamReader class handles the charset encoding of XML documents in
    050     * Files, raw streams and HTTP streams by offering a wide set of constructors.
    051     * <p>
    052     * By default the charset encoding detection is lenient, the constructor with
    053     * the lenient flag can be used for an script (following HTTP MIME and XML
    054     * specifications). All this is nicely explained by Mark Pilgrim in his blog, <a
    055     * href="http://diveintomark.org/archives/2004/02/13/xml-media-types">
    056     * Determining the character encoding of a feed</a>.
    057     * <p>
    058     * Originally developed for <a href="http://rome.dev.java.net">ROME</a> under
    059     * Apache License 2.0.
    060     *
    061     * @author Alejandro Abdelnur
    062     * @version $Id: XmlStreamReader.java 1052161 2010-12-23 03:12:09Z niallp $
    063     * @see org.apache.commons.io.output.XmlStreamWriter
    064     * @since Commons IO 2.0
    065     */
    066    public class XmlStreamReader extends Reader {
    067        private static final int BUFFER_SIZE = 4096;
    068
    069        private static final String UTF_8 = "UTF-8";
    070
    071        private static final String US_ASCII = "US-ASCII";
    072
    073        private static final String UTF_16BE = "UTF-16BE";
    074
    075        private static final String UTF_16LE = "UTF-16LE";
    076
    077        private static final String UTF_16 = "UTF-16";
    078
    079        private static final String EBCDIC = "CP1047";
    080
    081        private static final ByteOrderMark[] BOMS = new ByteOrderMark[] {
    082            ByteOrderMark.UTF_8,
    083            ByteOrderMark.UTF_16BE,
    084            ByteOrderMark.UTF_16LE
    085        };
    086        private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[] {
    087            new ByteOrderMark(UTF_8,    0x3C, 0x3F, 0x78, 0x6D),
    088            new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F),
    089            new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00),
    090            new ByteOrderMark(EBCDIC,   0x4C, 0x6F, 0xA7, 0x94)
    091        };
    092
    093
    094        private final Reader reader;
    095
    096        private final String encoding;
    097
    098        private final String defaultEncoding;
    099
    100        /**
    101         * Returns the default encoding to use if none is set in HTTP content-type,
    102         * XML prolog and the rules based on content-type are not adequate.
    103         * <p>
    104         * If it is NULL the content-type based rules are used.
    105         *
    106         * @return the default encoding to use.
    107         */
    108        public String getDefaultEncoding() {
    109            return defaultEncoding;
    110        }
    111
    112        /**
    113         * Creates a Reader for a File.
    114         * <p>
    115         * It looks for the UTF-8 BOM first, if none sniffs the XML prolog charset,
    116         * if this is also missing defaults to UTF-8.
    117         * <p>
    118         * It does a lenient charset encoding detection, check the constructor with
    119         * the lenient parameter for details.
    120         *
    121         * @param file File to create a Reader from.
    122         * @throws IOException thrown if there is a problem reading the file.
    123         */
    124        public XmlStreamReader(File file) throws IOException {
    125            this(new FileInputStream(file));
    126        }
    127
    128        /**
    129         * Creates a Reader for a raw InputStream.
    130         * <p>
    131         * It follows the same logic used for files.
    132         * <p>
    133         * It does a lenient charset encoding detection, check the constructor with
    134         * the lenient parameter for details.
    135         *
    136         * @param is InputStream to create a Reader from.
    137         * @throws IOException thrown if there is a problem reading the stream.
    138         */
    139        public XmlStreamReader(InputStream is) throws IOException {
    140            this(is, true);
    141        }
    142
    143        /**
    144         * Creates a Reader for a raw InputStream.
    145         * <p>
    146         * It follows the same logic used for files.
    147         * <p>
    148         * If lenient detection is indicated and the detection above fails as per
    149         * specifications it then attempts the following:
    150         * <p>
    151         * If the content type was 'text/html' it replaces it with 'text/xml' and
    152         * tries the detection again.
    153         * <p>
    154         * Else if the XML prolog had a charset encoding that encoding is used.
    155         * <p>
    156         * Else if the content type had a charset encoding that encoding is used.
    157         * <p>
    158         * Else 'UTF-8' is used.
    159         * <p>
    160         * If lenient detection is indicated an XmlStreamReaderException is never
    161         * thrown.
    162         *
    163         * @param is InputStream to create a Reader from.
    164         * @param lenient indicates if the charset encoding detection should be
    165         *        relaxed.
    166         * @throws IOException thrown if there is a problem reading the stream.
    167         * @throws XmlStreamReaderException thrown if the charset encoding could not
    168         *         be determined according to the specs.
    169         */
    170        public XmlStreamReader(InputStream is, boolean lenient) throws IOException {
    171            this(is, lenient, null);
    172        }
    173
    174        /**
    175         * Creates a Reader for a raw InputStream.
    176         * <p>
    177         * It follows the same logic used for files.
    178         * <p>
    179         * If lenient detection is indicated and the detection above fails as per
    180         * specifications it then attempts the following:
    181         * <p>
    182         * If the content type was 'text/html' it replaces it with 'text/xml' and
    183         * tries the detection again.
    184         * <p>
    185         * Else if the XML prolog had a charset encoding that encoding is used.
    186         * <p>
    187         * Else if the content type had a charset encoding that encoding is used.
    188         * <p>
    189         * Else 'UTF-8' is used.
    190         * <p>
    191         * If lenient detection is indicated an XmlStreamReaderException is never
    192         * thrown.
    193         *
    194         * @param is InputStream to create a Reader from.
    195         * @param lenient indicates if the charset encoding detection should be
    196         *        relaxed.
    197         * @param defaultEncoding The default encoding
    198         * @throws IOException thrown if there is a problem reading the stream.
    199         * @throws XmlStreamReaderException thrown if the charset encoding could not
    200         *         be determined according to the specs.
    201         */
    202        public XmlStreamReader(InputStream is, boolean lenient, String defaultEncoding) throws IOException {
    203            this.defaultEncoding = defaultEncoding;
    204            BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS);
    205            BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
    206            this.encoding = doRawStream(bom, pis, lenient);
    207            this.reader = new InputStreamReader(pis, encoding);
    208        }
    209
    210        /**
    211         * Creates a Reader using the InputStream of a URL.
    212         * <p>
    213         * If the URL is not of type HTTP and there is not 'content-type' header in
    214         * the fetched data it uses the same logic used for Files.
    215         * <p>
    216         * If the URL is a HTTP Url or there is a 'content-type' header in the
    217         * fetched data it uses the same logic used for an InputStream with
    218         * content-type.
    219         * <p>
    220         * It does a lenient charset encoding detection, check the constructor with
    221         * the lenient parameter for details.
    222         *
    223         * @param url URL to create a Reader from.
    224         * @throws IOException thrown if there is a problem reading the stream of
    225         *         the URL.
    226         */
    227        public XmlStreamReader(URL url) throws IOException {
    228            this(url.openConnection(), null);
    229        }
    230
    231        /**
    232         * Creates a Reader using the InputStream of a URLConnection.
    233         * <p>
    234         * If the URLConnection is not of type HttpURLConnection and there is not
    235         * 'content-type' header in the fetched data it uses the same logic used for
    236         * files.
    237         * <p>
    238         * If the URLConnection is a HTTP Url or there is a 'content-type' header in
    239         * the fetched data it uses the same logic used for an InputStream with
    240         * content-type.
    241         * <p>
    242         * It does a lenient charset encoding detection, check the constructor with
    243         * the lenient parameter for details.
    244         *
    245         * @param conn URLConnection to create a Reader from.
    246         * @param defaultEncoding The default encoding
    247         * @throws IOException thrown if there is a problem reading the stream of
    248         *         the URLConnection.
    249         */
    250        public XmlStreamReader(URLConnection conn, String defaultEncoding) throws IOException {
    251            this.defaultEncoding = defaultEncoding;
    252            boolean lenient = true;
    253            String contentType = conn.getContentType();
    254            InputStream is = conn.getInputStream();
    255            BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS);
    256            BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
    257            if (conn instanceof HttpURLConnection || contentType != null) {
    258                this.encoding = doHttpStream(bom, pis, contentType, lenient);
    259            } else {
    260                this.encoding = doRawStream(bom, pis, lenient);
    261            }
    262            this.reader = new InputStreamReader(pis, encoding);
    263        }
    264
    265        /**
    266         * Creates a Reader using an InputStream an the associated content-type
    267         * header.
    268         * <p>
    269         * First it checks if the stream has BOM. If there is not BOM checks the
    270         * content-type encoding. If there is not content-type encoding checks the
    271         * XML prolog encoding. If there is not XML prolog encoding uses the default
    272         * encoding mandated by the content-type MIME type.
    273         * <p>
    274         * It does a lenient charset encoding detection, check the constructor with
    275         * the lenient parameter for details.
    276         *
    277         * @param is InputStream to create the reader from.
    278         * @param httpContentType content-type header to use for the resolution of
    279         *        the charset encoding.
    280         * @throws IOException thrown if there is a problem reading the file.
    281         */
    282        public XmlStreamReader(InputStream is, String httpContentType)
    283                throws IOException {
    284            this(is, httpContentType, true);
    285        }
    286
    287        /**
    288         * Creates a Reader using an InputStream an the associated content-type
    289         * header. This constructor is lenient regarding the encoding detection.
    290         * <p>
    291         * First it checks if the stream has BOM. If there is not BOM checks the
    292         * content-type encoding. If there is not content-type encoding checks the
    293         * XML prolog encoding. If there is not XML prolog encoding uses the default
    294         * encoding mandated by the content-type MIME type.
    295         * <p>
    296         * If lenient detection is indicated and the detection above fails as per
    297         * specifications it then attempts the following:
    298         * <p>
    299         * If the content type was 'text/html' it replaces it with 'text/xml' and
    300         * tries the detection again.
    301         * <p>
    302         * Else if the XML prolog had a charset encoding that encoding is used.
    303         * <p>
    304         * Else if the content type had a charset encoding that encoding is used.
    305         * <p>
    306         * Else 'UTF-8' is used.
    307         * <p>
    308         * If lenient detection is indicated an XmlStreamReaderException is never
    309         * thrown.
    310         *
    311         * @param is InputStream to create the reader from.
    312         * @param httpContentType content-type header to use for the resolution of
    313         *        the charset encoding.
    314         * @param lenient indicates if the charset encoding detection should be
    315         *        relaxed.
    316         * @param defaultEncoding The default encoding
    317         * @throws IOException thrown if there is a problem reading the file.
    318         * @throws XmlStreamReaderException thrown if the charset encoding could not
    319         *         be determined according to the specs.
    320         */
    321        public XmlStreamReader(InputStream is, String httpContentType,
    322                boolean lenient, String defaultEncoding) throws IOException {
    323            this.defaultEncoding = defaultEncoding;
    324            BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, BUFFER_SIZE), false, BOMS);
    325            BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
    326            this.encoding = doHttpStream(bom, pis, httpContentType, lenient);
    327            this.reader = new InputStreamReader(pis, encoding);
    328        }
    329
    330        /**
    331         * Creates a Reader using an InputStream an the associated content-type
    332         * header. This constructor is lenient regarding the encoding detection.
    333         * <p>
    334         * First it checks if the stream has BOM. If there is not BOM checks the
    335         * content-type encoding. If there is not content-type encoding checks the
    336         * XML prolog encoding. If there is not XML prolog encoding uses the default
    337         * encoding mandated by the content-type MIME type.
    338         * <p>
    339         * If lenient detection is indicated and the detection above fails as per
    340         * specifications it then attempts the following:
    341         * <p>
    342         * If the content type was 'text/html' it replaces it with 'text/xml' and
    343         * tries the detection again.
    344         * <p>
    345         * Else if the XML prolog had a charset encoding that encoding is used.
    346         * <p>
    347         * Else if the content type had a charset encoding that encoding is used.
    348         * <p>
    349         * Else 'UTF-8' is used.
    350         * <p>
    351         * If lenient detection is indicated an XmlStreamReaderException is never
    352         * thrown.
    353         *
    354         * @param is InputStream to create the reader from.
    355         * @param httpContentType content-type header to use for the resolution of
    356         *        the charset encoding.
    357         * @param lenient indicates if the charset encoding detection should be
    358         *        relaxed.
    359         * @throws IOException thrown if there is a problem reading the file.
    360         * @throws XmlStreamReaderException thrown if the charset encoding could not
    361         *         be determined according to the specs.
    362         */
    363        public XmlStreamReader(InputStream is, String httpContentType,
    364                boolean lenient) throws IOException {
    365            this(is, httpContentType, lenient, null);
    366        }
    367
    368        /**
    369         * Returns the charset encoding of the XmlStreamReader.
    370         *
    371         * @return charset encoding.
    372         */
    373        public String getEncoding() {
    374            return encoding;
    375        }
    376
    377        /**
    378         * Invokes the underlying reader's <code>read(char[], int, int)</code> method.
    379         * @param buf the buffer to read the characters into
    380         * @param offset The start offset
    381         * @param len The number of bytes to read
    382         * @return the number of characters read or -1 if the end of stream
    383         * @throws IOException if an I/O error occurs
    384         */
    385        @Override
    386        public int read(char[] buf, int offset, int len) throws IOException {
    387            return reader.read(buf, offset, len);
    388        }
    389
    390        /**
    391         * Closes the XmlStreamReader stream.
    392         *
    393         * @throws IOException thrown if there was a problem closing the stream.
    394         */
    395        @Override
    396        public void close() throws IOException {
    397            reader.close();
    398        }
    399
    400        /**
    401         * Process the raw stream.
    402         *
    403         * @param bom BOMInputStream to detect byte order marks
    404         * @param pis BOMInputStream to guess XML encoding
    405         * @param lenient indicates if the charset encoding detection should be
    406         *        relaxed.
    407         * @return the encoding to be used
    408         * @throws IOException thrown if there is a problem reading the stream.
    409         */
    410        private String doRawStream(BOMInputStream bom, BOMInputStream pis, boolean lenient)
    411                throws IOException {
    412            String bomEnc      = bom.getBOMCharsetName();
    413            String xmlGuessEnc = pis.getBOMCharsetName();
    414            String xmlEnc = getXmlProlog(pis, xmlGuessEnc);
    415            try {
    416                return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);
    417            } catch (XmlStreamReaderException ex) {
    418                if (lenient) {
    419                    return doLenientDetection(null, ex);
    420                } else {
    421                    throw ex;
    422                }
    423            }
    424        }
    425
    426        /**
    427         * Process a HTTP stream.
    428         *
    429         * @param bom BOMInputStream to detect byte order marks
    430         * @param pis BOMInputStream to guess XML encoding
    431         * @param httpContentType The HTTP content type
    432         * @param lenient indicates if the charset encoding detection should be
    433         *        relaxed.
    434         * @return the encoding to be used
    435         * @throws IOException thrown if there is a problem reading the stream.
    436         */
    437        private String doHttpStream(BOMInputStream bom, BOMInputStream pis, String httpContentType,
    438                boolean lenient) throws IOException {
    439            String bomEnc      = bom.getBOMCharsetName();
    440            String xmlGuessEnc = pis.getBOMCharsetName();
    441            String xmlEnc = getXmlProlog(pis, xmlGuessEnc);
    442            try {
    443                return calculateHttpEncoding(httpContentType, bomEnc,
    444                        xmlGuessEnc, xmlEnc, lenient);
    445            } catch (XmlStreamReaderException ex) {
    446                if (lenient) {
    447                    return doLenientDetection(httpContentType, ex);
    448                } else {
    449                    throw ex;
    450                }
    451            }
    452        }
    453
    454        /**
    455         * Do lenient detection.
    456         *
    457         * @param httpContentType content-type header to use for the resolution of
    458         *        the charset encoding.
    459         * @param ex The thrown exception
    460         * @return the encoding
    461         * @throws IOException thrown if there is a problem reading the stream.
    462         */
    463        private String doLenientDetection(String httpContentType,
    464                XmlStreamReaderException ex) throws IOException {
    465            if (httpContentType != null && httpContentType.startsWith("text/html")) {
    466                httpContentType = httpContentType.substring("text/html".length());
    467                httpContentType = "text/xml" + httpContentType;
    468                try {
    469                    return calculateHttpEncoding(httpContentType, ex.getBomEncoding(),
    470                            ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true);
    471                } catch (XmlStreamReaderException ex2) {
    472                    ex = ex2;
    473                }
    474            }
    475            String encoding = ex.getXmlEncoding();
    476            if (encoding == null) {
    477                encoding = ex.getContentTypeEncoding();
    478            }
    479            if (encoding == null) {
    480                encoding = (defaultEncoding == null) ? UTF_8 : defaultEncoding;
    481            }
    482            return encoding;
    483        }
    484
    485        /**
    486         * Calculate the raw encoding.
    487         *
    488         * @param bomEnc BOM encoding
    489         * @param xmlGuessEnc XML Guess encoding
    490         * @param xmlEnc XML encoding
    491         * @return the raw encoding
    492         * @throws IOException thrown if there is a problem reading the stream.
    493         */
    494        String calculateRawEncoding(String bomEnc, String xmlGuessEnc,
    495                String xmlEnc) throws IOException {
    496
    497            // BOM is Null
    498            if (bomEnc == null) {
    499                if (xmlGuessEnc == null || xmlEnc == null) {
    500                    return (defaultEncoding == null ? UTF_8 : defaultEncoding);
    501                }
    502                if (xmlEnc.equals(UTF_16) &&
    503                   (xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) {
    504                    return xmlGuessEnc;
    505                }
    506                return xmlEnc;
    507            }
    508
    509            // BOM is UTF-8
    510            if (bomEnc.equals(UTF_8)) {
    511                if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) {
    512                    String msg = MessageFormat.format(RAW_EX_1, new Object[] { bomEnc, xmlGuessEnc, xmlEnc });
    513                    throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
    514                }
    515                if (xmlEnc != null && !xmlEnc.equals(UTF_8)) {
    516                    String msg = MessageFormat.format(RAW_EX_1, new Object[] { bomEnc, xmlGuessEnc, xmlEnc });
    517                    throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
    518                }
    519                return bomEnc;
    520            }
    521
    522            // BOM is UTF-16BE or UTF-16LE
    523            if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) {
    524                if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {
    525                    String msg = MessageFormat.format(RAW_EX_1, new Object[] { bomEnc, xmlGuessEnc, xmlEnc });
    526                    throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
    527                }
    528                if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) {
    529                    String msg = MessageFormat.format(RAW_EX_1, new Object[] { bomEnc, xmlGuessEnc, xmlEnc });
    530                    throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
    531                }
    532                return bomEnc;
    533            }
    534
    535            // BOM is something else
    536            String msg = MessageFormat.format(RAW_EX_2, new Object[] { bomEnc, xmlGuessEnc, xmlEnc });
    537            throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
    538        }
    539
    540
    541        /**
    542         * Calculate the HTTP encoding.
    543         *
    544         * @param httpContentType The HTTP content type
    545         * @param bomEnc BOM encoding
    546         * @param xmlGuessEnc XML Guess encoding
    547         * @param xmlEnc XML encoding
    548         * @param lenient indicates if the charset encoding detection should be
    549         *        relaxed.
    550         * @return the HTTP encoding
    551         * @throws IOException thrown if there is a problem reading the stream.
    552         */
    553        String calculateHttpEncoding(String httpContentType,
    554                String bomEnc, String xmlGuessEnc, String xmlEnc,
    555                boolean lenient) throws IOException {
    556
    557            // Lenient and has XML encoding
    558            if (lenient && xmlEnc != null) {
    559                return xmlEnc;
    560            }
    561
    562            // Determine mime/encoding content types from HTTP Content Type
    563            String cTMime = getContentTypeMime(httpContentType);
    564            String cTEnc  = getContentTypeEncoding(httpContentType);
    565            boolean appXml  = isAppXml(cTMime);
    566            boolean textXml = isTextXml(cTMime);
    567
    568            // Mime type NOT "application/xml" or "text/xml"
    569            if (!appXml && !textXml) {
    570                String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
    571                throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
    572            }
    573
    574            // No content type encoding
    575            if (cTEnc == null) {
    576                if (appXml) {
    577                    return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);
    578                } else {
    579                    return (defaultEncoding == null) ? US_ASCII : defaultEncoding;
    580                }
    581            }
    582
    583            // UTF-16BE or UTF-16LE content type encoding
    584            if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) {
    585                if (bomEnc != null) {
    586                    String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
    587                    throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
    588                }
    589                return cTEnc;
    590            }
    591
    592            // UTF-16 content type encoding
    593            if (cTEnc.equals(UTF_16)) {
    594                if (bomEnc != null && bomEnc.startsWith(UTF_16)) {
    595                    return bomEnc;
    596                }
    597                String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
    598                throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
    599            }
    600
    601            return cTEnc;
    602        }
    603
    604        /**
    605         * Returns MIME type or NULL if httpContentType is NULL.
    606         *
    607         * @param httpContentType the HTTP content type
    608         * @return The mime content type
    609         */
    610        static String getContentTypeMime(String httpContentType) {
    611            String mime = null;
    612            if (httpContentType != null) {
    613                int i = httpContentType.indexOf(";");
    614                if (i >= 0) {
    615                    mime = httpContentType.substring(0, i);
    616                } else {
    617                    mime = httpContentType;
    618                }
    619                mime = mime.trim();
    620            }
    621            return mime;
    622        }
    623
    624        private static final Pattern CHARSET_PATTERN = Pattern
    625                .compile("charset=[\"']?([.[^; \"']]*)[\"']?");
    626
    627        /**
    628         * Returns charset parameter value, NULL if not present, NULL if
    629         * httpContentType is NULL.
    630         *
    631         * @param httpContentType the HTTP content type
    632         * @return The content type encoding
    633         */
    634        static String getContentTypeEncoding(String httpContentType) {
    635            String encoding = null;
    636            if (httpContentType != null) {
    637                int i = httpContentType.indexOf(";");
    638                if (i > -1) {
    639                    String postMime = httpContentType.substring(i + 1);
    640                    Matcher m = CHARSET_PATTERN.matcher(postMime);
    641                    encoding = (m.find()) ? m.group(1) : null;
    642                    encoding = (encoding != null) ? encoding.toUpperCase() : null;
    643                }
    644            }
    645            return encoding;
    646        }
    647
    648        public static final Pattern ENCODING_PATTERN = Pattern.compile(
    649                "<\\?xml.*encoding[\\s]*=[\\s]*((?:\".[^\"]*\")|(?:'.[^']*'))",
    650                Pattern.MULTILINE);
    651
    652        /**
    653         * Returns the encoding declared in the <?xml encoding=...?>, NULL if none.
    654         *
    655         * @param is InputStream to create the reader from.
    656         * @param guessedEnc guessed encoding
    657         * @return the encoding declared in the <?xml encoding=...?>
    658         * @throws IOException thrown if there is a problem reading the stream.
    659         */
    660        private static String getXmlProlog(InputStream is, String guessedEnc)
    661                throws IOException {
    662            String encoding = null;
    663            if (guessedEnc != null) {
    664                byte[] bytes = new byte[BUFFER_SIZE];
    665                is.mark(BUFFER_SIZE);
    666                int offset = 0;
    667                int max = BUFFER_SIZE;
    668                int c = is.read(bytes, offset, max);
    669                int firstGT = -1;
    670                String xmlProlog = null;
    671                while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) {
    672                    offset += c;
    673                    max -= c;
    674                    c = is.read(bytes, offset, max);
    675                    xmlProlog = new String(bytes, 0, offset, guessedEnc);
    676                    firstGT = xmlProlog.indexOf('>');
    677                }
    678                if (firstGT == -1) {
    679                    if (c == -1) {
    680                        throw new IOException("Unexpected end of XML stream");
    681                    } else {
    682                        throw new IOException(
    683                                "XML prolog or ROOT element not found on first "
    684                                        + offset + " bytes");
    685                    }
    686                }
    687                int bytesRead = offset;
    688                if (bytesRead > 0) {
    689                    is.reset();
    690                    BufferedReader bReader = new BufferedReader(new StringReader(
    691                            xmlProlog.substring(0, firstGT + 1)));
    692                    StringBuffer prolog = new StringBuffer();
    693                    String line = bReader.readLine();
    694                    while (line != null) {
    695                        prolog.append(line);
    696                        line = bReader.readLine();
    697                    }
    698                    Matcher m = ENCODING_PATTERN.matcher(prolog);
    699                    if (m.find()) {
    700                        encoding = m.group(1).toUpperCase();
    701                        encoding = encoding.substring(1, encoding.length() - 1);
    702                    }
    703                }
    704            }
    705            return encoding;
    706        }
    707
    708        /**
    709         * Indicates if the MIME type belongs to the APPLICATION XML family.
    710         *
    711         * @param mime The mime type
    712         * @return true if the mime type belongs to the APPLICATION XML family,
    713         * otherwise false
    714         */
    715        static boolean isAppXml(String mime) {
    716            return mime != null &&
    717                   (mime.equals("application/xml") ||
    718                    mime.equals("application/xml-dtd") ||
    719                    mime.equals("application/xml-external-parsed-entity") ||
    720                   (mime.startsWith("application/") && mime.endsWith("+xml")));
    721        }
    722
    723        /**
    724         * Indicates if the MIME type belongs to the TEXT XML family.
    725         *
    726         * @param mime The mime type
    727         * @return true if the mime type belongs to the TEXT XML family,
    728         * otherwise false
    729         */
    730        static boolean isTextXml(String mime) {
    731            return mime != null &&
    732                  (mime.equals("text/xml") ||
    733                   mime.equals("text/xml-external-parsed-entity") ||
    734                  (mime.startsWith("text/") && mime.endsWith("+xml")));
    735        }
    736
    737        private static final String RAW_EX_1 =
    738            "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch";
    739
    740        private static final String RAW_EX_2 =
    741            "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM";
    742
    743        private static final String HTTP_EX_1 =
    744            "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL";
    745
    746        private static final String HTTP_EX_2 =
    747            "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch";
    748
    749        private static final String HTTP_EX_3 =
    750            "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME";
    751
    752    }