| /******************************************************************************* |
| * Copyright (C) 2021 the Eclipse BaSyx Authors |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| ******************************************************************************/ |
| package org.eclipse.basyx.aas.factory.aasx; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.UUID; |
| |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.transform.TransformerException; |
| import javax.xml.transform.stream.StreamResult; |
| |
| import org.apache.poi.openxml4j.exceptions.InvalidFormatException; |
| import org.apache.poi.openxml4j.opc.OPCPackage; |
| import org.apache.poi.openxml4j.opc.PackagePart; |
| import org.apache.poi.openxml4j.opc.PackagePartName; |
| import org.apache.poi.openxml4j.opc.PackagingURIHelper; |
| import org.apache.poi.openxml4j.opc.RelationshipSource; |
| import org.apache.poi.openxml4j.opc.TargetMode; |
| import org.apache.poi.openxml4j.opc.internal.MemoryPackagePart; |
| import org.eclipse.basyx.aas.factory.xml.MetamodelToXMLConverter; |
| import org.eclipse.basyx.aas.metamodel.api.IAssetAdministrationShell; |
| import org.eclipse.basyx.aas.metamodel.api.parts.asset.IAsset; |
| import org.eclipse.basyx.submodel.metamodel.api.ISubmodel; |
| import org.eclipse.basyx.submodel.metamodel.api.parts.IConceptDescription; |
| import org.eclipse.basyx.submodel.metamodel.api.submodelelement.ISubmodelElement; |
| import org.eclipse.basyx.submodel.metamodel.map.submodelelement.SubmodelElementCollection; |
| import org.eclipse.basyx.submodel.metamodel.map.submodelelement.dataelement.File; |
| import org.eclipse.basyx.vab.exception.provider.ResourceNotFoundException; |
| import org.eclipse.basyx.vab.modelprovider.VABPathTools; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| |
| /** |
| * This class can be used to generate an .aasx file from |
| * Metamodel Objects and the Files referred to in the Submodels |
| * |
| * @author conradi |
| * |
| */ |
| public class AASXFactory { |
| |
| private static Logger logger = LoggerFactory.getLogger(AASXFactory.class); |
| |
| private static final String MIME_PLAINTXT = "text/plain"; |
| private static final String MIME_XML = "application/xml"; |
| |
| private static final String ORIGIN_RELTYPE = "http://www.admin-shell.io/aasx/relationships/aasx-origin"; |
| private static final String ORIGIN_PATH = "/aasx/aasx-origin"; |
| private static final String ORIGIN_CONTENT = "Intentionally empty."; |
| |
| |
| private static final String AASSPEC_RELTYPE = "http://www.admin-shell.io/aasx/relationships/aas-spec"; |
| private static final String XML_PATH = "/aasx/xml/content.xml"; |
| |
| |
| private static final String AASSUPPL_RELTYPE = "http://www.admin-shell.io/aasx/relationships/aas-suppl"; |
| |
| |
| |
| /** |
| * Generates the .aasx file and writes it to the given OutputStream |
| * |
| * @param aasList the AASs to be saved in the .aasx |
| * @param assetList the Assets to be saved in the .aasx |
| * @param conceptDescriptionList the ConceptDescriptions to be saved in the .aasx |
| * @param submodelList the Submodels to be saved in the .aasx |
| * @param files the files referred to in the Submodels |
| * @param os the OutputStream the resulting .aasx is written to |
| * @throws IOException |
| * @throws TransformerException |
| * @throws ParserConfigurationException |
| */ |
| public static void buildAASX(Collection<IAssetAdministrationShell> aasList, Collection<IAsset> assetList, |
| Collection<IConceptDescription> conceptDescriptionList, Collection<ISubmodel> submodelList, Collection<InMemoryFile> files, OutputStream os) throws IOException, TransformerException, ParserConfigurationException { |
| |
| prepareFilePaths(submodelList); |
| |
| 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 = convertToXML(aasList, assetList, conceptDescriptionList, submodelList); |
| |
| // Save the XML to aasx/xml/content.xml |
| PackagePart xmlPart = createAASXPart(rootPackage, origin, XML_PATH, MIME_XML, AASSPEC_RELTYPE, xml.getBytes()); |
| |
| storeFilesInAASX(submodelList, files, rootPackage, xmlPart); |
| |
| saveAASX(os, rootPackage); |
| } |
| |
| /** |
| * Stores the files from the Submodels in the .aasx file |
| * |
| * @param submodelList the Submodels |
| * @param files the content of the files |
| * @param rootPackage the OPCPackage |
| * @param xmlPart the Part the files should be related to |
| */ |
| private static void storeFilesInAASX(Collection<ISubmodel> submodelList, Collection<InMemoryFile> files, |
| OPCPackage rootPackage, PackagePart xmlPart) { |
| |
| for(ISubmodel sm: submodelList) { |
| for(File file: findFileElements(sm.getSubmodelElements().values())) { |
| String filePath = file.getValue(); |
| try { |
| InMemoryFile content = findFileByPath(files, filePath); |
| logger.trace("Writing file '" + filePath + "' to .aasx."); |
| createAASXPart(rootPackage, xmlPart, filePath, file.getMimeType(), AASSUPPL_RELTYPE, content.getFileContent()); |
| } catch (ResourceNotFoundException e) { |
| // Log that a file is missing and continue building the .aasx |
| logger.warn("Could not add File '" + filePath + "'. It was not contained in given InMemoryFiles."); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Saves the OPCPackage to the given OutputStream |
| * |
| * @param os the Stream to be saved to |
| * @param rootPackage the Package to be saved |
| * @throws IOException |
| */ |
| private static void saveAASX(OutputStream os, OPCPackage rootPackage) throws IOException { |
| rootPackage.flush(); |
| rootPackage.save(os); |
| } |
| |
| /** |
| * Generates a UUID. Every element of the |
| * .aasx needs a unique Id according to the specification |
| * |
| * @return UUID |
| */ |
| private static String createUniqueID() { |
| // only letters or underscore as start of id allowed |
| // https://www.w3.org/TR/1999/REC-xml-names-19990114/#ns-qualnames |
| return "id_" + UUID.randomUUID().toString(); |
| } |
| |
| /** |
| * Creates a Part (a file in the .aasx) of the .aasx and adds it to the Package |
| * |
| * @param root the OPCPackage |
| * @param relateTo the Part of the OPC the relationship of the new Part should be added to |
| * @param path the path inside the .aasx where the new Part should be created |
| * @param mimeType the mime-type of the file |
| * @param relType the type of the Relationship |
| * @param content the data the new part should contain |
| * @return the created PackagePart; Returned in case it is needed late as a Part to relate to |
| */ |
| private static PackagePart createAASXPart(OPCPackage root, RelationshipSource relateTo, String path, String mimeType, String relType, byte[] content) { |
| if(mimeType == null || mimeType.equals("")) { |
| throw new RuntimeException("Could not create AASX Part '" + path + "'. No MIME_TYPE specified."); |
| } |
| |
| PackagePartName partName = null; |
| MemoryPackagePart part = null; |
| try { |
| partName = PackagingURIHelper.createPartName(path); |
| part = new MemoryPackagePart(root, partName, mimeType); |
| } catch (InvalidFormatException e) { |
| // This occurs if the given MIME-Type is not valid according to RFC2046 |
| throw new RuntimeException("Could not create AASX Part '" + path + "'", e); |
| } |
| writeDataToPart(part, content); |
| root.registerPartAndContentType(part); |
| // set TargetMode to EXTERNAL to force absolute file paths |
| // this step is necessary for compatibility reasons with AASXPackageExplorer |
| relateTo.addRelationship(partName, TargetMode.EXTERNAL, relType, createUniqueID()); |
| return part; |
| } |
| |
| /** |
| * Writes the content of a byte[] to a Part |
| * |
| * @param part the Part to be written to |
| * @param content the content to be written to the part |
| */ |
| private static void writeDataToPart(PackagePart part, byte[] content) { |
| try(OutputStream ostream = part.getOutputStream();) { |
| ostream.write(content); |
| ostream.flush(); |
| } catch (Exception e) { |
| throw new RuntimeException("Failed to write content to AASX Part '" + part.getPartName().getName() + "'", e); |
| } |
| } |
| |
| /** |
| * Uses the MetamodelToXMLConverter to generate the XML |
| */ |
| private static String convertToXML(Collection<IAssetAdministrationShell> aasList, Collection<IAsset> assetList, |
| Collection<IConceptDescription> conceptDescriptionList, Collection<ISubmodel> submodelList) throws TransformerException, ParserConfigurationException { |
| |
| StringWriter writer = new StringWriter(); |
| MetamodelToXMLConverter.convertToXML(aasList, assetList, conceptDescriptionList, submodelList, new StreamResult(writer)); |
| |
| return writer.toString(); |
| } |
| |
| /** |
| * Gets the File elements from a collection of elements |
| * Also recursively searches in SubmodelElementCollections |
| * |
| * @param elements the Elements to be searched for File elements |
| * @return the found Files |
| */ |
| private static Collection<File> findFileElements(Collection<ISubmodelElement> elements) { |
| Collection<File> files = new ArrayList<>(); |
| |
| for(ISubmodelElement 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).getSubmodelElements().values())); |
| } |
| } |
| |
| return files; |
| } |
| |
| /** |
| * Replaces the path in all File Elements with the result of preparePath |
| * |
| * @param submodels the Submodels |
| */ |
| private static void prepareFilePaths(Collection<ISubmodel> submodels) { |
| submodels.stream() |
| .forEach(sm -> findFileElements(sm.getSubmodelElements().values()).stream().forEach(f -> f.setValue(preparePath(f.getValue())))); |
| } |
| |
| /** |
| * Removes the serverpart from a path and ensures it starts with a slash |
| * |
| * @param path the path to be prepared |
| * @return the prepared path |
| */ |
| private static String preparePath(String path) { |
| String newPath = VABPathTools.getPathFromURL(path); |
| if(!newPath.startsWith("/")) { |
| newPath = "/" + newPath; |
| } |
| return newPath; |
| } |
| |
| /** |
| * Finds an InMemoryFile by its path |
| * |
| * @param files the InMemoryFiles |
| * @param path the path of the wanted file |
| * @return the InMemoryFile if it was found; else null |
| */ |
| private static InMemoryFile findFileByPath(Collection<InMemoryFile> files, String path) { |
| for(InMemoryFile file: files) { |
| if(preparePath(file.getPath()).equals(path)) { |
| return file; |
| } |
| } |
| throw new ResourceNotFoundException("The wanted file '" + path + "' was not found in the given files."); |
| } |
| } |