| /** |
| ******************************************************************************** |
| * 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 --></td> |
| * <td>0.8.0</td> |
| * </tr> |
| * <tr> |
| * <td style="text-align:right;">0.8.0 --></td> |
| * <td>0.8.1</td> |
| * </tr> |
| * <tr> |
| * <td style="text-align:right;">0.8.1 --></td> |
| * <td>0.8.2</td> |
| * </tr> |
| * <tr> |
| * <td style="text-align:right;">0.8.2 --></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><origfilename_withoutextension>_<input_amlatheaversion>.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); |
| } |
| |
| } |