NfsFileInputStream.java

/**
 * Copyright 2016-2018 Dell Inc. or its subsidiaries. All rights reserved.
 *
 * <p>Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
 * except in compliance with the License. A copy of the License is located at
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * <p>or in the "license" file accompanying this file. This file 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.emc.ecs.nfsclient.nfs.io;

import com.emc.ecs.nfsclient.nfs.NfsReadResponse;
import java.io.IOException;
import java.io.InputStream;

/**
 * The NFS equivalent of <code>java.io.FileInputStream</code>.
 *
 * @author seibed
 */
public class NfsFileInputStream extends InputStream {

  /** Constant for output. */
  public static final int EOF = -1;

  /** The underlying file. */
  private NfsFile<?, ?> _file;

  /** Current read position in the file, in bytes. */
  private long _offset;

  /** The byte buffer, used to hold data from the file during reading. */
  private final byte[] _bytes;

  /** The total number of bytes in the buffer. */
  private int _bytesInBuffer = 0;

  /** Current reading position in the buffer. */
  private int _currentBufferPosition = 0;

  /**
   * <ul>
   *   <li><code>true</code> if all data in the file has been returned and read,
   *   <li><code>false</code> otherwise.
   * </ul>
   */
  private boolean _isEof = false;

  /** Flag to prevent operations after closing. */
  private boolean _closed = false;

  /**
   * Creates a <code>NfsFileInputStream</code> by opening a connection to an actual NFS file, using
   * the specified <code>offset</code> and <code>maximumBufferSize</code>.
   *
   * <p>If the named file does not exist, is a directory rather than a regular file, or for some
   * other reason cannot be opened for reading then a <code>FileNotFoundException</code> is thrown.
   *
   * @param nfsFile The NFS file instance to be read.
   * @param offset The offset at which reading should start, in bytes.
   * @param maximumBufferSize The maximum buffer size to use in bytes.
   * @throws IOException If the file does not exist, is a directory rather than a regular file, or
   *     for some other reason cannot be opened for reading.
   */
  public NfsFileInputStream(NfsFile<?, ?> nfsFile, long offset, int maximumBufferSize)
      throws IOException {
    // Validate the offset.
    if (offset < 0) {
      throw new IllegalArgumentException("Cannot start reading before offset 0: " + offset);
    }

    // Validate the maximum buffer size.
    if (maximumBufferSize <= 0) {
      throw new IllegalArgumentException(
          "Cannot have a maximum buffer size <= 0: " + maximumBufferSize);
    }

    // Validate the file.
    if (!nfsFile.canRead()) {
      throw new IllegalArgumentException(
          "The file must be readable by the client: " + nfsFile.getAbsolutePath());
    }

    _file = nfsFile;
    _offset = offset;
    maximumBufferSize =
        Math.min(
            maximumBufferSize, (int) Math.min(_file.fsinfo().getFsInfo().rtmax, Integer.MAX_VALUE));
    _bytes = makeBytes(maximumBufferSize);
  }

  /**
   * Creates a <code>NfsFileInputStream</code> by opening a connection to an actual NFS file,
   * starting to read at offset 0 and using the specified maximum buffer size.
   *
   * <p>If the named file does not exist, is a directory rather than a regular file, or for some
   * other reason cannot be opened for reading then a <code>FileNotFoundException</code> is thrown.
   *
   * @param nfsFile The NFS file instance to be read.
   * @param maximumBufferSize The maximum buffer size to use in bytes.
   * @throws IOException If the file does not exist, is a directory rather than a regular file, or
   *     for some other reason cannot be opened for reading.
   */
  public NfsFileInputStream(NfsFile<?, ?> nfsFile, int maximumBufferSize) throws IOException {
    this(nfsFile, 0, maximumBufferSize);
  }

  /**
   * Creates a <code>NfsFileInputStream</code> by opening a connection to an actual NFS file,
   * starting to read at offset 0 and using the preferred buffer size as the maximums. This
   * constructor will generally give you the best performance.
   *
   * <p>If the named file does not exist, is a directory rather than a regular file, or for some
   * other reason cannot be opened for reading then a <code>FileNotFoundException</code> is thrown.
   *
   * @param nfsFile The NFS file instance to be read.
   * @throws IOException If the file does not exist, is a directory rather than a regular file, or
   *     for some other reason cannot be opened for reading.
   */
  public NfsFileInputStream(NfsFile<?, ?> nfsFile) throws IOException {
    this(nfsFile, (int) Math.min(nfsFile.fsinfo().getFsInfo().rtpref, Integer.MAX_VALUE));
  }

  /**
   * @return the byte array
   */
  private synchronized byte[] makeBytes(int maximumBufferSize) throws IOException {
    int bufferSize =
        Math.min((int) Math.min(_file.length() - _offset, Integer.MAX_VALUE), maximumBufferSize);
    if (bufferSize == 0) {
      _isEof = true;
    }
    return new byte[bufferSize];
  }

  /** {@inheritDoc} */
  @Override
  public synchronized int available() throws IOException {
    checkForClosed();
    return (int) Math.min(_file.length() - _offset + bytesLeftInBuffer(), Integer.MAX_VALUE);
  }

  /** {@inheritDoc} */
  @Override
  public synchronized void close() throws IOException {
    _closed = true;
    super.close();
  }

  /** {@inheritDoc} */
  @Override
  public synchronized void mark(int readlimit) {
    super.mark(readlimit);
  }

  /** {@inheritDoc} */
  @Override
  public boolean markSupported() {
    return false;
  }

  /** {@inheritDoc} */
  @Override
  public synchronized int read() throws IOException {
    byte[] b = new byte[1];
    int bytesRead = read(b, 0, 1);
    if (bytesRead == EOF) {
      return EOF;
    } else {
      // byte type in Java is from -128 to +127 and we are supposed to return 0-255
      return b[0] & 0xFF;
    }
  }

  /** {@inheritDoc} */
  @Override
  public synchronized int read(byte[] b) throws IOException {
    return read(b, 0, b.length);
  }

  /** {@inheritDoc} */
  @Override
  public synchronized int read(byte[] b, int off, int len) throws IOException {
    checkForClosed();
    if (b == null) {
      throw new NullPointerException();
    } else if ((off < 0) || (len < 0) || ((off + len) > b.length)) {
      throw new IndexOutOfBoundsException();
    } else if (len == 0) {
      return 0;
    }

    loadBytesAsNeeded();
    int bytesImmediatelyAvailable = bytesLeftInBuffer();
    if (len <= bytesImmediatelyAvailable) {
      System.arraycopy(_bytes, _currentBufferPosition, b, off, len);
      _currentBufferPosition += len;
      return len;
    }

    int bytesRead = EOF;
    if (bytesImmediatelyAvailable > 0) {
      bytesRead = read(b, off, bytesImmediatelyAvailable);
      if (bytesRead != EOF) {
        int furtherBytesRead = read(b, off + bytesRead, len - bytesRead);
        if (furtherBytesRead != EOF) {
          bytesRead += furtherBytesRead;
        }
      }
    }
    return bytesRead;
  }

  /** {@inheritDoc} */
  @Override
  public synchronized void reset() throws IOException {
    checkForClosed();
    super.reset();
  }

  /** {@inheritDoc} */
  @Override
  public synchronized long skip(long bytesToSkip) throws IOException {
    checkForClosed();
    long bytesSkipped = 0;
    while (bytesToSkip > bytesLeftInBuffer()) {
      bytesSkipped += bytesLeftInBuffer();
      bytesToSkip -= bytesLeftInBuffer();
      _currentBufferPosition = _bytesInBuffer;
      if (_isEof) {
        break;
      }
      loadBytesAsNeeded();
    }
    if ((bytesToSkip > 0) && (bytesToSkip <= bytesLeftInBuffer())) {
      _currentBufferPosition += (int) bytesToSkip;
      bytesSkipped += (int) bytesToSkip;
    }
    return bytesSkipped;
  }

  /**
   * @return The number of unread bytes in the buffer.
   */
  private int bytesLeftInBuffer() {
    return _bytesInBuffer - _currentBufferPosition;
  }

  /**
   * Convenience function.
   *
   * @throws IOException If the stream has been closed.
   */
  private void checkForClosed() throws IOException {
    if (_closed) {
      throw new IOException("This stream has been closed.");
    }
  }

  /**
   * If the buffer has no more bytes to be read, and there are bytes available in the file, load
   * more bytes.
   */
  private void loadBytesAsNeeded() throws IOException {
    if (available() <= 0) {
      _isEof = true;
    }

    while (!_isEof && (bytesLeftInBuffer() <= 0)) {
      _currentBufferPosition = 0;
      NfsReadResponse response = _file.read(_offset, _bytes.length, _bytes, _currentBufferPosition);
      _bytesInBuffer = response.getBytesRead();
      _offset += _bytesInBuffer;
      _isEof = response.isEof();
    }
  }
}