Skip to content

Commit

Permalink
Fixes XML serialization modifying the original environment (#157)
Browse files Browse the repository at this point in the history
* Fixes XML serialization modifying the original environment

---------

Signed-off-by: Frank Schnicke <frank.schnicke@iese.fraunhofer.de>
  • Loading branch information
FrankSchnicke authored Sep 1, 2023
1 parent a19fd8a commit fc4ce5f
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ public class ReflectionHelper {
/**
* List of all interfaces classes defined by the AAS.
*/
@SuppressWarnings("rawtypes")
public static final Set<Class> INTERFACES;
@SuppressWarnings("rawtypes")
public static final Set<Class> INTERFACES;
/**
* Expanded list of all mixin classes defined in the
* JSON_MIXINS_PACKAGE_NAME package together with the corresponding class
Expand All @@ -110,8 +110,8 @@ public class ReflectionHelper {
* DEFAULT_IMPLEMENTATION_PACKAGE_NAME package together with the interface
* from the MODEL_PACKAGE_NAME package they are implementing.
*/
@SuppressWarnings("rawtypes")
public static final List<ImplementationInfo> DEFAULT_IMPLEMENTATIONS;
@SuppressWarnings("rawtypes")
public static final List<ImplementationInfo> DEFAULT_IMPLEMENTATIONS;
/**
* List of interfaces from the MODEL_PACKAGE_NAME package that are known to
* not have any default implementation and therefore are excluded
Expand All @@ -121,8 +121,8 @@ public class ReflectionHelper {
/**
* List of enums from the MODEL_PACKAGE_NAME package.
*/
@SuppressWarnings("rawtypes")
public static final List<Class<Enum>> ENUMS;
@SuppressWarnings("rawtypes")
public static final List<Class<Enum>> ENUMS;

public static class ImplementationInfo<T> {

Expand Down Expand Up @@ -184,8 +184,8 @@ public static boolean hasDefaultImplementation(Class<?> interfaceType) {
* @return the default implementation type for given interfaceType or null
* if the class is no aas interface or does not have default implementation
*/
@SuppressWarnings("unchecked")
public static <T> Class<? extends T> getDefaultImplementation(Class<T> interfaceType) {
@SuppressWarnings("unchecked")
public static <T> Class<? extends T> getDefaultImplementation(Class<T> interfaceType) {
if (isDefaultImplementation(interfaceType)) {
return interfaceType;
}
Expand Down Expand Up @@ -306,7 +306,7 @@ public static Class<?> getMostSpecificTypeWithModelType(Class<?> clazz) {
XML_MIXINS = scanMixins(modelScan, XML_MIXINS_PACKAGE_NAME);
DEFAULT_IMPLEMENTATIONS = scanDefaultImplementations(modelScan);
INTERFACES = scanAasInterfaces();
ENUMS = modelScan.getAllEnums().loadClasses(Enum.class);
ENUMS = modelScan.getAllEnums().loadClasses(Enum.class);
INTERFACES_WITHOUT_DEFAULT_IMPLEMENTATION = getInterfacesWithoutDefaultImplementation(modelScan);
}

Expand All @@ -329,13 +329,13 @@ public static Set<Class<?>> getSuperTypes(Class<?> clazz, boolean recursive) {
return result;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private static List<ImplementationInfo> scanDefaultImplementations(ScanResult modelScan) {
@SuppressWarnings({ "rawtypes", "unchecked" })
private static List<ImplementationInfo> scanDefaultImplementations(ScanResult modelScan) {
ScanResult defaulImplementationScan = new ClassGraph()
.enableClassInfo()
.acceptPackagesNonRecursive(DEFAULT_IMPLEMENTATION_PACKAGE_NAME)
.scan();
List<ImplementationInfo> defaultImplementations = new ArrayList<>();
List<ImplementationInfo> defaultImplementations = new ArrayList<>();
defaulImplementationScan.getAllClasses()
.filter(x -> x.getSimpleName().startsWith(DEFAULT_IMPLEMENTATION_PREFIX))
.loadClasses()
Expand All @@ -347,7 +347,7 @@ private static List<ImplementationInfo> scanDefaultImplementations(ScanResult mo
logger.warn("could not find interface realized by default implementation class '{}'", x.getSimpleName());
} else {
Class<?> implementedClass = interfaceClassInfos.get(0).loadClass();
defaultImplementations.add(new ImplementationInfo(implementedClass, x));
defaultImplementations.add(new ImplementationInfo(implementedClass, x));
logger.debug("using default implementation class '{}' for interface '{}'",
x.getSimpleName(),
interfaceClassInfos.get(0).getName());
Expand All @@ -357,8 +357,8 @@ private static List<ImplementationInfo> scanDefaultImplementations(ScanResult mo
return defaultImplementations;
}

@SuppressWarnings("rawtypes")
private static Set<Class> scanAasInterfaces() {
@SuppressWarnings("rawtypes")
private static Set<Class> scanAasInterfaces() {
return DEFAULT_IMPLEMENTATIONS.stream().map(x -> x.interfaceType).collect(Collectors.toSet());
}

Expand Down Expand Up @@ -421,14 +421,16 @@ private ReflectionHelper() {
* Overrides empty list fields with null
* @param element to perform the empty-to-null conversion on
*/
public static void setEmptyListsToNull(Object element) {
public static List<Runnable> setEmptyListsToNull(Object element) {
List<Runnable> resetRunnables = new ArrayList<>();

Field[] fields = element.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
field.setAccessible(true);
try {
if (field.getType().isAssignableFrom(List.class) && field.get(element)!=null && ((List<?>) field.get(element)).isEmpty()) {
resetRunnables.add(createResetRunnable(element, field));
field.set(element, null);
}
} catch (IllegalAccessException e) {
Expand All @@ -437,5 +439,20 @@ public static void setEmptyListsToNull(Object element) {
field.setAccessible(false);
}

return resetRunnables;
}

private static Runnable createResetRunnable(Object element, Field field) throws IllegalAccessException {
List<?> originalValue = (List<?>) field.get(element);
Runnable resetRunnable = () -> {
field.setAccessible(true);
try {
field.set(element, originalValue);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
field.setAccessible(false);
};
return resetRunnable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ public void serialize(List<T> langStrings, JsonGenerator gen, SerializerProvider
ToXmlGenerator xgen = (ToXmlGenerator) gen;
xgen.writeStartObject();
for (T element : langStrings) {
ReflectionHelper.setEmptyListsToNull(element); // call is needed to prevent empty tags (e.g. statements.size=0 leads to
List<Runnable> resetRunnables = ReflectionHelper.setEmptyListsToNull(element); // call is needed to prevent empty tags (e.g. statements.size=0 leads to
// <statements />, which is not allowed according to the schema
xgen.writeFieldName(SubmodelElementManager.getXmlName(element.getClass()));
ser.serialize(element, xgen, serializers);

resetRunnables.stream().forEach(r -> r.run());
}
xgen.writeEndObject();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.serialization;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
Expand All @@ -39,7 +40,7 @@
public class AssetAdministrationShellEnvironmentSerializer extends JsonSerializer<Environment> {

private static final String[] SCHEMA_LOCATION = {"xsi:schemaLocation",
"https://admin-shell.io/aas/3/0 AAS.xsd" };
"https://admin-shell.io/aas/3/0 AAS.xsd" };

private static final QName AASENV_TAGNAME = new QName(AasXmlNamespaceContext.AAS_URI, "environment");
private static final QName AASLIST_TAGNAME = new QName(AasXmlNamespaceContext.AAS_URI, "assetAdministrationShells");
Expand Down Expand Up @@ -125,7 +126,11 @@ private void writeSubmodels(ToXmlGenerator xgen, List<Submodel> submodels) throw
private void writeWrappedArray(ToXmlGenerator xgen, QName wrapper, QName wrapped, List<?> list)
throws IOException {
// overwrite all empty list with null, as the schema does not allow empty XML lists
for (Object obj : list) ReflectionHelper.setEmptyListsToNull(obj);

List<Runnable> resetRunnables = new ArrayList<>();

for (Object obj : list)
resetRunnables.addAll(ReflectionHelper.setEmptyListsToNull(obj));

xgen.writeFieldName(wrapper.getLocalPart());
xgen.writeStartArray();
Expand All @@ -135,6 +140,8 @@ private void writeWrappedArray(ToXmlGenerator xgen, QName wrapper, QName wrapped
}
xgen.finishWrappedValue(wrapper, wrapped);
xgen.writeEndArray();

resetRunnables.stream().forEach(r -> r.run());
}

private void closeOpeningTag(ToXmlGenerator xgen) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ public void serialize(List<LangStringNameType> langStrings, JsonGenerator gen, S
ToXmlGenerator xgen = (ToXmlGenerator) gen;
xgen.writeStartObject();
for (LangStringNameType element : langStrings) {
ReflectionHelper.setEmptyListsToNull(element); // call is needed to prevent empty tags (e.g. statements.size=0 leads to
List<Runnable> resetRunnables = ReflectionHelper.setEmptyListsToNull(element); // call is needed to prevent empty tags (e.g. statements.size=0 leads to
// <statements />, which is not allowed according to the schema
xgen.writeFieldName(SubmodelElementManager.getXmlName(element.getClass()));
ser.serialize(element, xgen, serializers);
resetRunnables.stream().forEach(r -> r.run());
}
xgen.writeEndObject();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
import java.io.IOException;
import java.util.List;

import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.util.ReflectionHelper;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.internal.SubmodelElementManager;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;

import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.util.ReflectionHelper;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.internal.SubmodelElementManager;
import org.eclipse.digitaltwin.aas4j.v3.model.*;

public class SubmodelElementsSerializer extends JsonSerializer<List<SubmodelElement>> {

private SubmodelElementSerializer ser = new SubmodelElementSerializer();
Expand All @@ -39,9 +39,11 @@ public void serialize(List<SubmodelElement> value, JsonGenerator gen, Serializer
ToXmlGenerator xgen = (ToXmlGenerator) gen;
xgen.writeStartObject();
for (SubmodelElement element : value) {
ReflectionHelper.setEmptyListsToNull(element); // call is needed to prevent empty tags (e.g. statements.size=0 leads to <statements />, which is not allowed according to the schema
List<Runnable> resetRunnables = ReflectionHelper.setEmptyListsToNull(element); // call is needed to prevent empty tags (e.g. statements.size=0 leads to
// <statements />, which is not allowed according to the schema
xgen.writeFieldName(SubmodelElementManager.getXmlName(element.getClass()));
ser.serialize(element, xgen, serializers);
resetRunnables.stream().forEach(r -> r.run());
}
xgen.writeEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.eclipse.digitaltwin.aas4j.v3.dataformat.xml;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
Expand Down Expand Up @@ -109,6 +110,13 @@ public void serializeFull() throws SerializationException, SAXException {
validateXmlSerializer(AASFULL_FILE, AASFull.createEnvironment());
}

@Test
public void serializationDoesNotChangeEnvironment() throws SerializationException, SAXException {
Environment env = AASSimple.createEnvironment();
validateXmlSerializer(AASSIMPLE_FILE, env);
assertEquals(AASSimple.createEnvironment(), env);
}

@Test
public void validateConceptDescriptionAgainstXsdSchema() throws SerializationException, SAXException {
ConceptDescription object = AASSimple.createConceptDescriptionMaxRotationSpeed();
Expand Down

0 comments on commit fc4ce5f

Please sign in to comment.