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