/* * 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 com.ctrip.framework.foundation.internals.io; import static com.ctrip.framework.foundation.internals.io.IOUtils.EOF; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Comparator; import java.util.List; /** * 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 ByteOrderMark * @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia - Byte Order Mark</a> * @version $Id: BOMInputStream.java 1686527 2015-06-20 06:31:39Z krosenvold $ * @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(final 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(final InputStream delegate, final 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(final InputStream delegate, final 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(final ByteOrderMark bom1, final ByteOrderMark bom2) { final int len1 = bom1.length(); final int len2 = bom2.length(); if (len1 > len2) { return EOF; } 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(final InputStream delegate, final boolean include, final 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(final 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++] : EOF; } /** * Find a BOM with the specified bytes. * * @return The matched BOM or null if none matched */ private ByteOrderMark find() { for (final 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(final 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 { final 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(final 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++; } } final int secondCount = in.read(buf, off, len); return secondCount < 0 ? firstCount > 0 ? firstCount : EOF : 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(final 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(final 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 { int skipped = 0; while ((n > skipped) && (readFirstBytes() >= 0)) { skipped++; } return in.skip(n - skipped) + skipped; } }