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()]); 
  } 
} 
 
    
     
     
     
     
     
  
  |