All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edelta.lib.EdeltaVersionMigrator Maven / Gradle / Ivy

There is a newer version: 3.5.0
Show newest version
package edelta.lib;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.xmi.XMIResource;

import edelta.lib.EdeltaEngine.EdeltaRuntimeProvider;

/**
 * Handles the migration of model files according to the information configured.
 * 
 * It is meant to scan a path, loading model files, check which ones need to be migrated
 * from a version of metamodels to the next one. It repeats the scan until all model files
 * are migrated to the latest current version of metamodels.
 * 
 * @author Lorenzo Bettini
 */
public class EdeltaVersionMigrator {

	private static final String XMI_EXTENSION = "." + XMIResource.XMI_NS;

	private static final Logger LOG = Logger.getLogger(EdeltaVersionMigrator.class);

	private EdeltaModelManager modelManager = new EdeltaModelManager();

	private static class VersionMigrationEntry {
		private Collection uris;
		private EdeltaEngine engine;

		VersionMigrationEntry(Collection uris, EdeltaEngine engine) {
			this.uris = uris;
			this.engine = engine;
		}
	}

	private List versionMigrations = new ArrayList<>();

	private Set modelExtensions = new HashSet<>();

	public EdeltaVersionMigrator() {
		modelExtensions.add(XMI_EXTENSION);
	}

	/**
	 * By default, it loads only ".xmi" files as models.
	 * 
	 * @param modelFileExtension including the "."
	 */
	public void addModelFileExtension(String modelFileExtension) {
		modelExtensions.add(modelFileExtension);
	}

	/**
	 * Ensure that the nsURI is mapped to the loaded {@link EPackage}, so that
	 * when loading an XMI the referenced Ecore file is found by nsURI.
	 * 
	 * @param resource
	 */
	private void updatePackageRegistry(Resource resource) {
		var ePackage = EdeltaResourceUtils.getEPackage(resource);
		var resourceSet = resource.getResourceSet();
		updatePackageRegistry(ePackage, resourceSet);
	}

	/**
	 * Ensure that the nsURI is mapped to the loaded {@link EPackage}, in the
	 * specified {@link ResourceSet} so that when loading an XMI the referenced
	 * Ecore file is found by nsURI.
	 * 
	 * @param ePackage
	 * @param resourceSet
	 */
	private void updatePackageRegistry(EPackage ePackage, ResourceSet resourceSet) {
		resourceSet.getPackageRegistry()
			.put(ePackage.getNsURI(), ePackage);
	}

	/**
	 * See {@link EdeltaModelManager#loadEcoreFile(String)}.
	 * 
	 * @param ecorePath
	 * @throws IOException 
	 */
	public void loadEcore(String ecorePath) {
		var resource = modelManager.loadEcoreFile(ecorePath);
		updatePackageRegistry(resource);
	}

	/**
	 * See {@link EdeltaModelManager#loadEcoreFile(String, InputStream)}.
	 * 
	 * @param ecoreFile
	 * @param inputStream
	 * @throws IOException 
	 */
	public void loadEcore(String ecoreFile, InputStream inputStream) throws IOException {
		var resource = modelManager.loadEcoreFile(ecoreFile, inputStream);
		updatePackageRegistry(resource);
	}

	/**
	 * Loads an {@link EPackage}, assumed to be properly part of a
	 * {@link ResourceSet}, which represents the current (and latest) version of a
	 * metamodel.
	 * 
	 * See {@link EdeltaModelManager#loadEPackage(EPackage)}.
	 * 
	 * @param ePackage
	 */
	public void loadCurrentEPackage(EPackage ePackage) {
		modelManager.loadEPackage(ePackage);
		// since the EPackage might be in a different ResourceSet than our modelManager's one
		// we must ensure the registration is performed in the modelManager's ResourceSet
		updatePackageRegistry(ePackage, modelManager.getResourceSet());
	}

	/**
	 * Load all models in the given path (possibly recursively in subdirectories),
	 * using the configured file extensions, by default ".xmi" files (see
	 * {@link #addModelFileExtension(String)}).
	 * 
	 * @param path
	 * @throws IOException
	 */
	public void loadModelsFrom(String path) throws IOException {
		try (var stream = Files.walk(Paths.get(path))) {
			stream
				.filter(file -> !Files.isDirectory(file))
				.filter(file -> {
					var fileToString = file.toString();
					return modelExtensions.stream().anyMatch(fileToString::endsWith);
				})
				.forEach(file -> loadModel(file.toString()));
		}
	}

	public Resource loadModel(String path) {
		return modelManager.loadModelFile(path);
	}

	public void mapVersionMigration(Collection uris, EdeltaEngine edeltaEngine) {
		versionMigrations.add(new VersionMigrationEntry(uris, edeltaEngine));
	}

	/**
	 * Registers an {@link EdeltaRuntimeProvider} by recording the nsURIs handled by the
	 * corresponding {@link EdeltaRuntime} implementation and by loading the corresponding
	 * Ecore files from the classpath, e.g., with {@link Class#getResourceAsStream(String)}.
	 * 
	 * @param provider
	 * @throws IOException
	 */
	public void registerMigration(EdeltaRuntimeProvider provider) throws IOException {
		var tempRuntime = provider.apply(new EdeltaDefaultRuntime(modelManager));
		mapVersionMigration(tempRuntime.getMigratedNsURIs(), new EdeltaEngine(provider));
		var ecorePaths = tempRuntime.getMigratedEcorePaths();
		for (var ecorePath : ecorePaths) {
			loadEcore(ecorePath, tempRuntime.getClass().getResourceAsStream(ecorePath));
		}
	}

	/**
	 * Executes all the needed model migrations, saving the model files in-place.
	 * 
	 * @throws Exception
	 */
	public void execute() throws Exception {
		class MigrationData {
			private Set ecores;
			private Collection models;

			MigrationData(Set ecores, Collection models) {
				this.ecores = ecores;
				this.models = models;
			}
		}

		var migrationDatas = new HashMap();
		do {
			migrationDatas.clear();
			for (var resource : modelManager.getModelResources()) {
				var contents = resource.getContents();
				var ePackage = contents.get(0).eClass().getEPackage();
				versionMigrations.stream()
					.filter(versionMigration -> versionMigration.uris.contains(ePackage.getNsURI()))
					.forEach(versionMigration -> {
						var data = migrationDatas
							.computeIfAbsent(versionMigration,
								x -> new MigrationData(new HashSet<>(), new ArrayList<>()));
						data.ecores.add(ePackage);
						data.models.add(resource);
					});
			}
			Collection migratedModelResources = new ArrayList<>();
			for (var entry : migrationDatas.entrySet()) {
				var toApply = entry.getKey().engine;
				var ecoresToMigrate = entry.getValue().ecores;
				var modelsToMigrate = entry.getValue().models;

				var nsURIs = ecoresToMigrate.stream().map(e -> e.getNsURI()).toList();
				var modelPaths = modelsToMigrate.stream()
						.map(this::resourceToURIString)
						.collect(Collectors.toSet());
				nsURIs.forEach(e -> LOG.info("Stale Ecore nsURI: " + e));
				modelPaths.forEach(e -> LOG.info("Model requiring migration: " + e));

				// We assume EPackages have been loaded and registered by name and nsURI
				toApply.setOriginalModelManager(modelManager);
				toApply.execute();

				// note that we never save the evolved ecores: only the models
				// the ecores are meant to be part of the application code, not of the client project
				var evolvedModels = toApply.getEvolvingModelManager().getModelResources().stream()
						.filter(r -> modelPaths.contains(resourceToURIString(r)))
						.toList();
				saveInPlace(evolvedModels);
				migratedModelResources.addAll(evolvedModels);
			}
			if (!migratedModelResources.isEmpty()) {
				// reload only migrated models
				modelManager.clearModels();
				for (var model : migratedModelResources) {
					loadModelsFrom(resourceToURIString(model));
				}
				// it is important to save after all the migrations took place
				// this way, the models refer to the most up-to-date Ecore loaded
				// from the file system, and possible relative xsi:schemaLocation
				// point to the most up-to-date Ecore
				// REMEMBER: in this method, evolved Ecore files are NEVER saved to disk:
				// they are used in memory only to migrate models
				// Ecore files are meant to be in the Application code, not in the client
				saveInPlace(modelManager.getModelResources());
			}
		} while (!migrationDatas.isEmpty());
	}

	private String resourceToURIString(Resource resource) {
		return resource.getURI().path();
	}

	private void saveInPlace(Collection resources) throws IOException {
		for (var resource : resources) {
			LOG.info("Saving: " + resource.getURI());
			resource.save(null);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy