blob: 9e36227e2a34cbe91f3570e843d858b31f500225 [file] [log] [blame]
/**
********************************************************************************
* Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
*
* 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
*
* Contributors:
* Robert Bosch GmbH - initial API and implementation
********************************************************************************
*/
package org.eclipse.app4mc.amalthea.converters.common;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.eclipse.app4mc.amalthea.converters.common.utils.AmaltheaNamespaceRegistry;
import org.eclipse.app4mc.amalthea.converters.common.utils.HelperUtil;
import org.eclipse.app4mc.amalthea.converters.common.utils.ModelVersion;
import org.eclipse.app4mc.util.sessionlog.SessionLogger;
import org.jdom2.Namespace;
public final class MigrationHelper {
public static final String UNZIPPED_PREFIX = "unzipped_";
public static final String INVALID = "invalid";
public static final String MODEL_FILE_EXTENSION = "amxmi";
public static final String LINE_SEPARATOR = System.getProperty("line.separator");
private MigrationHelper() {
// empty default constructor
}
public static List<MigrationInputFile> populateModels(
List<File> inputModelFiles,
MigrationSettings migrationSettings) throws IOException {
ArrayList<MigrationInputFile> modelFiles = new ArrayList<>();
for (File inputFile : inputModelFiles) {
MigrationInputFile migModelFile = new MigrationInputFile();
migModelFile.setFile(inputFile.getCanonicalFile(), migrationSettings.getProject());
// set the model file version
try {
migModelFile.setModelVersion(getModelVersion(inputFile));
} catch (IllegalStateException e) {
migModelFile.setModelVersion(INVALID + ": " + e.getMessage());
}
modelFiles.add(migModelFile);
}
return modelFiles;
}
/**
* Save the converted documents carried in the given {@link MigrationSettings}.
*
* @param settings The {@link MigrationSettings} that contains the references to
* the converted Documents to save.
* @param logger The {@link SessionLogger} to use.
* @throws IOException If an error occurred on saving the output file.
*/
public static void saveFiles(MigrationSettings settings, SessionLogger logger) throws IOException {
List<MigrationInputFile> migModelFiles = settings.getMigModelFiles();
for (MigrationInputFile inputFile : migModelFiles) {
// skip inputs that have no Document set
if (inputFile.getDocument() == null) {
continue;
}
String outputDirectoryLocation = settings.getOutputDirectoryLocation();
String convertedFileName = inputFile.getFile() != null
? inputFile.getFile().getName()
: inputFile.getOriginalFile().getName();
File outputFile = null;
Path location = null;
if (outputDirectoryLocation != null && !outputDirectoryLocation.equals("")) {
location = Paths.get(outputDirectoryLocation);
}
else {
location = Paths.get(inputFile.getFile().getParentFile().getAbsolutePath());
}
Path outputFilePath = Paths.get(location.toString(), convertedFileName);
HelperUtil.saveFile(inputFile.getDocument(), outputFilePath.toString(), true, true);
outputFile = outputFilePath.toFile();
logger.info("Migrated model file saved @ : {0}", outputFile.getAbsolutePath());
// now that the migrated files are saved we need to check if the input was zipped
if (inputFile.isZipFile()) {
Path zipOutputPath = Paths.get(location.toString(), inputFile.getOriginalFile().getName());
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipOutputPath));
InputStream fis = Files.newInputStream(outputFilePath)) {
ZipEntry zipEntry = new ZipEntry(inputFile.getOriginalFile().getName());
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
}
logger.info("Created archive model file @ : {0}", zipOutputPath);
// delete the migrated unzipped file
Files.delete(outputFilePath);
}
}
}
/**
* This method is used to generate list of model migration steps which are required to migrate from one version to
* other. <br>
* <br>
* <b>Example</b>: Below are the steps which are required to migrate from 0.7.2 to 0.8.3:<br>
* <br>
*
* <table style="height: 65px;" width="120">
* <tbody>
* <tr>
* <td style="text-align:right;">0.7.2 --&gt;</td>
* <td>0.8.0</td>
* </tr>
* <tr>
* <td style="text-align:right;">0.8.0 --&gt;</td>
* <td>0.8.1</td>
* </tr>
* <tr>
* <td style="text-align:right;">0.8.1 --&gt;</td>
* <td>0.8.2</td>
* </tr>
* <tr>
* <td style="text-align:right;">0.8.2 --&gt;</td>
* <td>0.8.3</td>
* </tr>
* </tbody>
* </table>
*
* @param inputModelVersion
* This is the version present in input AMALTHEA model file (e.g: 0.7.2)
* @param outputModelVersion
* AMALTHEA model file should be migrated to this version (e.g: 0.8.3)
* @return
*/
public static Map<String, String> generateMigrationSteps(String inputModelVersion, String outputModelVersion) {
LinkedHashMap<String, String> migStepEntries = new LinkedHashMap<>();
/*
* Note: These are the various AMALTHEA model versions which are released.
* Order of the below list should be same as the release order
* -> as based on this order, model migration steps are prepared
*/
List<String> versions = ModelVersion.getAllSupportedVersions();
int inputModelVersionIndex = versions.indexOf(inputModelVersion);
int outputModelVersionIndex = versions.indexOf(outputModelVersion);
if (inputModelVersionIndex != -1 && outputModelVersionIndex != -1) {
for (int i = inputModelVersionIndex;
(i <= outputModelVersionIndex) && ((i + 1) <= outputModelVersionIndex);
i++) {
migStepEntries.put(versions.get(i), versions.get(i + 1));
}
}
return migStepEntries;
}
/**
* Verifies the model version of the input files set to the provided
* {@link MigrationSettings} and sets the input model version if the
* verification succeeds.
*
* @param migrationSettings The MigrationSettings that contain the input model
* file references and is used for migration
* processing.
* @return <code>true</code> if the model versions of the input files are valid,
* <code>false</code> if the model versions of the input files are not
* equal.
* @throws MigrationException if input model files contain ITEA model versions
* or invalid model versions that are not defined in
* {@link ModelVersion}.
*/
public static boolean isInputModelVersionValid(MigrationSettings migrationSettings) {
List<MigrationInputFile> modelsWithInvalidVersionInfo = getModelsWithInvalidVersionInfo(migrationSettings);
if (!modelsWithInvalidVersionInfo.isEmpty()) {
StringBuilder builder = new StringBuilder();
builder.append("Invalid model versions found for the below files ->");
builder.append(LINE_SEPARATOR);
builder.append(LINE_SEPARATOR);
for (MigrationInputFile migrationFile : modelsWithInvalidVersionInfo) {
builder.append(migrationFile.getOriginalFile().getAbsolutePath());
builder.append(LINE_SEPARATOR);
}
throw new MigrationException(builder.toString());
} else {
// try to identify and set the input model version
boolean inputModelVersionEqual = true;
String inputModelVersion = null;
for (MigrationInputFile migModelFile : migrationSettings.getMigModelFiles()) {
if (inputModelVersion == null) {
inputModelVersion = migModelFile.getModelVersion();
} else if (!inputModelVersion.equals(migModelFile.getModelVersion())) {
migModelFile.setVersionDifferent(true);
inputModelVersionEqual = false;
}
}
if (inputModelVersionEqual) {
migrationSettings.setInputModelVersion(inputModelVersion);
}
return inputModelVersionEqual;
}
}
/**
* This method is used to build list of model files for which version is
* invalid. Valid versions are described in {@link ModelVersion}.
*
* @param migrationSettings The settings that contain the input model file
* references.
* @return List of all input model files with invalid version information.
*/
private static List<MigrationInputFile> getModelsWithInvalidVersionInfo(MigrationSettings migrationSettings) {
List<MigrationInputFile> list = new ArrayList<>();
for (MigrationInputFile migModelFile : migrationSettings.getMigModelFiles()) {
String inputModelVersion = migModelFile.getModelVersion();
if (!ModelVersion.isValidVersion(inputModelVersion)) {
list.add(migModelFile);
}
}
return list;
}
/**
* Creates a backup file of the provided input file. The name of the backup file
* is defined in {@link #getBackupFileName(MigrationInputFile)}.
*
* @param migrationInputFile The input file for which a backup should be
* created.
* @return <code>true</code> if the backup file could be created and the backup
* file exists, <code>false</code> if the file does not exist.
*/
public static boolean createBackupFile(MigrationInputFile migrationInputFile) {
// Rename or copy the original file to filename_currentversion.amxmi
String newFileName = MigrationHelper.getBackupFileName(migrationInputFile);
SessionLogger logger = migrationInputFile.getSessionLogger();
try {
String filebackupName = migrationInputFile.getOriginalFile().getParent() + File.separator + newFileName;
Files.copy(migrationInputFile.getOriginalFile().toPath(), new File(filebackupName).toPath(), StandardCopyOption.REPLACE_EXISTING);
if (logger != null) {
logger.info("Original model file saved as {}", filebackupName);
}
if (new File(filebackupName).exists()) {
return true;
}
} catch (IOException e) {
if (logger != null) {
logger.error(e.getMessage(), e);
}
}
return false;
}
/**
* Try to construct the file name for the source file backup which should be
* backed up before migration. Intension is not to migrate the source file
* without making a copy of it. Source file name is
* <i>&lt;origfilename_withoutextension&gt;_&lt;input_amlatheaversion&gt;.amxmi</i>
* <p>
* Examples:<br>
* model.axmi(orig) -> model_0.9.4.amxmi
* </p>
* <p>
* If file name exists then the model version is appended before the extension e.g.,
* model_0.9.4_0.9.4.amxi
* </p>
*
* @param migrationInputFile The input file for which the backup file name is
* requested.
* @return filename of the input file backup.
*/
public static String getBackupFileName(MigrationInputFile migrationInputFile) {
String fileName = migrationInputFile.getOriginalFile().getName();
String newFileName = fileName.substring(0, fileName.lastIndexOf('.'));
String suffixString = "_" + migrationInputFile.getModelVersion();
String fileExtension = "." + MODEL_FILE_EXTENSION;
newFileName = newFileName + suffixString + fileExtension;
// check if file with newly constructed name already exists then append model
// version at end of name
if (new File(migrationInputFile.getOriginalFile().getParent() + File.separator + newFileName).exists()) {
newFileName = newFileName.replace(suffixString + fileExtension, suffixString + suffixString + fileExtension);
}
return newFileName;
}
/**
* Extract the model version of the given input model file.
*
* @param inputModelFile The model file for which the model version is
* requested.
* @return The model version of the given input model file or <i>invalid</i> if
* an error occurs on parsing or the model version is invalid.
* @throws IllegalStateException if parsing the input model file fails.
*/
public static String getModelVersion(File inputModelFile) {
String result = INVALID;
if (inputModelFile != null && inputModelFile.exists() && inputModelFile.getName().endsWith(MODEL_FILE_EXTENSION)) {
Path inputPath = Paths.get(inputModelFile.toURI());
if (isZipFile(inputModelFile)) {
result = getModelVersionFromZip(inputPath);
} else {
result = getModelVersionFromFile(inputPath);
}
}
return result;
}
/**
* Extract the model version of the given input model file.
*
* @param inputPath The {@link Path} to the model file.
* @return The model version of the given input model file or <i>invalid</i> if
* an error occurs on parsing or the model version is invalid.
* @throws IllegalStateException if parsing the input model file fails.
*/
public static String getModelVersionFromFile(Path inputPath) {
try (InputStream input = Files.newInputStream(inputPath)) {
return getModelVersion(input);
} catch (IOException | XMLStreamException e) {
throw new IllegalStateException("Error on parsing input model file for model version: " + e.getMessage());
}
}
/**
* Extract the model version of the given zipped input model file.
*
* @param inputPath The {@link Path} to the model file.
* @return The model version of the given input model file or <i>invalid</i> if
* an error occurs on parsing or the model version is invalid.
* @throws IllegalStateException if parsing the input model file fails.
*/
public static String getModelVersionFromZip(Path inputPath) {
try (ZipInputStream input = new ZipInputStream(Files.newInputStream(inputPath))) {
ZipEntry zipEntry = input.getNextEntry();
if (zipEntry != null) {
return getModelVersion(input);
}
} catch (IOException | XMLStreamException e) {
throw new IllegalStateException("Error on parsing input model file for model version: " + e.getMessage());
}
return INVALID;
}
private static String getModelVersion(InputStream input) throws XMLStreamException {
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
XMLEventReader reader = xmlInputFactory.createXMLEventReader(input);
while (reader.hasNext()) {
XMLEvent nextEvent = reader.nextEvent();
if (nextEvent.isStartElement()) {
StartElement startElement = nextEvent.asStartElement();
if ("am".equals(startElement.getName().getPrefix())) {
String url = startElement.getNamespaceURI("am");
Namespace namespace = Namespace.getNamespace("am", url);
ModelVersion version = AmaltheaNamespaceRegistry.getModelVersion(namespace);
if (version != null) {
return version.getVersion();
}
break;
}
}
}
return INVALID;
}
/**
* Check if the given file is a zip archive.
*
* @param file The {@link File} to check.
* @return <code>true</code> if the given file is a zip archive,
* <code>false</code> if not.
*/
public static boolean isZipFile(File file) {
boolean result = false;
if (file != null) {
try (ZipFile f = new ZipFile(file)) {
// zipped file detected
result = true;
} catch (IOException e) {
// IOException includes ZipException -> not a zip file
}
}
return result;
}
/**
* Extracts the given input file. Needed in case the .amxmi file is a zip
* archive that contains the model real model file.
*
* @param input The input file to extract.
* @return The File reference to the unzipped file.
* @throws IOException if an error occurs on the unzip operation.
*/
public static File temporaryUnzip(File input) throws IOException {
Path inputPath = Paths.get(input.toURI());
byte[] buffer = new byte[1024];
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(inputPath))) {
ZipEntry zipEntry = zis.getNextEntry();
Path unzipped = Paths.get(inputPath.getParent().toString(), UNZIPPED_PREFIX + zipEntry.getName());
try (OutputStream fos = Files.newOutputStream(unzipped)) {
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
zis.closeEntry();
return unzipped.toFile();
}
}
/**
* Checks if the file with the given filename is an Amalthea model file by
* checking the file extension.
*
* @param filename the name of the file to check.
* @return <code>true</code> if the given file is an Amalthea model file,
* <code>false</code> if not.
*/
public static boolean isModelFile(String filename) {
String fileExtension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
return isModelFileExtension(fileExtension);
}
/**
* Checks if the given file extension is an Amalthea model file extension.
*
* @param fileExtension the file extension to check.
* @return <code>true</code> if the given file extension is an Amalthea model
* file extension, <code>false</code> if not.
*/
public static boolean isModelFileExtension(String fileExtension) {
return MODEL_FILE_EXTENSION.equals(fileExtension);
}
}