package com.github.marschall.memoryfilesystem; import static com.github.marschall.memoryfilesystem.AutoReleaseLock.autoRelease; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.NonReadableChannelException; import java.nio.channels.WritableByteChannel; import java.nio.file.Path; import java.util.HashSet; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; abstract class BlockChannel extends FileChannel { volatile long position; final MemoryContents memoryContents; final boolean readable; /** * The {@link java.nio.channels.WritableByteChannel} documentation says * only one write operation may be in progress on a channel at a time. * * The {@link java.nio.channels.ReadableByteChannel} documentation says * only one read operation may be in progress on a channel at a time. */ private final Lock lock; /** * Lazily allocated. */ private Set<MemoryFileLock> fileLocks; final Path path; private final boolean deleteOnClose; BlockChannel(MemoryContents memoryContents, boolean readable, boolean deleteOnClose, Path path) { this.memoryContents = memoryContents; this.readable = readable; this.deleteOnClose = deleteOnClose; this.lock = new ReentrantLock(); this.path = path; } void closedCheck() throws ClosedChannelException { if (!this.isOpen()) { throw new ClosedChannelException(); } } abstract void writeCheck() throws ClosedChannelException; private void readCheck() throws ClosedChannelException { this.closedCheck(); if (!this.readable) { throw new NonReadableChannelException(); } } AutoRelease writeLock() throws ClosedChannelException { this.writeCheck(); return autoRelease(this.lock); } private AutoRelease readLock() throws ClosedChannelException { this.readCheck(); return autoRelease(this.lock); } @Override public int read(ByteBuffer dst) throws IOException { try (AutoRelease lock = this.readLock()) { int read = this.memoryContents.readShort(dst, this.position); if (read != -1) { this.position += read; } return read; } } @Override public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { this.validateOffsetAndLength(dsts, offset, length); try (AutoRelease lock = this.readLock()) { // we have to take care this value does not overflow since a single buffer // can hold more than Integer.MAX_VALUE bytes and this method returns a long long totalRead = 0L; for (int i = 0; i < length; ++i) { if (totalRead == Long.MAX_VALUE) { break; } // we have to make sure a buffers capacity is exhausted before reading into the // next buffer so we use the method that returns a long long read = this.memoryContents.read(dsts[offset + i], this.position, Long.MAX_VALUE - totalRead); if (read != -1) { // we could read data, update position and total counter this.position += read; totalRead += read; } else if (i == 0) { // we could not read and it was the first try a reading // (i.e. no previous attempt was made) return -1L; } else { // we could not read but previous attempts were successful // return result of previous attempts break; } } return totalRead; } } void validatePositionAndCount(long position, long count) { if (position < 0) { throw new IllegalArgumentException("position must be positive"); } if (count < 0) { throw new IllegalArgumentException("count must be positive"); } } void validateOffsetAndLength(ByteBuffer[] buffers, int offset, int length) { if (offset < 0) { throw new IndexOutOfBoundsException("offset must not be negative"); } if (offset >= buffers.length) { throw new IndexOutOfBoundsException("offset must be smaller than " + buffers.length); } if (length < 0) { throw new IndexOutOfBoundsException("length must not be negative"); } if (length > buffers.length - offset) { throw new IndexOutOfBoundsException("length too large"); } } @Override public int read(ByteBuffer dst, long position) throws IOException { try (AutoRelease lock = this.readLock()) { return this.memoryContents.readShort(dst, position); } } @Override public long transferTo(long position, long count, WritableByteChannel target) throws IOException { // TODO more validation this.validatePositionAndCount(position, count); try (AutoRelease lock = this.readLock()) { return this.memoryContents.transferTo(target, position, count); } } @Override public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { throw new UnsupportedOperationException("memory file system does not support mmapped IO"); } @Override public long position() throws IOException { this.closedCheck(); return this.position; } @Override public FileChannel position(long newPosition) throws IOException { if (newPosition < 0L) { throw new IllegalArgumentException("only a non-negative values are allowed, " + newPosition + " is invalid"); } this.closedCheck(); try (AutoRelease autoRelease = autoRelease(this.lock)) { this.position = newPosition; } return this; } @Override public long size() throws IOException { this.closedCheck(); return this.memoryContents.size(); } MemoryFileLock lock(MemoryFileLock l) throws IOException { try (AutoRelease lock = this.writeLock()) { MemoryFileLock fileLock = this.memoryContents.lock(l); this.addLock(fileLock); return fileLock; } } FileLock tryLock(MemoryFileLock l) throws IOException { try (AutoRelease lock = this.writeLock()) { MemoryFileLock fileLock = this.memoryContents.tryLock(l); if (fileLock != null) { this.addLock(fileLock); } return fileLock; } } @Override public FileLock lock(long position, long size, boolean shared) throws IOException { return this.lock(new MemoryFileLock(this, position, size, shared)); } @Override public FileLock tryLock(long position, long size, boolean shared) throws IOException { return this.tryLock(new MemoryFileLock(this, position, size, shared)); } private void addLock(MemoryFileLock fileLock) { if (this.fileLocks == null) { this.fileLocks = new HashSet<>(); } this.fileLocks.add(fileLock); } void removeLock(MemoryFileLock fileLock) throws IOException { try (AutoRelease lock = this.writeLock()) { this.fileLocks.remove(fileLock); this.memoryContents.unlock(fileLock); } } @Override protected void implCloseChannel() throws IOException { try (AutoRelease lock = autoRelease(this.lock)) { // update atime and mtime this.force(true); if (this.fileLocks != null) { for (MemoryFileLock fileLock : this.fileLocks) { this.memoryContents.unlock(fileLock); } } this.memoryContents.closedChannel(this.path, this.deleteOnClose); } } }