352 lines
11 KiB
Java
352 lines
11 KiB
Java
/*******************************************************************************
|
|
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
|
|
*
|
|
* 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 org.apache.jdbm;
|
|
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.SecretKey;
|
|
import javax.crypto.SecretKeyFactory;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.crypto.spec.PBEKeySpec;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
import java.io.IOError;
|
|
import java.security.spec.KeySpec;
|
|
|
|
/**
|
|
* Class used to configure and create DB. It uses builder pattern.
|
|
*/
|
|
public class DBMaker {
|
|
|
|
private byte cacheType = DBCacheRef.MRU;
|
|
private int mruCacheSize = 2048;
|
|
|
|
private String location = null;
|
|
|
|
private boolean disableTransactions = false;
|
|
private boolean lockingDisabled = false;
|
|
private boolean readonly = false;
|
|
private String password = null;
|
|
private boolean useAES256Bit = true;
|
|
private boolean useRandomAccessFile = false;
|
|
private boolean autoClearRefCacheOnLowMem = true;
|
|
private boolean closeOnJVMExit = false;
|
|
private boolean deleteFilesAfterCloseFlag = false;
|
|
|
|
|
|
private DBMaker(){}
|
|
|
|
/**
|
|
* Creates new DBMaker and sets file to load data from.
|
|
* @param file to load data from
|
|
* @return new DBMaker
|
|
*/
|
|
public static DBMaker openFile(String file){
|
|
DBMaker m = new DBMaker();
|
|
m.location = file;
|
|
return m;
|
|
}
|
|
|
|
/**
|
|
* Creates new DBMaker which uses in memory store. Data will be lost after JVM exits.
|
|
* @return new DBMaker
|
|
*/
|
|
public static DBMaker openMemory(){
|
|
return new DBMaker();
|
|
}
|
|
|
|
/**
|
|
* Open store in zip file
|
|
*
|
|
* @param zip file
|
|
* @return new DBMaker
|
|
*/
|
|
public static DBMaker openZip(String zip) {
|
|
DBMaker m = new DBMaker();
|
|
m.location = "$$ZIP$$://"+zip;
|
|
return m;
|
|
}
|
|
|
|
static String isZipFileLocation(String location){
|
|
String match = "$$ZIP$$://";
|
|
if( location.startsWith(match)){
|
|
return location.substring(match.length());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Use WeakReference for cache.
|
|
* This cache does not improve performance much,
|
|
* but prevents JDBM from creating multiple instances of the same object.
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker enableWeakCache() {
|
|
cacheType = DBCacheRef.WEAK;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Use SoftReference for cache.
|
|
* This cache greatly improves performance if you have enoguth memory.
|
|
* Instances in cache are Garbage Collected when memory gets low
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker enableSoftCache() {
|
|
cacheType = DBCacheRef.SOFT;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Use hard reference for cache.
|
|
* This greatly improves performance if there is enought memory
|
|
* Hard cache has smaller memory overhead then Soft or Weak, because
|
|
* reference objects and queue does not have to be maintained
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker enableHardCache() {
|
|
cacheType = DBCacheRef.SOFT;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Use 'Most Recently Used' cache with limited size.
|
|
* Oldest instances are released from cache when new instances are fetched.
|
|
* This cache is not cleared by GC. Is good for systems with limited memory.
|
|
* <p/>
|
|
* Default size for MRU cache is 2048 records.
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker enableMRUCache() {
|
|
cacheType = DBCacheRef.MRU;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Sets 'Most Recently Used' cache size. This cache is activated by default with size 2048
|
|
*
|
|
* @param cacheSize number of instances which will be kept in cache.
|
|
* @return this builder
|
|
*/
|
|
public DBMaker setMRUCacheSize(int cacheSize) {
|
|
if (cacheSize < 0) throw new IllegalArgumentException("Cache size is smaller than zero");
|
|
cacheType = DBCacheRef.MRU;
|
|
mruCacheSize = cacheSize;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* If reference (soft,weak or hard) cache is enabled,
|
|
* GC may not release references fast enough (or not at all in case of hard cache).
|
|
* So JDBM periodically checks amount of free heap memory.
|
|
* If free memory is less than 25% or 10MB,
|
|
* JDBM completely clears its reference cache to prevent possible memory issues.
|
|
* <p>
|
|
* Calling this method disables auto cache clearing when mem is low.
|
|
* And of course it can cause some out of memory exceptions.
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker disableCacheAutoClear(){
|
|
this.autoClearRefCacheOnLowMem = false;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Enabled storage encryption using AES cipher. JDBM supports both 128 bit and 256 bit encryption if JRE provides it.
|
|
* There are some restrictions on AES 256 bit and not all JREs have it by default.
|
|
* <p/>
|
|
* Storage can not be read (decrypted), unless the key is provided next time it is opened
|
|
*
|
|
* @param password used to encrypt store
|
|
* @param useAES256Bit if true strong AES 256 bit encryption is used. Otherwise more usual AES 128 bit is used.
|
|
* @return this builder
|
|
*/
|
|
public DBMaker enableEncryption(String password, boolean useAES256Bit) {
|
|
this.password = password;
|
|
this.useAES256Bit = useAES256Bit;
|
|
return this;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Make DB readonly.
|
|
* Update/delete/insert operation will throw 'UnsupportedOperationException'
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker readonly() {
|
|
readonly = true;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Disable cache completely
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker disableCache() {
|
|
cacheType = DBCacheRef.NONE;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Option to disable transaction (to increase performance at the cost of potential data loss).
|
|
* Transactions are enabled by default
|
|
* <p/>
|
|
* Switches off transactioning for the record manager. This means
|
|
* that a) a transaction log is not kept, and b) writes aren't
|
|
* synch'ed after every update. Writes are cached in memory and then flushed
|
|
* to disk every N writes. You may also flush writes manually by calling commit().
|
|
* This is useful when batch inserting into a new database.
|
|
* <p/>
|
|
* When using this, database must be properly closed before JVM shutdown.
|
|
* Failing to do so may and WILL corrupt store.
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker disableTransactions() {
|
|
this.disableTransactions = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Disable file system based locking (for file systems that do not support it).
|
|
*
|
|
* Locking is not supported by many remote or distributed file systems; such
|
|
* as Lustre and NFS. Attempts to perform locks will result in an
|
|
* IOException with the message "Function not implemented".
|
|
*
|
|
* Disabling locking will avoid this issue, though of course it comes with
|
|
* all the issues of uncontrolled file access.
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker disableLocking(){
|
|
this.lockingDisabled = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* By default JDBM uses mapped memory buffers to read from files.
|
|
* But this may behave strangely on some platforms.
|
|
* Safe alternative is to use old RandomAccessFile rather then mapped ByteBuffer.
|
|
* There is typically slower (pages needs to be copyed into memory on every write).
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker useRandomAccessFile(){
|
|
this.useRandomAccessFile = true;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Registers shutdown hook and close database on JVM exit, if it was not already closed;
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker closeOnExit(){
|
|
this.closeOnJVMExit = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Delete all storage files after DB is closed
|
|
*
|
|
* @return this builder
|
|
*/
|
|
public DBMaker deleteFilesAfterClose(){
|
|
this.deleteFilesAfterCloseFlag = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Opens database with settings earlier specified in this builder.
|
|
*
|
|
* @return new DB
|
|
* @throws java.io.IOError if db could not be opened
|
|
*/
|
|
public DB make() {
|
|
|
|
Cipher cipherIn = null;
|
|
Cipher cipherOut = null;
|
|
if (password != null) try {
|
|
//initialize ciphers
|
|
//this code comes from stack owerflow
|
|
//http://stackoverflow.com/questions/992019/java-256bit-aes-encryption/992413#992413
|
|
byte[] salt = new byte[]{3, -34, 123, 53, 78, 121, -12, -1, 45, -12, -48, 89, 11, 100, 99, 8};
|
|
|
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
|
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 1024, useAES256Bit?256:128);
|
|
SecretKey tmp = factory.generateSecret(spec);
|
|
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
|
|
|
|
String transform = "AES/CBC/NoPadding";
|
|
IvParameterSpec params = new IvParameterSpec(salt);
|
|
|
|
cipherIn = Cipher.getInstance(transform);
|
|
cipherIn.init(Cipher.ENCRYPT_MODE, secret, params);
|
|
|
|
cipherOut = Cipher.getInstance(transform);
|
|
cipherOut.init(Cipher.DECRYPT_MODE, secret, params);
|
|
|
|
//sanity check, try with page size
|
|
byte[] data = new byte[Storage.PAGE_SIZE];
|
|
byte[] encData = cipherIn.doFinal(data);
|
|
if (encData.length != Storage.PAGE_SIZE)
|
|
throw new Error("Page size changed after encryption, make sure you use '/NoPadding'");
|
|
byte[] data2 = cipherOut.doFinal(encData);
|
|
for (int i = 0; i < data.length; i++) {
|
|
if (data[i] != data2[i]) throw new Error("Encryption provided by JRE does not work");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
throw new IOError(e);
|
|
}
|
|
|
|
DBAbstract db = null;
|
|
|
|
|
|
if (cacheType == DBCacheRef.MRU){
|
|
db = new DBCacheMRU(location, readonly, disableTransactions, cipherIn, cipherOut,useRandomAccessFile,deleteFilesAfterCloseFlag, mruCacheSize,lockingDisabled);
|
|
}else if( cacheType == DBCacheRef.SOFT || cacheType == DBCacheRef.HARD || cacheType == DBCacheRef.WEAK) {
|
|
db = new DBCacheRef(location, readonly, disableTransactions, cipherIn, cipherOut,useRandomAccessFile,deleteFilesAfterCloseFlag, cacheType,autoClearRefCacheOnLowMem,lockingDisabled);
|
|
} else if (cacheType == DBCacheRef.NONE) {
|
|
db = new DBStore(location, readonly, disableTransactions, cipherIn, cipherOut,useRandomAccessFile,deleteFilesAfterCloseFlag,lockingDisabled);
|
|
} else {
|
|
throw new IllegalArgumentException("Unknown cache type: " + cacheType);
|
|
}
|
|
|
|
if(closeOnJVMExit){
|
|
db.addShutdownHook();
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
}
|