diff --git a/dataformat-aasx/pom.xml b/dataformat-aasx/pom.xml index 83054e60..121da213 100644 --- a/dataformat-aasx/pom.xml +++ b/dataformat-aasx/pom.xml @@ -16,6 +16,10 @@ ${project.groupId} aas4j-dataformat-xml + + ${project.groupId} + aas4j-dataformat-json + ${project.groupId} aas4j-dataformat-core diff --git a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXDeserializer.java b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXDeserializer.java index 234fe9cc..dc38c525 100644 --- a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXDeserializer.java +++ b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXDeserializer.java @@ -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; @@ -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; @@ -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; } /** @@ -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); } /** @@ -134,13 +199,14 @@ public List 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); } diff --git a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXSerializer.java b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXSerializer.java index 9414df8a..6f3727e0 100644 --- a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXSerializer.java +++ b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXSerializer.java @@ -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; @@ -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"; @@ -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"; @@ -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 @@ -96,25 +113,53 @@ public AASXSerializer(XmlSerializer xmlSerializer) { public void write(Environment environment, Collection 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 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); } diff --git a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXValidator.java b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXValidator.java index c11a8544..a92ed883 100644 --- a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXValidator.java +++ b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/AASXValidator.java @@ -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; @@ -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); } @@ -44,8 +48,15 @@ public AASXValidator(InputStream is) throws SAXException, IOException, InvalidFo * @throws InvalidFormatException specified URI is invalid */ public Set validateSchema() throws IOException, InvalidFormatException { - String file = deserializer.getXMLResourceString(); - return xmlValidator.validateSchema(file); + String file = deserializer.getResourceString(); + Set 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; } } diff --git a/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/MetamodelContentType.java b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/MetamodelContentType.java new file mode 100644 index 00000000..f6b1ccb4 --- /dev/null +++ b/dataformat-aasx/src/main/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/MetamodelContentType.java @@ -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 + +} diff --git a/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/deserialization/AASXDeserializerTest.java b/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/deserialization/AASXDeserializerTest.java index 1b01f877..84ab7eb7 100644 --- a/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/deserialization/AASXDeserializerTest.java +++ b/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/deserialization/AASXDeserializerTest.java @@ -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; @@ -65,8 +66,8 @@ 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); @@ -74,8 +75,17 @@ public void roundTrip() throws SerializationException, IOException, InvalidForma 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(); diff --git a/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/serialization/AASXSerializerTest.java b/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/serialization/AASXSerializerTest.java index 2e258efd..b5f86d6c 100644 --- a/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/serialization/AASXSerializerTest.java +++ b/dataformat-aasx/src/test/java/org/eclipse/digitaltwin/aas4j/v3/dataformat/aasx/serialization/AASXSerializerTest.java @@ -18,6 +18,7 @@ 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.AASFull; import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.AASSimple; import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException; @@ -42,6 +43,7 @@ public class AASXSerializerTest { private static final String RELS_PATH_URI = "file:///_rels/.rels"; private static final String XML_PATH_URI = "file:///aasx/xml/content.xml"; + private static final String JSON_PATH_URI = "file:///aasx/json/content.json"; private static final String ORIGIN_PATH_URI = "file:///aasx/aasx-origin"; private List fileList = new ArrayList<>(); @@ -56,10 +58,14 @@ public void testBuildAASXFull() throws IOException, TransformerException, Parser // This stream keeps the output of the AASXFactory only in memory ByteArrayOutputStream out = new ByteArrayOutputStream(); - - new AASXSerializer().write(AASFull.createEnvironment(), fileList, out); - - validateAASX(out, List.of(AASXSerializerTest::assertRootXml)); + // validate AASX with XML content + new AASXSerializer().write(AASFull.createEnvironment(), fileList, out, MetamodelContentType.XML); + validateAASX(out, XML_PATH_URI, List.of(AASXSerializerTest::assertRootXml)); + + out = new ByteArrayOutputStream(); + // validate AASX with JSON content + new AASXSerializer().write(AASFull.createEnvironment(), fileList, out, MetamodelContentType.JSON); + validateAASX(out, JSON_PATH_URI, List.of(AASXSerializerTest::assertRootJson)); } @Test @@ -75,13 +81,17 @@ public void testBuildAASXSimple() throws IOException, TransformerException, Pars // This stream keeps the output of the AASXFactory only in memory ByteArrayOutputStream out = new ByteArrayOutputStream(); - + // validate AASX with XML content new AASXSerializer().write(AASSimple.createEnvironment(), fileList, out); + validateAASX(out, XML_PATH_URI, List.of(AASXSerializerTest::assertRootXml, AASXSerializerTest::assertThumbnailReference)); - validateAASX(out, List.of(AASXSerializerTest::assertRootXml, AASXSerializerTest::assertThumbnailReference)); + out = new ByteArrayOutputStream(); + // validate AASX with JSON content + new AASXSerializer().write(AASSimple.createEnvironment(), fileList, out, MetamodelContentType.JSON); + validateAASX(out, JSON_PATH_URI, List.of(AASXSerializerTest::assertRootJson, AASXSerializerTest::assertThumbnailReference)); } - private void validateAASX(ByteArrayOutputStream byteStream, List> fileValidators) { + private void validateAASX(ByteArrayOutputStream byteStream, String contentFilePath, List> fileValidators) { ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(byteStream.toByteArray())); ZipEntry zipEntry; @@ -100,7 +110,7 @@ private void validateAASX(ByteArrayOutputStream byteStream, Listaas4j-dataformat-xml ${revision} + + ${project.groupId} + aas4j-dataformat-json + ${revision} + ${project.groupId} aas4j-model