/** * VMware Continuent Tungsten Replicator * Copyright (C) 2015 VMware, Inc. All rights reserved. * * Licensed 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. * * Initial developer(s): Robert Hodges * Contributor(s): */ package com.continuent.tungsten.common.io; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.FileChannel; import org.apache.log4j.Logger; /** * Merges the capabilities of the following stream classes into a single class: * FileInputStream, BufferedInputStream, and DataInputStream. This allows us to * manage buffered data reads from files efficiently. * * @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a> */ public class BufferedFileDataInput { private static Logger logger = Logger.getLogger(BufferedFileDataInput.class); // Read parameters. private File file; private int size; // Variables to control reading. private FileInputStream fileInput; private BufferedInputStream bufferedInput; private DataInputStream dataInput; private long offset; private long markOffset; private long available; private FileChannel fileChannel; /** * Creates instance positioned on start of file. * * @param file File from which to read * @param size Size of buffer for buffered I/O */ public BufferedFileDataInput(File file, int size) throws FileNotFoundException, IOException, InterruptedException { this.file = file; this.size = size; seek(0); } /** * Creates instance with default buffer size. */ public BufferedFileDataInput(File file) throws FileNotFoundException, IOException, InterruptedException { this(file, 1024); } /** * Returns the current offset position. */ public long getOffset() { return offset; } /** * Query the stream directly for the number of bytes available for immediate * read without blocking. This operation may result in a file system * metadata call. To find out if a specific number of bytes are known to be * available use waitForAvailable(). * * @return Number of bytes available for non-blocking read */ public long available() throws IOException, InterruptedException { try { available = fileChannel.size() - offset; return available; } catch (ClosedByInterruptException e) { // This is NIO's version of an interrupt, which we convert // for convenience of callers. At this point the channel is // no longer good, and further calls will fail. throw new InterruptedException(e.getClass().getName()); } } /** * Waits for a specified number of bytes to be available for a non-blocking * read. * * @param requested Number of bytes to read * @param waitMillis Milliseconds to wait before timeout * @return Number of bytes available for non-blocking read * @throws IOException Thrown if there is a problem checking for available * bytes * @throws InterruptedException Thrown if we are interrupted while waiting */ public long waitAvailable(int requested, int waitMillis) throws IOException, InterruptedException { // If we know there is already enough data to read, return immediately. if (available >= requested) return available; // Since there is not enough, wait until we see enough data to do a read // or exceed the timeout. long timeoutMillis = System.currentTimeMillis() + waitMillis; long nextReportMillis = System.currentTimeMillis() + 1000; while (available() < requested && System.currentTimeMillis() < timeoutMillis) { // We might get interrupted, in which case we want to terminate // the loop. if (Thread.interrupted()) throw new InterruptedException(); // Now bide a wee. Thread.sleep(50); if (System.currentTimeMillis() > nextReportMillis) { if (logger.isDebugEnabled()) logger.debug("Waited 1000ms for input to appear"); nextReportMillis = System.currentTimeMillis() + 1000; } } // Return number of bytes available for non-blocking read. return available; } /** * Mark stream to read up to limit. * * @param readLimit Number of bytes that may be read before resetting */ public void mark(int readLimit) { markOffset = offset; bufferedInput.mark(readLimit); } /** * Reset stream back to last mark. * * @throws IOException Thrown if mark has been invalidated or not set * @throws InterruptedException Thrown if we are interrupted */ public void reset() throws IOException, InterruptedException { try { bufferedInput.reset(); offset = markOffset; } catch (IOException e) { // Need to seek directly as mark is invalidated. this.seek(markOffset); } markOffset = -1; } /** * Skip requested number of bytes. * * @param bytes Number of bytes to skip * @return Number of bytes actually skipped * @throws IOException Thrown if seek not supported or other error */ public long skip(long bytes) throws IOException { long bytesSkipped = bufferedInput.skip(bytes); offset += bytesSkipped; available -= bytesSkipped; return bytesSkipped; } /** * Seek to a specific offset in the file. * * @param seekBytes Number of bytes from start of file * @throws IOException Thrown if offset cannot be found * @throws FileNotFoundException Thrown if file is not found * @throws InterruptedException Thrown if thread is interrupted */ public void seek(long seekBytes) throws FileNotFoundException, IOException, InterruptedException { try { // We do a close to avoid leaking file descriptors. Close might // generate a ClosedByInterruptException but it is hard to be sure. if (fileInput != null) { fileInput.close(); } // Refresh the file input. fileInput = new FileInputStream(file); fileChannel = fileInput.getChannel(); // Position on the correct location in the file. fileChannel.position(seekBytes); } catch (ClosedByInterruptException e) { // This is NIO's version of an interrupt, which we convert // for convenience of callers. At this point the channel is // no longer good, and further calls will fail. throw new InterruptedException(); } bufferedInput = new BufferedInputStream(fileInput, size); dataInput = new DataInputStream(bufferedInput); offset = seekBytes; markOffset = -1; // Determine number of bytes available immediately. This appears // to mitigate a race condition that occurs if the thread is interrupted // shortly after doing a seek. (See Google Issue 714.) available(); } /** * Reads a single byte. */ public byte readByte() throws IOException { byte v = dataInput.readByte(); offset += 1; available -= 1; return v; } /** Reads a single short. */ public short readShort() throws IOException { short v = dataInput.readShort(); offset += 2; available -= 2; return v; } /** Read a single integer. */ public int readInt() throws IOException { int v = dataInput.readInt(); offset += 4; available -= 4; return v; } /** Reads a single long. */ public long readLong() throws IOException { long v = dataInput.readLong(); offset += 8; available -= 8; return v; } /** * Reads a full byte array completely. * * @throws IOException Thrown if full byte array cannot be read */ public void readFully(byte[] bytes) throws IOException { readFully(bytes, 0, bytes.length); } /** * Reads a full byte array completely. * * @param bytes Buffer into which to read * @param start Starting byte position * @param len Number of bytes to read * @throws IOException Thrown if data cannot be read */ public void readFully(byte[] bytes, int start, int len) throws IOException { dataInput.readFully(bytes, start, len); offset += len; available -= len; } /** Close and release all resources. */ public void close() { try { // We don't try to trap NIO interrupts here as we are shutting down // anyway. if (fileChannel != null) fileChannel.close(); if (fileInput != null) fileInput.close(); } catch (IOException e) { logger.warn("Unable to close buffered file reader: file=" + file.getName() + " exception=" + e.getMessage()); } fileInput = null; bufferedInput = null; dataInput = null; offset = -1; available = 0; } /** * Print contents of the reader. */ public String toString() { StringBuffer sb = new StringBuffer(); sb.append(this.getClass().getSimpleName()); sb.append(" file=").append(file.getName()); sb.append(" size=").append(size); sb.append(" offset=").append(offset); return sb.toString(); } }