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.Locale;
033 import java.util.regex.Matcher;
034 import java.util.regex.Pattern;
035
036 import org.apache.commons.io.ByteOrderMark;
037
038 /**
039 * Character stream that handles all the necessary Voodo to figure out the
040 * charset encoding of the XML document within the stream.
041 * <p>
042 * IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader.
043 * This one IS a character stream.
044 * <p>
045 * All this has to be done without consuming characters from the stream, if not
046 * the XML parser will not recognized the document as a valid XML. This is not
047 * 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers
048 * right now, XmlStreamReader handles it and things work in all parsers).
049 * <p>
050 * The XmlStreamReader class handles the charset encoding of XML documents in
051 * Files, raw streams and HTTP streams by offering a wide set of constructors.
052 * <p>
053 * By default the charset encoding detection is lenient, the constructor with
054 * the lenient flag can be used for an script (following HTTP MIME and XML
055 * specifications). All this is nicely explained by Mark Pilgrim in his blog, <a
056 * href="http://diveintomark.org/archives/2004/02/13/xml-media-types">
057 * Determining the character encoding of a feed</a>.
058 * <p>
059 * Originally developed for <a href="http://rome.dev.java.net">ROME</a> under
060 * Apache License 2.0.
061 *
062 * @version $Id: XmlStreamReader.java 1304052 2012-03-22 20:55:29Z ggregory $
063 * @see org.apache.commons.io.output.XmlStreamWriter
064 * @since 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 (upcased)
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(Locale.US) : 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 }