|       import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.charset.Charset;
 import java.util.Collection;
 import java.util.logging.Logger;
 
 
 /**
 * <p>
 * <code>SmartEncodingInputStream</code> extends an <code>InputStream</code> with a special
 * constructor and a special method for dealing with text files encoded within different charsets.
 * </p>
 * <p>
 * It surrounds a normal <code>InputStream</code> whatever it may be (<code>FileInputStream</code>...). It reads a
 * buffer of a defined length. Then with this byte buffer, it uses the class
 * <code>CharsetToolkit</code> to parse this buffer and guess what the encoding is. All this steps
 * are done within the constructor. At this time, you can call the method <code>getReader()</code> to retrieve a
 * <code>Reader</code> created with the good charset, as guessed while parsing the first bytes of the file. This
 * <code>Reader</code> reads inside the <code>SmartEncodingInputStream</code>. It reads first in
 * the internal buffer, then when we reach the end of the buffer, the underlying InputStream is read with the default
 * read method.
 * </p>
 * <p>
 * Usage:
 * </p>
 *
 * <pre>
 * FileInputStream fis = new FileInputStream("utf-8.txt");
 * SmartEncodingInputStream smartIS = new SmartEncodingInputStream(fis);
 * Reader reader = smartIS.getReader();
 * BufferedReader bufReader = new BufferedReader(reader);
 *
 * String line;
 * while ((line = bufReader.readLine()) != null) {
 *   System.out.println(line);
 * }
 * </pre>
 *
 * Date: 23 juil. 2002
 *
 * @author Guillaume Laforge
 */
 public class SmartEncodingInputStream
 extends InputStream {
 private final InputStream is;
 private int bufferLength;
 private final byte[] buffer;
 private int counter;
 private final Charset charset;
 
 public static final int BUFFER_LENGTH_2KB = 2048;
 public static final int BUFFER_LENGTH_4KB = 4096;
 public static final int BUFFER_LENGTH_8KB = 8192;
 
 /**
 * <p>
 * Constructor of the <code>SmartEncodingInputStream</code> class. The wider the buffer is, the
 * most sure you are to have guessed the encoding of the <code>InputStream</code> you wished to get a
 * <code>Reader</code> from.
 * </p>
 * <p>
 * It is possible to defined
 * </p>
 *
 * @param is
 *          the <code>InputStream</code> of which we want to create a <code>Reader</code> with the encoding guessed
 *          from the first buffer of the file.
 * @param bufferLength
 *          the length of the buffer that is used to guess the encoding.
 * @param defaultCharset
 *          specifies the default <code>Charset</code> to use when an 8-bit <code>Charset</code> is guessed. This
 *          parameter may be null, in this case the default system charset is used as definied in the system property
 *          "file.encoding" read by the method <code>getDefaultSystemCharset()</code> from the class
 *          <code>CharsetToolkit</code>.
 * @param enforce8Bit
 *          enforce the use of the specified default <code>Charset</code> in case the encoding US-ASCII is recognized.
 * @throws IOException
 */
 public SmartEncodingInputStream(final InputStream is, final int bufferLength, final Charset defaultCharset,
 final boolean enforce8Bit) throws IOException {
 this.is = is;
 this.bufferLength = bufferLength;
 this.buffer = new byte[bufferLength];
 this.counter = 0;
 
 this.bufferLength = is.read(buffer);
 final CharsetToolkit charsetToolkit = new CharsetToolkit(buffer, defaultCharset);
 charsetToolkit.setEnforce8Bit(enforce8Bit);
 this.charset = charsetToolkit.guessEncoding();
 }
 
 /**
 * Constructor of the <code>SmartEncodingInputStream</code>. With this constructor, the default
 * <code>Charset</code> used when an 8-bit encoding is guessed does not need to be specified. The default system
 * charset will be used instead.
 *
 * @param is
 *          is the <code>InputStream</code> of which we want to create a <code>Reader</code> with the encoding guessed
 *          from the first buffer of the file.
 * @param bufferLength
 *          the length of the buffer that is used to guess the encoding.
 * @param defaultCharset
 *          specifies the default <code>Charset</code> to use when an 8-bit <code>Charset</code> is guessed. This
 *          parameter may be null, in this case the default system charset is used as definied in the system property
 *          "file.encoding" read by the method <code>getDefaultSystemCharset()</code> from the class
 *          <code>CharsetToolkit</code>.
 * @throws IOException
 */
 public SmartEncodingInputStream(final InputStream is, final int bufferLength, final Charset defaultCharset)
 throws IOException {
 this(is, bufferLength, defaultCharset, true);
 }
 
 /**
 * Constructor of the <code>SmartEncodingInputStream</code>. With this constructor, the default
 * <code>Charset</code> used when an 8-bit encoding is guessed does not need to be specified. The default system
 * charset will be used instead.
 *
 * @param is
 *          is the <code>InputStream</code> of which we want to create a <code>Reader</code> with the encoding guessed
 *          from the first buffer of the file.
 * @param bufferLength
 *          the length of the buffer that is used to guess the encoding.
 * @throws IOException
 */
 public SmartEncodingInputStream(final InputStream is, final int bufferLength) throws IOException {
 this(is, bufferLength, null, true);
 }
 
 /**
 * Constructor of the <code>SmartEncodingInputStream</code>. With this constructor, the default
 * <code>Charset</code> used when an 8-bit encoding is guessed does not need to be specified. The default system
 * charset will be used instead. The buffer length does not need to be specified either. A default buffer length of 4
 * KB is used.
 *
 * @param is
 *          is the <code>InputStream</code> of which we want to create a <code>Reader</code> with the encoding guessed
 *          from the first buffer of the file.
 * @throws IOException
 */
 public SmartEncodingInputStream(final InputStream is) throws IOException {
 this(is, SmartEncodingInputStream.BUFFER_LENGTH_8KB, null, true);
 }
 
 /**
 * Implements the method <code>read()</code> as defined in the <code>InputStream</code> interface. As a certain number
 * of bytes has already been read from the underlying <code>InputStream</code>, we first read the bytes of this
 * buffer, otherwise, we directly read the rest of the stream from the underlying <code>InputStream</code>.
 *
 * @return the total number of bytes read into the buffer, or <code>-1</code> is there is no more data because the end
 *         of the stream has been reached.
 * @throws IOException
 */
 @Override
 public int read()
 throws IOException {
 if (counter < bufferLength)
 return buffer[counter++];
 else
 return is.read();
 }
 
 /**
 * Gets a <code>Reader</code> with the right <code>Charset</code> as guessed by reading the beginning of the
 * underlying <code>InputStream</code>.
 *
 * @return a <code>Reader</code> defined with the right encoding.
 */
 public Reader getReader() {
 return new InputStreamReader(this, this.charset);
 }
 
 /**
 * Retrieves the <code>Charset</code> as guessed from the underlying <code>InputStream</code>.
 *
 * @return the <code>Charset</code> guessed.
 */
 public Charset getEncoding() {
 return this.charset;
 }
 }
 /**
 * <p>
 * Utility class to guess the encoding of a given byte array. The guess is
 * unfortunately not 100% sure. Especially for 8-bit charsets. It's not possible
 * to know which 8-bit charset is used. Except through statistical analysis. We
 * will then infer that the charset encountered is the same as the default
 * standard charset.
 * </p>
 * <p>
 * On the other hand, unicode files encoded in UTF-16 (low or big endian) or
 * UTF-8 files with a Byte Order Marker are easy to find. For UTF-8 files with
 * no BOM, if the buffer is wide enough, it's easy to guess.
 * </p>
 * <p>
 * Tested against a complicated UTF-8 file, Sun's implementation does not render
 * bad UTF-8 constructs as expected by the specification. But with a buffer wide
 * enough, the method guessEncoding() did behave correctly and recognized the
 * UTF-8 charset.
 * </p>
 * <p>
 * A byte buffer of 4KB or 8KB is sufficient to be able to guess the encoding.
 * </p>
 * <p>
 * Usage:
 * </p>
 *
 * <pre>
 * // guess the encoding
 * Charset guessedCharset = CharsetToolkit.guessEncoding(file, 4096);
 *
 * // create a reader with the charset we've just discovered
 * FileInputStream fis = new FileInputStream(file);
 * InputStreamReader isr = new InputStreamReader(fis, guessedCharset);
 * BufferedReader br = new BufferedReader(isr);
 *
 * // read the file content
 * String line;
 * while ((line = br.readLine()) != null) {
 *   System.out.println(line);
 * }
 * </pre>
 * <p>
 * Date: 18 juil. 2002
 * </p>
 *
 * @author Guillaume LAFORGE
 */
 class CharsetToolkit {
 private final byte[] buffer;
 private Charset defaultCharset;
 private boolean enforce8Bit = false;
 
 /**
 * Constructor of the <code>CharsetToolkit</code> utility class.
 *
 * @param buffer
 *            the byte buffer of which we want to know the encoding.
 */
 public CharsetToolkit(final byte[] buffer) {
 this.buffer = buffer;
 this.defaultCharset = getDefaultSystemCharset();
 }
 
 /**
 * Constructor of the <code>CharsetToolkit</code> utility class.
 *
 * @param buffer
 *            the byte buffer of which we want to know the encoding.
 * @param defaultCharset
 *            the default Charset to use in case an 8-bit charset is
 *            recognized.
 */
 public CharsetToolkit(final byte[] buffer, final Charset defaultCharset) {
 this.buffer = buffer;
 setDefaultCharset(defaultCharset);
 }
 
 /**
 * Defines the default <code>Charset</code> used in case the buffer
 * represents an 8-bit <code>Charset</code>.
 *
 * @param defaultCharset
 *            the default <code>Charset</code> to be returned by
 *            <code>guessEncoding()</code> if an 8-bit <code>Charset</code>
 *            is encountered.
 */
 public void setDefaultCharset(final Charset defaultCharset) {
 if (defaultCharset != null)
 this.defaultCharset = defaultCharset;
 else
 this.defaultCharset = getDefaultSystemCharset();
 }
 
 /**
 * If US-ASCII is recognized, enforce to return the default encoding, rather
 * than US-ASCII. It might be a file without any special character in the
 * range 128-255, but that may be or become a file encoded with the default
 * <code>charset</code> rather than US-ASCII.
 *
 * @param enforce
 *            a boolean specifying the use or not of US-ASCII.
 */
 public void setEnforce8Bit(final boolean enforce) {
 this.enforce8Bit = enforce;
 }
 
 /**
 * Gets the enforce8Bit flag, in case we do not want to ever get a US-ASCII
 * encoding.
 *
 * @return a boolean representing the flag of use of US-ASCII.
 */
 public boolean getEnforce8Bit() {
 return this.enforce8Bit;
 }
 
 /**
 * Retrieves the default Charset
 *
 * @return
 */
 public Charset getDefaultCharset() {
 return defaultCharset;
 }
 
 /**
 * <p>
 * Guess the encoding of the provided buffer.
 * </p>
 * If Byte Order Markers are encountered at the beginning of the buffer, we
 * immidiately return the charset implied by this BOM. Otherwise, the file
 * would not be a human readable text file.</p>
 * <p>
 * If there is no BOM, this method tries to discern whether the file is
 * UTF-8 or not. If it is not UTF-8, we assume the encoding is the default
 * system encoding (of course, it might be any 8-bit charset, but usually,
 * an 8-bit charset is the default one).
 * </p>
 * <p>
 * It is possible to discern UTF-8 thanks to the pattern of characters with
 * a multi-byte sequence.
 * </p>
 *
 * <pre>
 * UCS-4 range (hex.)        UTF-8 octet sequence (binary)
 * 0000 0000-0000 007F       0xxxxxxx
 * 0000 0080-0000 07FF       110xxxxx 10xxxxxx
 * 0000 0800-0000 FFFF       1110xxxx 10xxxxxx 10xxxxxx
 * 0001 0000-001F FFFF       11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
 * 0020 0000-03FF FFFF       111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 * 0400 0000-7FFF FFFF       1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 * </pre>
 * <p>
 * With UTF-8, 0xFE and 0xFF never appear.
 * </p>
 *
 * @return the Charset recognized.
 */
 public Charset guessEncoding() {
 // if the file has a Byte Order Marker, we can assume the file is in
 // UTF-xx
 // otherwise, the file would not be human readable
 if (hasUTF8Bom(buffer))
 return Charset.forName("UTF-8");
 if (hasUTF16LEBom(buffer))
 return Charset.forName("UTF-16LE");
 if (hasUTF16BEBom(buffer))
 return Charset.forName("UTF-16BE");
 
 // if a byte has its most significant bit set, the file is in UTF-8 or
 // in the default encoding
 // otherwise, the file is in US-ASCII
 boolean highOrderBit = false;
 
 // if the file is in UTF-8, high order bytes must have a certain value,
 // in order to be valid
 // if it's not the case, we can assume the encoding is the default
 // encoding of the system
 boolean validU8Char = true;
 
 // TODO the buffer is not read up to the end, but up to length - 6
 
 final int length = buffer.length;
 int i = 0;
 while (i < length - 6) {
 final byte b0 = buffer[i];
 final byte b1 = buffer[i + 1];
 final byte b2 = buffer[i + 2];
 final byte b3 = buffer[i + 3];
 final byte b4 = buffer[i + 4];
 final byte b5 = buffer[i + 5];
 if (b0 < 0) {
 // a high order bit was encountered, thus the encoding is not
 // US-ASCII
 // it may be either an 8-bit encoding or UTF-8
 highOrderBit = true;
 // a two-bytes sequence was encoutered
 if (isTwoBytesSequence(b0)) {
 // there must be one continuation byte of the form 10xxxxxx,
 // otherwise the following characteris is not a valid UTF-8
 // construct
 if (!isContinuationChar(b1))
 validU8Char = false;
 else
 i++;
 }
 // a three-bytes sequence was encoutered
 else if (isThreeBytesSequence(b0)) {
 // there must be two continuation bytes of the form
 // 10xxxxxx,
 // otherwise the following characteris is not a valid UTF-8
 // construct
 if (!(isContinuationChar(b1) && isContinuationChar(b2)))
 validU8Char = false;
 else
 i += 2;
 }
 // a four-bytes sequence was encoutered
 else if (isFourBytesSequence(b0)) {
 // there must be three continuation bytes of the form
 // 10xxxxxx,
 // otherwise the following characteris is not a valid UTF-8
 // construct
 if (!(isContinuationChar(b1) && isContinuationChar(b2) && isContinuationChar(b3)))
 validU8Char = false;
 else
 i += 3;
 }
 // a five-bytes sequence was encoutered
 else if (isFiveBytesSequence(b0)) {
 // there must be four continuation bytes of the form
 // 10xxxxxx,
 // otherwise the following characteris is not a valid UTF-8
 // construct
 if (!(isContinuationChar(b1) && isContinuationChar(b2)
 && isContinuationChar(b3) && isContinuationChar(b4)))
 validU8Char = false;
 else
 i += 4;
 }
 // a six-bytes sequence was encoutered
 else if (isSixBytesSequence(b0)) {
 // there must be five continuation bytes of the form
 // 10xxxxxx,
 // otherwise the following characteris is not a valid UTF-8
 // construct
 if (!(isContinuationChar(b1) && isContinuationChar(b2)
 && isContinuationChar(b3) && isContinuationChar(b4) && isContinuationChar(b5)))
 validU8Char = false;
 else
 i += 5;
 } else
 validU8Char = false;
 }
 if (!validU8Char)
 break;
 i++;
 }
 // if no byte with an high order bit set, the encoding is US-ASCII
 // (it might have been UTF-7, but this encoding is usually internally
 // used only by mail systems)
 if (!highOrderBit) {
 // returns the default charset rather than US-ASCII if the
 // enforce8Bit flag is set.
 if (this.enforce8Bit)
 return this.defaultCharset;
 else
 return Charset.forName("US-ASCII");
 }
 // if no invalid UTF-8 were encountered, we can assume the encoding is
 // UTF-8,
 // otherwise the file would not be human readable
 if (validU8Char)
 return Charset.forName("UTF-8");
 // finally, if it's not UTF-8 nor US-ASCII, let's assume the encoding is
 // the default encoding
 return this.defaultCharset;
 }
 
 public static Charset guessEncoding(final File f, final int bufferLength)
 throws FileNotFoundException, IOException {
 final FileInputStream fis = new FileInputStream(f);
 final byte[] buffer = new byte[bufferLength];
 fis.read(buffer);
 fis.close();
 final CharsetToolkit toolkit = new CharsetToolkit(buffer);
 toolkit.setDefaultCharset(getDefaultSystemCharset());
 return toolkit.guessEncoding();
 }
 
 public static Charset guessEncoding(final File f, final int bufferLength,
 final Charset defaultCharset) throws FileNotFoundException,
 IOException {
 final FileInputStream fis = new FileInputStream(f);
 final byte[] buffer = new byte[bufferLength];
 fis.read(buffer);
 fis.close();
 final CharsetToolkit toolkit = new CharsetToolkit(buffer);
 toolkit.setDefaultCharset(defaultCharset);
 return toolkit.guessEncoding();
 }
 
 /**
 * If the byte has the form 10xxxxx, then it's a continuation byte of a
 * multiple byte character;
 *
 * @param b
 *            a byte.
 * @return true if it's a continuation char.
 */
 private static boolean isContinuationChar(final byte b) {
 return -128 <= b && b <= -65;
 }
 
 /**
 * If the byte has the form 110xxxx, then it's the first byte of a two-bytes
 * sequence character.
 *
 * @param b
 *            a byte.
 * @return true if it's the first byte of a two-bytes sequence.
 */
 private static boolean isTwoBytesSequence(final byte b) {
 return -64 <= b && b <= -33;
 }
 
 /**
 * If the byte has the form 1110xxx, then it's the first byte of a
 * three-bytes sequence character.
 *
 * @param b
 *            a byte.
 * @return true if it's the first byte of a three-bytes sequence.
 */
 private static boolean isThreeBytesSequence(final byte b) {
 return -32 <= b && b <= -17;
 }
 
 /**
 * If the byte has the form 11110xx, then it's the first byte of a
 * four-bytes sequence character.
 *
 * @param b
 *            a byte.
 * @return true if it's the first byte of a four-bytes sequence.
 */
 private static boolean isFourBytesSequence(final byte b) {
 return -16 <= b && b <= -9;
 }
 
 /**
 * If the byte has the form 11110xx, then it's the first byte of a
 * five-bytes sequence character.
 *
 * @param b
 *            a byte.
 * @return true if it's the first byte of a five-bytes sequence.
 */
 private static boolean isFiveBytesSequence(final byte b) {
 return -8 <= b && b <= -5;
 }
 
 /**
 * If the byte has the form 1110xxx, then it's the first byte of a six-bytes
 * sequence character.
 *
 * @param b
 *            a byte.
 * @return true if it's the first byte of a six-bytes sequence.
 */
 private static boolean isSixBytesSequence(final byte b) {
 return -4 <= b && b <= -3;
 }
 
 /**
 * Retrieve the default charset of the system.
 *
 * @return the default <code>Charset</code>.
 */
 public static Charset getDefaultSystemCharset() {
 return Charset.forName(System.getProperty("file.encoding"));
 }
 
 /**
 * Has a Byte Order Marker for UTF-8 (Used by Microsoft's Notepad and other
 * editors).
 *
 * @param bom
 *            a buffer.
 * @return true if the buffer has a BOM for UTF8.
 */
 private static boolean hasUTF8Bom(final byte[] bom) {
 return (bom[0] == -17 && bom[1] == -69 && bom[2] == -65);
 }
 
 /**
 * Has a Byte Order Marker for UTF-16 Low Endian (ucs-2le, ucs-4le, and
 * ucs-16le).
 *
 * @param bom
 *            a buffer.
 * @return true if the buffer has a BOM for UTF-16 Low Endian.
 */
 private static boolean hasUTF16LEBom(final byte[] bom) {
 return (bom[0] == -1 && bom[1] == -2);
 }
 
 /**
 * Has a Byte Order Marker for UTF-16 Big Endian (utf-16 and ucs-2).
 *
 * @param bom
 *            a buffer.
 * @return true if the buffer has a BOM for UTF-16 Big Endian.
 */
 private static boolean hasUTF16BEBom(final byte[] bom) {
 return (bom[0] == -2 && bom[1] == -1);
 }
 
 /**
 * Retrieves all the available <code>Charset</code>s on the platform, among
 * which the default <code>charset</code>.
 *
 * @return an array of <code>Charset</code>s.
 */
 public static Charset[] getAvailableCharsets() {
 final Collection collection = Charset.availableCharsets().values();
 return (Charset[]) collection.toArray(new Charset[collection.size()]);
 }
 }
 
 
 
 
 
 
 
 
 |