content: /*
   * 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.
   */
  package org.apache.commons.io.input;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.Arrays;
  import java.util.Comparator;
  import java.util.List;
  
  import org.apache.commons.io.ByteOrderMark;
  
  /**
   * This class is used to wrap a stream that includes an encoded {@link ByteOrderMark} as its first bytes.
   * 
   * This class detects these bytes and, if required, can automatically skip them and return the subsequent byte as the
   * first byte in the stream.
   * 
   * The {@link ByteOrderMark} implementation has the following pre-defined BOMs:
   * <ul>
   * <li>UTF-8 - {@link ByteOrderMark#UTF_8}</li>
   * <li>UTF-16BE - {@link ByteOrderMark#UTF_16LE}</li>
   * <li>UTF-16LE - {@link ByteOrderMark#UTF_16BE}</li>
   * <li>UTF-32BE - {@link ByteOrderMark#UTF_32LE}</li>
   * <li>UTF-32LE - {@link ByteOrderMark#UTF_32BE}</li>
   * </ul>
   * 
   * 
   * <h3>Example 1 - Detect and exclude a UTF-8 BOM</h3>
   * 
   * <pre>
   * BOMInputStream bomIn = new BOMInputStream(in);
   * if (bomIn.hasBOM()) {
   *     // has a UTF-8 BOM
   * }
   * </pre>
   * 
   * <h3>Example 2 - Detect a UTF-8 BOM (but don't exclude it)</h3>
   * 
   * <pre>
   * boolean include = true;
   * BOMInputStream bomIn = new BOMInputStream(in, include);
   * if (bomIn.hasBOM()) {
   *     // has a UTF-8 BOM
   * }
   * </pre>
   * 
   * <h3>Example 3 - Detect Multiple BOMs</h3>
   * 
   * <pre>
   * BOMInputStream bomIn = new BOMInputStream(in, 
   *   ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,
   *   ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE
   *   );
   * if (bomIn.hasBOM() == false) {
   *     // No BOM found
   * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {
   *     // has a UTF-16LE BOM
   * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {
   *     // has a UTF-16BE BOM
   * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32LE)) {
   *     // has a UTF-32LE BOM
   * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32BE)) {
   *     // has a UTF-32BE BOM
   * }
   * </pre>
   * 
   * @see org.apache.commons.io.ByteOrderMark
   * @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia - Byte Order Mark</a>
   * @version $Id: BOMInputStream.java 1346400 2012-06-05 14:48:01Z ggregory $
   * @since 2.0
   */
  public class BOMInputStream extends ProxyInputStream {
      private final boolean include;
      /**
       * BOMs are sorted from longest to shortest.
       */
      private final List<ByteOrderMark> boms;
      private ByteOrderMark byteOrderMark;
      private int[] firstBytes;
      private int fbLength;
      private int fbIndex;
      private int markFbIndex;
      private boolean markedAtStart;
  
      /**
       * Constructs a new BOM InputStream that excludes a {@link ByteOrderMark#UTF_8} BOM.
       * 
       * @param delegate
       *            the InputStream to delegate to
       */
      public BOMInputStream(InputStream delegate) {
          this(delegate, false, ByteOrderMark.UTF_8);
      }
  
      /**
       * Constructs a new BOM InputStream that detects a a {@link ByteOrderMark#UTF_8} and optionally includes it.
       * 
       * @param delegate
       *            the InputStream to delegate to
       * @param include
       *            true to include the UTF-8 BOM or false to exclude it
       */
      public BOMInputStream(InputStream delegate, boolean include) {
          this(delegate, include, ByteOrderMark.UTF_8);
      }
  
      /**
       * Constructs a new BOM InputStream that excludes the specified BOMs.
       * 
       * @param delegate
       *            the InputStream to delegate to
       * @param boms
       *            The BOMs to detect and exclude
       */
      public BOMInputStream(InputStream delegate, ByteOrderMark... boms) {
          this(delegate, false, boms);
      }
  
      /**
       * Compares ByteOrderMark objects in descending length order.
       */
      private static final Comparator<ByteOrderMark> ByteOrderMarkLengthComparator = new Comparator<ByteOrderMark>() {
  
          public int compare(ByteOrderMark bom1, ByteOrderMark bom2) {
              int len1 = bom1.length();
              int len2 = bom2.length();
              if (len1 > len2) {
                  return -1;
              }
              if (len2 > len1) {
                  return 1;
              }
              return 0;
          }
      };
  
      /**
       * Constructs a new BOM InputStream that detects the specified BOMs and optionally includes them.
       * 
       * @param delegate
       *            the InputStream to delegate to
       * @param include
       *            true to include the specified BOMs or false to exclude them
       * @param boms
       *            The BOMs to detect and optionally exclude
       */
      public BOMInputStream(InputStream delegate, boolean include, ByteOrderMark... boms) {
          super(delegate);
          if (boms == null || boms.length == 0) {
              throw new IllegalArgumentException("No BOMs specified");
          }
          this.include = include;
          // Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes.
          Arrays.sort(boms, ByteOrderMarkLengthComparator);
          this.boms = Arrays.asList(boms);
  
      }
  
      /**
       * Indicates whether the stream contains one of the specified BOMs.
       * 
       * @return true if the stream has one of the specified BOMs, otherwise false if it does not
       * @throws IOException
       *             if an error reading the first bytes of the stream occurs
       */
      public boolean hasBOM() throws IOException {
          return getBOM() != null;
      }
  
      /**
       * Indicates whether the stream contains the specified BOM.
       * 
       * @param bom
       *            The BOM to check for
       * @return true if the stream has the specified BOM, otherwise false if it does not
       * @throws IllegalArgumentException
       *             if the BOM is not one the stream is configured to detect
       * @throws IOException
       *             if an error reading the first bytes of the stream occurs
       */
      public boolean hasBOM(ByteOrderMark bom) throws IOException {
          if (!boms.contains(bom)) {
              throw new IllegalArgumentException("Stream not configure to detect " + bom);
          }
          return byteOrderMark != null && getBOM().equals(bom);
      }
  
      /**
       * Return the BOM (Byte Order Mark).
       * 
       * @return The BOM or null if none
       * @throws IOException
       *             if an error reading the first bytes of the stream occurs
       */
      public ByteOrderMark getBOM() throws IOException {
          if (firstBytes == null) {
              fbLength = 0;
              // BOMs are sorted from longest to shortest
              final int maxBomSize = boms.get(0).length();
              firstBytes = new int[maxBomSize];
              // Read first maxBomSize bytes
              for (int i = 0; i < firstBytes.length; i++) {
                  firstBytes[i] = in.read();
                  fbLength++;
                  if (firstBytes[i] < 0) {
                      break;
                  }
              }
              // match BOM in firstBytes
              byteOrderMark = find();
              if (byteOrderMark != null) {
                  if (!include) {
                      if (byteOrderMark.length() < firstBytes.length) {
                          fbIndex = byteOrderMark.length();
                      } else {
                          fbLength = 0;
                      }
                  }
              }
          }
          return byteOrderMark;
      }
  
      /**
       * Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}.
       * 
       * @return The BOM charset Name or null if no BOM found
       * @throws IOException
       *             if an error reading the first bytes of the stream occurs
       * 
       */
      public String getBOMCharsetName() throws IOException {
          getBOM();
          return byteOrderMark == null ? null : byteOrderMark.getCharsetName();
      }
  
      /**
       * This method reads and either preserves or skips the first bytes in the stream. It behaves like the single-byte
       * <code>read()</code> method, either returning a valid byte or -1 to indicate that the initial bytes have been
       * processed already.
       * 
       * @return the byte read (excluding BOM) or -1 if the end of stream
       * @throws IOException
       *             if an I/O error occurs
       */
      private int readFirstBytes() throws IOException {
          getBOM();
          return fbIndex < fbLength ? firstBytes[fbIndex++] : -1;
      }
  
      /**
       * Find a BOM with the specified bytes.
       * 
       * @return The matched BOM or null if none matched
       */
      private ByteOrderMark find() {
          for (ByteOrderMark bom : boms) {
              if (matches(bom)) {
                  return bom;
              }
          }
          return null;
      }
  
      /**
       * Check if the bytes match a BOM.
       * 
       * @param bom
       *            The BOM
       * @return true if the bytes match the bom, otherwise false
       */
      private boolean matches(ByteOrderMark bom) {
          // if (bom.length() != fbLength) {
          // return false;
          // }
          // firstBytes may be bigger than the BOM bytes
          for (int i = 0; i < bom.length(); i++) {
              if (bom.get(i) != firstBytes[i]) {
                  return false;
              }
          }
          return true;
      }
  
      // ----------------------------------------------------------------------------
      // Implementation of InputStream
      // ----------------------------------------------------------------------------
  
      /**
       * Invokes the delegate's <code>read()</code> method, detecting and optionally skipping BOM.
       * 
       * @return the byte read (excluding BOM) or -1 if the end of stream
       * @throws IOException
       *             if an I/O error occurs
       */
      @Override
      public int read() throws IOException {
          int b = readFirstBytes();
          return b >= 0 ? b : in.read();
      }
  
      /**
       * Invokes the delegate's <code>read(byte[], int, int)</code> method, detecting and optionally skipping BOM.
       * 
       * @param buf
       *            the buffer to read the bytes into
       * @param off
       *            The start offset
       * @param len
       *            The number of bytes to read (excluding BOM)
       * @return the number of bytes read or -1 if the end of stream
       * @throws IOException
       *             if an I/O error occurs
       */
      @Override
      public int read(byte[] buf, int off, int len) throws IOException {
          int firstCount = 0;
          int b = 0;
          while (len > 0 && b >= 0) {
              b = readFirstBytes();
              if (b >= 0) {
                  buf[off++] = (byte) (b & 0xFF);
                  len--;
                  firstCount++;
              }
          }
          int secondCount = in.read(buf, off, len);
          return secondCount < 0 ? firstCount > 0 ? firstCount : -1 : firstCount + secondCount;
      }
  
      /**
       * Invokes the delegate's <code>read(byte[])</code> method, detecting and optionally skipping BOM.
       * 
       * @param buf
       *            the buffer to read the bytes into
       * @return the number of bytes read (excluding BOM) or -1 if the end of stream
       * @throws IOException
       *             if an I/O error occurs
       */
      @Override
      public int read(byte[] buf) throws IOException {
          return read(buf, 0, buf.length);
      }
  
      /**
       * Invokes the delegate's <code>mark(int)</code> method.
       * 
       * @param readlimit
       *            read ahead limit
       */
      @Override
      public synchronized void mark(int readlimit) {
          markFbIndex = fbIndex;
          markedAtStart = firstBytes == null;
          in.mark(readlimit);
      }
  
      /**
       * Invokes the delegate's <code>reset()</code> method.
       * 
       * @throws IOException
       *             if an I/O error occurs
       */
      @Override
      public synchronized void reset() throws IOException {
          fbIndex = markFbIndex;
          if (markedAtStart) {
              firstBytes = null;
          }
  
          in.reset();
      }
  
      /**
       * Invokes the delegate's <code>skip(long)</code> method, detecting and optionallyskipping BOM.
       * 
       * @param n
       *            the number of bytes to skip
       * @return the number of bytes to skipped or -1 if the end of stream
       * @throws IOException
       *             if an I/O error occurs
       */
      @Override
      public long skip(long n) throws IOException {
          while (n > 0 && readFirstBytes() >= 0) {
              n--;
          }
          return in.skip(n);
      }
  }
  