/** * Copyright (c) 2006-2012 IBM Corporation and others * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM - Initial API and implementation */ package org.eclipse.emf.ecore.xcore.exporter; import java.io.BufferedInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.emf.codegen.ecore.genmodel.GenBase; import org.eclipse.emf.codegen.ecore.genmodel.GenClassifier; import org.eclipse.emf.codegen.ecore.genmodel.GenFeature; import org.eclipse.emf.codegen.ecore.genmodel.GenModel; import org.eclipse.emf.codegen.ecore.genmodel.GenModelFactory; import org.eclipse.emf.codegen.ecore.genmodel.GenModelPackage; import org.eclipse.emf.codegen.ecore.genmodel.GenPackage; import org.eclipse.emf.codegen.util.ImportManager; import org.eclipse.emf.common.util.BasicDiagnostic; import org.eclipse.emf.common.util.Diagnostic; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.Monitor; import org.eclipse.emf.common.util.TreeIterator; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EDataType; import org.eclipse.emf.ecore.EGenericType; import org.eclipse.emf.ecore.EModelElement; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EPackage.Registry; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.plugin.EcorePlugin; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.xcore.XGenericType; import org.eclipse.emf.ecore.xcore.XImportDirective; import org.eclipse.emf.ecore.xcore.XPackage; import org.eclipse.emf.ecore.xcore.XReference; import org.eclipse.emf.ecore.xcore.XcoreFactory; import org.eclipse.emf.ecore.xcore.XcorePackage; import org.eclipse.emf.ecore.xcore.mappings.XcoreMapper; import org.eclipse.emf.ecore.xcore.scoping.XcoreImportedNamespaceAwareScopeProvider; import org.eclipse.emf.ecore.xcore.ui.quickfix.XcoreClasspathUpdater; import org.eclipse.emf.ecore.xcore.util.EcoreXcoreBuilder; import org.eclipse.emf.ecore.xcore.util.XcoreGenModelBuilder; import org.eclipse.emf.ecore.xcore.util.XcoreGenModelInitializer; import org.eclipse.emf.exporter.ModelExporter; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.xtext.formatting2.FormatterPreferenceValuesProvider; import org.eclipse.xtext.formatting2.FormatterRequest; import org.eclipse.xtext.formatting2.IFormatter2; import org.eclipse.xtext.formatting2.regionaccess.ITextReplacement; import org.eclipse.xtext.formatting2.regionaccess.internal.NodeModelBasedRegionAccessBuilder; import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.naming.IQualifiedNameProvider; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.parser.IParseResult; import org.eclipse.xtext.preferences.IPreferenceValues; import org.eclipse.xtext.preferences.TypedPreferenceValues; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.resource.SaveOptions; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.scoping.IScope; import org.eclipse.xtext.scoping.IScopeProvider; import org.eclipse.xtext.ui.XtextProjectHelper; import org.eclipse.xtext.util.RuntimeIOException; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Provider; public class XcoreExporter extends ModelExporter { private static String SAVE_FAMILY = "Save"; public static Diagnostic RETRY = new BasicDiagnostic(); @Inject Provider<EcoreXcoreBuilder> ecoreXcoreBuilderProvider; @Inject XcoreGenModelBuilder genModelBuilder; @Inject XcoreGenModelInitializer genModelInitializer; @Inject XcoreMapper mapper; @Inject private IScopeProvider scopeProvider; @Inject private IQualifiedNameProvider qualifiedNameProvider; @Inject private IQualifiedNameConverter qualifiedNameConverter; @Inject protected IFormatter2 formatter; @Inject private FormatterPreferenceValuesProvider preferencesProvider; @Inject private Provider<ResourceSet> resourceSetProvider; private static final Set<String> IMPLICIT_ALIASES = Sets.newHashSet(); static { for (EDataType eDataType : XcoreImportedNamespaceAwareScopeProvider.IMPLICIT_ALIASES) { IMPLICIT_ALIASES.add("org.eclipse.emf.ecore." + eDataType.getName()); } } @Override public String getID() { return "org.eclipse.emf.ecore.xcore.exporter"; } @Override public void dispose() { super.dispose(); } @Override protected String getDefaultArtifactLocation(EPackage ePackage) { return getEPackageToGenPackageMap().get(ePackage).getPrefix() + ".xcore"; } @Override protected String doCheckEPackageArtifactLocation(String location, String packageName) { if (!location.endsWith(".xcore")) { return XcoreExporterPlugin.INSTANCE.getString("_UI_InvalidArtifactFileNameExtension_message"); } return super.doCheckEPackageArtifactLocation(location, packageName); } @Override protected Diagnostic doExport(Monitor monitor, ModelExporter.ExportData exportData) throws Exception { // Add natures and libraries if necessary. // Because the builder needs to run to update the index, if we do add a library, we need to return early to allow the workspace modify operation to complete. // After that's done, we want to retry, at which point there should be no libraries added the second time. // boolean retry = false; for (URI xcoreLocationURI : exportData.genPackageToArtifactURI.values()) { // Add the Xtext nature if it's absent. // IFile xcoreFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(xcoreLocationURI.toPlatformString(true))); final IProject xcoreProject = xcoreFile.getProject(); if (xcoreProject.isAccessible()) { if (!xcoreProject.hasNature(XtextProjectHelper.NATURE_ID)) { IProjectDescription description = xcoreProject.getDescription(); String[] natures = description.getNatureIds(); String[] newNatures = new String[natures.length + 1]; System.arraycopy(natures, 0, newNatures, 0, natures.length); newNatures[natures.length] = XtextProjectHelper.NATURE_ID; description.setNatureIds(newNatures); xcoreProject.setDescription(description, null); } XcoreClasspathUpdater xcoreClasspathUpdater = new XcoreClasspathUpdater(); IJavaProject xcoreJavaProject = JavaCore.create(xcoreProject); retry = xcoreClasspathUpdater.addBundle(xcoreJavaProject, "org.eclipse.emf.ecore.xcore.lib", null) || retry; retry = xcoreClasspathUpdater.addBundle(xcoreJavaProject, "org.eclipse.xtext.xbase.lib", null) || retry; } } if (retry) { return RETRY; } final List<Runnable> runnables = new ArrayList<Runnable>(); for (Map.Entry<GenPackage, URI> entry : exportData.genPackageToArtifactURI.entrySet()) { GenPackage genPackage = entry.getKey(); URI xcoreLocationURI = entry.getValue(); // Create an appropriate resource set for Xcore models. // final ResourceSet resourceSet = resourceSetProvider.get(); final Map<URI, URI> uriMap = resourceSet.getURIConverter().getURIMap(); Registry packageRegistry = resourceSet.getPackageRegistry(); uriMap.putAll(EcorePlugin.computePlatformURIMap(true)); // Load a clone of the GenModel in the new resource set. // GenModel inputGenModel = (GenModel)resourceSet.getEObject(EcoreUtil.getURI(getGenModel()), true); inputGenModel.reconcile(); Resource inputResource = inputGenModel.eResource(); final XtextResource outputResource = (XtextResource)resourceSet.createResource(xcoreLocationURI); // Create a fresh new GenModel for the packages we just loaded. // GenModel genModel = GenModelFactory.eINSTANCE.createGenModel(); List<EPackage> ePackageClones = new ArrayList<EPackage>(); for (GenPackage inputGenPackage : inputGenModel.getGenPackages()) { ePackageClones.add(inputGenPackage.getEcorePackage()); } genModel.initialize(ePackageClones); // Add it to the resource and initialize it like we do any other new GenModel // inputResource.getContents().add(genModel); genModelInitializer.initialize(genModel, true); // Walk the two GenModels in lockstep and compare settings. // We need to know which ones should be converted to annotations and which would be redundant because they represent defaults. // final GenPackage inputGenPackage = (GenPackage)resourceSet.getEObject(EcoreUtil.getURI(genPackage), true); new Object() { void visit(GenBase genBase1, GenBase genBase2) { if (genBase1.eClass() == genBase2.eClass()) { // TODO handle multi-valued attribues and references. // for (EAttribute eAttribute : genBase1.eClass().getEAllAttributes()) { if (!eAttribute.isMany() && genBase1.eIsSet(eAttribute)) { Object value1 = genBase1.eGet(eAttribute); Object value2 = genBase2.eGet(eAttribute); if (value1 == null ? value2 != null : !value1.equals(value2)) { // For the case of the GenModel, we record its annotations on the package. // EModelElement eModelElement = genBase2.getEcoreModelElement(); if (eModelElement == null && genBase2 instanceof GenModel) { eModelElement = inputGenPackage.getEcorePackage(); } EcoreUtil.setAnnotation(eModelElement, GenModelPackage.eNS_URI, eAttribute.getName(), EcoreUtil.convertToString(eAttribute.getEAttributeType(), value1)); } for (Iterator<EObject> i = genBase1.eContents().iterator(), j = genBase2.eContents().iterator(); i.hasNext() && j.hasNext();) { EObject content1 = i.next(); EObject content2 = j.next(); if (content1 instanceof GenBase && content2 instanceof GenBase) { visit((GenBase)content1, (GenBase)content2); } } } } } } }.visit(inputGenModel, genModel); // Use the builder to create an appropriate Xcore instance for populating the output resource. // EcoreXcoreBuilder ecoreXcoreBuilder = ecoreXcoreBuilderProvider.get(); ecoreXcoreBuilder.initialize(inputGenModel); XPackage xPackage = ecoreXcoreBuilder.getXPackage(inputGenPackage.getEcorePackage()); outputResource.getContents().add(xPackage); outputResource.getContents().add(inputGenModel); outputResource.getContents().add(inputGenPackage.getEcorePackage()); // Put all the specialize internal GenPackages in appropriately named resources so the serializer can resolve them. // Xtext complains about duplicate resources in the resource set if we put them in the original resource set. // ResourceSet additionalResources = resourceSetProvider.get(); GenPackage ecoreGenPackage = inputGenModel.getEcoreGenPackage(); if (ecoreGenPackage != null) { URI ecoreResourceURI = URI.createPlatformResourceURI("/org.eclipse.emf.ecore/model/Ecore.genmodel", false); packageRegistry.put(ecoreResourceURI.toString(), ecoreGenPackage.getEcorePackage()); Resource ecoreResource = additionalResources.createResource(ecoreResourceURI); ecoreResource.getContents().add(ecoreGenPackage.getGenModel()); } GenPackage xmlTypeGenPackage = inputGenModel.getXMLTypeGenPackage(); if (xmlTypeGenPackage != null) { URI xmlTypeResourceURI = URI.createPlatformResourceURI("/org.eclipse.emf.ecore/model/XMLType.genmodel", false); packageRegistry.put(xmlTypeResourceURI.toString(), xmlTypeGenPackage.getEcorePackage()); Resource xmlTypeResource = additionalResources.createResource(xmlTypeResourceURI); xmlTypeResource.getContents().add(xmlTypeGenPackage.getGenModel()); } GenPackage xmlNamespaceGenPackage = inputGenModel.getXMLNamespaceGenPackage(); if (xmlNamespaceGenPackage != null) { URI xmlNamespaceResourceURI = URI.createPlatformResourceURI("/org.eclipse.emf.ecore/model/XMLNamespace.genmodel", false); packageRegistry.put(xmlNamespaceResourceURI.toString(), xmlNamespaceGenPackage.getEcorePackage()); Resource xmlNamespaceResource = additionalResources.createResource(xmlNamespaceResourceURI); xmlNamespaceResource.getContents().add(xmlNamespaceGenPackage.getGenModel()); } // Do the final linking step and build the map. // ecoreXcoreBuilder.link(); genModelBuilder.buildMap(inputGenModel); // Use an import manager to create imports for all the things we reference in the Xcore instance. // ImportManager importManager = new ImportManager(inputGenPackage.getInterfacePackageName()) { @Override protected boolean shouldImport(String packageName, String shortName, String importName) { return true; } }; Map<EGenericType, XGenericType> genericTypeMap = ecoreXcoreBuilder.getGenericTypeMap(); for (TreeIterator<EObject> i = inputGenPackage.getEcorePackage().eAllContents(); i.hasNext();) { EObject eObject = i.next(); if (eObject instanceof EGenericType) { EGenericType eGenericType = (EGenericType)eObject; EClassifier eClassifier = eGenericType.getEClassifier(); if (eClassifier != null) { GenClassifier genClassifier = inputGenModel.findGenClassifier(eClassifier); QualifiedName qualifiedName = qualifiedNameProvider.getFullyQualifiedName(genClassifier); String qualifiedNameValue = qualifiedNameConverter.toString(qualifiedName); if (!IMPLICIT_ALIASES.contains(qualifiedNameValue)) { importManager.addImport(qualifiedNameValue); } if (genClassifier.eResource() != outputResource) { // We need to ensure that if we resolve to a different instance, we switch to use that so that serialization will find the right instance. // XGenericType xGenericType = genericTypeMap.get(eGenericType); IScope scope = scopeProvider.getScope(xGenericType, XcorePackage.Literals.XGENERIC_TYPE__TYPE); IEObjectDescription genClassifierDescription = scope.getSingleElement(qualifiedName); if (genClassifierDescription != null) { EObject resolvedGenClassifier = resourceSet.getEObject(genClassifierDescription.getEObjectURI(), true); if (resolvedGenClassifier != null && resolvedGenClassifier != genClassifier) { xGenericType.setType((GenClassifier)resolvedGenClassifier); } } } } } else if (eObject instanceof EReference) { EReference eReference = (EReference)eObject; XReference xReference = (XReference)mapper.getToXcoreMapping(eReference).getXcoreElement(); EList<GenFeature> keys = xReference.getKeys(); GenFeature opposite = xReference.getOpposite(); if (opposite != null && opposite.eResource() != outputResource) { IScope scope = scopeProvider.getScope(xReference, XcorePackage.Literals.XREFERENCE__OPPOSITE); IEObjectDescription genFeatureDescription = scope.getSingleElement(QualifiedName.create(opposite.getName())); if (genFeatureDescription != null) { EObject resolvedGenFeature = resourceSet.getEObject(genFeatureDescription.getEObjectURI(), true); if (resolvedGenFeature != null) { xReference.setOpposite((GenFeature)resolvedGenFeature); } } } if (!keys.isEmpty()) { IScope scope = scopeProvider.getScope(xReference, XcorePackage.Literals.XREFERENCE__KEYS); for (ListIterator<GenFeature> k = keys.listIterator(); k.hasNext(); ) { GenFeature key = k.next(); if (key.eResource() != outputResource) { IEObjectDescription genFeatureDescription = scope.getSingleElement(QualifiedName.create(key.getName())); if (genFeatureDescription != null) { EObject resolvedGenFeature = resourceSet.getEObject(genFeatureDescription.getEObjectURI(), true); if (resolvedGenFeature != null) { k.set((GenFeature)resolvedGenFeature); } } } } } } else if (eObject instanceof EPackage) { i.prune(); } } // Convert the needed imports to import directives. // for (String qualifiedName : importManager.getImports()) { XImportDirective xImportDirective = XcoreFactory.eINSTANCE.createXImportDirective(); xImportDirective.setImportedNamespace(qualifiedName); xPackage.getImportDirectives().add(xImportDirective); } // Save the final result. // final Map<Object, Object> options = new HashMap<Object, Object>(); SaveOptions.newBuilder().format().noValidation().getOptions().addTo(options); Runnable runnable = new Runnable() { @Override public void run() { try { // Temporarily clear the mappings so that the serializer properly finds the non-normalized URIs in the index. // Map<URI, URI> copyiedURIMap = new HashMap<URI, URI>(uriMap); uriMap.clear(); outputResource.save(options); uriMap.putAll(copyiedURIMap); outputResource.unload(); outputResource.load(null); IParseResult parseResult = outputResource.getParseResult(); if (parseResult != null) { IPreferenceValues configuration = preferencesProvider.getPreferenceValues(outputResource); try { FormatterRequest request = new FormatterRequest(); request.setTextRegionAccess(new NodeModelBasedRegionAccessBuilder().withResource(outputResource).create()); request.setPreferences(new TypedPreferenceValues(configuration)); request.addRegion(parseResult.getRootNode().getTotalTextRegion()); List<ITextReplacement> edits = formatter.format(request); BufferedInputStream bufferedInputStream = new BufferedInputStream(resourceSet.getURIConverter().createInputStream(outputResource.getURI())); byte[] input = new byte[bufferedInputStream.available()]; bufferedInputStream.read(input); bufferedInputStream.close(); String text = new String(input, outputResource.getEncoding()); StringBuilder builder = new StringBuilder(text); for (int i = edits.size(); --i >= 0; ) { ITextReplacement replacement = edits.get(i); int offset = replacement.getOffset(); builder.replace(offset, offset + replacement.getLength(), replacement.getReplacementText()); } OutputStream outputStream = resourceSet.getURIConverter().createOutputStream(outputResource.getURI()); outputStream.write(builder.toString().getBytes(outputResource.getEncoding())); outputStream.close(); } catch (Throwable throwable) { XcoreExporterPlugin.INSTANCE.log(throwable); } } } catch (IOException exception) { throw new RuntimeIOException(exception); } } }; runnables.add(runnable); } // Do this in a job so that Xtext nature we added has a chance to build the index needed by the serializer. // Job job = new Job("Save") { @Override public boolean belongsTo(Object family) { return family == SAVE_FAMILY; } @Override protected IStatus run(IProgressMonitor monitor) { try { IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceRunnable runnable = new IWorkspaceRunnable() { @Override public void run(IProgressMonitor monitor) throws CoreException { for (Runnable runnable : runnables) { runnable.run(); } } }; workspace.run(runnable, null, IWorkspace.AVOID_UPDATE, null); return Status.OK_STATUS; } catch (Exception exception) { return Status.CANCEL_STATUS; } } }; job.schedule(); return Diagnostic.OK_INSTANCE; } public void waitForSaveJob() { try { Job.getJobManager().join(SAVE_FAMILY, new NullProgressMonitor()); } catch (Throwable e) { // Ignore } } protected URI getReferencedGenPackageArtifactURI(ModelExporter.ExportData exportData, GenPackage genPackage) { URI artifactURI = exportData.referencedGenPackagesToArtifactURI.get(genPackage); if (artifactURI == null) { artifactURI = exportData.genPackageToArtifactURI.get(genPackage); if (artifactURI == null) { for (Map.Entry<GenPackage, URI> entry : exportData.referencedGenPackagesToArtifactURI.entrySet()) { GenPackage referencedGenPackage = entry.getKey(); if (genPackage.getNSURI().equals(referencedGenPackage.getNSURI()) && genPackage.getEcorePackage().getName().equals(referencedGenPackage.getEcorePackage().getName())) { artifactURI = entry.getValue(); } } } } return artifactURI; } @Override protected void adjustGenModel() { // Ignore all packages not actually contained in the GenModel. // We don't need information about where referenced GenPackages are located. // GenModel genModel = getGenModel(); for (ListIterator<EPackage> i = getEPackages().listIterator(); i.hasNext();) { if (!EcoreUtil.isAncestor(genModel, getEPackageToGenPackageMap().get(i.next()))) { i.remove(); } } super.adjustGenModel(); } }