From 6c2b04e4ee0e2413a38922828ec55093354aa7ec Mon Sep 17 00:00:00 2001 From: fvolz Date: Tue, 27 Feb 2024 15:22:57 +0100 Subject: [PATCH] [Bugfix] Files in SubmodelElementLists and NullPointerException when serializing/deserializing empty files (#250) * visit all elements (also list) and add null check * skip empty files when serializing AASX * add tests for empty files and files in submodelElementList * fix null test * change serializer test to AASFull provokes java.lang.NullPointerException: Cannot invoke "String.startsWith(String)" because "path" is null * fix serializer test, add thumbnail to AASFull * fix missing import * revert AASFull change, change AASXSerializerTest * revert unused import * remove unused imports --- .../v3/dataformat/aasx/AASXDeserializer.java | 31 ++++---------- .../v3/dataformat/aasx/AASXSerializer.java | 40 ++++++++----------- .../deserialization/AASXDeserializerTest.java | 32 +++++++++++++++ .../serialization/AASXSerializerTest.java | 27 +++++++++---- 4 files changed, 76 insertions(+), 54 deletions(-) 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 f6ab72a1..234fe9cc 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 @@ -21,7 +21,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -33,11 +32,10 @@ import org.apache.poi.openxml4j.opc.PackagingURIHelper; 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.xml.XmlDeserializer; import org.eclipse.digitaltwin.aas4j.v3.model.Environment; import org.eclipse.digitaltwin.aas4j.v3.model.File; -import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; -import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -207,27 +205,14 @@ private List parseReferencedFilePathsFromAASX() throws IOException, Inva && aas.getAssetInformation().getDefaultThumbnail() != null && aas.getAssetInformation().getDefaultThumbnail().getPath() != null) .forEach(aas -> paths.add(aas.getAssetInformation().getDefaultThumbnail().getPath())); - environment.getSubmodels().forEach(sm -> paths.addAll(parseElements(sm.getSubmodelElements()))); - return paths; - } - - /** - * Gets the file paths from a collection of ISubmodelElement - * - * @param elements the submodel elements to process - * @return the Paths from the File elements - */ - private List parseElements(Collection elements) { - List paths = new ArrayList<>(); - for (SubmodelElement element : elements) { - if (element instanceof File) { - File file = (File) element; - paths.add(file.getValue()); - } else if (element instanceof SubmodelElementCollection) { - SubmodelElementCollection collection = (SubmodelElementCollection) element; - paths.addAll(parseElements(collection.getValue())); + new AssetAdministrationShellElementWalkerVisitor() { + @Override + public void visit(File file) { + if(file != null && file.getValue() != null) { + paths.add(file.getValue()); + } } - } + }.visit(environment); return paths; } 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 8eb4a909..4d2d9e40 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 @@ -33,12 +33,10 @@ import org.apache.poi.openxml4j.opc.internal.MemoryPackagePart; 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.xml.XmlSerializer; import org.eclipse.digitaltwin.aas4j.v3.model.Environment; import org.eclipse.digitaltwin.aas4j.v3.model.File; -import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; -import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; -import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,7 +92,7 @@ public AASXSerializer(XmlSerializer xmlSerializer) { */ public void write(Environment environment, Collection files, OutputStream os) throws SerializationException, IOException { - prepareFilePaths(environment.getSubmodels()); + prepareFilePaths(environment); OPCPackage rootPackage = OPCPackage.create(os); @@ -129,9 +127,8 @@ private void storeFilesInAASX(Environment environment, Collection .forEach(aas -> createParts(files, AASXUtils.removeFilePartOfURI(aas.getAssetInformation().getDefaultThumbnail().getPath()), rootPackage, xmlPart, aas.getAssetInformation().getDefaultThumbnail().getContentType())); - environment.getSubmodels().forEach(sm -> - findFileElements(sm.getSubmodelElements()).forEach(file -> createParts(files, - AASXUtils.removeFilePartOfURI(file.getValue()), rootPackage, xmlPart, file.getContentType()))); + findFileElements(environment).forEach(file -> createParts(files, + AASXUtils.removeFilePartOfURI(file.getValue()), rootPackage, xmlPart, file.getContentType())); } /** @@ -226,35 +223,32 @@ private void writeDataToPart(PackagePart part, byte[] content) { } /** - * Gets the File elements from a collection of elements Also recursively + * Gets the File elements from an environment * searches in SubmodelElementCollections * - * @param elements the Elements to be searched for File elements + * @param environment the Environment * @return the found Files */ - private Collection findFileElements(Collection elements) { + private Collection findFileElements(Environment environment) { Collection files = new ArrayList<>(); - - for (SubmodelElement element : elements) { - if (element instanceof File) { - files.add((File) element); - } else if (element instanceof SubmodelElementCollection) { - // Recursive call to deal with SubmodelElementCollections - files.addAll(findFileElements(((SubmodelElementCollection) element).getValue())); + new AssetAdministrationShellElementWalkerVisitor() { + @Override + public void visit(File file) { + if(file != null && file.getValue() != null) { + files.add(file); + } } - } - + }.visit(environment); return files; } /** * Replaces the path in all File Elements with the result of preparePath * - * @param submodels the Submodels + * @param environment the Environment */ - private void prepareFilePaths(Collection submodels) { - submodels.stream() - .forEach(sm -> findFileElements(sm.getSubmodelElements()).stream().forEach(f -> f.setValue(preparePath(f.getValue())))); + private void prepareFilePaths(Environment environment) { + findFileElements(environment).forEach(f -> f.setValue(preparePath(f.getValue()))); } /** 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 4cd02eeb..1b01f877 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 @@ -44,6 +44,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultEnvironment; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultFile; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -92,6 +93,37 @@ public void relatedFilesAreOnlyResolvedIfWithinAASX() throws IOException, Serial assertEquals(Collections.singletonList(inMemoryFile), deserializer.getRelatedFiles()); } + @Test + public void emptyFiles() throws IOException, SerializationException, InvalidFormatException, DeserializationException { + File emptyFile = new DefaultFile.Builder().idShort("emptyFile").contentType(null).value(null).build(); + Submodel fileSm = new DefaultSubmodel.Builder().id("doesNotMatter").submodelElements(emptyFile).build(); + Environment env = new DefaultEnvironment.Builder().submodels(fileSm).build(); + + java.io.File file = tempFolder.newFile("output.aasx"); + new AASXSerializer().write(env, null, new FileOutputStream(file)); + + InputStream in = new FileInputStream(file); + AASXDeserializer deserializer = new AASXDeserializer(in); + assertTrue(deserializer.getRelatedFiles().isEmpty()); + } + + @Test + public void filesInElementList() throws IOException, SerializationException, InvalidFormatException, DeserializationException { + DefaultSubmodelElementList elementList = new DefaultSubmodelElementList.Builder().value(createFileSubmodelElements()).build(); + Submodel fileSm = new DefaultSubmodel.Builder().id("doesNotMatter").submodelElements(elementList).build(); + Environment env = new DefaultEnvironment.Builder().submodels(fileSm).build(); + + byte[] image = { 0, 1, 2, 3, 4 }; + InMemoryFile inMemoryFile = new InMemoryFile(image, "file:///aasx/internalFile.jpg"); + + java.io.File file = tempFolder.newFile("output.aasx"); + new AASXSerializer().write(env, Collections.singleton(inMemoryFile), new FileOutputStream(file)); + + InputStream in = new FileInputStream(file); + AASXDeserializer deserializer = new AASXDeserializer(in); + assertEquals(Collections.singletonList(inMemoryFile), deserializer.getRelatedFiles()); + } + private static List createFileSubmodelElements() { File internalFile = new DefaultFile.Builder().idShort("internalFile").contentType("image/jpeg").value("file:///aasx/internalFile.jpg").build(); File externalFile = new DefaultFile.Builder().idShort("externalFile").contentType("image/jpeg").value("http://doesNotMatter.com/image").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 aa23825e..ebbe0c64 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,9 +18,9 @@ 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.core.AASFull; import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.AASSimple; import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException; -import org.junit.Before; import org.junit.Test; import javax.xml.parsers.ParserConfigurationException; @@ -43,19 +43,30 @@ public class AASXSerializerTest { private List fileList = new ArrayList<>(); - @Before - public void setup() throws IOException { + @Test + public void testBuildAASXFull() throws IOException, TransformerException, ParserConfigurationException, SerializationException { byte[] operationManualContent = { 0, 1, 2, 3, 4 }; + InMemoryFile file = new InMemoryFile(operationManualContent, "file:///TestFile.pdf"); + fileList.add(file); + // This stream can be used to write the .aasx directly to a file + // FileOutputStream out = new FileOutputStream("path/to/test.aasx"); + + // This stream keeps the output of the AASXFactory only in memory + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + new AASXSerializer().write(AASFull.createEnvironment(), fileList, out); + + validateAASX(out); + } + + @Test + public void testBuildAASXSimple() throws IOException, TransformerException, ParserConfigurationException, SerializationException { byte[] thumbnail = { 0, 1, 2, 3, 4 }; + byte[] operationManualContent = { 0, 1, 2, 3, 4 }; InMemoryFile file = new InMemoryFile(operationManualContent, "file:///aasx/OperatingManual.pdf"); InMemoryFile inMemoryFileThumbnail = new InMemoryFile(thumbnail, "file:///master/verwaltungsschale-detail-part1.png"); fileList.add(file); fileList.add(inMemoryFileThumbnail); - } - - @Test - public void testBuildAASX() throws IOException, TransformerException, ParserConfigurationException, SerializationException { - // This stream can be used to write the .aasx directly to a file // FileOutputStream out = new FileOutputStream("path/to/test.aasx");