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.
/*
* Copyright 2014 dorkbox, llc
*
* 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.
*/
package dorkbox.util.storage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.nio.channels.Channels;
import java.nio.channels.FileLock;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import dorkbox.util.OS;
import dorkbox.util.serialization.SerializationManager;
// a note on file locking between c and java
// http://panks-dev.blogspot.de/2008/04/linux-file-locks-java-and-others.html
// Also, file locks on linux are ADVISORY. if an app doesn't care about locks, then it can do stuff -- even if locked by another app
@SuppressWarnings("unused")
class StorageBase {
protected final Logger logger;
// File pointer to the data start pointer header.
private static final long VERSION_HEADER_LOCATION = 0;
// File pointer to the num records header.
private static final long NUM_RECORDS_HEADER_LOCATION = 4;
// File pointer to the data start pointer header.
private static final long DATA_START_HEADER_LOCATION = 8;
// Total length in bytes of the global database headers.
static final int FILE_HEADERS_REGION_LENGTH = 16;
// must be volatile
// The in-memory index (for efficiency, all of the record info is cached in memory).
private volatile HashMap memoryIndex;
private final Object singleWriterLock = new Object[0];
// Recommended for best performance while adhering to the "single writer principle". Must be static-final
private static final AtomicReferenceFieldUpdater memoryREF =
AtomicReferenceFieldUpdater.newUpdater(StorageBase.class, HashMap.class, "memoryIndex");
// determines how much the index will grow by
private final Float weight;
// The keys are weak! When they go, the map entry is removed!
private final ReentrantLock referenceLock = new ReentrantLock();
// file/raf that are used
private final File baseFile;
private final RandomAccessFile randomAccessFile;
/**
* Version number of database (4 bytes).
*/
private int databaseVersion = 0;
/**
* Number of records (4 bytes).
*/
private int numberOfRecords;
/**
* File pointer to the first byte of the record data (8 bytes).
*/
private long dataPosition;
// save references to these, so they don't have to be created/destroyed any time there is I/O
private final SerializationManager serializationManager;
private final Output output;
private final Input input;
// input/output write buffer size before flushing to/from the file
private static final int BUFFER_SIZE = 1024;
/**
* Creates or opens a new database file.
*/
StorageBase(final File filePath, final SerializationManager serializationManager, final Logger logger) throws IOException {
this.serializationManager = serializationManager;
this.logger = logger;
if (logger != null) {
logger.info("Opening storage file: '{}'", filePath.getAbsolutePath());
}
this.baseFile = filePath;
boolean newStorage = !filePath.exists();
if (newStorage) {
File parentFile = this.baseFile.getParentFile();
if (parentFile != null && !parentFile.exists()) {
if (!parentFile.mkdirs()) {
throw new IOException("Unable to create dirs for: " + filePath);
}
}
}
this.randomAccessFile = new RandomAccessFile(this.baseFile, "rw");
if (newStorage || this.randomAccessFile.length() <= FILE_HEADERS_REGION_LENGTH) {
setVersion(this.randomAccessFile, 0);
setRecordCount(this.randomAccessFile, 0);
// pad the metadata with 21 records, so there is about 1k of padding before the data starts
long indexPointer = Metadata.getMetaDataPointer(21);
setDataStartPosition(indexPointer);
// have to make sure we can read header info (even if it's blank)
this.randomAccessFile.setLength(indexPointer);
}
else {
this.randomAccessFile.seek(VERSION_HEADER_LOCATION);
this.databaseVersion = this.randomAccessFile.readInt();
this.numberOfRecords = this.randomAccessFile.readInt();
this.dataPosition = this.randomAccessFile.readLong();
if (this.randomAccessFile.length() < this.dataPosition) {
if (logger != null) {
logger.error("Corrupted storage file!");
}
throw new IllegalArgumentException("Unable to parse header information from storage. Maybe it's corrupted?");
}
}
//noinspection AutoBoxing
if (logger != null) {
logger.info("Storage version: {}", this.databaseVersion);
}
// If we want to use compression (no need really, since this file is small already),
// then we have to make sure it's sync'd on flush AND have actually call outputStream.flush().
final InputStream inputStream = Channels.newInputStream(randomAccessFile.getChannel());
final OutputStream outputStream = Channels.newOutputStream(randomAccessFile.getChannel());
// read/write 1024 bytes at a time
output = new Output(outputStream, BUFFER_SIZE);
input = new Input(inputStream, BUFFER_SIZE);
this.weight = 0.5F;
// synchronized is used here to ensure the "single writer principle", and make sure that ONLY one thread at a time can enter this
// section. Because of this, we can have unlimited reader threads all going at the same time, without contention.
synchronized (singleWriterLock) {
this.memoryIndex = new HashMap(this.numberOfRecords);
if (!newStorage) {
Metadata meta;
for (int index = 0; index < this.numberOfRecords; index++) {
meta = Metadata.readHeader(this.randomAccessFile, index);
if (meta == null) {
// because we guarantee that empty metadata are ALWAYS at the end of the section, if we get a null one, break!
break;
}
this.memoryIndex.put(meta.key, meta);
}
if (this.memoryIndex.size() != (this.numberOfRecords)) {
setRecordCount(this.randomAccessFile, this.memoryIndex.size());
if (logger != null) {
logger.warn("Mismatch record count in storage, auto-correcting size.");
}
}
}
}
}
/**
* Returns the current number of records in the database.
*/
final
int size() {
// wrapper flushes first (protected by lock)
// not protected by lock
// access a snapshot of the memoryIndex (single-writer-principle)
HashMap memoryIndex = memoryREF.get(this);
return memoryIndex.size();
}
/**
* Checks if there is a record belonging to the given key.
*/
final
boolean contains(StorageKey key) {
// protected by lock
// access a snapshot of the memoryIndex (single-writer-principle)
HashMap memoryIndex = memoryREF.get(this);
return memoryIndex.containsKey(key);
}
/**
* @return an object for a specified key ONLY FROM THE REFERENCE CACHE
*/
final
T getCached(StorageKey key) {
// protected by lock
// access a snapshot of the memoryIndex (single-writer-principle)
HashMap memoryIndex = memoryREF.get(this);
Metadata meta = (Metadata) memoryIndex.get(key);
if (meta == null) {
return null;
}
// now stuff it into our reference cache so subsequent lookups are fast!
//noinspection Duplicates
try {
this.referenceLock.lock();
// if we have registered it, get it!
WeakReference