/*
* Copyright (c) 2015 NOVA, All rights reserved.
* This library is free software, licensed under GNU Lesser General Public License version 3
*
* This file is part of NOVA.
*
* NOVA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NOVA 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOVA. If not, see <http://www.gnu.org/licenses/>.
*/
package nova.core.retention;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
import javax.json.Json;
import javax.json.JsonValue;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonParser;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.LongStream;
/**
* The data class is capable of storing named data.
* <p>
* Data types supported:<br>
* - {@link Boolean}<br>
* - {@link Byte}<br>
* - {@link Short}<br>
* - {@link Integer}<br>
* - {@link Long}<br>
* - {@link Character}<br>
* - {@link Float}<br>
* - {@link Double}<br>
* - {@link String}<br>
* <br>
* - {@link BigInteger}<br>
* - {@link BigDecimal}<br>
* - {@link Enum Enumerator}<br>
* - {@link Storable} (Converted into Data)<br>
* - {@link Data}<br>
* - {@link Collection} (Converted into Data)<br>
* - {@link Vector3D}<br>
* - {@link Vector2D}<br>
* - {@link Class}<br>
* - {@link UUID}<br>
* @author Calclavia
*/
//TODO: Add collection and array support
public class Data extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public static Class<?>[] dataTypes = {
Boolean.class,
Byte.class,
Short.class,
Integer.class,
Long.class,
Character.class,
Float.class,
Double.class,
String.class,
BigInteger.class,
BigDecimal.class,
//Special data types that all convert into Data.
Enum.class,
Storable.class,
Data.class,
Collection.class,
Vector3D.class,
Vector2D.class,
Class.class,
UUID.class
};
/**
* The pattern denoting the illegal suffix for keys.
* It is reserved for NOVA wrapper uses.
*/
public static final Pattern ILLEGAL_SUFFIX = Pattern.compile("::nova\\.\\w*$", Pattern.CASE_INSENSITIVE);
public String className;
public Data() {
}
public Data(Class<?> clazz) {
className = clazz.getName();
super.put("class", className);
}
/**
* Saves an object, serializing its data.
* This map can be reloaded and its class with be reconstructed.
*
* @param storable - The object to store.
* @return The data of the object.
*/
public static Data serialize(Storable storable) {
Data data = new Data(storable.getClass());
storable.save(data);
return data;
}
/**
* Saves an object, serializing its data.
* This map can be reloaded and its class with be reconstructed.
*
* @param value - The object to store.
* @return The data of the object.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static Data serialize(Object value) {
try {
if (value instanceof Enum) {
Data enumData = new Data(value.getClass());
enumData.put("value", ((Enum) value).name());
return enumData;
} else if (value instanceof Vector3D) {
Data vectorData = new Data(Vector3D.class);
vectorData.put("x", ((Vector3D) value).getX());
vectorData.put("y", ((Vector3D) value).getY());
vectorData.put("z", ((Vector3D) value).getZ());
return vectorData;
} else if (value instanceof Vector2D) {
Data vectorData = new Data(Vector2D.class);
vectorData.put("x", ((Vector2D) value).getX());
vectorData.put("y", ((Vector2D) value).getY());
return vectorData;
} else if (value instanceof BigInteger) {
Data bigNumData = new Data(value.getClass());
bigNumData.put("value", ((BigInteger) value).toString());
return bigNumData;
} else if (value instanceof BigDecimal) {
Data bigNumData = new Data(value.getClass());
bigNumData.put("value", ((BigDecimal) value).toString());
return bigNumData;
} else if (value instanceof UUID) {
Data uuidData = new Data(UUID.class);
uuidData.put("uuid", value.toString());
return uuidData;
} else if (value instanceof Class) {
Data classData = new Data(Class.class);
classData.put("name", ((Class) value).getName());
return classData;
} else if (value instanceof Storable) {
return serialize((Storable) value);
} else {
return (Data) value;
}
} catch (Exception e) {
throw new DataException(e);
}
}
/**
* Loads an object from its stored data, with an unknown class.
* The class of the object must be stored within the data.
*
* @param data - The data
* @param <T> - The object type
* @return The object loaded with given data.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static <T> T unserialize(Data data) {
try {
Class<T> clazz = (Class<T>) Class.forName((String) data.get("class"));
if (clazz.isEnum()) {
return (T) Enum.valueOf((Class<? extends Enum>)clazz, data.get("value"));
} else if (clazz == Vector3D.class) {
return (T) new Vector3D(data.get("x"), data.get("y"), data.get("z"));
} else if (clazz == Vector2D.class) {
return (T) new Vector2D(data.get("x"), (double) data.get("y"));
} else if (clazz == UUID.class) {
return (T) UUID.fromString(data.get("uuid"));
} else if (clazz == Collection.class) {
ArrayList<T> ret = new ArrayList<>(data.size());
LongStream.range(0, data.size() - 1).forEachOrdered(i -> ret.add(data.get(Long.toUnsignedString(i))));
return (T) ret;
} else if (clazz == Class.class) {
return (T) Class.forName(data.get("name"));
} else if (Storable.class.isAssignableFrom(clazz)) {
return (T) unserialize((Class<? extends Storable>) clazz, data);
} else {
throw new IllegalArgumentException(data.className);
}
} catch (Exception e) {
throw new DataException(e);
}
}
/**
* Loads an object from its stored data, given its class.
* @param clazz - The class to load
* @param data - The data
* @return The object loaded with given data.
*/
public static <T extends Storable> T unserialize(Class<T> clazz, Data data) {
try {
T storable = clazz.newInstance();
storable.load(data);
return storable;
} catch (Exception e) {
throw new DataException(e);
}
}
@Override
public void putAll(Map<? extends String, ?> m) {
//TODO: More efficient way to do this?
m.forEach(this::put);
}
public void putAll(Data m) {
super.putAll(m);
}
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public Object put(String key, Object value) {
assert key != null && value != null;
assert !key.equals("class");
final Object check = value;
assert Arrays.stream(dataTypes).anyMatch(clazz -> clazz.isAssignableFrom(check.getClass()));
if (value instanceof Enum) {
Data enumData = new Data(value.getClass());
enumData.put("value", ((Enum) value).name());
value = enumData;
} else if (value instanceof Vector3D) {
Data vectorData = new Data(Vector3D.class);
vectorData.put("x", ((Vector3D) value).getX());
vectorData.put("y", ((Vector3D) value).getY());
vectorData.put("z", ((Vector3D) value).getZ());
value = vectorData;
} else if (value instanceof Vector2D) {
Data vectorData = new Data(Vector2D.class);
vectorData.put("x", ((Vector2D) value).getX());
vectorData.put("y", ((Vector2D) value).getY());
value = vectorData;
} else if (value instanceof UUID) {
Data uuidData = new Data(UUID.class);
uuidData.put("uuid", value.toString());
return uuidData;
} else if (value instanceof Class) {
Data classData = new Data(Class.class);
classData.put("name", ((Class) value).getName());
return classData;
} else if (value instanceof Collection) {
Data collectionData = new Data(value.getClass());
collectionData.put("isCollection", true);
long l = 0;
for (Object obj : (Collection<?>)value)
collectionData.put(Long.toString(l++), obj);
value = collectionData;
} else if (value instanceof Storable) {
value = serialize((Storable) value);
}
return super.put(key, value);
}
/**
* A pre-cast version of get.
*
* @param key - The key
* @param <T> - The type
* @return The value
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) super.get(key);
}
public <T extends Enum<T>> T getEnum(String key) {
Data enumData = get(key);
try {
@SuppressWarnings("unchecked")
Class<T> enumClass = (Class<T>) Class.forName(enumData.className);
return Enum.valueOf(enumClass, enumData.get("value"));
} catch (Exception e) {
throw new DataException(e);
}
}
public Vector3D getVector3D(String key) {
Data data = get(key);
return new Vector3D(data.get("x"), data.get("y"), data.get("z"));
}
public Vector2D getVector2D(String key) {
Data data = get(key);
return new Vector2D(data.get("x"), (double) data.get("y"));
}
public <T extends Storable> T getStorable(String key) {
Data storableData = get(key);
try {
@SuppressWarnings("unchecked")
Class<T> storableClass = (Class<T>) Class.forName(storableData.className);
T obj = storableClass.newInstance();
obj.load(storableData);
return obj;
} catch (Exception e) {
throw new DataException(e);
}
}
@SuppressWarnings("unchecked")
public <T> List<T> getCollection(String key) {
Data data = this.get(key);
ArrayList<T> ret = new ArrayList<>(data.size());
LongStream.range(0, data.size() - 1).forEachOrdered(i -> ret.add(data.get(Long.toUnsignedString(i))));
return ret;
}
public <T> Class<T> getClass(String key) {
Data classData = get(key);
try {
@SuppressWarnings("unchecked")
Class<T> classClass = (Class<T>) Class.forName(classData.className);
return classClass;
} catch (Exception e) {
throw new DataException(e);
}
}
public UUID getUUID(String key) {
Data data = get(key);
return UUID.fromString(data.get("uuid"));
}
public void toJson(JsonGenerator writer) {
toJSON(this, writer);
}
public String toJson() {
return toJSON(this);
}
public static void toJSON(Data data, JsonGenerator writer) {
writeData(writer, Objects.requireNonNull(data), null);
}
public static String toJSON(Data data) {
StringWriter writer = new StringWriter();
try (JsonGenerator gen = Json.createGenerator(writer)) {
toJSON(data, gen);
}
return writer.toString();
}
public static Data fromJSON(JsonParser reader) {
Set<Data> rootDatas = new HashSet<>();
LinkedList<Data> dataStack = new LinkedList<>();
LinkedList<String> nameStack = new LinkedList<>();
LinkedList<Boolean> arrayStack = new LinkedList<>();
while (reader.hasNext()) {
switch (reader.next()) {
case START_ARRAY: {
arrayStack.push(true);
dataStack.push(new Data());
dataStack.peek().put("isCollection", true);
nameStack.push("0");
break;
} case START_OBJECT: {
arrayStack.push(false);
dataStack.push(new Data());
break;
} case KEY_NAME: {
nameStack.push(reader.getString());
break;
} case VALUE_STRING: {
String key = getKey(nameStack, arrayStack);
dataStack.peek().put(key, reader.getString());
break;
} case VALUE_NUMBER: {
String key = getKey(nameStack, arrayStack);
BigDecimal number = reader.getBigDecimal();
if (reader.isIntegralNumber()) {
try {
dataStack.peek().put(key, number.intValueExact());
} catch (ArithmeticException ex) {
try {
dataStack.peek().put(key, number.longValueExact());
} catch (ArithmeticException ex1) {
dataStack.peek().put(key, number.toBigInteger());
}
}
} else {
double d = number.doubleValue();
if (d == Double.POSITIVE_INFINITY || d == Double.NEGATIVE_INFINITY)
dataStack.peek().put(key, number);
else
dataStack.peek().put(key, d);
}
break;
} case VALUE_TRUE: {
String key = getKey(nameStack, arrayStack);
dataStack.peek().put(key, true);
break;
} case VALUE_FALSE: {
String key = getKey(nameStack, arrayStack);
dataStack.peek().put(key, false);
break;
} case VALUE_NULL: { // Ignore nulls
getKey(nameStack, arrayStack);
break;
} case END_OBJECT: case END_ARRAY: {
arrayStack.pop();
Data data = dataStack.pop();
if (!dataStack.isEmpty())
dataStack.peek().put(nameStack.pop(), data);
else
rootDatas.add(data);
break;
} default:
throw new AssertionError(reader.next().name());
}
}
if (!rootDatas.isEmpty()) {
if (rootDatas.size() == 1) {
return rootDatas.stream().findFirst().get();
} else {
Data root = new Data();
root.put("isCollection", true);
long l = 0;
for (Data data : rootDatas)
root.put(Long.toUnsignedString(l++), data);
return root;
}
}
return new Data();
}
public static Data fromJSON(String string) {
try (JsonParser reader = Json.createParser(new StringReader(string))) {
return fromJSON(reader);
}
}
private static String getKey(LinkedList<String> nameStack, LinkedList<Boolean> arrayStack) {
if (arrayStack.peek()) {
long name = Long.parseUnsignedLong(nameStack.pop());
nameStack.push(Long.toUnsignedString(name + 1));
return Long.toUnsignedString(name);
} else {
return nameStack.pop();
}
}
private static void writeData(JsonGenerator writer, Data data, String key) {
boolean isCollection;
if (data.containsKey("isCollection") && Boolean.TRUE.equals(data.get("isCollection"))) {
isCollection = true;
if (key == null)
writer.writeStartArray();
else
writer.writeStartArray(key);
} else {
isCollection = false;
if (key == null)
writer.writeStartObject();
else
writer.writeStartObject(key);
}
if (!isCollection)
data.forEach((k, v) -> {
if (v instanceof Data) {
writeData(writer, (Data) v, k);
} else if (v instanceof Number) {
if (v instanceof Byte) {
writer.write(k, (Byte) v);
} else if (v instanceof Short) {
writer.write(k, (Short) v);
} else if (v instanceof Integer) {
writer.write(k, (Integer) v);
} else if (v instanceof Long) {
writer.write(k, (Long) v);
} else if (v instanceof Float) {
writer.write(k, (Float) v);
} else if (v instanceof Double) {
writer.write(k, (Double) v);
} else if (v instanceof BigInteger) {
writer.write(k, (BigInteger) v);
} else if (v instanceof BigDecimal) {
writer.write(k, (BigDecimal) v);
}
} else {
if (v instanceof Boolean) {
writer.write(k, (Boolean) v);
} else if (v instanceof Character) {
writer.write(k, (Character) v);
} else if (v instanceof String) {
writer.write(k, (String) v);
} else if (v instanceof JsonValue) {
writer.write(k, (JsonValue) v);
}
}
});
else
LongStream.range(0, data.size()).mapToObj(l -> data.get(Long.toUnsignedString(l))).forEachOrdered(v -> {
if (v instanceof Data) {
writeData(writer, (Data) v, null);
} else if (v instanceof Number) {
if (v instanceof Byte) {
writer.write((Byte) v);
} else if (v instanceof Short) {
writer.write((Short) v);
} else if (v instanceof Integer) {
writer.write((Integer) v);
} else if (v instanceof Long) {
writer.write((Long) v);
} else if (v instanceof Float) {
writer.write((Float) v);
} else if (v instanceof Double) {
writer.write((Double) v);
} else if (v instanceof BigInteger) {
writer.write((BigInteger) v);
} else if (v instanceof BigDecimal) {
writer.write((BigDecimal) v);
}
} else {
if (v instanceof Boolean) {
writer.write((Boolean) v);
} else if (v instanceof Character) {
writer.write((Character) v);
} else if (v instanceof String) {
writer.write((String) v);
} else if (v instanceof JsonValue) {
writer.write((JsonValue) v);
}
}
});
writer.writeEnd();
}
}