/* Copyright (c) 2007 Jython Developers */
package org.python.core.io;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import com.kenai.constantine.platform.Errno;
import org.jruby.ext.posix.util.Platform;
import org.python.core.Py;
import org.python.core.PyString;
import org.python.core.util.RelativeFile;
import org.python.modules.posix.PosixModule;
/**
* Raw I/O implementation for OS files.
*
* @author Philip Jenvey
*/
public class FileIO extends RawIOBase {
/** The underlying file channel */
private FileChannel fileChannel;
/** The underlying RandomAccessFile, if known. May be null */
private RandomAccessFile file;
/** The underlying FileOutputStream, if known. May be null */
private FileOutputStream fileOutputStream;
/** true if the file is opened for reading ('r') */
private boolean reading;
/** true if the file is opened for writing ('w', 'a', or '+') */
private boolean writing;
/** true if the file is in appending mode ('a') */
private boolean appending;
/** true if the file is opened for reading and writing ('+') */
private boolean plus;
/** true if write will emulate O_APPEND mode */
private boolean emulateAppend;
/**
* @see #FileIO(PyString name, String mode)
*/
public FileIO(String name, String mode) {
this(Py.newString(name), mode);
}
/**
* Construct a FileIO instance for the specified file name.
*
* The mode can be 'r', 'w' or 'a' for reading (default), writing
* or appending. Add a '+' to the mode to allow simultaneous
* reading and writing.
*
* @param name the name of the file
* @param mode a raw io file mode String
*/
public FileIO(PyString name, String mode) {
parseMode(mode);
File absPath = new RelativeFile(name.toString());
try {
if (appending && !(reading || plus)) {
// Take advantage of FileOutputStream's append mode
fromFileOutputStream(absPath);
} else {
fromRandomAccessFile(absPath);
emulateAppend = appending;
}
} catch (FileNotFoundException fnfe) {
if (absPath.isDirectory()) {
throw Py.IOError(Errno.EISDIR, name);
}
if ((writing && !absPath.canWrite())
|| fnfe.getMessage().endsWith("(Permission denied)")) {
throw Py.IOError(Errno.EACCES, name);
}
throw Py.IOError(Errno.ENOENT, name);
}
initPosition();
}
/**
* Construct a FileIO instance with the given FileChannel.
*
* The mode can be 'r', 'w' or 'a' for reading (default), writing
* or appending. Add a '+' to the mode to allow simultaneous
* reading and writing.
*
* @param fileChannel a FileChannel object
* @param mode a raw io file mode String
*/
public FileIO(FileChannel fileChannel, String mode) {
parseMode(mode);
this.fileChannel = fileChannel;
initPosition();
}
/**
* Parse the Python mode string.
*
* The mode can be 'r', 'w' or 'a' for reading (default), writing
* or appending. Add a '+' to the mode to allow simultaneous
* reading and writing.
*
* @param mode a raw io file mode String
*/
private void parseMode(String mode) {
boolean rwa = false;
for (int i = 0; i < mode.length(); i++) {
switch (mode.charAt(i)) {
case 'r':
if (plus || rwa) {
badMode();
}
reading = rwa = true;
break;
case 'w':
if (plus || rwa) {
badMode();
}
writing = rwa = true;
break;
case 'a':
if (plus || rwa) {
badMode();
}
appending = writing = rwa = true;
break;
case '+':
if (plus || !rwa) {
badMode();
}
writing = plus = true;
break;
default:
throw Py.ValueError("invalid mode: '" + mode + "'");
}
}
if (!rwa) {
badMode();
}
}
/**
* Open the underlying FileChannel from a RandomAccessFile.
*
* @param absPath The absolute path File to open
*/
private void fromRandomAccessFile(File absPath) throws FileNotFoundException {
String rafMode = "r" + (writing ? "w" : "");
if (plus && reading && !absPath.isFile()) {
// suppress "permission denied"
writing = false;
throw new FileNotFoundException("");
}
file = new RandomAccessFile(absPath, rafMode);
fileChannel = file.getChannel();
}
/**
* Open the underlying FileChannel from a FileOutputStream in append mode, as opposed
* to a RandomAccessFile, for the use of the OS's underlying O_APPEND mode. This can
* only be used by 'a' (not 'a+') mode.
*
* @param absPath The absolute path File to open
*/
private void fromFileOutputStream(File absPath) throws FileNotFoundException {
fileOutputStream = new FileOutputStream(absPath, true);
fileChannel = fileOutputStream.getChannel();
}
/**
* Raise a value error due to a mode string not containing exactly
* one r/w/a/+ character.
*
*/
private void badMode() {
throw Py.ValueError("Must have exactly one of read/write/append mode");
}
/**
* Set the appropriate file position for writing/appending modes.
*
*/
private void initPosition() {
if (appending) {
seek(0, 2);
} else if (writing && !reading) {
try {
fileChannel.truncate(0);
} catch (IOException ioe) {
// On Solaris and Linux, ftruncate(3C) returns EINVAL if not a regular
// file whereas, e.g., open(os.devnull, "w") works. Because we have to
// simulate the "w" mode in Java, we suppress the exception.
// Likewise Windows returns ERROR_INVALID_FUNCTION in that case and
// ERROR_INVALID_HANDLE on ttys. Identifying those by the IOException
// message is tedious as their messages are localized, so we suppress them
// all =[
if (Platform.IS_WINDOWS ||
((Platform.IS_SOLARIS || Platform.IS_LINUX)
&& Errno.EINVAL.description().equals(ioe.getMessage()))) {
return;
}
throw Py.IOError(ioe);
}
}
}
@Override
public boolean isatty() {
checkClosed();
if (file == null || fileOutputStream == null) {
return false;
}
try {
return PosixModule.getPOSIX().isatty(file != null
? file.getFD() : fileOutputStream.getFD());
} catch (IOException e) {
return false;
}
}
@Override
public int readinto(ByteBuffer buf) {
checkClosed();
checkReadable();
try {
return fileChannel.read(buf);
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
}
/**
* Read bytes into each of the specified ByteBuffers via scatter
* i/o.
*
* @param bufs {@inheritDoc}
* @return {@inheritDoc}
*/
@Override
public long readinto(ByteBuffer[] bufs) {
checkClosed();
checkReadable();
try {
return fileChannel.read(bufs);
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
}
/**
* Read until EOF with one readinto() call.
*
* Takes advantage of the fact that the underlying file's size is
* available.
*
* @return {@inheritDoc}
*/
@Override
public ByteBuffer readall() {
checkClosed();
checkReadable();
// NOTE: This could be less accurate than multiple reads if
// the file is growing
long toRead;
try {
toRead = Math.max(0, fileChannel.size() - fileChannel.position());
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
if (toRead > Integer.MAX_VALUE) {
throw Py.OverflowError("requested number of bytes is more than a Python string can "
+ "hold");
}
ByteBuffer all = ByteBuffer.allocate((int)toRead);
readinto(all);
all.flip();
return all;
}
@Override
public int write(ByteBuffer buf) {
checkClosed();
checkWritable();
try {
return !emulateAppend ? fileChannel.write(buf) :
fileChannel.write(buf, fileChannel.position());
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
}
/**
* Write bytes from each of the specified ByteBuffers via gather
* i/o.
*
* @param bufs {@inheritDoc}
* @return {@inheritDoc}
*/
@Override
public long write(ByteBuffer[] bufs) {
checkClosed();
checkWritable();
try {
return !emulateAppend ? fileChannel.write(bufs) : writeAppend(bufs);
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
}
/**
* Write multiple ByteBuffers while emulating O_APPEND mode.
*
* @param bufs an array of ByteBuffers
* @return the number of bytes written as a long
*/
private long writeAppend(ByteBuffer[] bufs) throws IOException {
long count = 0;
int bufCount;
for (ByteBuffer buf : bufs) {
if (!buf.hasRemaining()) {
continue;
}
if ((bufCount = fileChannel.write(buf, fileChannel.position())) == 0) {
break;
}
count += bufCount;
}
return count;
}
@Override
public long seek(long pos, int whence) {
checkClosed();
try {
switch (whence) {
case 0:
break;
case 1:
pos += fileChannel.position();
break;
case 2:
pos += fileChannel.size();
break;
default:
throw Py.IOError(Errno.EINVAL);
}
fileChannel.position(pos);
return pos;
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
}
@Override
public long tell() {
checkClosed();
try {
return fileChannel.position();
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
}
@Override
public long truncate(long size) {
checkClosed();
checkWritable();
try {
long oldPosition = fileChannel.position();
fileChannel.truncate(size);
// seek necessary on Windows only, see <http://bugs.python.org/issue801631>
fileChannel.position(oldPosition);
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
return size;
}
@Override
public void close() {
if (closed()) {
return;
}
try {
fileChannel.close();
} catch (IOException ioe) {
throw Py.IOError(ioe);
}
super.close();
}
@Override
public OutputStream asOutputStream() {
return writing ? Channels.newOutputStream(fileChannel) : super.asOutputStream();
}
@Override
public InputStream asInputStream() {
return readable() ? Channels.newInputStream(fileChannel) : super.asInputStream();
}
@Override
public boolean readable() {
return reading || plus;
}
@Override
public boolean writable() {
return writing;
}
@Override
public FileChannel getChannel() {
return fileChannel;
}
}