Moved Serializers to their own project
This commit is contained in:
parent
654e6ca584
commit
41f060dede
@ -1,294 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 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.serialization;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
|
||||||
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
|
||||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
|
||||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
|
||||||
import org.bouncycastle.math.ec.ECCurve;
|
|
||||||
import org.bouncycastle.math.ec.ECPoint;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.KryoException;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
import com.esotericsoftware.reflectasm.FieldAccess;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only public keys are ever sent across the wire.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class EccPrivateKeySerializer extends Serializer<ECPrivateKeyParameters> {
|
|
||||||
|
|
||||||
// we use ASM here
|
|
||||||
private static final FieldAccess ecCurveAccess = FieldAccess.get(ECCurve.class);
|
|
||||||
private static final int ecCoordIndex = ecCurveAccess.getIndex("coord");
|
|
||||||
|
|
||||||
|
|
||||||
private static final byte usesName = (byte) 1;
|
|
||||||
private static final byte usesOid = (byte) 2;
|
|
||||||
|
|
||||||
public static
|
|
||||||
void write(Output output, ECPrivateKeyParameters key) throws KryoException {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
ECDomainParameters parameters = key.getParameters();
|
|
||||||
ECCurve curve = parameters.getCurve();
|
|
||||||
|
|
||||||
EccPrivateKeySerializer.serializeCurve(output, curve);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
BigInteger n = parameters.getN();
|
|
||||||
ECPoint g = parameters.getG();
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = n.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
serializeECPoint(g, output);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getD()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static
|
|
||||||
ECPrivateKeyParameters read(Input input) throws KryoException {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
ECCurve curve = EccPrivateKeySerializer.deserializeCurve(input);
|
|
||||||
|
|
||||||
// N
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger n = new BigInteger(bytes);
|
|
||||||
|
|
||||||
|
|
||||||
// G
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
ECPoint g = curve.decodePoint(bytes);
|
|
||||||
|
|
||||||
|
|
||||||
// D
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger D = new BigInteger(bytes);
|
|
||||||
|
|
||||||
|
|
||||||
ECDomainParameters ecDomainParameters = new ECDomainParameters(curve, g, n);
|
|
||||||
|
|
||||||
return new ECPrivateKeyParameters(D, ecDomainParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
void serializeCurve(Output output, ECCurve curve) throws KryoException {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
// save out if it's a NAMED curve, or a UN-NAMED curve. If it is named, we can do less work.
|
|
||||||
String curveName = curve.getClass()
|
|
||||||
.getSimpleName();
|
|
||||||
|
|
||||||
if (CustomNamedCurves.getByName(curveName) != null) {
|
|
||||||
// we use the name instead of serializing the full curve
|
|
||||||
output.writeInt(usesName, true);
|
|
||||||
output.writeString(curveName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (curveName.endsWith("Curve")) {
|
|
||||||
String cleanedName = curveName.substring(0, curveName.indexOf("Curve"));
|
|
||||||
|
|
||||||
if (!cleanedName.isEmpty()) {
|
|
||||||
ASN1ObjectIdentifier oid = CustomNamedCurves.getOID(cleanedName);
|
|
||||||
if (oid != null) {
|
|
||||||
// we use the OID (instead of serializing the entire curve)
|
|
||||||
output.writeInt(usesOid, true);
|
|
||||||
curveName = oid.getId();
|
|
||||||
output.writeString(curveName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to serialize the ENTIRE curve.
|
|
||||||
// save out the curve info
|
|
||||||
BigInteger a = curve.getA()
|
|
||||||
.toBigInteger();
|
|
||||||
BigInteger b = curve.getB()
|
|
||||||
.toBigInteger();
|
|
||||||
BigInteger order = curve.getOrder();
|
|
||||||
BigInteger cofactor = curve.getCofactor();
|
|
||||||
BigInteger q = curve.getField()
|
|
||||||
.getCharacteristic();
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = a.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = b.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = order.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = cofactor.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = q.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
// coordinate system
|
|
||||||
int coordinateSystem = curve.getCoordinateSystem();
|
|
||||||
output.writeInt(coordinateSystem, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
ECCurve deserializeCurve(Input input) throws KryoException {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
ECCurve curve;
|
|
||||||
|
|
||||||
int serializationType = input.readInt(true);
|
|
||||||
|
|
||||||
// lookup via name
|
|
||||||
if (serializationType == usesName) {
|
|
||||||
String curveName = input.readString();
|
|
||||||
X9ECParameters x9Curve = CustomNamedCurves.getByName(curveName);
|
|
||||||
curve = x9Curve.getCurve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// this means we just lookup the curve via the OID
|
|
||||||
else if (serializationType == usesOid) {
|
|
||||||
String oid = input.readString();
|
|
||||||
X9ECParameters x9Curve = CustomNamedCurves.getByOID(new ASN1ObjectIdentifier(oid));
|
|
||||||
curve = x9Curve.getCurve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to read in the entire curve information.
|
|
||||||
else {
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger a = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger b = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger order = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger cofactor = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger q = new BigInteger(bytes);
|
|
||||||
|
|
||||||
|
|
||||||
// coord system
|
|
||||||
int coordinateSystem = input.readInt(true);
|
|
||||||
|
|
||||||
curve = new ECCurve.Fp(q, a, b, order, cofactor);
|
|
||||||
ecCurveAccess.setInt(curve, ecCoordIndex, coordinateSystem);
|
|
||||||
}
|
|
||||||
return curve;
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
void serializeECPoint(ECPoint point, Output output) throws KryoException {
|
|
||||||
if (point.isInfinity()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ECPoint normed = point.normalize();
|
|
||||||
|
|
||||||
byte[] X = normed.getXCoord()
|
|
||||||
.getEncoded();
|
|
||||||
byte[] Y = normed.getYCoord()
|
|
||||||
.getEncoded();
|
|
||||||
|
|
||||||
int length = 1 + X.length + Y.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
|
|
||||||
output.write(0x04);
|
|
||||||
output.write(X);
|
|
||||||
output.write(Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, ECPrivateKeyParameters key) throws KryoException {
|
|
||||||
write(output, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
ECPrivateKeyParameters read(Kryo kryo, Input input, Class type) throws KryoException {
|
|
||||||
return read(input);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 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.serialization;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
|
||||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
|
||||||
import org.bouncycastle.math.ec.ECCurve;
|
|
||||||
import org.bouncycastle.math.ec.ECPoint;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.KryoException;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only public keys are ever sent across the wire.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class EccPublicKeySerializer extends Serializer<ECPublicKeyParameters> {
|
|
||||||
|
|
||||||
public static
|
|
||||||
void write(Output output, ECPublicKeyParameters key) throws KryoException {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
ECDomainParameters parameters = key.getParameters();
|
|
||||||
ECCurve curve = parameters.getCurve();
|
|
||||||
|
|
||||||
EccPrivateKeySerializer.serializeCurve(output, curve);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
BigInteger n = parameters.getN();
|
|
||||||
ECPoint g = parameters.getG();
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = n.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
EccPrivateKeySerializer.serializeECPoint(g, output);
|
|
||||||
EccPrivateKeySerializer.serializeECPoint(key.getQ(), output);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static
|
|
||||||
ECPublicKeyParameters read(Input input) throws KryoException {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
ECCurve curve = EccPrivateKeySerializer.deserializeCurve(input);
|
|
||||||
|
|
||||||
|
|
||||||
// N
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger n = new BigInteger(bytes);
|
|
||||||
|
|
||||||
|
|
||||||
// G
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
ECPoint g = curve.decodePoint(bytes);
|
|
||||||
|
|
||||||
|
|
||||||
ECDomainParameters ecDomainParameters = new ECDomainParameters(curve, g, n);
|
|
||||||
|
|
||||||
// Q
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
ECPoint Q = curve.decodePoint(bytes);
|
|
||||||
|
|
||||||
return new ECPublicKeyParameters(Q, ecDomainParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, ECPublicKeyParameters key) throws KryoException {
|
|
||||||
write(output, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
ECPublicKeyParameters read(Kryo kryo, Input input, Class type) throws KryoException {
|
|
||||||
return read(input);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package dorkbox.util.serialization;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize the path of a file instead of the File object
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class FileSerializer extends Serializer<File> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, File file) {
|
|
||||||
output.writeString(file.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
File read(final Kryo kryo, final Input input, final Class<? extends File> type) {
|
|
||||||
String path = input.readString();
|
|
||||||
return new File(path);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 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.serialization;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.IESParameters;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only public keys are ever sent across the wire.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class IesParametersSerializer extends Serializer<IESParameters> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, IESParameters key) {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
///////////
|
|
||||||
bytes = key.getDerivationV();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
///////////
|
|
||||||
bytes = key.getEncodingV();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
///////////
|
|
||||||
output.writeInt(key.getMacKeySize(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
IESParameters read(Kryo kryo, Input input, Class type) {
|
|
||||||
int length;
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
byte[] derivation = new byte[length];
|
|
||||||
input.readBytes(derivation, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
byte[] encoding = new byte[length];
|
|
||||||
input.readBytes(encoding, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
int macKeySize = input.readInt(true);
|
|
||||||
|
|
||||||
return new IESParameters(derivation, encoding, macKeySize);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 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.serialization;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.IESWithCipherParameters;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only public keys are ever sent across the wire.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class IesWithCipherParametersSerializer extends Serializer<IESWithCipherParameters> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, IESWithCipherParameters key) {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
///////////
|
|
||||||
bytes = key.getDerivationV();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
///////////
|
|
||||||
bytes = key.getEncodingV();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
///////////
|
|
||||||
output.writeInt(key.getMacKeySize(), true);
|
|
||||||
|
|
||||||
|
|
||||||
///////////
|
|
||||||
output.writeInt(key.getCipherKeySize(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
IESWithCipherParameters read(Kryo kryo, Input input, Class type) {
|
|
||||||
int length;
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
byte[] derivation = new byte[length];
|
|
||||||
input.readBytes(derivation, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
byte[] encoding = new byte[length];
|
|
||||||
input.readBytes(encoding, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
int macKeySize = input.readInt(true);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
int cipherKeySize = input.readInt(true);
|
|
||||||
|
|
||||||
return new IESWithCipherParameters(derivation, encoding, macKeySize, cipherKeySize);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 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.serialization;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only public keys are ever sent across the wire.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class RsaPrivateKeySerializer extends Serializer<RSAPrivateCrtKeyParameters> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, RSAPrivateCrtKeyParameters key) {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getDP()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getDQ()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getExponent()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getModulus()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getP()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getPublicExponent()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getQ()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getQInv()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
RSAPrivateCrtKeyParameters read(Kryo kryo, Input input, Class type) {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger DP = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger DQ = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger exponent = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger modulus = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger P = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger publicExponent = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger q = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger qInv = new BigInteger(bytes);
|
|
||||||
|
|
||||||
return new RSAPrivateCrtKeyParameters(modulus, publicExponent, exponent, P, q, DP, DQ, qInv);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 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.serialization;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.RSAKeyParameters;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only public keys are ever sent across the wire.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class RsaPublicKeySerializer extends Serializer<RSAKeyParameters> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(Kryo kryo, Output output, RSAKeyParameters key) {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
///////////
|
|
||||||
bytes = key.getModulus()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
bytes = key.getExponent()
|
|
||||||
.toByteArray();
|
|
||||||
length = bytes.length;
|
|
||||||
|
|
||||||
output.writeInt(length, true);
|
|
||||||
output.writeBytes(bytes, 0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
RSAKeyParameters read(Kryo kryo, Input input, Class type) {
|
|
||||||
byte[] bytes;
|
|
||||||
int length;
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger modulus = new BigInteger(bytes);
|
|
||||||
|
|
||||||
/////////////
|
|
||||||
length = input.readInt(true);
|
|
||||||
bytes = new byte[length];
|
|
||||||
input.readBytes(bytes, 0, length);
|
|
||||||
BigInteger exponent = new BigInteger(bytes);
|
|
||||||
|
|
||||||
return new RSAKeyParameters(false, modulus, exponent);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package dorkbox.util.serialization;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Registration;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
|
|
||||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer;
|
|
||||||
|
|
||||||
public
|
|
||||||
class SerializationDefaults {
|
|
||||||
/**
|
|
||||||
* Allows for the kryo registration of sensible defaults in a common, well used way.
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
void register(Kryo kryo) {
|
|
||||||
// these are registered using the default serializers. We don't customize these, because we don't care about it.
|
|
||||||
kryo.register(String.class);
|
|
||||||
kryo.register(String[].class);
|
|
||||||
|
|
||||||
kryo.register(int[].class);
|
|
||||||
kryo.register(short[].class);
|
|
||||||
kryo.register(float[].class);
|
|
||||||
kryo.register(double[].class);
|
|
||||||
kryo.register(long[].class);
|
|
||||||
kryo.register(byte[].class);
|
|
||||||
kryo.register(char[].class);
|
|
||||||
kryo.register(boolean[].class);
|
|
||||||
|
|
||||||
kryo.register(Integer[].class);
|
|
||||||
kryo.register(Short[].class);
|
|
||||||
kryo.register(Float[].class);
|
|
||||||
kryo.register(Double[].class);
|
|
||||||
kryo.register(Long[].class);
|
|
||||||
kryo.register(Byte[].class);
|
|
||||||
kryo.register(Character[].class);
|
|
||||||
kryo.register(Boolean[].class);
|
|
||||||
|
|
||||||
kryo.register(Object[].class);
|
|
||||||
kryo.register(Object[][].class);
|
|
||||||
kryo.register(Class.class);
|
|
||||||
|
|
||||||
kryo.register(Exception.class);
|
|
||||||
kryo.register(IOException.class);
|
|
||||||
kryo.register(RuntimeException.class);
|
|
||||||
kryo.register(NullPointerException.class);
|
|
||||||
|
|
||||||
// necessary for the transport of exceptions.
|
|
||||||
kryo.register(StackTraceElement.class);
|
|
||||||
kryo.register(StackTraceElement[].class);
|
|
||||||
|
|
||||||
kryo.register(ArrayList.class);
|
|
||||||
kryo.register(HashMap.class);
|
|
||||||
kryo.register(HashSet.class);
|
|
||||||
|
|
||||||
kryo.register(Collections.emptyList().getClass());
|
|
||||||
kryo.register(Collections.emptySet().getClass());
|
|
||||||
kryo.register(Collections.emptyMap().getClass());
|
|
||||||
|
|
||||||
kryo.register(Collections.emptyNavigableSet().getClass());
|
|
||||||
kryo.register(Collections.emptyNavigableMap().getClass());
|
|
||||||
|
|
||||||
|
|
||||||
// hacky way to register unmodifiable serializers
|
|
||||||
Kryo kryoHack = new Kryo() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
Registration register(final Class type, final Serializer serializer) {
|
|
||||||
kryo.register(type, serializer);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
UnmodifiableCollectionsSerializer.registerSerializers(kryoHack);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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.serialization;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.KryoException;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
public
|
|
||||||
interface SerializationManager<IO> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the lowest, next available integer ID and the {@link Kryo#getDefaultSerializer(Class) default serializer}.
|
|
||||||
* If the class is already registered, the existing entry is updated with the new serializer.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this
|
|
||||||
* method. The order must be the same at deserialization as it was for serialization.
|
|
||||||
*/
|
|
||||||
<T> SerializationManager register(Class<T> clazz);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the specified ID. If the ID is already in use by the same type, the old entry is overwritten. If the ID
|
|
||||||
* is already in use by a different type, a {@link KryoException} is thrown.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* IDs must be the same at deserialization as they were for serialization.
|
|
||||||
*
|
|
||||||
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
|
|
||||||
* these IDs can be repurposed.
|
|
||||||
*/
|
|
||||||
<T> SerializationManager register(Class<T> clazz, int id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the lowest, next available integer ID and the specified serializer. If the class is already registered,
|
|
||||||
* the existing entry is updated with the new serializer.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this
|
|
||||||
* method. The order must be the same at deserialization as it was for serialization.
|
|
||||||
*/
|
|
||||||
<T> SerializationManager register(Class<T> clazz, Serializer<T> serializer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the specified ID and serializer. If the ID is already in use by the same type, the old entry is
|
|
||||||
* overwritten. If the ID is already in use by a different type, a {@link KryoException} is thrown.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* IDs must be the same at deserialization as they were for serialization.
|
|
||||||
*
|
|
||||||
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
|
|
||||||
* these IDs can be repurposed.
|
|
||||||
*/
|
|
||||||
<T> SerializationManager register(Class<T> clazz, Serializer<T> serializer, int id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits until a kryo is available to write, using CAS operations to prevent having to synchronize.
|
|
||||||
* <p/>
|
|
||||||
* There is a small speed penalty if there were no kryo's available to use.
|
|
||||||
*/
|
|
||||||
void write(IO buffer, Object message) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads an object from the buffer.
|
|
||||||
*
|
|
||||||
* @param length should ALWAYS be the length of the expected object!
|
|
||||||
*/
|
|
||||||
Object read(IO buffer, int length) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the class and object using an available kryo instance
|
|
||||||
*/
|
|
||||||
void writeFullClassAndObject(Output output, Object value) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a class read from the input
|
|
||||||
*/
|
|
||||||
Object readFullClassAndObject(final Input input) throws IOException;
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.IOException;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
import com.esotericsoftware.minlog.Log;
|
|
||||||
|
|
||||||
import dorkbox.util.bytes.ByteBuffer2;
|
|
||||||
import dorkbox.util.serialization.SerializationDefaults;
|
|
||||||
import dorkbox.util.serialization.SerializationManager;
|
|
||||||
|
|
||||||
class DefaultStorageSerializationManager implements SerializationManager<ByteBuffer2> {
|
|
||||||
private Kryo kryo = new Kryo() {{
|
|
||||||
// we don't want logging from Kryo...
|
|
||||||
Log.set(Log.LEVEL_ERROR);
|
|
||||||
}};
|
|
||||||
|
|
||||||
public
|
|
||||||
DefaultStorageSerializationManager() {
|
|
||||||
SerializationDefaults.register(kryo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> SerializationManager<ByteBuffer2> register(final Class<T> clazz) {
|
|
||||||
kryo.register(clazz);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> SerializationManager<ByteBuffer2> register(final Class<T> clazz, final int id) {
|
|
||||||
kryo.register(clazz, id);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> SerializationManager<ByteBuffer2> register(final Class<T> clazz, final Serializer<T> serializer) {
|
|
||||||
kryo.register(clazz, serializer);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> SerializationManager<ByteBuffer2> register(final Class<T> type, final Serializer<T> serializer, final int id) {
|
|
||||||
kryo.register(type, serializer, id);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void write(final ByteBuffer2 buffer, final Object message) {
|
|
||||||
final Output output = new Output();
|
|
||||||
writeFullClassAndObject(output, message);
|
|
||||||
buffer.writeBytes(output.getBuffer());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
Object read(final ByteBuffer2 buffer, final int length) throws IOException {
|
|
||||||
final Input input = new Input();
|
|
||||||
buffer.readBytes(input.getBuffer());
|
|
||||||
|
|
||||||
final Object o = readFullClassAndObject(input);
|
|
||||||
buffer.skip(input.position());
|
|
||||||
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void writeFullClassAndObject(final Output output, final Object value) {
|
|
||||||
kryo.writeClassAndObject(output, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
Object readFullClassAndObject(final Input input) {
|
|
||||||
return kryo.readClassAndObject(input);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,390 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
|
|
||||||
import dorkbox.util.DelayTimer;
|
|
||||||
import dorkbox.util.serialization.SerializationManager;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nothing spectacular about this storage -- it allows for persistent storage of objects to disk.
|
|
||||||
* <p/>
|
|
||||||
* Be wary of opening the database file in different JVM instances. Even with file-locks, you can corrupt the data.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"Convert2Diamond", "Convert2Lambda"})
|
|
||||||
class DiskStorage implements Storage {
|
|
||||||
// null if we are a read-only storage
|
|
||||||
private final DelayTimer timer;
|
|
||||||
|
|
||||||
|
|
||||||
// must be volatile
|
|
||||||
private volatile HashMap<StorageKey, Object> actionMap = new HashMap<StorageKey, Object>();
|
|
||||||
|
|
||||||
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<DiskStorage, HashMap> actionMapREF =
|
|
||||||
AtomicReferenceFieldUpdater.newUpdater(DiskStorage.class, HashMap.class, "actionMap");
|
|
||||||
|
|
||||||
private final StorageBase storage;
|
|
||||||
|
|
||||||
private final AtomicInteger references = new AtomicInteger(1);
|
|
||||||
private final AtomicBoolean isOpen = new AtomicBoolean(false);
|
|
||||||
private final long milliSeconds;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates or opens a new database file.
|
|
||||||
*/
|
|
||||||
DiskStorage(File storageFile,
|
|
||||||
SerializationManager serializationManager,
|
|
||||||
final boolean readOnly,
|
|
||||||
final long saveDelayInMilliseconds,
|
|
||||||
final Logger logger) throws IOException {
|
|
||||||
this.storage = new StorageBase(storageFile, serializationManager, logger);
|
|
||||||
this.milliSeconds = saveDelayInMilliseconds;
|
|
||||||
|
|
||||||
if (readOnly) {
|
|
||||||
this.timer = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.timer = new DelayTimer("Storage Writer", false, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
Map<StorageKey, Object> actions;
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// do a fast swap on the actionMap.
|
|
||||||
actions = DiskStorage.this.actionMap;
|
|
||||||
DiskStorage.this.actionMap = new HashMap<StorageKey, Object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
DiskStorage.this.storage.doActionThings(actions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isOpen.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of objects in the database.
|
|
||||||
* <p/>
|
|
||||||
* SLOW because this must save all data to disk first!
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
int size() {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush actions
|
|
||||||
// timer action runs on THIS thread, not timer thread
|
|
||||||
if (timer != null) {
|
|
||||||
this.timer.delay(0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storage.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there is a object corresponding to the given key.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
boolean contains(StorageKey key) {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
// access a snapshot of the actionMap (single-writer-principle)
|
|
||||||
final HashMap actionMap = actionMapREF.get(this);
|
|
||||||
|
|
||||||
// check if our pending actions has it, or if our storage index has it
|
|
||||||
return actionMap.containsKey(key) || this.storage.contains(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a object using the specific key, and casts it to the expected class
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
<T> T get(StorageKey key) {
|
|
||||||
return get0(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the saved data (or null) for the specified key. Also saves the data as default data.
|
|
||||||
*
|
|
||||||
* @param data If there is no object in the DB with the specified key, this value will be the default (and will be saved to the db)
|
|
||||||
*
|
|
||||||
* @return NULL if the saved data was the wrong type for the specified key.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public
|
|
||||||
<T> T get(StorageKey key, T data) {
|
|
||||||
Object source = get0(key);
|
|
||||||
|
|
||||||
if (source == null) {
|
|
||||||
// returned was null, so we should save the default value
|
|
||||||
put(key, data);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
final Class<?> expectedClass = data.getClass();
|
|
||||||
final Class<?> savedCLass = source.getClass();
|
|
||||||
|
|
||||||
if (!expectedClass.isAssignableFrom(savedCLass)) {
|
|
||||||
String message = "Saved value type '" + savedCLass + "' is different than expected value '" + expectedClass + "'";
|
|
||||||
|
|
||||||
if (storage.logger != null) {
|
|
||||||
storage.logger.error(message);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
System.err.print(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T) source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a object from pending or from storage
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
<T> T get0(StorageKey key) {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
// access a snapshot of the actionMap (single-writer-principle)
|
|
||||||
final HashMap actionMap = actionMapREF.get(this);
|
|
||||||
|
|
||||||
// if the object in is pending, we get it from there
|
|
||||||
Object object = actionMap.get(key);
|
|
||||||
|
|
||||||
if (object != null) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
T returnObject = (T) object;
|
|
||||||
return returnObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not found, so we have to go find it on disk
|
|
||||||
return this.storage.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given data to storage with the associated key.
|
|
||||||
* <p/>
|
|
||||||
* Also will update existing data. If the new contents do not fit in the original space, then the update is handled by
|
|
||||||
* deleting the old data and adding the new.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void put(StorageKey key, Object object) {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timer != null) {
|
|
||||||
// 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) {
|
|
||||||
// push action to map
|
|
||||||
actionMap.put(key, object);
|
|
||||||
}
|
|
||||||
|
|
||||||
// timer action runs on TIMER thread, not this thread
|
|
||||||
this.timer.delay(this.milliSeconds);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Unable to put on a read-only storage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an object from storage.
|
|
||||||
*
|
|
||||||
* @return true if the delete was successful. False if there were problems deleting the data.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
boolean delete(StorageKey key) {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
// timer action runs on THIS thread, not timer thread
|
|
||||||
if (timer != null) {
|
|
||||||
// flush to storage, so we know if there were errors deleting from disk
|
|
||||||
this.timer.delay(0L);
|
|
||||||
return this.storage.delete(key);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new RuntimeException("Unable to delete on a read-only storage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes and removes this storage from the storage system. This is the same as calling {@link StorageSystem#close(Storage)}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void close() {
|
|
||||||
StorageSystem.close(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the database and file.
|
|
||||||
*/
|
|
||||||
void closeFully() {
|
|
||||||
// timer action runs on THIS thread, not timer thread
|
|
||||||
if (timer != null) {
|
|
||||||
this.timer.delay(0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
// have to "close" it after we run the timer!
|
|
||||||
this.isOpen.set(false);
|
|
||||||
this.storage.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the file that backs this storage
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
File getFile() {
|
|
||||||
return this.storage.getFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the backing file size.
|
|
||||||
*
|
|
||||||
* @return -1 if there was an error
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
long getFileSize() {
|
|
||||||
// timer action runs on THIS thread, not timer thread
|
|
||||||
if (timer != null) {
|
|
||||||
this.timer.delay(0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storage.getFileSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if there are objects queued to be written?
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
boolean hasWriteWaiting() {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection SimplifiableIfStatement
|
|
||||||
if (timer != null) {
|
|
||||||
return this.timer.isWaiting();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the delay in milliseconds this will wait after the last action to flush the data to the disk
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
long getSaveDelay() {
|
|
||||||
return this.milliSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the version of data stored in the database
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
int getVersion() {
|
|
||||||
return this.storage.getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the version of data stored in the database
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void setVersion(int version) {
|
|
||||||
this.storage.setVersion(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
void increaseReference() {
|
|
||||||
this.references.incrementAndGet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return true when this is the last reference
|
|
||||||
*/
|
|
||||||
boolean decrementReference() {
|
|
||||||
return this.references.decrementAndGet() <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected
|
|
||||||
Object clone() throws CloneNotSupportedException {
|
|
||||||
return super.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the storage to disk, immediately.
|
|
||||||
* <p/>
|
|
||||||
* This will save ALL of the pending save actions to the file
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final
|
|
||||||
void save() {
|
|
||||||
if (!this.isOpen.get()) {
|
|
||||||
throw new RuntimeException("Unable to act on closed storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
// timer action runs on THIS thread, not timer thread
|
|
||||||
if (timer != null) {
|
|
||||||
this.timer.delay(0L);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Unable to save on a read-only storage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,200 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.File;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage that is in memory only (and is not persisted to disk)
|
|
||||||
*/
|
|
||||||
class MemoryStorage implements Storage {
|
|
||||||
|
|
||||||
|
|
||||||
// must be volatile
|
|
||||||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
|
||||||
private volatile HashMap<StorageKey, Object> storage = new HashMap<StorageKey, Object>();
|
|
||||||
|
|
||||||
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<MemoryStorage, HashMap> storageREF =
|
|
||||||
AtomicReferenceFieldUpdater.newUpdater(MemoryStorage.class, HashMap.class, "storage");
|
|
||||||
|
|
||||||
private int version;
|
|
||||||
|
|
||||||
|
|
||||||
MemoryStorage() {}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of objects in the database.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
int size() {
|
|
||||||
// access a snapshot of the storage (single-writer-principle)
|
|
||||||
HashMap storage = storageREF.get(this);
|
|
||||||
return storage.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there is a object corresponding to the given key.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
boolean contains(final StorageKey key) {
|
|
||||||
// access a snapshot of the storage (single-writer-principle)
|
|
||||||
HashMap storage = storageREF.get(this);
|
|
||||||
return storage.containsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a object using the specific key, and casts it to the expected class
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> T get(final StorageKey key) {
|
|
||||||
// access a snapshot of the storage (single-writer-principle)
|
|
||||||
HashMap storage = storageREF.get(this);
|
|
||||||
return (T) storage.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> T get(final StorageKey key, final T data) {
|
|
||||||
// access a snapshot of the storage (single-writer-principle)
|
|
||||||
HashMap storage = storageREF.get(this);
|
|
||||||
|
|
||||||
final Object o = storage.get(key);
|
|
||||||
if (o == null) {
|
|
||||||
storage.put(key, data);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return (T) o;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given data to storage with the associated key.
|
|
||||||
* <p/>
|
|
||||||
* Also will update existing data. If the new contents do not fit in the original space, then the update is handled by
|
|
||||||
* deleting the old data and adding the new.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void put(final StorageKey key, final Object object) {
|
|
||||||
// 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) {
|
|
||||||
storage.put(key, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an object from storage.
|
|
||||||
*
|
|
||||||
* @return true if the delete was successful. False if there were problems deleting the data.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized
|
|
||||||
boolean delete(final StorageKey key) {
|
|
||||||
// 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) {
|
|
||||||
storage.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return null. There is no file that backs this storage
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
File getFile() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the backing file size.
|
|
||||||
*
|
|
||||||
* @return 0. There is no file that backs this storage
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
long getFileSize() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return false. Writes to in-memory storage are immediate.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
boolean hasWriteWaiting() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return 0. There is no file that backs this storage
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
long getSaveDelay() {
|
|
||||||
return 0L;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the version of data stored in the database
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized
|
|
||||||
int getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the version of data stored in the database
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized
|
|
||||||
void setVersion(final int version) {
|
|
||||||
this.version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There is no file that backs this storage, so writes are immediate and saves do nothgin
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void save() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In-memory storage systems do not have a backing file, so there is nothing to close
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void close() {
|
|
||||||
StorageSystem.close(this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,318 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.nio.channels.FileLock;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.io.Input;
|
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
|
||||||
|
|
||||||
import dorkbox.util.serialization.SerializationManager;
|
|
||||||
|
|
||||||
public
|
|
||||||
class Metadata {
|
|
||||||
// The length of a key in the index.
|
|
||||||
// SHA256 is 32 bytes long.
|
|
||||||
private static final int KEY_SIZE = 32;
|
|
||||||
|
|
||||||
// Number of bytes in the record header.
|
|
||||||
private static final int POINTER_INFO_SIZE = 16;
|
|
||||||
|
|
||||||
// The total length of one index entry - the key length plus the record header length.
|
|
||||||
static final int INDEX_ENTRY_LENGTH = KEY_SIZE + POINTER_INFO_SIZE;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the key to the index
|
|
||||||
*/
|
|
||||||
final StorageKey key;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates this header's position in the file index.
|
|
||||||
*/
|
|
||||||
volatile int indexPosition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File pointer to the first byte of record data (8 bytes).
|
|
||||||
*/
|
|
||||||
volatile long dataPointer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actual number of bytes of data held in this record (4 bytes).
|
|
||||||
*/
|
|
||||||
volatile int dataCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of bytes of data that this record can hold (4 bytes).
|
|
||||||
*/
|
|
||||||
volatile int dataCapacity;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The object that has been registered to this key. This is for automatic saving of data (if it's changed)
|
|
||||||
*/
|
|
||||||
volatile WeakReference<Object> objectReferenceCache;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a file pointer in the index pointing to the first byte in the KEY located at the given index position.
|
|
||||||
*/
|
|
||||||
static
|
|
||||||
long getMetaDataPointer(int position) {
|
|
||||||
return StorageBase.FILE_HEADERS_REGION_LENGTH + ((long) INDEX_ENTRY_LENGTH) * position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a file pointer in the index pointing to the first byte in the RECORD pointer located at the given index
|
|
||||||
* position.
|
|
||||||
*/
|
|
||||||
private static
|
|
||||||
long getDataPointer(int position) {
|
|
||||||
return Metadata.getMetaDataPointer(position) + KEY_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private
|
|
||||||
Metadata(StorageKey key) {
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* we don't know how much data there is until AFTER we write the data
|
|
||||||
*/
|
|
||||||
Metadata(StorageKey key, int recordIndex, long dataPointer) {
|
|
||||||
this(key, recordIndex, dataPointer, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private
|
|
||||||
Metadata(StorageKey key, int recordIndex, long dataPointer, int dataCapacity) {
|
|
||||||
if (key.getBytes().length > KEY_SIZE) {
|
|
||||||
throw new IllegalArgumentException("Bad record key size: " + dataCapacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.indexPosition = recordIndex;
|
|
||||||
this.dataPointer = dataPointer;
|
|
||||||
|
|
||||||
// we don't always know the size!
|
|
||||||
this.dataCapacity = dataCapacity;
|
|
||||||
this.dataCount = dataCapacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
int getFreeSpace() {
|
|
||||||
return this.dataCapacity - this.dataCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the Nth HEADER (key + metadata) from the index.
|
|
||||||
*/
|
|
||||||
static
|
|
||||||
Metadata readHeader(RandomAccessFile file, int position) throws IOException {
|
|
||||||
byte[] buf = new byte[KEY_SIZE];
|
|
||||||
long origHeaderKeyPointer = Metadata.getMetaDataPointer(position);
|
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(origHeaderKeyPointer, INDEX_ENTRY_LENGTH, true);
|
|
||||||
|
|
||||||
file.seek(origHeaderKeyPointer);
|
|
||||||
file.readFully(buf);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
Metadata r = new Metadata(new StorageKey(buf));
|
|
||||||
r.indexPosition = position;
|
|
||||||
|
|
||||||
long recordHeaderPointer = Metadata.getDataPointer(position);
|
|
||||||
lock = file.getChannel()
|
|
||||||
.lock(origHeaderKeyPointer, KEY_SIZE, true);
|
|
||||||
|
|
||||||
file.seek(recordHeaderPointer);
|
|
||||||
r.dataPointer = file.readLong();
|
|
||||||
r.dataCapacity = file.readInt();
|
|
||||||
r.dataCount = file.readInt();
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
if (r.dataPointer == 0L || r.dataCapacity == 0L || r.dataCount == 0L) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeMetaDataInfo(RandomAccessFile file) throws IOException {
|
|
||||||
long recordKeyPointer = Metadata.getMetaDataPointer(this.indexPosition);
|
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(recordKeyPointer, KEY_SIZE, false);
|
|
||||||
file.seek(recordKeyPointer);
|
|
||||||
file.write(this.key.getBytes());
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeDataInfo(RandomAccessFile file) throws IOException {
|
|
||||||
long recordHeaderPointer = getDataPointer(this.indexPosition);
|
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(recordHeaderPointer, POINTER_INFO_SIZE, false);
|
|
||||||
|
|
||||||
file.seek(recordHeaderPointer);
|
|
||||||
file.writeLong(this.dataPointer);
|
|
||||||
file.writeInt(this.dataCapacity);
|
|
||||||
file.writeInt(this.dataCount);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a record to the new INDEX
|
|
||||||
*/
|
|
||||||
void moveRecord(RandomAccessFile file, int newIndex) throws IOException {
|
|
||||||
byte[] buf = new byte[KEY_SIZE];
|
|
||||||
|
|
||||||
long origHeaderKeyPointer = Metadata.getMetaDataPointer(this.indexPosition);
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(origHeaderKeyPointer, INDEX_ENTRY_LENGTH, true);
|
|
||||||
|
|
||||||
file.seek(origHeaderKeyPointer);
|
|
||||||
file.readFully(buf);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
long newHeaderKeyPointer = Metadata.getMetaDataPointer(newIndex);
|
|
||||||
lock = file.getChannel()
|
|
||||||
.lock(newHeaderKeyPointer, INDEX_ENTRY_LENGTH, false);
|
|
||||||
|
|
||||||
file.seek(newHeaderKeyPointer);
|
|
||||||
file.write(buf);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
// System.err.println("updating ptr: " + this.indexPosition + " -> " + newIndex + " @ " + newHeaderKeyPointer + "-" + (newHeaderKeyPointer+INDEX_ENTRY_LENGTH));
|
|
||||||
this.indexPosition = newIndex;
|
|
||||||
|
|
||||||
writeDataInfo(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a record DATA to the new position, and update record header info
|
|
||||||
*/
|
|
||||||
void moveData(RandomAccessFile file, long position) throws IOException {
|
|
||||||
// now we move it to the end of the file.
|
|
||||||
// we ALSO trim the free space off.
|
|
||||||
byte[] data = readDataRaw(file);
|
|
||||||
|
|
||||||
this.dataPointer = position;
|
|
||||||
this.dataCapacity = this.dataCount;
|
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(position, this.dataCount, false);
|
|
||||||
|
|
||||||
// update the file size
|
|
||||||
file.setLength(position + this.dataCount);
|
|
||||||
|
|
||||||
// System.err.print("moving data: " + this.indexPosition + " @ " + this.dataPointer + "-" + (this.dataPointer+data.length) + " -- ");
|
|
||||||
// Sys.printArray(data, data.length, false, 0);
|
|
||||||
// save the data
|
|
||||||
file.seek(position);
|
|
||||||
file.write(data);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
// update header pointer info
|
|
||||||
writeDataInfo(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the record data for the given record header.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
byte[] readDataRaw(RandomAccessFile file) throws IOException {
|
|
||||||
|
|
||||||
byte[] buf = new byte[this.dataCount];
|
|
||||||
// System.err.print("Reading data: " + this.indexPosition + " @ " + this.dataPointer + "-" + (this.dataPointer+this.dataCount) + " -- ");
|
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(this.dataPointer, this.dataCount, true);
|
|
||||||
file.seek(this.dataPointer);
|
|
||||||
file.readFully(buf);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
// Sys.printArray(buf, buf.length, false, 0);
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the record data for the given record header.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static
|
|
||||||
<T> T readData(final SerializationManager serializationManager, final Input input) throws IOException {
|
|
||||||
// this is to reset the internal buffer of 'input'
|
|
||||||
input.setInputStream(input.getInputStream());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
T readObject = (T) serializationManager.readFullClassAndObject(input);
|
|
||||||
return readObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes data to the end of the file (which is where the datapointer is at). This must be locked/released in calling methods!
|
|
||||||
*/
|
|
||||||
static
|
|
||||||
int writeData(final SerializationManager serializationManager,
|
|
||||||
final Object data,
|
|
||||||
final Output output) throws IOException {
|
|
||||||
|
|
||||||
output.reset();
|
|
||||||
|
|
||||||
serializationManager.writeFullClassAndObject(output, data);
|
|
||||||
output.flush();
|
|
||||||
|
|
||||||
return (int) output.total();
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeDataRaw(ByteArrayOutputStream byteArrayOutputStream, RandomAccessFile file) throws IOException {
|
|
||||||
this.dataCount = byteArrayOutputStream.size();
|
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
|
||||||
.lock(this.dataPointer, this.dataCount, false);
|
|
||||||
|
|
||||||
FileOutputStream out = new FileOutputStream(file.getFD());
|
|
||||||
file.seek(this.dataPointer);
|
|
||||||
byteArrayOutputStream.writeTo(out);
|
|
||||||
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
String toString() {
|
|
||||||
return "RecordHeader [dataPointer=" + this.dataPointer + ", dataCount=" + this.dataCount + ", dataCapacity=" + this.dataCapacity +
|
|
||||||
", indexPosition=" + this.indexPosition + "]";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public
|
|
||||||
interface Storage {
|
|
||||||
/**
|
|
||||||
* Returns the number of objects in the database.
|
|
||||||
*/
|
|
||||||
int size();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there is a object corresponding to the given key.
|
|
||||||
*/
|
|
||||||
boolean contains(StorageKey key);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a object using the specific key, and casts it to the expected class
|
|
||||||
*/
|
|
||||||
<T> T get(StorageKey key);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the saved data for the specified key.
|
|
||||||
*
|
|
||||||
* @param key The key used to check if data already exists.
|
|
||||||
* @param data This is the default value, and if there is no value with the key in the DB this default value will be saved.
|
|
||||||
*/
|
|
||||||
<T> T get(StorageKey key, T data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given data to storage with the associated key.
|
|
||||||
* <p/>
|
|
||||||
* Also will update existing data. If the new contents do not fit in the original space, then the update is handled by
|
|
||||||
* deleting the old data and adding the new.
|
|
||||||
*/
|
|
||||||
void put(StorageKey key, Object data);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an object from storage.
|
|
||||||
*
|
|
||||||
* @return true if the delete was successful. False if there were problems deleting the data.
|
|
||||||
*/
|
|
||||||
boolean delete(StorageKey key);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the file that backs this storage
|
|
||||||
*/
|
|
||||||
File getFile();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the backing file size.
|
|
||||||
*
|
|
||||||
* @return -1 if there was an error
|
|
||||||
*/
|
|
||||||
long getFileSize();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if there are objects queued to be written?
|
|
||||||
*/
|
|
||||||
boolean hasWriteWaiting();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the delay in milliseconds this will wait after the last action to flush the data to the disk
|
|
||||||
*/
|
|
||||||
long getSaveDelay();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the version of data stored in the database
|
|
||||||
*/
|
|
||||||
int getVersion();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the version of data stored in the database
|
|
||||||
*/
|
|
||||||
void setVersion(int version);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the storage to disk, immediately.
|
|
||||||
* <p/>
|
|
||||||
* This will save the ALL of the pending save actions to the file
|
|
||||||
*/
|
|
||||||
void save();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes this storage system
|
|
||||||
*/
|
|
||||||
void close();
|
|
||||||
}
|
|
@ -1,833 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.os.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<StorageKey, Metadata> 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<StorageBase, HashMap> 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<StorageKey, Metadata>(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> 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<Object> ref = meta.objectReferenceCache;
|
|
||||||
|
|
||||||
if (ref != null) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
T referenceObject = (T) ref.get();
|
|
||||||
return referenceObject;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.referenceLock.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return an object for a specified key form referenceCache FIRST, then from DISK. NULL if it doesn't exist or there was an error.
|
|
||||||
*/
|
|
||||||
final
|
|
||||||
<T> T get(StorageKey key) {
|
|
||||||
// NOT 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 get it from our reference cache so subsequent lookups are fast!
|
|
||||||
//noinspection Duplicates
|
|
||||||
try {
|
|
||||||
this.referenceLock.lock();
|
|
||||||
|
|
||||||
// if we have registered it, get it!
|
|
||||||
WeakReference<Object> ref = meta.objectReferenceCache;
|
|
||||||
|
|
||||||
if (ref != null) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
T referenceObject = (T) ref.get();
|
|
||||||
return referenceObject;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.referenceLock.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
// System.err.println("--Reading data from: " + meta.dataPointer);
|
|
||||||
|
|
||||||
// else, we have to load it from disk
|
|
||||||
this.randomAccessFile.seek(meta.dataPointer);
|
|
||||||
|
|
||||||
T readRecordData = Metadata.readData(this.serializationManager, this.input);
|
|
||||||
|
|
||||||
if (readRecordData != null) {
|
|
||||||
// now stuff it into our reference cache for future lookups!
|
|
||||||
try {
|
|
||||||
this.referenceLock.lock();
|
|
||||||
|
|
||||||
meta.objectReferenceCache = new WeakReference<Object>(readRecordData);
|
|
||||||
} finally {
|
|
||||||
this.referenceLock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return readRecordData;
|
|
||||||
} catch (Exception e) {
|
|
||||||
String message = e.getMessage();
|
|
||||||
int index = message.indexOf(OS.LINE_SEPARATOR);
|
|
||||||
if (index > -1) {
|
|
||||||
message = message.substring(0, index);
|
|
||||||
}
|
|
||||||
if (logger != null) {
|
|
||||||
logger.error("Error reading data from disk: {}", message);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
System.err.print("Error reading data from disk: " + message);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a record
|
|
||||||
*
|
|
||||||
* @return true if the delete was successful. False if there were problems deleting the data.
|
|
||||||
*/
|
|
||||||
final
|
|
||||||
boolean delete(StorageKey key) {
|
|
||||||
// pending ops flushed (protected by lock)
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
Metadata delRec = this.memoryIndex.get(key);
|
|
||||||
|
|
||||||
try {
|
|
||||||
deleteRecordData(delRec, delRec.dataCapacity);
|
|
||||||
|
|
||||||
// delete the record index
|
|
||||||
int currentNumRecords = this.memoryIndex.size();
|
|
||||||
if (delRec.indexPosition != currentNumRecords - 1) {
|
|
||||||
Metadata last = Metadata.readHeader(this.randomAccessFile, currentNumRecords - 1);
|
|
||||||
assert last != null;
|
|
||||||
|
|
||||||
last.moveRecord(this.randomAccessFile, delRec.indexPosition);
|
|
||||||
}
|
|
||||||
this.memoryIndex.remove(key);
|
|
||||||
|
|
||||||
|
|
||||||
setRecordCount(this.randomAccessFile, currentNumRecords - 1);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Error while deleting data from disk", e);
|
|
||||||
} else {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the database and file.
|
|
||||||
*/
|
|
||||||
final
|
|
||||||
void close() {
|
|
||||||
// pending ops flushed (protected by lock)
|
|
||||||
// not protected by lock
|
|
||||||
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.info("Closing storage file: '{}'", this.baseFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.randomAccessFile.getFD()
|
|
||||||
.sync();
|
|
||||||
this.input.close();
|
|
||||||
this.randomAccessFile.close();
|
|
||||||
|
|
||||||
// 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.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Error while closing the file", e);
|
|
||||||
} else {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the backing file size.
|
|
||||||
*
|
|
||||||
* @return -1 if there was an error
|
|
||||||
*/
|
|
||||||
long getFileSize() {
|
|
||||||
// protected by actionLock
|
|
||||||
try {
|
|
||||||
return this.randomAccessFile.length();
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Error getting file size for {}", this.baseFile.getAbsolutePath(), e);
|
|
||||||
} else {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return -1L;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the file that backs this storage
|
|
||||||
*/
|
|
||||||
final
|
|
||||||
File getFile() {
|
|
||||||
return this.baseFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given data to storage.
|
|
||||||
* <p/>
|
|
||||||
* Also will update existing data. If the new contents do not fit in the original space, then the update is handled by
|
|
||||||
* deleting the old data and adding the new.
|
|
||||||
* <p/>
|
|
||||||
* Will also save the object in a cache.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
void save0(StorageKey key, Object object) {
|
|
||||||
Metadata metaData;
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
metaData = this.memoryIndex.get(key);
|
|
||||||
int currentRecordCount = this.numberOfRecords;
|
|
||||||
|
|
||||||
if (metaData != null) {
|
|
||||||
// now we have to UPDATE instead of add!
|
|
||||||
try {
|
|
||||||
if (currentRecordCount == 1) {
|
|
||||||
// if we are the ONLY one, then we can do things differently.
|
|
||||||
// just dump the data again to disk.
|
|
||||||
FileLock lock = this.randomAccessFile.getChannel()
|
|
||||||
.lock(this.dataPosition,
|
|
||||||
Long.MAX_VALUE - this.dataPosition,
|
|
||||||
false); // don't know how big it is, so max value it
|
|
||||||
|
|
||||||
this.randomAccessFile.seek(this.dataPosition); // this is the end of the file, we know this ahead-of-time
|
|
||||||
Metadata.writeData(this.serializationManager, object, this.output);
|
|
||||||
// have to re-specify the capacity and size
|
|
||||||
//noinspection NumericCastThatLosesPrecision
|
|
||||||
int sizeOfWrittenData = (int) (this.randomAccessFile.length() - this.dataPosition);
|
|
||||||
|
|
||||||
metaData.dataCapacity = sizeOfWrittenData;
|
|
||||||
metaData.dataCount = sizeOfWrittenData;
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// this is comparatively slow, since we serialize it first to get the size, then we put it in the file.
|
|
||||||
ByteArrayOutputStream dataStream = getDataAsByteArray(this.serializationManager, object);
|
|
||||||
|
|
||||||
int size = dataStream.size();
|
|
||||||
if (size > metaData.dataCapacity) {
|
|
||||||
deleteRecordData(metaData, size);
|
|
||||||
// stuff this record to the end of the file, since it won't fit in it's current location
|
|
||||||
metaData.dataPointer = this.randomAccessFile.length();
|
|
||||||
// have to make sure that the CAPACITY of the new one is the SIZE of the new data!
|
|
||||||
// and since it is going to the END of the file, we do that.
|
|
||||||
metaData.dataCapacity = size;
|
|
||||||
metaData.dataCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: should check to see if the data is different. IF SO, then we write, otherwise nothing!
|
|
||||||
|
|
||||||
metaData.writeDataRaw(dataStream, this.randomAccessFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
metaData.writeDataInfo(this.randomAccessFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Error while saving data to disk", e);
|
|
||||||
} else {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// metadata == null...
|
|
||||||
try {
|
|
||||||
// set the number of records that this storage has
|
|
||||||
setRecordCount(this.randomAccessFile, currentRecordCount + 1);
|
|
||||||
|
|
||||||
// This will make sure that there is room to write a new record. This is zero indexed.
|
|
||||||
// this will skip around if moves occur
|
|
||||||
ensureIndexCapacity(this.randomAccessFile);
|
|
||||||
|
|
||||||
// append record to end of file
|
|
||||||
long length = this.randomAccessFile.length();
|
|
||||||
|
|
||||||
// System.err.println("--Writing data to: " + length);
|
|
||||||
|
|
||||||
metaData = new Metadata(key, currentRecordCount, length);
|
|
||||||
metaData.writeMetaDataInfo(this.randomAccessFile);
|
|
||||||
|
|
||||||
// add new entry to the index
|
|
||||||
this.memoryIndex.put(key, metaData);
|
|
||||||
|
|
||||||
// save out the data. Because we KNOW that we are writing this to the end of the file,
|
|
||||||
// there are some tricks we can use.
|
|
||||||
|
|
||||||
// don't know how big it is, so max value it
|
|
||||||
FileLock lock = this.randomAccessFile.getChannel()
|
|
||||||
.lock(0, Long.MAX_VALUE, false);
|
|
||||||
|
|
||||||
// this is the end of the file, we know this ahead-of-time
|
|
||||||
this.randomAccessFile.seek(length);
|
|
||||||
|
|
||||||
int total = Metadata.writeData(this.serializationManager, object, this.output);
|
|
||||||
lock.release();
|
|
||||||
|
|
||||||
metaData.dataCount = metaData.dataCapacity = total;
|
|
||||||
// have to save it.
|
|
||||||
metaData.writeDataInfo(this.randomAccessFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Error while writing data to disk", e);
|
|
||||||
} else {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// put the object in the reference cache so we can read/get it later on
|
|
||||||
metaData.objectReferenceCache = new WeakReference<Object>(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static
|
|
||||||
ByteArrayOutputStream getDataAsByteArray(SerializationManager serializationManager, Object data) throws IOException {
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
||||||
Output output = new Output(outputStream, 1024); // write 1024 at a time
|
|
||||||
|
|
||||||
serializationManager.writeFullClassAndObject(output, data);
|
|
||||||
output.flush();
|
|
||||||
|
|
||||||
outputStream.flush();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
return outputStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
void doActionThings(Map<StorageKey, Object> actions) {
|
|
||||||
|
|
||||||
// actions is thrown away after this invocation. GC can pick it up.
|
|
||||||
// we are only interested in the LAST action that happened for some data.
|
|
||||||
// items to be "autosaved" are automatically injected into "actions".
|
|
||||||
final Set<Entry<StorageKey, Object>> entries = actions.entrySet();
|
|
||||||
for (Entry<StorageKey, Object> entry : entries) {
|
|
||||||
StorageKey key = entry.getKey();
|
|
||||||
Object object = entry.getValue();
|
|
||||||
|
|
||||||
// our action list is for explicitly saving objects (but not necessarily "registering" them to be auto-saved
|
|
||||||
save0(key, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////
|
|
||||||
/////////////////////
|
|
||||||
// private/index only methods
|
|
||||||
/////////////////////
|
|
||||||
/////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "intelligent" move strategy.
|
|
||||||
* <p/>
|
|
||||||
* we should increase by some weight (ie: .5) would increase the number of allocated
|
|
||||||
* record headers by 50%, instead of just incrementing them by one at a time.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
int getWeightedNewRecordCount(int numberOfRecords) {
|
|
||||||
//noinspection AutoUnboxing,NumericCastThatLosesPrecision
|
|
||||||
return numberOfRecords + 1 + (int) (numberOfRecords * this.weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// protected by singleWriterLock
|
|
||||||
private
|
|
||||||
void deleteRecordData(Metadata deletedRecord, int sizeOfDataToAdd) throws IOException {
|
|
||||||
if (this.randomAccessFile.length() == deletedRecord.dataPointer + deletedRecord.dataCapacity) {
|
|
||||||
// shrink file since this is the last record in the file
|
|
||||||
FileLock lock = this.randomAccessFile.getChannel()
|
|
||||||
.lock(deletedRecord.dataPointer, Long.MAX_VALUE - deletedRecord.dataPointer, false);
|
|
||||||
this.randomAccessFile.setLength(deletedRecord.dataPointer);
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// we MIGHT be the FIRST record
|
|
||||||
Metadata first = index_getMetaDataFromData(this.dataPosition);
|
|
||||||
if (first == deletedRecord) {
|
|
||||||
// the record to delete is the FIRST (of many) in the file.
|
|
||||||
// the FASTEST way to delete is to grow the number of allowed records!
|
|
||||||
// Another option is to move the #2 data to the first data, but then there is the same gap after #2.
|
|
||||||
|
|
||||||
int numberOfRecords = this.numberOfRecords;
|
|
||||||
|
|
||||||
// "intelligent" move strategy.
|
|
||||||
int newNumberOfRecords = getWeightedNewRecordCount(numberOfRecords);
|
|
||||||
long endIndexPointer = Metadata.getMetaDataPointer(newNumberOfRecords);
|
|
||||||
|
|
||||||
long endOfDataPointer = deletedRecord.dataPointer + deletedRecord.dataCapacity;
|
|
||||||
long newEndOfDataPointer = endOfDataPointer - sizeOfDataToAdd;
|
|
||||||
|
|
||||||
if (endIndexPointer < this.dataPosition && endIndexPointer <= newEndOfDataPointer) {
|
|
||||||
// one option is to shrink the RECORD section to fit the new data
|
|
||||||
setDataStartPosition(newEndOfDataPointer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// option two is to grow the RECORD section, and put the data at the end of the file
|
|
||||||
setDataStartPosition(endOfDataPointer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Metadata previous = index_getMetaDataFromData(deletedRecord.dataPointer - 1);
|
|
||||||
if (previous != null) {
|
|
||||||
// append space of deleted record onto previous record
|
|
||||||
previous.dataCapacity += deletedRecord.dataCapacity;
|
|
||||||
previous.writeDataInfo(this.randomAccessFile);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// because there is no "previous", that means we MIGHT be the FIRST record
|
|
||||||
// well, we're not the first record. which one is RIGHT before us?
|
|
||||||
// it should be "previous", so something messed up
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Trying to delete an object, and it's in a weird state");
|
|
||||||
} else {
|
|
||||||
System.err.println("Trying to delete an object, and it's in a weird state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the number of records header to the file.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
void setVersion(RandomAccessFile file, int versionNumber) throws IOException {
|
|
||||||
this.databaseVersion = versionNumber;
|
|
||||||
|
|
||||||
FileLock lock = this.randomAccessFile.getChannel()
|
|
||||||
.lock(VERSION_HEADER_LOCATION, 4, false);
|
|
||||||
file.seek(VERSION_HEADER_LOCATION);
|
|
||||||
file.writeInt(versionNumber);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the number of records header to the file.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
void setRecordCount(RandomAccessFile file, int numberOfRecords) throws IOException {
|
|
||||||
if (this.numberOfRecords != numberOfRecords) {
|
|
||||||
this.numberOfRecords = numberOfRecords;
|
|
||||||
|
|
||||||
// System.err.println("Set recordCount: " + numberOfRecords);
|
|
||||||
|
|
||||||
FileLock lock = this.randomAccessFile.getChannel()
|
|
||||||
.lock(NUM_RECORDS_HEADER_LOCATION, 4, false);
|
|
||||||
file.seek(NUM_RECORDS_HEADER_LOCATION);
|
|
||||||
file.writeInt(numberOfRecords);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the data start position to the file.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
void setDataStartPosition(long dataPositionPointer) throws IOException {
|
|
||||||
FileLock lock = this.randomAccessFile.getChannel()
|
|
||||||
.lock(DATA_START_HEADER_LOCATION, 8, false);
|
|
||||||
|
|
||||||
// System.err.println("Setting data position: " + dataPositionPointer);
|
|
||||||
dataPosition = dataPositionPointer;
|
|
||||||
|
|
||||||
randomAccessFile.seek(DATA_START_HEADER_LOCATION);
|
|
||||||
randomAccessFile.writeLong(dataPositionPointer);
|
|
||||||
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
int getVersion() {
|
|
||||||
return this.databaseVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVersion(int versionNumber) {
|
|
||||||
try {
|
|
||||||
setVersion(this.randomAccessFile, versionNumber);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (this.logger != null) {
|
|
||||||
this.logger.error("Unable to set the version number", e);
|
|
||||||
} else {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the record to which the target file pointer belongs - meaning the specified location in the file is part
|
|
||||||
* of the record data of the RecordHeader which is returned. Returns null if the location is not part of a record.
|
|
||||||
* (O(n) mem accesses)
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
Metadata index_getMetaDataFromData(long targetFp) {
|
|
||||||
// access a snapshot of the memoryIndex (single-writer-principle)
|
|
||||||
HashMap memoryIndex = memoryREF.get(this);
|
|
||||||
Iterator iterator = memoryIndex.values().iterator();
|
|
||||||
|
|
||||||
//noinspection WhileLoopReplaceableByForEach
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Metadata next = (Metadata) iterator.next();
|
|
||||||
if (targetFp >= next.dataPointer && targetFp < next.dataPointer + next.dataCapacity) {
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure index capacity. This operation makes sure the INDEX REGION is large enough to accommodate additional entries.
|
|
||||||
*/
|
|
||||||
private
|
|
||||||
void ensureIndexCapacity(RandomAccessFile file) throws IOException {
|
|
||||||
// because we are zero indexed, this is ALSO the index where the record will START
|
|
||||||
int numberOfRecords = this.numberOfRecords;
|
|
||||||
|
|
||||||
// +1 because this is where that index will END (the start of the NEXT one)
|
|
||||||
long endIndexPointer = Metadata.getMetaDataPointer(numberOfRecords + 1);
|
|
||||||
|
|
||||||
// just set the data position to the end of the file, since we don't have any data yet.
|
|
||||||
if (endIndexPointer > file.length() && numberOfRecords == 0) {
|
|
||||||
file.setLength(endIndexPointer);
|
|
||||||
setDataStartPosition(endIndexPointer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we have to check, is there room for just 1 more entry?
|
|
||||||
long readDataPosition = this.dataPosition;
|
|
||||||
if (endIndexPointer < readDataPosition) {
|
|
||||||
// we have room for this entry.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// otherwise, we have to grow our index.
|
|
||||||
Metadata first;
|
|
||||||
// "intelligent" move strategy.
|
|
||||||
int newNumberOfRecords = getWeightedNewRecordCount(numberOfRecords);
|
|
||||||
endIndexPointer = Metadata.getMetaDataPointer(newNumberOfRecords);
|
|
||||||
|
|
||||||
|
|
||||||
// sometimes the endIndexPointer is in the middle of data, so we cannot move a record to where
|
|
||||||
// data already exists, we have to move it to the end. Since we GUARANTEE that there is never "free space" at the
|
|
||||||
// end of a file, this is ok
|
|
||||||
if (endIndexPointer > file.length()) {
|
|
||||||
// make sure we adjust the file size
|
|
||||||
file.setLength(endIndexPointer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
endIndexPointer = file.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
// we know that the start of the NEW data position has to be here.
|
|
||||||
setDataStartPosition(endIndexPointer);
|
|
||||||
|
|
||||||
|
|
||||||
long writeDataPosition = endIndexPointer;
|
|
||||||
|
|
||||||
// if we only have ONE record left, and we move it to the end, then no reason to keep looking for records.
|
|
||||||
while (endIndexPointer > readDataPosition && numberOfRecords > 0) {
|
|
||||||
// this is the FIRST record that is in our data section
|
|
||||||
first = index_getMetaDataFromData(readDataPosition);
|
|
||||||
if (first == null) {
|
|
||||||
//nothing is here, so keep checking
|
|
||||||
readDataPosition += Metadata.INDEX_ENTRY_LENGTH;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// System.err.println("\nMoving record: " + first.indexPosition + " -> " + writeDataPosition);
|
|
||||||
first.moveData(file, writeDataPosition);
|
|
||||||
|
|
||||||
int dataCapacity = first.dataCapacity;
|
|
||||||
readDataPosition += dataCapacity;
|
|
||||||
writeDataPosition += dataCapacity;
|
|
||||||
numberOfRecords--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package dorkbox.util.storage;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo;
|
|
||||||
import com.esotericsoftware.kryo.KryoException;
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
|
|
||||||
public
|
|
||||||
interface StorageBuilder {
|
|
||||||
/**
|
|
||||||
* Builds the storage using the specified configuration
|
|
||||||
*/
|
|
||||||
Storage build();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the lowest, next available integer ID and the {@link Kryo#getDefaultSerializer(Class) default serializer}.
|
|
||||||
* If the class is already registered, the existing entry is updated with the new serializer.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this
|
|
||||||
* method. The order must be the same at deserialization as it was for serialization.
|
|
||||||
*/
|
|
||||||
<T> StorageBuilder register(Class<T> clazz);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the specified ID. If the ID is already in use by the same type, the old entry is overwritten. If the ID
|
|
||||||
* is already in use by a different type, a {@link KryoException} is thrown.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* IDs must be the same at deserialization as they were for serialization.
|
|
||||||
*
|
|
||||||
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
|
|
||||||
* these IDs can be repurposed.
|
|
||||||
*/
|
|
||||||
<T> StorageBuilder register(Class<T> clazz, int id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the lowest, next available integer ID and the specified serializer. If the class is already registered,
|
|
||||||
* the existing entry is updated with the new serializer.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* Because the ID assigned is affected by the IDs registered before it, the order classes are registered is important when using this
|
|
||||||
* method. The order must be the same at deserialization as it was for serialization.
|
|
||||||
*/
|
|
||||||
<T> StorageBuilder register(Class<T> clazz, Serializer<T> serializer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the class using the specified ID and serializer. If the ID is already in use by the same type, the old entry is
|
|
||||||
* overwritten. If the ID is already in use by a different type, a {@link KryoException} is thrown.
|
|
||||||
* <p>
|
|
||||||
* Registering a primitive also affects the corresponding primitive wrapper.
|
|
||||||
* <p>
|
|
||||||
* IDs must be the same at deserialization as they were for serialization.
|
|
||||||
*
|
|
||||||
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
|
|
||||||
* these IDs can be repurposed.
|
|
||||||
*/
|
|
||||||
<T> StorageBuilder register(Class<T> clazz, Serializer<T> serializer, int id);
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 dorkbox.util.HashUtil;
|
|
||||||
import dorkbox.util.bytes.ByteArrayWrapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a ByteArrayWrapper that is really a SHA256 hash of the bytes.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
class StorageKey extends ByteArrayWrapper {
|
|
||||||
public
|
|
||||||
StorageKey(String key) {
|
|
||||||
super(HashUtil.getSha256(key), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
StorageKey(byte[] key) {
|
|
||||||
super(key, false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,344 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.helpers.NOPLogger;
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Serializer;
|
|
||||||
|
|
||||||
import dorkbox.os.OS;
|
|
||||||
import dorkbox.util.FileUtil;
|
|
||||||
import dorkbox.util.serialization.SerializationManager;
|
|
||||||
|
|
||||||
public
|
|
||||||
class StorageSystem {
|
|
||||||
private static final Map<File, Storage> storages = new HashMap<File, Storage>(1);
|
|
||||||
|
|
||||||
// Make sure that the timer is run on shutdown. A HARD shutdown will just POW! kill it, a "nice" shutdown will run the hook
|
|
||||||
private static final Thread shutdownHook = new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
StorageSystem.shutdown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
static {
|
|
||||||
// add a shutdown hook to make sure that we properly flush/shutdown storage.
|
|
||||||
Runtime.getRuntime()
|
|
||||||
.addShutdownHook(shutdownHook);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a persistent, on-disk storage system. Writes to disk are queued, so it is recommended to NOT edit/change an object after
|
|
||||||
* it has been put into storage, or whenever it does changes, make sure to put it back into storage (to update the saved record)
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
DiskBuilder Disk() {
|
|
||||||
return new DiskBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an in-memory only storage system
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
MemoryBuilder Memory() {
|
|
||||||
return new MemoryBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the specified storage system based on the file used
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
void close(final File file) {
|
|
||||||
synchronized (storages) {
|
|
||||||
Storage storage = storages.get(file);
|
|
||||||
if (storage != null) {
|
|
||||||
if (storage instanceof DiskStorage) {
|
|
||||||
final DiskStorage diskStorage = (DiskStorage) storage;
|
|
||||||
boolean isLastOne = diskStorage.decrementReference();
|
|
||||||
if (isLastOne) {
|
|
||||||
diskStorage.closeFully();
|
|
||||||
storages.remove(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the specified storage system
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
void close(final Storage storage) {
|
|
||||||
synchronized (storages) {
|
|
||||||
File file = storage.getFile();
|
|
||||||
close(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves and closes all open storage systems
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
void shutdown() {
|
|
||||||
synchronized (storages) {
|
|
||||||
Collection<Storage> values = storages.values();
|
|
||||||
for (Storage storage : values) {
|
|
||||||
if (storage instanceof DiskStorage) {
|
|
||||||
final DiskStorage diskStorage = (DiskStorage) storage;
|
|
||||||
//noinspection StatementWithEmptyBody
|
|
||||||
while (!diskStorage.decrementReference()) {
|
|
||||||
}
|
|
||||||
diskStorage.closeFully();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storages.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes (if in use) and deletes the specified storage file.
|
|
||||||
* <p>
|
|
||||||
* The file is checked to see if it is in use by the storage system first, and closes if so.
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
void delete(File file) {
|
|
||||||
synchronized (storages) {
|
|
||||||
Storage remove = storages.remove(file);
|
|
||||||
if (remove instanceof DiskStorage) {
|
|
||||||
((DiskStorage) remove).closeFully();
|
|
||||||
}
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes (if in use) and deletes the specified storage.
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
void delete(Storage storage) {
|
|
||||||
File file = storage.getFile();
|
|
||||||
delete(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a persistent, on-disk storage system. Writes to disk are queued, so it is recommended to NOT edit/change an object after
|
|
||||||
* it has been put into storage, or whenever it does changes, make sure to put it back into storage (to update the saved record)
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static
|
|
||||||
class DiskBuilder implements StorageBuilder {
|
|
||||||
public File file;
|
|
||||||
public SerializationManager<?> serializationManager = new DefaultStorageSerializationManager(); // default
|
|
||||||
public boolean readOnly = false;
|
|
||||||
public Logger logger = null;
|
|
||||||
public long saveDelayInMilliseconds = 3000L; // default
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the file to write to on disk when saving objects
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder file(File file) {
|
|
||||||
this.file = FileUtil.normalize(file);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the file to write to on disk when saving objects
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder file(String file) {
|
|
||||||
this.file = FileUtil.normalize(file);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the serialization manager to use. This is what serializes the files (which are then saved to disk)
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder serializer(SerializationManager<?> serializationManager) {
|
|
||||||
this.serializationManager = serializationManager;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark this storage system as read only
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder readOnly() {
|
|
||||||
this.readOnly = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark this storage system as read only
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder setSaveDelay(long saveDelayInMilliseconds) {
|
|
||||||
this.saveDelayInMilliseconds = saveDelayInMilliseconds;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns a logger to use for the storage system. If null, then only errors will be logged to the error console.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder logger(final Logger logger) {
|
|
||||||
this.logger = logger;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns a No Operation (NOP) logger which will ignore everything. This is not recommended for normal use, as it will also
|
|
||||||
* suppress serialization errors.
|
|
||||||
*/
|
|
||||||
public
|
|
||||||
DiskBuilder noLogger() {
|
|
||||||
this.logger = NOPLogger.NOP_LOGGER;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz) {
|
|
||||||
this.serializationManager.register(clazz);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz, final int id) {
|
|
||||||
this.serializationManager.register(clazz, id);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz, final Serializer<T> serializer) {
|
|
||||||
this.serializationManager.register(clazz, serializer);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz, final Serializer<T> serializer, final int id) {
|
|
||||||
this.serializationManager.register(clazz, serializer, id);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes the storage system
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
Storage build() {
|
|
||||||
if (this.file == null) {
|
|
||||||
throw new IllegalArgumentException("file cannot be null!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we load from a NEW storage at the same location as an ALREADY EXISTING storage,
|
|
||||||
// without saving the existing storage first --- whoops!
|
|
||||||
synchronized (storages) {
|
|
||||||
Storage storage = storages.get(this.file);
|
|
||||||
|
|
||||||
if (storage != null) {
|
|
||||||
if (storage instanceof DiskStorage) {
|
|
||||||
boolean waiting = storage.hasWriteWaiting();
|
|
||||||
// we want this storage to be in a fresh state
|
|
||||||
if (waiting) {
|
|
||||||
storage.save();
|
|
||||||
}
|
|
||||||
((DiskStorage) storage).increaseReference();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new RuntimeException("Unable to change storage types for: " + this.file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
storage = new DiskStorage(this.file, this.serializationManager, this.readOnly, this.saveDelayInMilliseconds, this.logger);
|
|
||||||
storages.put(this.file, storage);
|
|
||||||
} catch (IOException e) {
|
|
||||||
String message = e.getMessage();
|
|
||||||
int index = message.indexOf(OS.LINE_SEPARATOR);
|
|
||||||
if (index > -1) {
|
|
||||||
message = message.substring(0, index);
|
|
||||||
}
|
|
||||||
if (logger != null) {
|
|
||||||
logger.error("Unable to open storage file at {}. {}", this.file, message);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
System.err.print("Unable to open storage file at " + this.file + ". " + message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return storage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an in-memory only storage system. This storage system DOES NOT care about serializing data, so `register` has no effect.
|
|
||||||
*/
|
|
||||||
public static
|
|
||||||
class MemoryBuilder implements StorageBuilder {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the storage system
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
Storage build() {
|
|
||||||
return new MemoryStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz, final int id) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz, final Serializer<T> serializer) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
<T> StorageBuilder register(final Class<T> clazz, final Serializer<T> serializer, final int id) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user