blob: eaf16e6d523d005c1efe65b9ad5e8625dfa94cbf [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2014 EclipseSource Muenchen GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lucas - initial API and implementation
* Johannes Faltermeier - initial API and implementation
******************************************************************************/
package org.eclipse.emf.ecp.view.edapt;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EPackage.Registry;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.emf.ecp.spi.view.migrator.NameSpaceHandler;
import org.eclipse.emf.ecp.spi.view.migrator.SAXUtil;
import org.eclipse.emf.ecp.spi.view.migrator.string.StringViewModelMigrator;
import org.eclipse.emf.ecp.spi.view.migrator.string.StringViewModelMigratorUtil;
import org.eclipse.emf.ecp.view.migrator.ViewModelMigrationException;
import org.eclipse.emf.ecp.view.migrator.ViewModelMigrator;
import org.eclipse.emf.ecp.view.spi.model.util.VViewResourceFactoryImpl;
import org.eclipse.emf.edapt.common.IResourceSetFactory;
import org.eclipse.emf.edapt.migration.MigrationException;
import org.eclipse.emf.edapt.migration.execution.Migrator;
import org.eclipse.emf.edapt.migration.execution.MigratorRegistry;
import org.eclipse.emf.edapt.spi.history.Change;
import org.eclipse.emf.edapt.spi.history.History;
import org.eclipse.emf.edapt.spi.history.HistoryFactory;
import org.eclipse.emf.edapt.spi.history.MigrationChange;
import org.eclipse.emf.edapt.spi.history.Release;
import org.eclipse.emf.edapt.spi.migration.MigrationPlugin;
import org.osgi.framework.Bundle;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* A {@link ViewModelMigrator} using edapt.
*
* @author Lucas
* @author jfaltermeier
*
*/
public class EdaptViewModelMigrator implements ViewModelMigrator, StringViewModelMigrator {
private static final String ECORE_NS_URI = "http://www.eclipse.org/emf/2002/Ecore"; //$NON-NLS-1$
private static final Comparator<String> RELEASE_COMPERATOR = new Comparator<String>() {
@Override
public int compare(String left, String right) {
return Integer.valueOf(left).compareTo(Integer.valueOf(right));
}
};
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.spi.view.migrator.string.StringViewModelMigrator#checkMigration(java.lang.String)
* @since 1.8
*/
@Override
public boolean checkMigration(String serializedViewModel) {
return checkMigration(StringViewModelMigratorUtil.getNamespaceURIs(serializedViewModel));
}
@Override
public boolean checkMigration(final URI resourceURI) {
return checkMigration(getNamespaceURIs(resourceURI));
}
private boolean checkMigration(final List<String> nsUris) {
boolean allReleasesAreLatest = true;
final List<Release> releases = new ArrayList<Release>();
for (final String nsUri : nsUris) {
final Migrator migrator = MigratorRegistry.getInstance()
.getMigrator(nsUri);
if (migrator == null) {
// no migrator registered. assume all is fine
continue;
}
final Release nsRelease = getReleaseFromMigrator(nsUri, migrator);
releases.add(nsRelease);
if (!migrator.getLatestRelease().equals(nsRelease)) {
allReleasesAreLatest = false;
break;
}
}
return allReleasesAreLatest;
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.spi.view.migrator.string.StringViewModelMigrator#performMigration(java.lang.String)
* @since 1.8
*/
@Override
public String performMigration(String serializedViewModel) throws ViewModelMigrationException {
PrintWriter printWriter = null;
File tempViewModelFile = null;
Scanner scanner = null;
try {
tempViewModelFile = File.createTempFile("view", ".view"); //$NON-NLS-1$//$NON-NLS-2$
tempViewModelFile.deleteOnExit();
printWriter = new PrintWriter(tempViewModelFile);
printWriter.print(serializedViewModel);
printWriter.flush();
printWriter.close();
performMigration(URI.createFileURI(tempViewModelFile.getAbsolutePath()));
scanner = new Scanner(tempViewModelFile, "UTF-8"); //$NON-NLS-1$
return scanner.useDelimiter("\\A").next(); //$NON-NLS-1$
} catch (final IOException ex) {
throw new ViewModelMigrationException(ex);
} finally {
if (printWriter != null) {
printWriter.close();
}
if (scanner != null) {
scanner.close();
}
if (tempViewModelFile != null) {
tempViewModelFile.delete();
}
}
}
@Override
public void performMigration(final URI resourceURI) throws ViewModelMigrationException {
final History history = HistoryFactory.eINSTANCE.createHistory();
final Release sourceRelease = HistoryFactory.eINSTANCE.createRelease();
final List<Release> targetReleases = new ArrayList<Release>();
/*
* Analyse the domain model of the view. Generate a history for ecores which have no registered history and add
* them to source release. Mapping contains information about which uris from the domain have histories and
* which have no history.
*/
final NSURIMapping domainMapping = addChangesForDomainModel(resourceURI, sourceRelease);
/* Get NS URIs with a registered history (domain and view model) */
final List<String> nsUris = getNamespaceURIsWithHistory(resourceURI, domainMapping);
/* Combine the changes from the histories in one big history */
orderHistoriesAndCombineChanges(sourceRelease, targetReleases, nsUris);
/* add releases to history */
history.getReleases().add(sourceRelease);
history.getReleases().addAll(targetReleases);
history.getReleases().add(HistoryFactory.eINSTANCE.createRelease()); // expected by history editor
try {
/* Save history file */
final URI uri = saveHistoryFile(history);
/* Trigger the migration */
final Migrator migrator = new Migrator(uri, new CustomMigrationClassLoader());
migrator.setResourceSetFactory(new IResourceSetFactory() {
@Override
public ResourceSet createResourceSet() {
final ResourceSetImpl resourceSet = new ResourceSetImpl();
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(
Resource.Factory.Registry.DEFAULT_EXTENSION, new VViewResourceFactoryImpl());
return resourceSet;
}
});
final Release release = migrator.getRelease(0);
migrator.migrateAndSave(Collections.singletonList(resourceURI), release, null,
new NullProgressMonitor());
} catch (final IOException ex) {
throw new ViewModelMigrationException(ex);
} catch (final MigrationException ex) {
throw new ViewModelMigrationException(ex);
} finally {
reregisterExtensionMigrators();
}
}
/**
* @param history
* @return
* @throws IOException
*/
private URI saveHistoryFile(final History history) throws IOException {
final File historyFile = File.createTempFile("EMFFormsMigration", ".history"); //$NON-NLS-1$//$NON-NLS-2$
historyFile.deleteOnExit();
final URI uri = URI.createFileURI(historyFile.getAbsolutePath());
final Resource resource = new ResourceSetImpl().createResource(uri);
resource.getContents().add(history);
resource.save(Collections.singletonMap(XMLResource.OPTION_ENCODING, "UTF-8")); //$NON-NLS-1$
return uri;
}
/**
* @param history
* @param sourceRelease
* @param targetReleases
* @param nsUris
*/
private void orderHistoriesAndCombineChanges(final Release sourceRelease,
final List<Release> targetReleases, final List<String> nsUris) {
/* build a dependency tree for all view models with a history */
final PackageDependencyGraph viewModelPackageGraph = new PackageDependencyGraph();
final Map<String, Migrator> nsURIToMigratorMap = new LinkedHashMap<String, Migrator>();
final Map<String, String> latestToCurrentNSURIMap = new LinkedHashMap<String, String>();
for (final String nsUri : nsUris) {
final org.eclipse.emf.edapt.migration.execution.Migrator migrator = MigratorRegistry.getInstance()
.getMigrator(nsUri);
nsURIToMigratorMap.put(nsUri, migrator);
/* get available uri from history */
for (final String migratorNSURI : migrator.getNsURIs()) {
if (Registry.INSTANCE.containsKey(migratorNSURI)) {
viewModelPackageGraph.addPackage(migratorNSURI);
if (latestToCurrentNSURIMap.containsKey(migratorNSURI)) {
final String string = latestToCurrentNSURIMap.get(migratorNSURI);
final String olderNSURI = string.compareTo(nsUri) < 0 ? string : nsUri;
latestToCurrentNSURIMap.put(migratorNSURI, olderNSURI);
} else {
latestToCurrentNSURIMap.put(migratorNSURI, nsUri);
}
break;
}
}
}
/* using the dependency tree, combine changes from all registered histories based on the release label. */
final Map<String, List<Change>> sourceReleaseNameToChangesMap = new LinkedHashMap<String, List<Change>>();
final Map<String, List<Change>> targetReleaseNameToChangesMap = new LinkedHashMap<String, List<Change>>();
final Iterator<Set<String>> viewModelPackageIterator = viewModelPackageGraph.getIerator();
while (viewModelPackageIterator.hasNext()) {
final Set<String> latestURIs = viewModelPackageIterator.next();
for (final String latestURI : latestURIs) {
if (!latestToCurrentNSURIMap.keySet().contains(latestURI)) {
continue;
}
final String nsUri = latestToCurrentNSURIMap.get(latestURI);
final org.eclipse.emf.edapt.migration.execution.Migrator migrator = nsURIToMigratorMap
.get(nsUri);
final Release nsRelease = getReleaseFromMigrator(nsUri, migrator);
// add changes to source release
final int sourceIndex = nsRelease.getNumber();
for (int i = 0; i <= sourceIndex; i++) {
final Release currentRelease = migrator.getRelease(i);
if (!sourceReleaseNameToChangesMap.containsKey(currentRelease.getLabel())) {
sourceReleaseNameToChangesMap.put(currentRelease.getLabel(), new ArrayList<Change>());
}
sourceReleaseNameToChangesMap.get(currentRelease.getLabel()).addAll(currentRelease.getChanges());
}
// add changes to target release
final int targetIndex = migrator.getLatestRelease().getNumber();
if (targetIndex > sourceIndex) {
for (int i = sourceIndex + 1; i <= targetIndex; i++) {
final Release currentRelease = migrator.getRelease(i);
if (!targetReleaseNameToChangesMap.containsKey(currentRelease.getLabel())) {
targetReleaseNameToChangesMap.put(currentRelease.getLabel(), new ArrayList<Change>());
}
targetReleaseNameToChangesMap.get(currentRelease.getLabel())
.addAll(currentRelease.getChanges());
}
}
}
}
/* Based on combined changes from all histories insert the changed to source release and target releases */
fillReleases(sourceRelease, targetReleases, sourceReleaseNameToChangesMap, targetReleaseNameToChangesMap);
}
/**
* @param sourceRelease
* @param targetReleases
* @param sourceReleaseNameToChangesMap
* @param targetReleaseNameToChangesMap
*/
private void fillReleases(final Release sourceRelease, final List<Release> targetReleases,
final Map<String, List<Change>> sourceReleaseNameToChangesMap,
final Map<String, List<Change>> targetReleaseNameToChangesMap) {
final Set<String> sourceReleaseNames = new TreeSet<String>(RELEASE_COMPERATOR);
sourceReleaseNames.addAll(sourceReleaseNameToChangesMap.keySet());
final Set<String> targetReleaseNames = new TreeSet<String>(RELEASE_COMPERATOR);
targetReleaseNames.addAll(targetReleaseNameToChangesMap.keySet());
for (final String release : sourceReleaseNames) {
final List<Change> list = sourceReleaseNameToChangesMap.get(release);
sourceRelease.getChanges().addAll(list);
sourceRelease.setDate(new Date());
sourceRelease.setLabel("source"); //$NON-NLS-1$
}
for (final String release : targetReleaseNames) {
final Release targetRelease = HistoryFactory.eINSTANCE.createRelease();
targetRelease.setDate(new Date());
targetRelease.setLabel(release);
targetReleases.add(targetRelease);
final List<Change> list = targetReleaseNameToChangesMap.get(release);
targetRelease.getChanges().addAll(list);
// move custom migrations to end of target release, because every custom migration issues a validation.
// TODO it would be great if we could config edapt to not validate after each custom migration.
final List<Integer> indexes = new ArrayList<Integer>();
for (int i = 0; i < targetRelease.getChanges().size(); i++) {
final Change currentChange = targetRelease.getChanges().get(i);
if (MigrationChange.class.isInstance(currentChange)) {
indexes.add(i);
}
}
final int lastIndex = targetRelease.getChanges().size() - 1;
for (int i = indexes.size() - 1; i > -1; i--) {
final Change changeToMove = targetRelease.getChanges().get(indexes.get(i));
targetRelease.getChanges().move(lastIndex, changeToMove);
}
}
}
/**
* @param resourceURI
* @param domainMapping
* @return
*/
private List<String> getNamespaceURIsWithHistory(final URI resourceURI, final NSURIMapping domainMapping) {
final List<String> nsUris = getNamespaceURIs(resourceURI);
for (final String nsURI : domainMapping.getNsURIsWithGeneratedHistory()) {
nsUris.remove(nsURI);
}
nsUris.addAll(domainMapping.getNsURIsWithHistory());
return nsUris;
}
/**
* Returns the ns uris for which a history was created.
*
* @param resourceURI
* @return
*/
private NSURIMapping addChangesForDomainModel(URI resourceURI, Release release) {
// get rootpackage nsuri from view
// check if package is available in registry -> generate history
final Set<String> nsURIsWithHistory = new LinkedHashSet<String>();
final Set<String> nsURIsWithGeneratedHistory = new LinkedHashSet<String>();
final PackageDependencyGraph domainModelPackageGraph = new PackageDependencyGraph();
domainModelPackageGraph.addPackage(ECORE_NS_URI);
final Set<String> rootPackageURIs = getRootPackageURI(resourceURI);
for (String rootPackageNsUri : rootPackageURIs) {
final Migrator rootMigrator = MigratorRegistry.getInstance().getMigrator(rootPackageNsUri);
if (rootMigrator != null) {
for (final String migratorNSURI : rootMigrator.getNsURIs()) {
if (Registry.INSTANCE.containsKey(migratorNSURI)) {
nsURIsWithHistory.add(rootPackageNsUri);
rootPackageNsUri = migratorNSURI;
break;
}
}
}
domainModelPackageGraph.addPackage(rootPackageNsUri);
}
final List<EPackage> rootPackages = new ArrayList<EPackage>();
final Iterator<Set<String>> packageIterator = domainModelPackageGraph.getIerator();
while (packageIterator.hasNext()) {
final Set<String> nsURIs = packageIterator.next();
for (final String nsURI : nsURIs) {
/* there is no history available from the extensionpoint */
if (MigratorRegistry.getInstance().getMigrator(nsURI) == null) {
final EPackage ePackage = Registry.INSTANCE.getEPackage(nsURI);
rootPackages.add(ePackage);
nsURIsWithGeneratedHistory.add(nsURI);
} else {
nsURIsWithHistory.add(nsURI);
}
}
}
@SuppressWarnings("restriction")
final org.eclipse.emf.edapt.history.recorder.HistoryGenerator rootGenerator = new org.eclipse.emf.edapt.history.recorder.HistoryGenerator(
rootPackages);
@SuppressWarnings("restriction")
final List<Change> rootChanges = rootGenerator.generate().getFirstRelease().getChanges();
release.getChanges().addAll(rootChanges);
return new NSURIMapping(nsURIsWithHistory, nsURIsWithGeneratedHistory);
}
/**
* @param nsUri The name space URI of the package whose {@link Release} is wanted.
* @param migrator The {@link Migrator} which should provide the {@link Release}.
* @return
*/
private Release getReleaseFromMigrator(final String nsUri, final Migrator migrator) {
final Map<String, Set<Release>> releaseMap = migrator.getReleaseMap();
final Set<Release> nsReleases = releaseMap.get(nsUri);
/* in case multiple are found pick the newest one */
return nsReleases.stream()
.sorted(this::compareReleases)
.findFirst()
.orElse(null);
}
private int compareReleases(Release r1, Release r2) {
return r2.getNumber() - r1.getNumber();
}
/**
* @return the namespaces of all models used in the given resource.
*/
private List<String> getNamespaceURIs(URI resourceURI) {
@SuppressWarnings("restriction")
final File file = org.eclipse.emf.edapt.internal.common.URIUtils.getJavaFile(resourceURI);
// read all namespaces from root element with SAX
final NameSpaceHandler handler = new NameSpaceHandler();
executeContentHandler(file, handler);
return handler.getNsURIs();
}
/**
* @return the namespaces of all models used in the given resource.
*/
private Set<String> getRootPackageURI(URI resourceURI) {
@SuppressWarnings("restriction")
final File file = org.eclipse.emf.edapt.internal.common.URIUtils.getJavaFile(resourceURI);
// read root package namespace uri with SAX
final RootPackageHandler handler = new RootPackageHandler();
executeContentHandler(file, handler);
if (handler.foundRootEClass()) {
return Collections.singleton(handler.getRootPackageURI());
}
final RootPackageCalculationHandler calcHandler = new RootPackageCalculationHandler();
executeContentHandler(file, calcHandler);
return calcHandler.getUsedPackages();
}
private static void executeContentHandler(File file, final DefaultHandler contentHandler) {
try {
SAXUtil.executeContentHandler(new FileReader(file), contentHandler);
} catch (final FileNotFoundException ex) {
}
}
/** Register all migrators from extensions. */
private void reregisterExtensionMigrators() {
final IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
final IConfigurationElement[] configurationElements = extensionRegistry
.getConfigurationElementsFor("org.eclipse.emf.edapt.migrators"); //$NON-NLS-1$
for (final IConfigurationElement configurationElement : configurationElements) {
registerExtensionMigrator(configurationElement);
}
}
/** Register migrator for one extension. */
@SuppressWarnings("restriction")
private void registerExtensionMigrator(
IConfigurationElement configurationElement) {
final String migrationPath = configurationElement.getAttribute("path"); //$NON-NLS-1$
final IContributor contributor = configurationElement.getContributor();
final String bundleName = contributor.getName();
final Bundle bundle = Platform.getBundle(bundleName);
final URI migratorURI = URI.createPlatformPluginURI("/" + bundleName + "/" //$NON-NLS-1$ //$NON-NLS-2$
+ migrationPath, true);
try {
MigratorRegistry.getInstance().registerMigrator(migratorURI,
new org.eclipse.emf.edapt.internal.migration.execution.internal.BundleClassLoader(bundle));
} catch (final MigrationException e) {
org.eclipse.emf.edapt.internal.common.LoggingUtils.logError(MigrationPlugin.getPlugin(), e);
}
}
/** Content handler for extraction of the root package namespace URI using SAX. */
private static class RootPackageHandler extends DefaultHandler {
/** The root package's namespace URI. */
private String rootPackageURI = ""; //$NON-NLS-1$
private boolean rootEClassFound;
/**
* {@inheritDoc}
*
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String,
* org.xml.sax.Attributes)
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
super.startElement(uri, localName, qName, attributes);
if (localName.equals("rootEClass")) { //$NON-NLS-1$
final String rawUri = attributes.getValue("href"); //$NON-NLS-1$
rootPackageURI = rawUri.split("#")[0]; //$NON-NLS-1$
rootEClassFound = true;
throw new SAXException();
}
}
/**
* Whether an element with the rootEClass attribute has been found.
*
* @return <code>true</code> if found, <code>false</code> otherwise
*/
public boolean foundRootEClass() {
return rootEClassFound;
}
/** Returns the root package's namespace URI. */
public String getRootPackageURI() {
return rootPackageURI;
}
}
/**
* Content handler for finding all used NS-URIs in a view model.
*
* @author jfaltermeier
*
*/
private static class RootPackageCalculationHandler extends DefaultHandler {
/** The root package's namespace URI. */
private final Set<String> packages = new LinkedHashSet<String>();
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
super.startElement(uri, localName, qName, attributes);
final String href = attributes.getValue("href"); //$NON-NLS-1$
final String type = attributes.getValue("xsi:type"); //$NON-NLS-1$
if (href == null || type == null) {
return;
}
if (!type.endsWith("EAttribute") && !type.endsWith("EReference")) { //$NON-NLS-1$ //$NON-NLS-2$
return;
}
packages.add(href.split("#")[0]); //$NON-NLS-1$
}
/** Returns the root package's namespace URI. */
public Set<String> getUsedPackages() {
return packages;
}
}
/**
* Mapping for domain nsURIs.
*
* @author jfaltermeier
*
*/
private static class NSURIMapping {
private final Set<String> nsURIsWithHistory;
private final Set<String> nsURIsWithGeneratedHistory;
/**
* Default constructor.
*
* @param nsURIsWithHistory ns uris of models with a history available at the extension point
* @param nsURIsWithGeneratedHistory ns uris of models for which we generated a history
*/
NSURIMapping(Set<String> nsURIsWithHistory, Set<String> nsURIsWithGeneratedHistory) {
this.nsURIsWithHistory = nsURIsWithHistory;
this.nsURIsWithGeneratedHistory = nsURIsWithGeneratedHistory;
}
public Set<String> getNsURIsWithHistory() {
return nsURIsWithHistory;
}
public Set<String> getNsURIsWithGeneratedHistory() {
return nsURIsWithGeneratedHistory;
}
}
}