Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes XML serialization modifying the original environment #157

Merged
merged 2 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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