org.conqat.lib.commons.serialization.utils.SerializationMigrationUtils Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.lib.commons.serialization.utils;
import static com.google.common.collect.Iterables.getOnlyElement;
import static java.util.stream.Collectors.toCollection;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.collections.UnmodifiableList;
import org.conqat.lib.commons.io.SerializationUtils;
import org.conqat.lib.commons.serialization.SerializedEntityBase;
import org.conqat.lib.commons.serialization.SerializedEntityParser;
import org.conqat.lib.commons.serialization.SerializedEntityPool;
import org.conqat.lib.commons.serialization.SerializedEntitySerializer;
import org.conqat.lib.commons.serialization.classes.SerializedClass;
import org.conqat.lib.commons.serialization.classes.SerializedComplexFieldBase;
import org.conqat.lib.commons.serialization.classes.SerializedFieldBase;
import org.conqat.lib.commons.serialization.objects.SerializedArrayObject;
import org.conqat.lib.commons.serialization.objects.SerializedClassValues;
import org.conqat.lib.commons.serialization.objects.SerializedEnumLiteral;
import org.conqat.lib.commons.serialization.objects.SerializedObject;
import org.conqat.lib.commons.serialization.objects.SerializedObjectBase;
import org.conqat.lib.commons.serialization.objects.SerializedStringObject;
import com.google.common.base.Function;
/**
* Utility methods for migrating serialized objects.
*/
public class SerializationMigrationUtils {
/** Transforms all strings in a (de)serialized string set. */
public static void transformStringsInHashSet(SerializedObject setObject, Function transformation,
SerializedEntityPool pool) throws IOException {
SerializedClass setClass = findClassInHierarchy(setObject, HashSet.class);
SerializedClassValues fieldSets = setObject.getFieldSet(setObject.getPlainClassHierarchy().indexOf(setClass));
for (Object data : fieldSets.getPreFieldData()) {
if (!(data instanceof Integer)) {
continue;
}
SerializedEntityBase containedElement = pool.getEntity((Integer) data, SerializedEntityBase.class);
if (containedElement instanceof SerializedStringObject) {
SerializedStringObject stringContent = (SerializedStringObject) containedElement;
stringContent.setValue(transformation.apply(stringContent.getValue()));
}
}
}
/** Returns all objects in a (de)serialized {@link ArrayList}. */
@SuppressWarnings("unchecked")
public static List getObjectsFromArrayList(SerializedObject listObject,
SerializedEntityPool pool) throws IOException {
SerializedClass listClass = findClassInHierarchy(listObject, ArrayList.class);
SerializedClassValues fieldSets = listObject
.getFieldSet(listObject.getPlainClassHierarchy().indexOf(listClass));
List objects = new ArrayList<>();
for (Object data : fieldSets.getPostFieldData()) {
if (!(data instanceof Integer)) {
continue;
}
SerializedEntityBase containedElement = pool.getEntity((Integer) data, SerializedEntityBase.class);
objects.add((T) containedElement);
}
return objects;
}
/**
* Tries to find a given class in the {@link SerializedObject}'s classHierarchy. Fails with an
* assertion error if the given class is not in the class hierarchy of the given serialized object.
*
* @param setObject
* The object that defines the class hierarchy.
* @param classToFind
* A class in the class hierarchy of the runtime type of #setObject.
*/
public static SerializedClass findClassInHierarchy(SerializedObject setObject, Class classToFind)
throws IOException {
for (SerializedClass classObject : setObject.getPlainClassHierarchy()) {
if (classToFind.getName().equals(classObject.getName())) {
return classObject;
}
}
CCSMAssert.fail("Only works on " + classToFind.getName() + " objects!");
return null;
}
/**
* Renames the given entity class, reads it from the pool and returns it. The given class must be a
* root entity in the pool.
*/
@SuppressWarnings("unchecked")
public static T serializeRootEntities(SerializedEntityPool entityPool)
throws AssertionError, IOException, ClassNotFoundException {
List rootEntities = entityPool.getRootEntities();
byte[] newValue = SerializedEntitySerializer.serializeToBytes(rootEntities);
return (T) SerializationUtils.deserializeFromByteArray(newValue);
}
/**
* Creates a SerializedObject for an ArrayList with the given objects as entries.
*
* {@link #copy(SerializedEntityPool, SerializedEntityBase, SerializedEntityPool)} provides a more
* generic way to accomplish this, if in a slightly more indirect way.
*/
public static SerializedObject createSerializedArrayListObject(List entries,
SerializedEntityPool entityPool) throws IOException {
ArrayList arrayList = entries.stream().map(WrappedHandle::of)
.collect(toCollection(ArrayList::new));
byte[] bytes = SerializationUtils.serializeToByteArray(arrayList);
SerializedEntityPool temporaryPool = SerializedEntityParser.parse(bytes);
return copy(temporaryPool, getOnlyElement(temporaryPool.getRootEntities(SerializedObject.class)), entityPool);
}
/**
* Reconstructs a {@link SerializedEntityBase} as a {@link Serializable} by serializing it to a
* sequence of bytes and then deserializing that using Java's serialization framework.
*/
public static Serializable reconstructSerializable(SerializedEntityBase serializedEntity)
throws IOException, ClassNotFoundException {
byte[] bytes = SerializedEntitySerializer.serializeToBytes(List.of(serializedEntity));
return SerializationUtils.deserializeFromByteArray(bytes);
}
/**
* When {@link #copy(SerializedEntityPool, SerializedEntityBase, SerializedEntityPool)} recursively
* copies a {@link SerializedObject} from a source to target {@link SerializedEntityPool}, the
* copies are assigned new handles in the target pool. This is the correct behavior. However,
* occasionally one wants (part of) the copy to refer to an object already in the target
* pool. In this case, a {@link WrappedHandle} offers a way out. When a {@link WrappedHandle} is
* seen as a {@link SerializedObject} to be copied, instead of copying it like any other object the
* target pool, the wrapped handle is simply replaced by the handle of an object (that must already
* exist in the target pool!).
*
* The primary use case is wrapping an object in some source pool into an object that is cumbersome
* to create as a {@link SerializedObject} but easy to create in plain Java:
*
*
* SerializedEntityPool originalPool = ...;
* SerializedObject originalObject = ...;
*
* Set wrapper = Collections.singleton(WrappedHandle.of(originalObject));
* byte[] wrapperSerialization = SerializationUtils.serializeToByteArray(wrapper);
* SerializedEntityPool temporaryPool = SerializedEntityParser.parse(wrapperSerialization);
* SerializedObject serializedWrapper = temporaryPool.getRootEntities().get(0);
*
* copy(temporaryPool, serializedWrapper, originalPool);
*
*
* This adds all necessary entities like the singleton set and its classes to the original pool
* without first deserializing and then re-serializing the original object.
*/
public static class WrappedHandle implements Serializable {
private static final long serialVersionUID = 1;
@SuppressWarnings("unused") // Read via getFieldValue in unwrap
private final int handle;
private WrappedHandle(int handle) {
this.handle = handle;
}
/** Wraps the handle of the given {@linkplain SerializedObjectBase serialized object}. */
public static WrappedHandle of(SerializedObjectBase wrappedObject) {
return new WrappedHandle(wrappedObject.getHandle());
}
/**
* Unwraps a handle, assuming the given {@link SerializedObject} is a serialized instance of
* {@link WrappedHandle} in the given {@link SerializedEntityPool}.
*/
private static SerializedObjectBase unwrap(SerializedObject serializedWrapper, SerializedEntityPool entityPool)
throws IOException {
int wrappedHandle = (int) serializedWrapper.getFieldValue("handle");
return entityPool.getEntity(wrappedHandle, SerializedObjectBase.class);
}
/**
* Determines whether the given {@linkplain SerializedEntityBase serialized entity} is a serialized
* instance of {@link WrappedHandle}.
*/
public static boolean isWrappedHandle(SerializedEntityBase serializedEntity) throws IOException {
if (!(serializedEntity instanceof SerializedObject)) {
return false;
}
SerializedObject serializedObject = (SerializedObject) serializedEntity;
SerializedClass serializedClass = serializedObject.getSerializedClass();
return serializedClass.getName().equals(WrappedHandle.class.getName());
}
}
/**
* Copies an entity from one {@link SerializedEntityPool} to another. This method has some
* limitations:
*
* - It does not support all kinds of {@link SerializedEntityBase} yet.
* - It makes some assumptions about how custom serialization logic (i.e., a {@code writeObject}
* method) operates. In particular, it assumes that any {@code int}s in
* {@linkplain SerializedClassValues#getPreFieldData() pre-} or
* {@linkplain SerializedClassValues#getPostFieldData() post-field data} encode an object handle.
* This is true for common collection classes like {@link java.util.HashMap}.
*
* If you are using this method for a data structure, it is hence essential to
* check with a unit test that copying the exact data structure in question works correctly.
*
* @throws UnsupportedOperationException
* If copying (parts of) the entity is not yet supported or the entity violates some
* assumptions we (have to) make.
*/
public static SerializedEntityBase copy(SerializedEntityPool sourcePool, SerializedEntityBase sourceEntity,
SerializedEntityPool targetPool) throws IOException {
if (WrappedHandle.isWrappedHandle(sourceEntity)) {
return WrappedHandle.unwrap((SerializedObject) sourceEntity, targetPool);
} else if (sourceEntity instanceof SerializedObject) {
return copy(sourcePool, (SerializedObject) sourceEntity, targetPool);
} else if (sourceEntity instanceof SerializedStringObject) {
SerializedStringObject sourceString = (SerializedStringObject) sourceEntity;
String sourceStringValue = sourceString.getValue();
return new SerializedStringObject(sourceStringValue, targetPool);
} else if (sourceEntity instanceof SerializedEnumLiteral) {
return copy(sourcePool, (SerializedEnumLiteral) sourceEntity, targetPool);
} else if (sourceEntity instanceof SerializedArrayObject) {
return copy(sourcePool, (SerializedArrayObject) sourceEntity, targetPool);
} else if (sourceEntity instanceof SerializedClass) {
return copy(sourcePool, (SerializedClass) sourceEntity, targetPool);
} else {
throw new UnsupportedOperationException("Unsupported entity kind: " + sourceEntity.getClass());
}
}
private static SerializedObject copy(SerializedEntityPool sourcePool, SerializedObject sourceObject,
SerializedEntityPool targetPool) throws IOException {
SerializedClass sourceClass = sourceObject.getSerializedClass();
List sourceClassHierarchy = sourceObject.getPlainClassHierarchy();
if (sourceClassHierarchy.size() != sourceObject.getFieldSetCount()) {
throw new UnsupportedOperationException(
"We assume that every non-Object superclass contributes a field set");
}
List targetFieldSets = new ArrayList<>(sourceObject.getFieldSetCount());
for (int i = 0; i < sourceObject.getFieldSetCount(); i++) {
targetFieldSets.add(
copyFieldSet(sourcePool, sourceClassHierarchy.get(i), sourceObject.getFieldSet(i), targetPool));
}
SerializedEntityBase targetClass = copy(sourcePool, sourceClass, targetPool);
int targetClassHandle = targetClass.getHandle();
return new SerializedObject(targetClassHandle, targetFieldSets, targetPool);
}
private static SerializedEnumLiteral copy(SerializedEntityPool sourcePool, SerializedEnumLiteral sourceEnumLiteral,
SerializedEntityPool targetPool) throws IOException {
SerializedClass sourceEnumClass = sourceEnumLiteral.getSerializedClass();
SerializedClass targetEnumClass = copy(sourcePool, sourceEnumClass, targetPool);
return new SerializedEnumLiteral(targetPool, targetEnumClass.getHandle(), sourceEnumLiteral.getLiteralName());
}
private static SerializedArrayObject copy(SerializedEntityPool sourcePool, SerializedArrayObject sourceArrayObject,
SerializedEntityPool targetPool) throws IOException {
SerializedFieldBase elementType = sourceArrayObject.getElementType();
SerializedClass sourceArrayClass = sourceArrayObject.getSerializedClass();
SerializedClass targetClass = copy(sourcePool, sourceArrayClass, targetPool);
SerializedArrayObject targetArrayObject = new SerializedArrayObject(targetPool, targetClass.getHandle(),
elementType);
for (int i = 0; i < sourceArrayObject.size(); i++) {
if (elementType instanceof SerializedComplexFieldBase) {
int sourceElementHandle = (int) sourceArrayObject.getValue(i);
int targetElementHandle = copyHandle(sourcePool, sourceElementHandle, targetPool);
targetArrayObject.addValue(targetElementHandle);
} else {
Object sourceElementValue = sourceArrayObject.getValue(i);
targetArrayObject.addValue(sourceElementValue);
}
}
return targetArrayObject;
}
private static SerializedClass copy(SerializedEntityPool sourcePool, SerializedClass sourceClass,
SerializedEntityPool targetPool) throws IOException {
SerializedClass targetClass = targetPool.findClass(sourceClass.getName());
if (targetClass != null) {
return targetClass;
}
targetClass = new SerializedClass(sourceClass.getName(), sourceClass.getSerialVersionUid(),
sourceClass.getClassDescriptionFlags(), targetPool);
SerializedClass sourceSuperClass = sourcePool.getEntity(sourceClass.getSuperClassHandle(),
SerializedClass.class);
if (sourceSuperClass != null) {
SerializedEntityBase targetSuperClass = copy(sourcePool, sourceSuperClass, targetPool);
targetClass.setSuperClassHandle(targetSuperClass.getHandle());
}
for (SerializedFieldBase sourceClassField : sourceClass.getFields()) {
targetClass.addField(sourceClassField);
}
return targetClass;
}
private static SerializedClassValues copyFieldSet(SerializedEntityPool sourcePool, SerializedClass sourceClass,
SerializedClassValues sourceFieldSet, SerializedEntityPool targetPool) throws IOException {
UnmodifiableList sourceClassFields = sourceClass.getFields();
if (sourceClassFields.size() != sourceFieldSet.getSize()) {
throw new UnsupportedOperationException("We assume that every field is represented in the field set");
}
SerializedClassValues targetFieldSet = new SerializedClassValues(sourceFieldSet.getSize());
appendData(sourcePool, sourceFieldSet, true, targetPool, targetFieldSet);
for (int i = 0; i < sourceFieldSet.getSize(); i++) {
SerializedFieldBase sourceClassField = sourceClassFields.get(i);
if (sourceClassField instanceof SerializedComplexFieldBase) {
int sourceFieldValueHandle = (int) sourceFieldSet.getValue(i);
int targetFieldValueHandle = copyHandle(sourcePool, sourceFieldValueHandle, targetPool);
targetFieldSet.setValue(i, targetFieldValueHandle);
} else {
Object sourceFieldValue = sourceFieldSet.getValue(i);
targetFieldSet.setValue(i, sourceFieldValue);
}
}
appendData(sourcePool, sourceFieldSet, false, targetPool, targetFieldSet);
return targetFieldSet;
}
private static void appendData(SerializedEntityPool sourcePool, SerializedClassValues sourceClassValues,
boolean isPreFields, SerializedEntityPool targetPool, SerializedClassValues targetClassValues)
throws IOException, UnsupportedOperationException {
List