EncodingInfo encInfo)
throws
IOException,
UnsupportedCharsetException {
RewindableInputStream stream;
stream = new RewindableInputStream(istream, false);
//
// Phase 1. Reading first four bytes and determining encoding scheme.
final byte[] b4 = new byte[4];
int count = 0;
for (; count < 4; count++) {
int b = stream.read();
if (-1 != b) {
b4[count] = (byte) b;
} else {
break;
}
}
if (LOGGER.isLoggable(Level.FINE)) {
// Such number of concatenating strings makes me sick.
// But using StringBuffer will make this uglier, not?
LOGGER.fine("First 4 bytes of XML doc are : "
+ Integer.toHexString((int) b4[0] & 0xff).toUpperCase()
+ " ('" + (char) b4[0] + "') "
+ Integer.toHexString((int) b4[1] & 0xff).toUpperCase()
+ " ('" + (char) b4[1] + "') "
+ Integer.toHexString((int) b4[2] & 0xff).toUpperCase()
+ " ('" + (char) b4[2] + "') "
+ Integer.toHexString((int) b4[3] & 0xff).toUpperCase()
+ " ('" + (char) b4[3] + "')"
);
}
/*
* `getEncodingName()` is capable of detecting following encoding
* schemes:
* "UTF-8", "UTF-16LE", "UTF-16BE", "ISO-10646-UCS-4",
* or "CP037". It cannot distinguish between UTF-16 (without BOM)
* and "ISO-10646-UCS-2", so latter will be interpreted as UTF-16
* for the purpose of reading XML declaration. There shouldn't be
* much trouble though as (I believe) these formats are identical for
* the Basic Multilingual Plane, except that UTF-16-encoded text
* can contain values from surrogate range and valid UCS-2 input
* cannot (imho).
* This ugly form of copying charset data is required to maintain
* "reference integrity" of encInfo variable. As it can be possibly
* used after this method call, it should point to the same memory
* structure, and assignment or cloning doesn't work for me there.
*/
encInfo.copyFrom(getEncodingName(b4, count));
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Charset detection phase 1. Inferred encoding: " +
encInfo.toString());
}
// Rewinding to beginning of data
stream.reset();
String ENCODING = encInfo.getEncoding().toUpperCase(Locale.ENGLISH);
Boolean isBigEndian = encInfo.isBigEndian();
boolean hasBOM = encInfo.hasBOM();
/*
* Special case UTF-8 files with BOM created by Microsoft
* tools. It's more efficient to consume the BOM than make
* the reader perform extra checks. -Ac
*/
if (hasBOM && ENCODING.equals("UTF-8")) {
// ignore first three bytes...
stream.skip(3);
}
/*
* The specifics of `getEncodingName` work is that it always returns
* UTF-16 with BOM as either UTF-16LE or UTF-16BE, and
* InputStreamReader doesn't expect BOM coming with UTF-16LE|BE
* encoded data. So this BOM should also be removed, if present.
*/
if (count > 1 && (ENCODING.equals("UTF-16LE") ||
ENCODING.equals("UTF-16BE"))) {
int b0 = b4[0] & 0xFF;
int b1 = b4[1] & 0xFF;
if ((b0 == 0xFF && b1 == 0xFE) || (b0 == 0xFE && b1 == 0xFF)) {
// ignore first two bytes...
stream.skip(2);
}
}
Reader reader = null;
/*
* We must use custom class to read UCS-4 data, my JVM doesn't support
* this encoding scheme by default and I doubt other JVMs are.
*
* There was another specific reader for UTF-8 encoding in Xerces
* (org.apache.xerces.impl.io.UTF8Reader), which they say is
* optimized one. May be it is really better than JVM's default
* decoding algorithm but I doubt the necessity of porting just
* another (not so small) class in order to "efficiently" extract
* a couple of chars from XML declaration. Still I may be mistaking
* there. Moreover, Xerces' UTF8Reader has some internal dependencies
* and it will take much more effort to extract it from there.
*
* Also, at this stage it is quite impossible to have "ISO-10646-UCS-2"
* as a value for ENCODING.
*
* You can avoid possible bugs in UCSReader by commenting out this
* block of code together with following `if`. Then you will get an
* UnsupportedEncodingException for UCS-4 encoded data.
*/
if ("ISO-10646-UCS-4".equals(ENCODING)) {
if (null != isBigEndian) {
boolean isBE = isBigEndian.booleanValue();
if (isBE) {
reader = new UCSReader(stream, UCSReader.UCS4BE);
} else {
reader = new UCSReader(stream, UCSReader.UCS4LE);
}
} else {
// Fatal error, UCSReader will fail to decode this properly
String s = "Unsupported byte order for ISO-10646-UCS-4 encoding.";
throw new UnsupportedCharsetException(s);
}
}
if (null == reader) {
reader = new InputStreamReader(stream, ENCODING);
}
//
// Phase 2. Reading XML declaration and extracting charset info from it.
String declEncoding = getXmlEncoding(reader);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Charset detection phase 2. Charset in XML declaration "
+ "is `" + declEncoding + "`.");
}
stream.reset();
/*
* Now RewindableInputStream is allowed to return more than one byte
* per read operation. It also will not buffer bytes read using
* `read(byte[], int, int)` method.
*/
stream.setChunkedMode(true);
/*
* Reusing existing reader if possible, creating new one only if
* declared charset name differs from guessed one
*/