blob: 1213ccbec7b44f6075904c496a6f59641458e6eb [file] [log] [blame]
/*******************************************************************************
* 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.");
}
}