Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.h2.store.fs.FilePathNioMem Maven / Gradle / Ivy
/*
* Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.store.fs;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.NonWritableChannelException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.h2.api.ErrorCode;
import org.h2.compress.CompressLZF;
import org.h2.message.DbException;
import org.h2.util.MathUtils;
/**
* This file system keeps files fully in memory. There is an option to compress
* file blocks to save memory.
*/
public class FilePathNioMem extends FilePath {
private static final TreeMap MEMORY_FILES =
new TreeMap<>();
/**
* The percentage of uncompressed (cached) entries.
*/
float compressLaterCachePercent = 1;
@Override
public FilePathNioMem getPath(String path) {
FilePathNioMem p = new FilePathNioMem();
p.name = getCanonicalPath(path);
return p;
}
@Override
public long size() {
return getMemoryFile().length();
}
@Override
public void moveTo(FilePath newName, boolean atomicReplace) {
synchronized (MEMORY_FILES) {
if (!atomicReplace && !name.equals(newName.name) &&
MEMORY_FILES.containsKey(newName.name)) {
throw DbException.get(ErrorCode.FILE_RENAME_FAILED_2, name, newName + " (exists)");
}
FileNioMemData f = getMemoryFile();
f.setName(newName.name);
MEMORY_FILES.remove(name);
MEMORY_FILES.put(newName.name, f);
}
}
@Override
public boolean createFile() {
synchronized (MEMORY_FILES) {
if (exists()) {
return false;
}
getMemoryFile();
}
return true;
}
@Override
public boolean exists() {
if (isRoot()) {
return true;
}
synchronized (MEMORY_FILES) {
return MEMORY_FILES.get(name) != null;
}
}
@Override
public void delete() {
if (isRoot()) {
return;
}
synchronized (MEMORY_FILES) {
MEMORY_FILES.remove(name);
}
}
@Override
public List newDirectoryStream() {
ArrayList list = new ArrayList<>();
synchronized (MEMORY_FILES) {
for (String n : MEMORY_FILES.tailMap(name).keySet()) {
if (n.startsWith(name)) {
list.add(getPath(n));
} else {
break;
}
}
return list;
}
}
@Override
public boolean setReadOnly() {
return getMemoryFile().setReadOnly();
}
@Override
public boolean canWrite() {
return getMemoryFile().canWrite();
}
@Override
public FilePathNioMem getParent() {
int idx = name.lastIndexOf('/');
return idx < 0 ? null : getPath(name.substring(0, idx));
}
@Override
public boolean isDirectory() {
if (isRoot()) {
return true;
}
// TODO in memory file system currently
// does not really support directories
synchronized (MEMORY_FILES) {
return MEMORY_FILES.get(name) == null;
}
}
@Override
public boolean isAbsolute() {
// TODO relative files are not supported
return true;
}
@Override
public FilePathNioMem toRealPath() {
return this;
}
@Override
public long lastModified() {
return getMemoryFile().getLastModified();
}
@Override
public void createDirectory() {
if (exists() && isDirectory()) {
throw DbException.get(ErrorCode.FILE_CREATION_FAILED_1,
name + " (a file with this name already exists)");
}
// TODO directories are not really supported
}
@Override
public OutputStream newOutputStream(boolean append) throws IOException {
FileNioMemData obj = getMemoryFile();
FileNioMem m = new FileNioMem(obj, false);
return new FileChannelOutputStream(m, append);
}
@Override
public InputStream newInputStream() {
FileNioMemData obj = getMemoryFile();
FileNioMem m = new FileNioMem(obj, true);
return new FileChannelInputStream(m, true);
}
@Override
public FileChannel open(String mode) {
FileNioMemData obj = getMemoryFile();
return new FileNioMem(obj, "r".equals(mode));
}
private FileNioMemData getMemoryFile() {
synchronized (MEMORY_FILES) {
FileNioMemData m = MEMORY_FILES.get(name);
if (m == null) {
m = new FileNioMemData(name, compressed(), compressLaterCachePercent);
MEMORY_FILES.put(name, m);
}
return m;
}
}
protected boolean isRoot() {
return name.equals(getScheme() + ":");
}
/**
* Get the canonical path of a file (with backslashes replaced with forward
* slashes).
*
* @param fileName the file name
* @return the canonical path
*/
protected static String getCanonicalPath(String fileName) {
fileName = fileName.replace('\\', '/');
int idx = fileName.lastIndexOf(':') + 1;
if (fileName.length() > idx && fileName.charAt(idx) != '/') {
fileName = fileName.substring(0, idx) + "/" + fileName.substring(idx);
}
return fileName;
}
@Override
public String getScheme() {
return "nioMemFS";
}
/**
* Whether the file should be compressed.
*
* @return true if it should be compressed.
*/
boolean compressed() {
return false;
}
}
/**
* A memory file system that compresses blocks to conserve memory.
*/
class FilePathNioMemLZF extends FilePathNioMem {
@Override
boolean compressed() {
return true;
}
@Override
public FilePathNioMem getPath(String path) {
if (!path.startsWith(getScheme())) {
throw new IllegalArgumentException(path +
" doesn't start with " + getScheme());
}
int idx1 = path.indexOf(':');
int idx2 = path.lastIndexOf(':');
final FilePathNioMemLZF p = new FilePathNioMemLZF();
if (idx1 != -1 && idx1 != idx2) {
p.compressLaterCachePercent = Float.parseFloat(path.substring(idx1 + 1, idx2));
}
p.name = getCanonicalPath(path);
return p;
}
@Override
protected boolean isRoot() {
return name.lastIndexOf(':') == name.length() - 1;
}
@Override
public String getScheme() {
return "nioMemLZF";
}
}
/**
* This class represents an in-memory file.
*/
class FileNioMem extends FileBase {
/**
* The file data.
*/
FileNioMemData data;
private final boolean readOnly;
private long pos;
FileNioMem(FileNioMemData data, boolean readOnly) {
this.data = data;
this.readOnly = readOnly;
}
@Override
public long size() {
return data.length();
}
@Override
public FileChannel truncate(long newLength) throws IOException {
// compatibility with JDK FileChannel#truncate
if (readOnly) {
throw new NonWritableChannelException();
}
if (data == null) {
throw new ClosedChannelException();
}
if (newLength < size()) {
data.touch(readOnly);
pos = Math.min(pos, newLength);
data.truncate(newLength);
}
return this;
}
@Override
public FileChannel position(long newPos) {
this.pos = (int) newPos;
return this;
}
@Override
public int write(ByteBuffer src) throws IOException {
if (data == null) {
throw new ClosedChannelException();
}
int len = src.remaining();
if (len == 0) {
return 0;
}
data.touch(readOnly);
// offset is 0 because we start writing from src.position()
pos = data.readWrite(pos, src, 0, len, true);
src.position(src.position() + len);
return len;
}
@Override
public int read(ByteBuffer dst) throws IOException {
if (data == null) {
throw new ClosedChannelException();
}
int len = dst.remaining();
if (len == 0) {
return 0;
}
long newPos = data.readWrite(pos, dst, dst.position(), len, false);
len = (int) (newPos - pos);
if (len <= 0) {
return -1;
}
dst.position(dst.position() + len);
pos = newPos;
return len;
}
@Override
public int read(ByteBuffer dst, long position) throws IOException {
if (data == null) {
throw new ClosedChannelException();
}
int len = dst.remaining();
if (len == 0) {
return 0;
}
long newPos;
newPos = data.readWrite(position, dst, dst.position(), len, false);
len = (int) (newPos - position);
if (len <= 0) {
return -1;
}
dst.position(dst.position() + len);
return len;
}
@Override
public long position() {
return pos;
}
@Override
public void implCloseChannel() throws IOException {
pos = 0;
data = null;
}
@Override
public void force(boolean metaData) throws IOException {
// do nothing
}
@Override
public synchronized FileLock tryLock(long position, long size,
boolean shared) throws IOException {
if (data == null) {
throw new ClosedChannelException();
}
if (shared) {
if (!data.lockShared()) {
return null;
}
} else {
if (!data.lockExclusive()) {
return null;
}
}
return new FileLock(FakeFileChannel.INSTANCE, position, size, shared) {
@Override
public boolean isValid() {
return true;
}
@Override
public void release() throws IOException {
data.unlock();
}
};
}
@Override
public String toString() {
return data == null ? "" : data.getName();
}
}
/**
* This class contains the data of an in-memory random access file.
* Data compression using the LZF algorithm is supported as well.
*/
class FileNioMemData {
private static final int CACHE_MIN_SIZE = 8;
private static final int BLOCK_SIZE_SHIFT = 16;
private static final int BLOCK_SIZE = 1 << BLOCK_SIZE_SHIFT;
private static final int BLOCK_SIZE_MASK = BLOCK_SIZE - 1;
private static final ByteBuffer COMPRESSED_EMPTY_BLOCK;
private static final ThreadLocal LZF_THREAD_LOCAL =
new ThreadLocal() {
@Override
protected CompressLZF initialValue() {
return new CompressLZF();
}
};
/** the output buffer when compressing */
private static final ThreadLocal COMPRESS_OUT_BUF_THREAD_LOCAL =
new ThreadLocal() {
@Override
protected byte[] initialValue() {
return new byte[BLOCK_SIZE * 2];
}
};
/**
* The hash code of the name.
*/
final int nameHashCode;
private final CompressLaterCache compressLaterCache =
new CompressLaterCache<>(CACHE_MIN_SIZE);
private String name;
private final boolean compress;
private final float compressLaterCachePercent;
private long length;
private AtomicReference[] buffers;
private long lastModified;
private boolean isReadOnly;
private boolean isLockedExclusive;
private int sharedLockCount;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
static {
final byte[] n = new byte[BLOCK_SIZE];
final byte[] output = new byte[BLOCK_SIZE * 2];
int len = new CompressLZF().compress(n, BLOCK_SIZE, output, 0);
COMPRESSED_EMPTY_BLOCK = ByteBuffer.allocateDirect(len);
COMPRESSED_EMPTY_BLOCK.put(output, 0, len);
}
@SuppressWarnings("unchecked")
FileNioMemData(String name, boolean compress, float compressLaterCachePercent) {
this.name = name;
this.nameHashCode = name.hashCode();
this.compress = compress;
this.compressLaterCachePercent = compressLaterCachePercent;
buffers = new AtomicReference[0];
lastModified = System.currentTimeMillis();
}
/**
* Lock the file in exclusive mode if possible.
*
* @return if locking was successful
*/
synchronized boolean lockExclusive() {
if (sharedLockCount > 0 || isLockedExclusive) {
return false;
}
isLockedExclusive = true;
return true;
}
/**
* Lock the file in shared mode if possible.
*
* @return if locking was successful
*/
synchronized boolean lockShared() {
if (isLockedExclusive) {
return false;
}
sharedLockCount++;
return true;
}
/**
* Unlock the file.
*/
synchronized void unlock() {
if (isLockedExclusive) {
isLockedExclusive = false;
} else {
sharedLockCount = Math.max(0, sharedLockCount - 1);
}
}
/**
* This small cache compresses the data if an element leaves the cache.
*/
static class CompressLaterCache extends LinkedHashMap {
private static final long serialVersionUID = 1L;
private int size;
CompressLaterCache(int size) {
super(size, (float) 0.75, true);
this.size = size;
}
@Override
public synchronized V put(K key, V value) {
return super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if (size() < size) {
return false;
}
CompressItem c = (CompressItem) eldest.getKey();
c.data.compressPage(c.page);
return true;
}
public void setCacheSize(int size) {
this.size = size;
}
}
/**
* Represents a compressed item.
*/
static class CompressItem {
/**
* The file data.
*/
public final FileNioMemData data;
/**
* The page to compress.
*/
public final int page;
public CompressItem(FileNioMemData data, int page) {
this.data = data;
this.page = page;
}
@Override
public int hashCode() {
return page ^ data.nameHashCode;
}
@Override
public boolean equals(Object o) {
if (o instanceof CompressItem) {
CompressItem c = (CompressItem) o;
return c.data == data && c.page == page;
}
return false;
}
}
private void addToCompressLaterCache(int page) {
CompressItem c = new CompressItem(this, page);
compressLaterCache.put(c, c);
}
private ByteBuffer expandPage(int page) {
final ByteBuffer d = buffers[page].get();
if (d.capacity() == BLOCK_SIZE) {
// already expanded, or not compressed
return d;
}
synchronized (d) {
if (d.capacity() == BLOCK_SIZE) {
return d;
}
ByteBuffer out = ByteBuffer.allocateDirect(BLOCK_SIZE);
if (d != COMPRESSED_EMPTY_BLOCK) {
d.position(0);
CompressLZF.expand(d, out);
}
buffers[page].compareAndSet(d, out);
return out;
}
}
/**
* Compress the data in a byte array.
*
* @param page which page to compress
*/
void compressPage(int page) {
final ByteBuffer d = buffers[page].get();
synchronized (d) {
if (d.capacity() != BLOCK_SIZE) {
// already compressed
return;
}
final byte[] compressOutputBuffer = COMPRESS_OUT_BUF_THREAD_LOCAL.get();
int len = LZF_THREAD_LOCAL.get().compress(d, 0, compressOutputBuffer, 0);
ByteBuffer out = ByteBuffer.allocateDirect(len);
out.put(compressOutputBuffer, 0, len);
buffers[page].compareAndSet(d, out);
}
}
/**
* Update the last modified time.
*
* @param openReadOnly if the file was opened in read-only mode
*/
void touch(boolean openReadOnly) throws IOException {
if (isReadOnly || openReadOnly) {
throw new IOException("Read only");
}
lastModified = System.currentTimeMillis();
}
/**
* Get the file length.
*
* @return the length
*/
long length() {
return length;
}
/**
* Truncate the file.
*
* @param newLength the new length
*/
void truncate(long newLength) {
rwLock.writeLock().lock();
try {
changeLength(newLength);
long end = MathUtils.roundUpLong(newLength, BLOCK_SIZE);
if (end != newLength) {
int lastPage = (int) (newLength >>> BLOCK_SIZE_SHIFT);
ByteBuffer d = expandPage(lastPage);
for (int i = (int) (newLength & BLOCK_SIZE_MASK); i < BLOCK_SIZE; i++) {
d.put(i, (byte) 0);
}
if (compress) {
addToCompressLaterCache(lastPage);
}
}
} finally {
rwLock.writeLock().unlock();
}
}
@SuppressWarnings("unchecked")
private void changeLength(long len) {
length = len;
len = MathUtils.roundUpLong(len, BLOCK_SIZE);
int blocks = (int) (len >>> BLOCK_SIZE_SHIFT);
if (blocks != buffers.length) {
final AtomicReference[] newBuffers = new AtomicReference[blocks];
System.arraycopy(buffers, 0, newBuffers, 0,
Math.min(buffers.length, newBuffers.length));
for (int i = buffers.length; i < blocks; i++) {
newBuffers[i] = new AtomicReference<>(COMPRESSED_EMPTY_BLOCK);
}
buffers = newBuffers;
}
compressLaterCache.setCacheSize(Math.max(CACHE_MIN_SIZE, (int) (blocks *
compressLaterCachePercent / 100)));
}
/**
* Read or write.
*
* @param pos the position
* @param b the byte array
* @param off the offset within the byte array
* @param len the number of bytes
* @param write true for writing
* @return the new position
*/
long readWrite(long pos, ByteBuffer b, int off, int len, boolean write) {
final java.util.concurrent.locks.Lock lock = write ? rwLock.writeLock()
: rwLock.readLock();
lock.lock();
try {
long end = pos + len;
if (end > length) {
if (write) {
changeLength(end);
} else {
len = (int) (length - pos);
}
}
while (len > 0) {
final int l = (int) Math.min(len, BLOCK_SIZE - (pos & BLOCK_SIZE_MASK));
final int page = (int) (pos >>> BLOCK_SIZE_SHIFT);
final ByteBuffer block = expandPage(page);
int blockOffset = (int) (pos & BLOCK_SIZE_MASK);
if (write) {
final ByteBuffer srcTmp = b.slice();
final ByteBuffer dstTmp = block.duplicate();
srcTmp.position(off);
srcTmp.limit(off + l);
dstTmp.position(blockOffset);
dstTmp.put(srcTmp);
} else {
// duplicate, so this can be done concurrently
final ByteBuffer tmp = block.duplicate();
tmp.position(blockOffset);
tmp.limit(l + blockOffset);
int oldPosition = b.position();
b.position(off);
b.put(tmp);
// restore old position
b.position(oldPosition);
}
if (compress) {
addToCompressLaterCache(page);
}
off += l;
pos += l;
len -= l;
}
return pos;
} finally {
lock.unlock();
}
}
/**
* Set the file name.
*
* @param name the name
*/
void setName(String name) {
this.name = name;
}
/**
* Get the file name
*
* @return the name
*/
String getName() {
return name;
}
/**
* Get the last modified time.
*
* @return the time
*/
long getLastModified() {
return lastModified;
}
/**
* Check whether writing is allowed.
*
* @return true if it is
*/
boolean canWrite() {
return !isReadOnly;
}
/**
* Set the read-only flag.
*
* @return true
*/
boolean setReadOnly() {
isReadOnly = true;
return true;
}
}