Skip to content

Commit

Permalink
GH-287: Add JSON support for serializing and deserializing AASX files (
Browse files Browse the repository at this point in the history
…#288)

* GH-287: Add JSON support for serializing and deserializing AASX files

* GH-287: rework after code review
  • Loading branch information
tobiaskraft committed Apr 17, 2024
1 parent e75f4ff commit 64e6ae4
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 41 deletions.
4 changes: 4 additions & 0 deletions dataformat-aasx/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<groupId>${project.groupId}</groupId>
<artifactId>aas4j-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aas4j-dataformat-json</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aas4j-dataformat-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.internal.AASXUtils;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.internal.visitor.AssetAdministrationShellElementWalkerVisitor;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.XmlDeserializer;
import org.eclipse.digitaltwin.aas4j.v3.model.Environment;
import org.eclipse.digitaltwin.aas4j.v3.model.File;
Expand All @@ -52,7 +53,8 @@ public class AASXDeserializer {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;


private final XmlDeserializer deserializer;
private final XmlDeserializer xmlDeserializer;
private final JsonDeserializer jsonDeserializer;

private Environment environment;
private final OPCPackage aasxRoot;
Expand All @@ -66,20 +68,40 @@ public class AASXDeserializer {
*/
public AASXDeserializer(InputStream inputStream) throws InvalidFormatException, IOException {
aasxRoot = OPCPackage.open(inputStream);
this.deserializer = new XmlDeserializer();
this.xmlDeserializer = new XmlDeserializer();
this.jsonDeserializer = new JsonDeserializer();
}

/**
* Constructor for custom XML deserialization
*
* @param deserializer a custom deserializer used for deserializing the aas environment
* Constructor for custom deserialization
*
* @param xmlDeserializer a custom XML deserializer used for deserializing the aas environment
* @param inputStream an input stream to an aasx package that can be read with this instance
* @throws InvalidFormatException if aasx package format is invalid
* @throws IOException if creating input streams for aasx fails
*/
public AASXDeserializer(XmlDeserializer xmlDeserializer,
InputStream inputStream) throws InvalidFormatException, IOException {
aasxRoot = OPCPackage.open(inputStream);
this.xmlDeserializer = xmlDeserializer;
this.jsonDeserializer = new JsonDeserializer();
}

/**
* Constructor for custom deserialization
*
* @param xmlDeserializer a custom XML deserializer used for deserializing the aas environment
* @param jsonDeserializer a custom JSON deserializer used for deserializing the aas environment
* @param inputStream an input stream to an aasx package that can be read with this instance
* @throws InvalidFormatException if aasx package format is invalid
* @throws IOException if creating input streams for aasx fails
*/
public AASXDeserializer(XmlDeserializer deserializer, InputStream inputStream) throws InvalidFormatException, IOException {
public AASXDeserializer(XmlDeserializer xmlDeserializer,
JsonDeserializer jsonDeserializer,
InputStream inputStream) throws InvalidFormatException, IOException {
aasxRoot = OPCPackage.open(inputStream);
this.deserializer = deserializer;
this.xmlDeserializer = xmlDeserializer;
this.jsonDeserializer = jsonDeserializer;
}

/**
Expand All @@ -96,18 +118,61 @@ public Environment read() throws InvalidFormatException, IOException, Deserializ
if (environment != null) {
return environment;
}
environment = deserializer.read(getXMLResourceString(aasxRoot));
if (MetamodelContentType.XML.equals(getContentType())) {
environment = xmlDeserializer.read(getResourceString(aasxRoot));
}
if (MetamodelContentType.JSON.equals(getContentType())) {
environment = jsonDeserializer.read(getResourceString(aasxRoot), Environment.class);
}
return environment;
}

/**
* Currently XML and JSON are supported for deserializing.
* @return The content type of the metafile
* @throws InvalidFormatException if aasx package format is invalid
* @throws IOException if creating input streams for aasx fails
*/
protected MetamodelContentType getContentType() throws InvalidFormatException, IOException {
MetamodelContentType contentType;
PackagePart packagePart = getPackagePart(aasxRoot);
// We also check for the none official content types "test/xml" and "text/json", which are commonly used
switch (packagePart.getContentType()) {
case "text/xml":
case "application/xml":
contentType = MetamodelContentType.XML;
break;
case "text/json":
case "application/json":
contentType = MetamodelContentType.JSON;
break;
default:
throw new RuntimeException("The following content type is not supported: " + packagePart.getContentType());
}
return contentType;
}

/**
* Return the Content of the xml file in the aasx-package as String
*
*
* @deprecated This method will be replaced by the method {@link AASXDeserializer#getResourceString()}.
*
* @throws InvalidFormatException if aasx package format is invalid
* @throws IOException if creating input streams for aasx fails
*/
@Deprecated
public String getXMLResourceString() throws InvalidFormatException, IOException {
return getXMLResourceString(this.aasxRoot);
return getResourceString(this.aasxRoot);
}

/**
* Return the Content of the xml or json file in the aasx-package as String
*
* @throws InvalidFormatException if aasx package format is invalid
* @throws IOException if creating input streams for aasx fails
*/
public String getResourceString() throws InvalidFormatException, IOException {
return getResourceString(this.aasxRoot);
}

/**
Expand All @@ -134,13 +199,14 @@ public List<InMemoryFile> getRelatedFiles() throws InvalidFormatException, IOExc
return files;
}

private String getXMLResourceString(OPCPackage aasxPackage) throws InvalidFormatException, IOException {
private PackagePart getPackagePart(OPCPackage aasxPackage) throws InvalidFormatException, IOException {
PackagePart originPart = getOriginPart(aasxPackage);

PackageRelationshipCollection originRelationships = getXMLDocumentRelation(originPart);
return originPart.getRelatedPart(originRelationships.getRelationship(0));
}

PackagePart xmlPart = originPart.getRelatedPart(originRelationships.getRelationship(0));

private String getResourceString(OPCPackage aasxPackage) throws InvalidFormatException, IOException {
PackagePart xmlPart = getPackagePart(aasxPackage);
return readContentFromPackagePart(xmlPart);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.internal.AASXUtils;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.internal.visitor.AssetAdministrationShellElementWalkerVisitor;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.XmlSerializer;
import org.eclipse.digitaltwin.aas4j.v3.model.Environment;
import org.eclipse.digitaltwin.aas4j.v3.model.File;
Expand All @@ -49,6 +50,7 @@ public class AASXSerializer {

private static final String MIME_PLAINTXT = "text/plain";
private static final String MIME_XML = "application/xml";
private static final String MIME_JSON = "application/json";

public static final String OPC_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
public static final String AASX_NAMESPACE = "http://admin-shell.io/aasx/relationships";
Expand All @@ -59,6 +61,7 @@ public class AASXSerializer {

public static final String AASSPEC_RELTYPE = AASX_NAMESPACE + "/aas-spec";
public static final String XML_PATH = "/aasx/xml/content.xml";
public static final String JSON_PATH = "/aasx/json/content.json";

public static final String AASSUPPL_RELTYPE = AASX_NAMESPACE + "/aas-suppl";

Expand All @@ -67,25 +70,39 @@ public class AASXSerializer {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

private final XmlSerializer xmlSerializer;
private final JsonSerializer jsonSerializer;

/**
* Default constructor
*/
public AASXSerializer() {
this.xmlSerializer = new XmlSerializer();
this.jsonSerializer = new JsonSerializer();
}

/**
* Constructor with a custom serializer for serializing the aas environment
* Constructor with a custom XML serializer for serializing the aas environment
*
* @param xmlSerializer a custom serializer used for serializing the aas environment
* @param xmlSerializer a custom serializer used for serializing the aas environment in XML
*/
public AASXSerializer(XmlSerializer xmlSerializer) {
this.xmlSerializer = xmlSerializer;
this.jsonSerializer = new JsonSerializer();
}

/**
* Generates the .aasx file and writes it to the given OutputStream
* Constructor with custom serializers for serializing the aas environment
*
* @param xmlSerializer a custom serializer used for serializing the aas environment in XML
* @param jsonSerializer a custom serializer used for serializing the aas environment in JSON
*/
public AASXSerializer(XmlSerializer xmlSerializer, JsonSerializer jsonSerializer) {
this.xmlSerializer = xmlSerializer;
this.jsonSerializer = jsonSerializer;
}

/**
* Generates the .aasx file and writes it to the given OutputStream, by using XML as the default content type.
*
* @param environment the aas environment that will be included in the aasx package as an xml serialization
* @param files related inMemory files that belong to the given aas environment
Expand All @@ -96,25 +113,53 @@ public AASXSerializer(XmlSerializer xmlSerializer) {
public void write(Environment environment, Collection<InMemoryFile> files, OutputStream os)
throws SerializationException, IOException {

write(environment, files, os, MetamodelContentType.XML);
}

/**
* Generates the .aasx file and writes it to the given OutputStream
*
* @param environment the aas environment that will be included in the aasx package as an xml serialization
* @param files related inMemory files that belong to the given aas environment
* @param os an output stream for writing the aasx package
* @param contentType the content type for the metamodel serialization
* @throws SerializationException if serializing the given elements fails
* @throws IOException if creating output streams for aasx fails
*/
public void write(Environment environment, Collection<InMemoryFile> files, OutputStream os, MetamodelContentType contentType)
throws SerializationException, IOException {

OPCPackage rootPackage = OPCPackage.create(os);

// Create the empty aasx-origin file
PackagePart origin = createAASXPart(rootPackage, rootPackage, ORIGIN_PATH, MIME_PLAINTXT, ORIGIN_RELTYPE,
ORIGIN_CONTENT.getBytes());

// Convert the given Metamodels to XML
String xml = xmlSerializer.write(environment);

// Save the XML to aasx/xml/content.xml
PackagePart xmlPart = createAASXPart(rootPackage, origin, XML_PATH, MIME_XML, AASSPEC_RELTYPE, xml.getBytes(DEFAULT_CHARSET));
PackagePart packagePart;
switch (contentType) {
case JSON:
// Convert the given Metamodels to JSON
String json = jsonSerializer.write(environment);
// Save the JSON to aasx/json/content.json
packagePart = createAASXPart(rootPackage, origin, JSON_PATH, MIME_JSON, AASSPEC_RELTYPE, json.getBytes(DEFAULT_CHARSET));
break;
case XML:
// Convert the given Metamodels to XML
String xml = xmlSerializer.write(environment);
// Save the XML to aasx/xml/content.xml
packagePart = createAASXPart(rootPackage, origin, XML_PATH, MIME_XML, AASSPEC_RELTYPE, xml.getBytes(DEFAULT_CHARSET));
break;
default:
throw new IllegalArgumentException("Unsupported content type: " + contentType);
}

environment.getAssetAdministrationShells().stream().filter(aas -> aas.getAssetInformation() != null
&& aas.getAssetInformation().getDefaultThumbnail() != null
&& aas.getAssetInformation().getDefaultThumbnail().getPath() != null)
.forEach(aas -> createParts(files,
AASXUtils.removeFilePartOfURI(aas.getAssetInformation().getDefaultThumbnail().getPath()),
rootPackage, rootPackage, aas.getAssetInformation().getDefaultThumbnail().getContentType(), AAS_THUMBNAIL_RELTYPE));
storeFilesInAASX(environment, files, rootPackage, xmlPart);
&& aas.getAssetInformation().getDefaultThumbnail() != null
&& aas.getAssetInformation().getDefaultThumbnail().getPath() != null)
.forEach(aas -> createParts(files,
AASXUtils.removeFilePartOfURI(aas.getAssetInformation().getDefaultThumbnail().getPath()),
rootPackage, rootPackage, aas.getAssetInformation().getDefaultThumbnail().getContentType(), AAS_THUMBNAIL_RELTYPE));
storeFilesInAASX(environment, files, rootPackage, packagePart);

saveAASX(os, rootPackage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSchemaValidator;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.xml.XmlSchemaValidator;
import org.xml.sax.SAXException;

Expand All @@ -29,10 +31,12 @@
public class AASXValidator {

private XmlSchemaValidator xmlValidator;
private JsonSchemaValidator jsonValidator;
private AASXDeserializer deserializer;

public AASXValidator(InputStream is) throws SAXException, IOException, InvalidFormatException {
this.xmlValidator = new XmlSchemaValidator();
this.jsonValidator = new JsonSchemaValidator();
this.deserializer = new AASXDeserializer(is);
}

Expand All @@ -44,8 +48,15 @@ public AASXValidator(InputStream is) throws SAXException, IOException, InvalidFo
* @throws InvalidFormatException specified URI is invalid
*/
public Set<String> validateSchema() throws IOException, InvalidFormatException {
String file = deserializer.getXMLResourceString();
return xmlValidator.validateSchema(file);
String file = deserializer.getResourceString();
Set<String> errorMessages = null;
if (MetamodelContentType.XML.equals(deserializer.getContentType())) {
errorMessages = xmlValidator.validateSchema(file);
}
else if (MetamodelContentType.JSON.equals(deserializer.getContentType())) {
errorMessages = jsonValidator.validateSchema(file);
}
return errorMessages;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2024 the original author or authors.
*
* 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.eclipse.digitaltwin.aas4j.v3.dataformat.aasx;

/**
* Supported ContentType's for serializing and deserializing Metamodels.
*/
public enum MetamodelContentType {

JSON,
XML

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.AASXDeserializer;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.AASXSerializer;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.InMemoryFile;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.MetamodelContentType;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.AASSimple;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException;
Expand Down Expand Up @@ -65,17 +66,26 @@ public void roundTrip() throws SerializationException, IOException, InvalidForma
fileList.add(inMemoryFile);
fileList.add(inMemoryFileThumbnail);

java.io.File file = tempFolder.newFile("output.aasx");

// check round trip with XML content
java.io.File file = tempFolder.newFile("output-xml.aasx");
new AASXSerializer().write(AASSimple.createEnvironment(), fileList, new FileOutputStream(file));

InputStream in = new FileInputStream(file);
AASXDeserializer deserializer = new AASXDeserializer(in);

assertEquals(AASSimple.createEnvironment(), deserializer.read());
assertTrue(CollectionUtils.isEqualCollection(fileList, deserializer.getRelatedFiles()));
}

// check round trip with JSON content
file = tempFolder.newFile("output-json.aasx");
new AASXSerializer().write(AASSimple.createEnvironment(), fileList, new FileOutputStream(file), MetamodelContentType.JSON);

in = new FileInputStream(file);
deserializer = new AASXDeserializer(in);

assertEquals(AASSimple.createEnvironment(), deserializer.read());
assertTrue(CollectionUtils.isEqualCollection(fileList, deserializer.getRelatedFiles()));
}
@Test
public void relatedFilesAreOnlyResolvedIfWithinAASX() throws IOException, SerializationException, InvalidFormatException, DeserializationException {
Submodel fileSm = new DefaultSubmodel.Builder().id("doesNotMatter").submodelElements(createFileSubmodelElements()).build();
Expand Down

0 comments on commit 64e6ae4

Please sign in to comment.