Development Class Java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
/**
 * This is an input stream that is unicode BOM aware. This allows you to e.g.
 * read Windows Notepad Unicode files as Velocity templates.
 * 
 * It allows you to check the actual encoding of a file by calling
 * {@link #getEncodingFromStream()} on the input stream reader.
 * 
 * This class is not thread safe! When more than one thread wants to use an
 * instance of UnicodeInputStream, the caller must provide synchronization.
 * 
 * @author Aki Nieminen
 * @author Henning P. Schmiedehausen
 * @version $Id: UnicodeInputStream.java 685685 2008-08-13 21:43:27Z nbubna $
 * @since 1.5
 */
public class UnicodeInputStream extends InputStream {
  /** BOM Marker for UTF 8. See http://www.unicode.org/unicode/faq/utf_bom.html */
  public static final UnicodeBOM UTF8_BOM = new UnicodeBOM("UTF-8", new byte[] { (byte) 0xef,
      (byte) 0xbb, (byte) 0xbf });
  /**
   * BOM Marker for UTF 16, little endian. See
   * http://www.unicode.org/unicode/faq/utf_bom.html
   */
  public static final UnicodeBOM UTF16LE_BOM = new UnicodeBOM("UTF-16LE", new byte[] { (byte) 0xff,
      (byte) 0xfe });
  /**
   * BOM Marker for UTF 16, big endian. See
   * http://www.unicode.org/unicode/faq/utf_bom.html
   */
  public static final UnicodeBOM UTF16BE_BOM = new UnicodeBOM("UTF-16BE", new byte[] { (byte) 0xfe,
      (byte) 0xff });
  /**
   * BOM Marker for UTF 32, little endian. See
   * http://www.unicode.org/unicode/faq/utf_bom.html
   * 
   * TODO: Does Java actually support this?
   */
  public static final UnicodeBOM UTF32LE_BOM = new UnicodeBOM("UTF-32LE", new byte[] { (byte) 0xff,
      (byte) 0xfe, (byte) 0x00, (byte) 0x00 });
  /**
   * BOM Marker for UTF 32, big endian. See
   * http://www.unicode.org/unicode/faq/utf_bom.html
   * 
   * TODO: Does Java actually support this?
   */
  public static final UnicodeBOM UTF32BE_BOM = new UnicodeBOM("UTF-32BE", new byte[] { (byte) 0x00,
      (byte) 0x00, (byte) 0xfe, (byte) 0xff });
  /** The maximum amount of bytes to read for a BOM */
  private static final int MAX_BOM_SIZE = 4;
  /** Buffer for BOM reading */
  private byte[] buf = new byte[MAX_BOM_SIZE];
  /** Buffer pointer. */
  private int pos = 0;
  /** The stream encoding as read from the BOM or null. */
  private final String encoding;
  /** True if the BOM itself should be skipped and not read. */
  private final boolean skipBOM;
  private final PushbackInputStream inputStream;
  /**
   * Creates a new UnicodeInputStream object. Skips a BOM which defines the file
   * encoding.
   * 
   * @param inputStream
   *          The input stream to use for reading.
   */
  public UnicodeInputStream(final InputStream inputStream) throws IllegalStateException,
      IOException {
    this(inputStream, true);
  }
  /**
   * Creates a new UnicodeInputStream object.
   * 
   * @param inputStream
   *          The input stream to use for reading.
   * @param skipBOM
   *          If this is set to true, a BOM read from the stream is discarded.
   *          This parameter should normally be true.
   */
  public UnicodeInputStream(final InputStream inputStream, boolean skipBOM)
      throws IllegalStateException, IOException {
    super();
    this.skipBOM = skipBOM;
    this.inputStream = new PushbackInputStream(inputStream, MAX_BOM_SIZE);
    try {
      this.encoding = readEncoding();
    } catch (IOException ioe) {
      IllegalStateException ex = new IllegalStateException("Could not read BOM from Stream");
      // ExceptionUtils.setCause(ex, ioe);
      throw ex;
    }
  }
  /**
   * Returns true if the input stream discards the BOM.
   * 
   * @return True if the input stream discards the BOM.
   */
  public boolean isSkipBOM() {
    return skipBOM;
  }
  /**
   * Read encoding based on BOM.
   * 
   * @return The encoding based on the BOM.
   * 
   * @throws IllegalStateException
   *           When a problem reading the BOM occured.
   */
  public String getEncodingFromStream() {
    return encoding;
  }
  /**
   * This method gets the encoding from the stream contents if a BOM exists. If
   * no BOM exists, the encoding is undefined.
   * 
   * @return The encoding of this streams contents as decided by the BOM or null
   *         if no BOM was found.
   */
  protected String readEncoding() throws IOException {
    pos = 0;
    UnicodeBOM encoding = null;
    // read first byte.
    if (readByte()) {
      // Build a list of matches
      //
      // 00 00 FE FF --> UTF 32 BE
      // EF BB BF --> UTF 8
      // FE FF --> UTF 16 BE
      // FF FE --> UTF 16 LE
      // FF FE 00 00 --> UTF 32 LE
      switch (buf[0]) {
      case (byte) 0x00: // UTF32 BE
        encoding = match(UTF32BE_BOM, null);
        break;
      case (byte) 0xef: // UTF8
        encoding = match(UTF8_BOM, null);
        break;
      case (byte) 0xfe: // UTF16 BE
        encoding = match(UTF16BE_BOM, null);
        break;
      case (byte) 0xff: // UTF16/32 LE
        encoding = match(UTF16LE_BOM, null);
        if (encoding != null) {
          encoding = match(UTF32LE_BOM, encoding);
        }
        break;
      default:
        encoding = null;
        break;
      }
    }
    pushback(encoding);
    return (encoding != null) ? encoding.getEncoding() : null;
  }
  private final UnicodeBOM match(final UnicodeBOM matchEncoding, final UnicodeBOM noMatchEncoding)
      throws IOException {
    byte[] bom = matchEncoding.getBytes();
    for (int i = 0; i < bom.length; i++) {
      if (pos <= i) // Byte has not yet been read
      {
        if (!readByte()) {
          return noMatchEncoding;
        }
      }
      if (bom[i] != buf[i]) {
        return noMatchEncoding;
      }
    }
    return matchEncoding;
  }
  private final boolean readByte() throws IOException {
    int res = inputStream.read();
    if (res == -1) {
      return false;
    }
    if (pos >= buf.length) {
      throw new IOException("BOM read error");
    }
    buf[pos++] = (byte) res;
    return true;
  }
  private final void pushback(final UnicodeBOM matchBOM) throws IOException {
    int count = pos; // By default, all bytes are pushed back.
    int start = 0;
    if (matchBOM != null && skipBOM) {
      // We have a match (some bytes are part of the BOM)
      // and we want to skip the BOM. Push back only the bytes
      // after the BOM.
      start = matchBOM.getBytes().length;
      count = (pos - start);
      if (count < 0) {
        throw new IllegalStateException("Match has more bytes than available!");
      }
    }
    inputStream.unread(buf, start, count);
  }
  /**
   * @see java.io.InputStream#close()
   */
  public void close() throws IOException {
    inputStream.close();
  }
  /**
   * @see java.io.InputStream#available()
   */
  public int available() throws IOException {
    return inputStream.available();
  }
  /**
   * @see java.io.InputStream#mark(int)
   */
  public void mark(final int readlimit) {
    inputStream.mark(readlimit);
  }
  /**
   * @see java.io.InputStream#markSupported()
   */
  public boolean markSupported() {
    return inputStream.markSupported();
  }
  /**
   * @see java.io.InputStream#read()
   */
  public int read() throws IOException {
    return inputStream.read();
  }
  /**
   * @see java.io.InputStream#read(byte[])
   */
  public int read(final byte[] b) throws IOException {
    return inputStream.read(b);
  }
  /**
   * @see java.io.InputStream#read(byte[], int, int)
   */
  public int read(final byte[] b, final int off, final int len) throws IOException {
    return inputStream.read(b, off, len);
  }
  /**
   * @see java.io.InputStream#reset()
   */
  public void reset() throws IOException {
    inputStream.reset();
  }
  /**
   * @see java.io.InputStream#skip(long)
   */
  public long skip(final long n) throws IOException {
    return inputStream.skip(n);
  }
  /**
   * Helper class to bundle encoding and BOM marker.
   * 
   * @author Henning P. Schmiedehausen
   * @version $Id: UnicodeInputStream.java 685685 2008-08-13 21:43:27Z nbubna $
   */
  static final class UnicodeBOM {
    private final String encoding;
    private final byte[] bytes;
    private UnicodeBOM(final String encoding, final byte[] bytes) {
      this.encoding = encoding;
      this.bytes = bytes;
    }
    String getEncoding() {
      return encoding;
    }
    byte[] getBytes() {
      return bytes;
    }
  }
}