![JAR search and dependency download from the Maven repository](/logo.png)
com.moebiusgames.xdata.XData Maven / Gradle / Ivy
/*
* Copyright (C) 2013 Florian Frankenberger.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package com.moebiusgames.xdata;
import com.moebiusgames.xdata.marshaller.DateMarshaller;
import com.moebiusgames.xdata.marshaller.URLMarshaller;
import com.moebiusgames.xdata.streams.CountingDataInputStream;
import com.moebiusgames.xdata.streams.MessageDigestInputStream;
import com.moebiusgames.xdata.streams.MessageDigestOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Main class for storing and loading xdata files
*
* Sample:
*
*
* final static DataKey<String> MY_KEY = DataKey.create("mykey", String.class);
//...
DataNode node = new DataNode();
node.setObject(MY_KEY, "hello world");
XData.store(node, new File("somefile.xdata"));
//...
DataNode restoredNode = XData.load(new File("somefile.xdata"));
//do sth with the data in the node e.g.
System.out.println(node.getDeSerializedObject(MY_KEY));
* @author Florian Frankenberger
*/
public class XData {
private static final String CHECKSUM_ALGORITHM = "SHA-256";
private static final int CHECKSUM_ALGORITHM_LENGTH = 32;
public static enum ChecksumValidation {
/**
* no validation will occur
*/
NONE,
/**
* validates the data only if there is an embedded checksum
*/
VALIDATE_IF_AVAILABLE,
/**
* validates the checksum but throws an exception if there
* is no checksum embedded within the xfile stream
*/
VALIDATE_OR_THROW_EXCEPTION
}
private static final DummyProgressListener DUMMY_PROGRESS_LISTENER = new DummyProgressListener();
private static final byte[] XDATA_HEADER = new byte[] {'x', 'd', 'a', 't', 'a'};
private static final DataKey META_CLASS_NAME = DataKey.create("_meta_classname", String.class);
private static final Map, Serializer>> PRIMITIVE_SERIALIZERS_BY_CLASS = new HashMap<>();
private static final Map> PRIMITIVE_SERIALIZERS_BY_ID = new HashMap<>();
private static final List> DEFAULT_MARSHALLERS = new ArrayList<>();
private static final int VAL_NULL = 0;
private static final int VAL_ELEMENT = 1;
private static final int VAL_LIST = 2;
private static final int VAL_NODE = 3;
private static final int VAL_REFERENCE = 4;
static {
//default marshallers
DEFAULT_MARSHALLERS.add(new DateMarshaller());
DEFAULT_MARSHALLERS.add(new URLMarshaller());
//primitive serializers
final IntSerializer intSerializer = new IntSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Integer.class, intSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(int.class, intSerializer);
final LongSerializer longSerializer = new LongSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Long.class, longSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(long.class, longSerializer);
final ShortSerializer shortSerializer = new ShortSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Short.class, shortSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(short.class, shortSerializer);
final ByteSerializer byteSerializer = new ByteSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Byte.class, byteSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(byte.class, byteSerializer);
final CharSerializer charSerializer = new CharSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Character.class, charSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(char.class, charSerializer);
final FloatSerializer floatSerializer = new FloatSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Float.class, floatSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(float.class, floatSerializer);
final DoubleSerializer doubleSerializer = new DoubleSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Double.class, doubleSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(double.class, doubleSerializer);
final BooleanSerializer booleanSerializer = new BooleanSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(Boolean.class, booleanSerializer);
PRIMITIVE_SERIALIZERS_BY_CLASS.put(boolean.class, booleanSerializer);
final StringSerializer stringSerializer = new StringSerializer();
PRIMITIVE_SERIALIZERS_BY_CLASS.put(String.class, stringSerializer);
for (Serializer> serializer : PRIMITIVE_SERIALIZERS_BY_CLASS.values()) {
PRIMITIVE_SERIALIZERS_BY_ID.put(serializer.getSerializerId(), serializer);
}
}
private XData() {
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, AbstractDataMarshaller>... marshallers) throws IOException {
return load(file, DUMMY_PROGRESS_LISTENER, marshallers);
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param checksumValidation
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, ChecksumValidation checksumValidation, AbstractDataMarshaller>... marshallers) throws IOException {
return load(file, checksumValidation, DUMMY_PROGRESS_LISTENER, marshallers);
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param ignoreMissingMarshallers if this is set true then no IOException is thrown
* when a marshaller is missing.
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, boolean ignoreMissingMarshallers, AbstractDataMarshaller>... marshallers) throws IOException {
return load(file, DUMMY_PROGRESS_LISTENER, ignoreMissingMarshallers, marshallers);
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param progressListener
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, ProgressListener progressListener, AbstractDataMarshaller>... marshallers) throws IOException {
return load(file, progressListener, false, marshallers);
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param checksumValidation
* @param progressListener
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, ChecksumValidation checksumValidation, ProgressListener progressListener,
AbstractDataMarshaller>... marshallers) throws IOException {
return load(file, checksumValidation, progressListener, false, marshallers);
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param progressListener
* @param ignoreMissingMarshallers if this is set true then no IOException is thrown
* when a marshaller is missing.
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, ProgressListener progressListener,
boolean ignoreMissingMarshallers, AbstractDataMarshaller>... marshallers) throws IOException {
return load(new FileInputStream(file), ChecksumValidation.VALIDATE_IF_AVAILABLE, progressListener, ignoreMissingMarshallers, marshallers);
}
/**
* loads a xdata file from disk using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param file
* @param checksumValidation
* @param progressListener
* @param ignoreMissingMarshallers if this is set true then no IOException is thrown
* when a marshaller is missing.
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(File file, ChecksumValidation checksumValidation, ProgressListener progressListener,
boolean ignoreMissingMarshallers, AbstractDataMarshaller>... marshallers) throws IOException {
return load(new FileInputStream(file), checksumValidation, progressListener, ignoreMissingMarshallers, marshallers);
}
/**
* loads a xdata file from from an inputstream using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param in
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(InputStream in, AbstractDataMarshaller>... marshallers) throws IOException {
return load(in, DUMMY_PROGRESS_LISTENER, marshallers);
}
/**
* loads a xdata file from from an inputstream using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param in
* @param checksumValidation
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(InputStream in, ChecksumValidation checksumValidation,
AbstractDataMarshaller>... marshallers) throws IOException {
return load(in, checksumValidation, DUMMY_PROGRESS_LISTENER, marshallers);
}
/**
* loads a xdata file from from an inputstream using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param in
* @param ignoreMissingMarshallers if this is set true then no IOException is thrown
* when a marshaller is missing.
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(InputStream in, boolean ignoreMissingMarshallers, AbstractDataMarshaller>... marshallers) throws IOException {
return load(in, ChecksumValidation.VALIDATE_IF_AVAILABLE, DUMMY_PROGRESS_LISTENER, ignoreMissingMarshallers, marshallers);
}
/**
* loads a xdata file from from an inputstream using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param in
* @param progressListener
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(InputStream in, ProgressListener progressListener, AbstractDataMarshaller>... marshallers) throws IOException {
return load(in, ChecksumValidation.VALIDATE_IF_AVAILABLE, progressListener, false, marshallers);
}
/**
* loads a xdata file from from an inputstream using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param in
* @param checksumValidation
* @param progressListener
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(InputStream in, ChecksumValidation checksumValidation, ProgressListener progressListener,
AbstractDataMarshaller>... marshallers) throws IOException {
return load(in, checksumValidation, progressListener, false, marshallers);
}
/**
* loads a xdata file from an inputstream using the given marshallers. For all classes other
* than these a special marshaller is required to map the class' data to a data node
* deSerializedObject:
*
* - Boolean
* - Long
* - Integer
* - String
* - Float
* - Double
* - Byte
* - Short
* - Character
* - DataNode
* - List<?>
*
*
* Also take a look at {@link com.moebiusgames.xdata.marshaller}. There are a bunch of
* standard marshallers that ARE INCLUDED by default. So you don't need to add them here
* to work.
*
* @param in
* @param checksumValidation
* @param progressListener
* @param ignoreMissingMarshallers if this is set true then no IOException is thrown
* when a marshaller is missing.
* @param marshallers
* @return
* @throws IOException
*/
public static DataNode load(InputStream in, ChecksumValidation checksumValidation, ProgressListener progressListener,
boolean ignoreMissingMarshallers, AbstractDataMarshaller>... marshallers) throws IOException {
final Map> marshallerMap = generateMarshallerMap(false, Arrays.asList(marshallers));
marshallerMap.putAll(generateMarshallerMap(false, DEFAULT_MARSHALLERS));
final GZIPInputStream gzipInputStream = new GZIPInputStream(in);
try {
InputStream inputStream = gzipInputStream;
MessageDigestInputStream messageDigestInputStream = null;
if (checksumValidation != ChecksumValidation.NONE) {
messageDigestInputStream = new MessageDigestInputStream(inputStream, CHECKSUM_ALGORITHM);
inputStream = messageDigestInputStream;
}
CountingDataInputStream dIn = new CountingDataInputStream(inputStream);
checkHeader(dIn);
final DataNode firstDataNode = deSerialize(dIn, marshallerMap,
ignoreMissingMarshallers, progressListener);
byte[] checksumCalcualted = null;
if (checksumValidation != ChecksumValidation.NONE) {
checksumCalcualted = messageDigestInputStream.getDigest();
}
final int checksumAvailable = dIn.read();
if (checksumValidation == ChecksumValidation.VALIDATE_IF_AVAILABLE
|| checksumValidation == ChecksumValidation.VALIDATE_OR_THROW_EXCEPTION) {
byte[] checksumValue = new byte[CHECKSUM_ALGORITHM_LENGTH];
int readBytes = dIn.read(checksumValue);
if (checksumValidation == ChecksumValidation.VALIDATE_IF_AVAILABLE
&& readBytes == CHECKSUM_ALGORITHM_LENGTH) {
if (!Arrays.equals(checksumValue, checksumCalcualted)) {
throw new IOException("Checksum is invalid.");
}
} else
if (checksumValidation == ChecksumValidation.VALIDATE_OR_THROW_EXCEPTION) {
if (checksumAvailable == -1 || readBytes != CHECKSUM_ALGORITHM_LENGTH) {
throw new IOException("File contains no embedded checksum");
}
if (!Arrays.equals(checksumValue, checksumCalcualted)) {
throw new IOException("Checksum is invalid.");
}
}
}
return firstDataNode;
} catch (NoSuchAlgorithmException ex) {
throw new IOException("Checksum algorithm not available", ex);
} finally {
gzipInputStream.close();
}
}
/**
* explicitly validates the given xdata file against the embedded checksum,
* if there is no checksum or the checksum does not correspond to the data,
* then false is returned. Otherwise true is returned.
*
* @param file
* @return
*/
public static boolean validate(File file) throws IOException {
return validate(new FileInputStream(file));
}
/**
* explicitly validates the given xdata stream against the embedded checksum,
* if there is no checksum or the checksum does not correspond to the data,
* then false is returned. Otherwise true is returned.
*
* @param in
* @return
*/
public static boolean validate(InputStream in) throws IOException {
final Map> marshallerMap = generateMarshallerMap(false, Collections.EMPTY_LIST);
marshallerMap.putAll(generateMarshallerMap(false, DEFAULT_MARSHALLERS));
final GZIPInputStream gzipInputStream = new GZIPInputStream(in);
try {
MessageDigestInputStream messageDigestInputStream = new MessageDigestInputStream(gzipInputStream, CHECKSUM_ALGORITHM);
CountingDataInputStream dIn = new CountingDataInputStream(messageDigestInputStream);
checkHeader(dIn);
deSerialize(dIn, marshallerMap, true, DUMMY_PROGRESS_LISTENER);
byte[] checksumCalcualted = messageDigestInputStream.getDigest();
final int checksumAvailable = dIn.read();
byte[] checksumValue = new byte[CHECKSUM_ALGORITHM_LENGTH];
if (checksumAvailable == -1) {
return false; //no stored checksum
}
int readBytes = dIn.read(checksumValue);
if (readBytes == CHECKSUM_ALGORITHM_LENGTH) {
return Arrays.equals(checksumValue, checksumCalcualted);
} else {
return false; //checksum length too short or too long
}
} catch (NoSuchAlgorithmException ex) {
throw new IOException("Checksum algorithm not available", ex);
} finally {
gzipInputStream.close();
}
}
private static void checkHeader(CountingDataInputStream dIn) throws IOException {
//check the header
for (int i = 0; i < XDATA_HEADER.length; ++i) {
if (dIn.readByte() != XDATA_HEADER[i]) {
throw new IOException("not a xdata file");
}
}
}
private static DataNode deSerialize(CountingDataInputStream dIn,
Map> marshallerMap,
boolean ignoreMissingMarshallers,
ProgressListener progressListener) throws IOException {
final Deque stack = new LinkedList<>();
final Map referenceableObjectMap = new HashMap<>();
//first object needs to be a DataNode
final Object firstObject = deSerializeElement(stack, dIn, referenceableObjectMap);
if (firstObject == null || !(firstObject instanceof DataNodeDeSerializerFrame)) {
throw new IOException("First data structure in a xdata file needs to be a DataNode");
}
DataNodeDeSerializerFrame firstDataNode = (DataNodeDeSerializerFrame) firstObject;
progressListener.onTotalSteps(firstDataNode.size);
stack.push(firstDataNode);
while (!stack.isEmpty()) {
final DeSerializerFrame frame = stack.peek();
if (frame.hasNext()) {
while (frame.hasNext()) {
if (frame.next(stack, dIn, referenceableObjectMap, marshallerMap, ignoreMissingMarshallers)) {
if (frame == firstDataNode) {
progressListener.onStep();
}
break; //stack changed so jump out
}
if (frame == firstDataNode) {
progressListener.onStep();
}
}
} else {
frame.unMarshal(stack, dIn, referenceableObjectMap, marshallerMap, ignoreMissingMarshallers);
stack.pop();
}
}
final Object firstDeSerializedObject = firstDataNode.getDeSerializedObject();
if (!(firstDeSerializedObject instanceof DataNode)) {
throw new IOException("first object in xdata file MUST be a DataNode but was "
+ firstDeSerializedObject.getClass().getCanonicalName());
}
return (DataNode) firstDeSerializedObject;
}
private static Object deSerializeElement(Deque stack,
CountingDataInputStream dIn,
Map referenceableObjectMap) throws IOException {
final long positionInStream = dIn.getPosition();
final int id = dIn.readByte();
int length;
switch (id) {
case VAL_NULL:
return null;
case VAL_ELEMENT:
return deSerializePrimitive(dIn);
case VAL_NODE:
length = dIn.readInt();
final DataNodeDeSerializerFrame dataNodeDeSerializerFrame =
new DataNodeDeSerializerFrame(length, positionInStream);
stack.push(dataNodeDeSerializerFrame);
return dataNodeDeSerializerFrame;
case VAL_LIST:
length = dIn.readInt();
final ListDeSerializerFrame listDeSerializerFrame = new ListDeSerializerFrame(length);
stack.push(listDeSerializerFrame);
return listDeSerializerFrame;
case VAL_REFERENCE:
return deSerializeReference(dIn, referenceableObjectMap);
default:
throw new IOException("Unknown value code " + String.format("%02x", id));
}
}
private static Object deSerializePrimitive(CountingDataInputStream dIn) throws IOException {
final byte elementType = dIn.readByte();
final Serializer