/* * Copyright (c) 2007, 2009 Borland Software Corporation * * 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 */ package org.eclipse.gmf.internal.xpand.ant; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; import java.util.Set; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EcorePackage; import org.eclipse.emf.ecore.EPackage.Registry; import org.eclipse.emf.ecore.impl.EPackageRegistryImpl; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.gmf.internal.xpand.Activator; import org.eclipse.gmf.internal.xpand.BufferOutput; import org.eclipse.gmf.internal.xpand.BuiltinMetaModel; import org.eclipse.gmf.internal.xpand.ResourceManager; import org.eclipse.gmf.internal.xpand.StreamsHolder; import org.eclipse.gmf.internal.xpand.model.AmbiguousDefinitionException; import org.eclipse.gmf.internal.xpand.model.EvaluationException; import org.eclipse.gmf.internal.xpand.model.ExecutionContext; import org.eclipse.gmf.internal.xpand.model.ExecutionContextImpl; import org.eclipse.gmf.internal.xpand.model.Scope; import org.eclipse.gmf.internal.xpand.model.Variable; import org.eclipse.gmf.internal.xpand.model.XpandDefinition; import org.eclipse.gmf.internal.xpand.util.BundleResourceManager; import org.eclipse.gmf.internal.xpand.xtend.ast.QvtResource; import org.eclipse.m2m.internal.qvt.oml.expressions.Module; import org.eclipse.m2m.internal.qvt.oml.runtime.util.OCLEnvironmentWithQVTAccessFactory; import org.eclipse.ocl.ParserException; import org.eclipse.ocl.ecore.EcoreEvaluationEnvironment; import org.eclipse.ocl.ecore.EcoreFactory; import org.eclipse.ocl.ecore.OCL; import org.eclipse.ocl.ecore.OCLExpression; import org.eclipse.ocl.ecore.OCL.Helper; import org.eclipse.ocl.ecore.OCL.Query; import org.osgi.framework.Bundle; /** * Redistributable API for Xpand evaluation * @author artem */ public final class XpandFacade { private static final String IMPLICIT_VAR_NAME = "self"; //$NON-NLS-1$ private static final String IMPLICIT_VAR_NAME_BACKWARD_COMPATIBILITY = "this"; //$NON-NLS-1$ private final LinkedList<Variable> myGlobals = new LinkedList<Variable>(); private final LinkedList<URL> myLocations = new LinkedList<URL>(); private final LinkedList<String> myImportedModels = new LinkedList<String>(); private final LinkedList<String> myExtensionFiles = new LinkedList<String>(); private boolean myEnforceReadOnlyNamedStreamsAfterAccess = false; private ExecutionContext myXpandCtx; private BufferOutput myBufferOut; private HashMap<Object, StreamsHolder> myStreamsHolders; private final StringBuilder myOut = new StringBuilder(); private Map<String, URI> myMetamodelURI2LocationMap = new HashMap<String, URI>(); private final ResourceSet myResourceSet; private Map<String, URI> mySchemaLocations; public XpandFacade(ResourceSet resourceSet) { myResourceSet = resourceSet; } /** * Sort of copy constructor, create a new facade pre-initialized with values * of existing one. * @param chain facade to copy settings (globals, locations, metamodels, extensions, loaders) from, can't be <code>null</code>. */ public XpandFacade(XpandFacade chain) { assert chain != null; myGlobals.addAll(chain.myGlobals); myLocations.addAll(chain.myLocations); myImportedModels.addAll(chain.myImportedModels); myExtensionFiles.addAll(chain.myExtensionFiles); // // not necessary, but doesn't seem to hurt myXpandCtx = chain.myXpandCtx; // new state is formed with cloning myResourceSet = chain.myResourceSet; myMetamodelURI2LocationMap = chain.myMetamodelURI2LocationMap; mySchemaLocations = chain.mySchemaLocations; } /** * Named streams (those created by <<FILE file slotName>> syntax) may be put into a strict mode that prevents write operations * after the contents of the stream have been accessed. By default, named streams are not in strict mode. * @param value */ public void setEnforceReadOnlyNamedStreamsAfterAccess(boolean value) { myEnforceReadOnlyNamedStreamsAfterAccess = value; } public void addGlobal(String name, Object value) { assert name != null; for (Iterator<Variable> it = myGlobals.listIterator(); it.hasNext();) { if (it.next().getName().equals(name)) { it.remove(); } } if (name == null || value == null) { return; } myGlobals.addFirst(new Variable(name, null, value)); clearAllContexts(); } public void addLocation(String url) throws MalformedURLException { addLocation(new URL(url)); } public void addLocation(URL url) { assert url != null; myLocations.addLast(url); clearAllContexts(); } public void registerMetamodel(String nsUri, URI location) { if (!myMetamodelURI2LocationMap.containsKey(nsUri)) { myMetamodelURI2LocationMap.put(nsUri, location); } } public void setSchemaLocations(Map<String, URI> schemaLocations) { mySchemaLocations = schemaLocations; } /** * Registers a class loader to load Java classes accessed from templates and/or expressions. * @param loader ClassLoader to load classes though * @deprecated QVT-based dialect of Xpand does not use classload contexts. */ @Deprecated public void addClassLoadContext(ClassLoader loader) { //do nothing } /** * Register a bundle to load Java classes from (i.e. JAVA functions in Xtend) * @param bundle - generally obtained from {@link org.eclipse.core.runtime.Platform#getBundle(String)}, should not be null. * @deprecated QVT-based dialect of Xpand does not use classload contexts. */ @Deprecated public void addClassLoadContext(Bundle bundle) { //do nothing } public void addMetamodel(String metamodel) { if (myImportedModels.contains(metamodel)) { return; } myImportedModels.add(metamodel); } /** * @param extensionFile double-colon separated qualified name of qvto file */ public void addExtensionFile(String extensionFile) { if (myExtensionFiles.contains(extensionFile)) { return; } myExtensionFiles.add(extensionFile); } public <T> T evaluate(String expression, Object target) { // XXX perhaps, need to check for target == null and do not set 'this' then return evaluate(expression, Collections.singletonMap("self", target)); } /** * @param expression xtend expression to evaluate * @param context should not be <code>null</code> * @return */ @SuppressWarnings("unchecked") public <T> T evaluate(String expression, Map<String,?> context) { assert context != null; // nevertheless, prevent NPE. ResourceManager rm; if (myLocations.isEmpty()) { try { // use current default path as root // use canonicalFile to get rid of dot after it get resolved to // current dir rm = new BundleResourceManager(new File(".").getCanonicalFile().toURI().toURL()); } catch (IOException ex) { // should not happen rm = null; } } else { rm = new BundleResourceManager(myLocations.toArray(new URL[myLocations.size()])); } Set<Module> importedModules = getImportedModules(rm); OCLEnvironmentWithQVTAccessFactory factory = new OCLEnvironmentWithQVTAccessFactory(importedModules, getAllVisibleModels()); OCL ocl = OCL.newInstance(factory); Object thisValue = null; if (context != null) { for (Map.Entry<String, ?> nextEntry : context.entrySet()) { String varName = nextEntry.getKey(); Object varValue = nextEntry.getValue(); if (IMPLICIT_VAR_NAME.equals(varName) || IMPLICIT_VAR_NAME_BACKWARD_COMPATIBILITY.equals(varName)) { assert thisValue == null; //prevent simultaneous this and self thisValue = varValue; continue; } EClassifier varType = BuiltinMetaModel.getType(getXpandContext(), varValue); org.eclipse.ocl.ecore.Variable oclVar = EcoreFactory.eINSTANCE.createVariable(); oclVar.setName(varName); oclVar.setType(varType); ocl.getEnvironment().addElement(varName, oclVar, true); } } Helper oclHelper = ocl.createOCLHelper(); if (thisValue != null) { oclHelper.setContext(BuiltinMetaModel.getType(getXpandContext(), thisValue)); } else { oclHelper.setContext(ocl.getEnvironment().getOCLStandardLibrary().getOclVoid()); } OCLExpression exp; try { exp = oclHelper.createQuery(expression); } catch (ParserException e) { // e.printStackTrace(); throw new EvaluationException(e); } Query query = ocl.createQuery(exp); EcoreEvaluationEnvironment ee = (EcoreEvaluationEnvironment) query.getEvaluationEnvironment(); if (context != null) { for (Map.Entry<String, ?> nextEntry : context.entrySet()) { String varName = nextEntry.getKey(); Object varValue = nextEntry.getValue(); if (!IMPLICIT_VAR_NAME.equals(varName) && !IMPLICIT_VAR_NAME_BACKWARD_COMPATIBILITY.equals(varName)) { ee.add(varName, varValue); } } } Object result; if (thisValue != null) { result = query.evaluate(thisValue); } else { result = query.evaluate(); } if (result == ocl.getEnvironment().getOCLStandardLibrary().getOclInvalid()) { return null; //XXX: or throw an exception? } return (T) result; } private Set<Module> getImportedModules(ResourceManager rm) { Set<Module> result = new HashSet<Module>(); for (String extensionFile : myExtensionFiles) { QvtResource qvtResource = rm.loadQvtResource(extensionFile); result.addAll(qvtResource.getModules()); } return result; } private EPackage.Registry getAllVisibleModels() { assert myImportedModels != null; // TODO respect meta-models imported not only with nsURI EPackage.Registry result = new EPackageRegistryImpl(); for (String namespace : myImportedModels) { EPackage pkg = Activator.findMetaModel(namespace); if (pkg != null) { result.put(namespace, pkg); } } if (result.isEmpty()) { // hack for tests result.put(EcorePackage.eNS_URI, EcorePackage.eINSTANCE); } return result; } public String xpand(String templateName, Object target, Object... arguments) { if (target == null) { return null; } clearOut(); ExecutionContext ctx = getXpandContext(); try { new org.eclipse.gmf.internal.xpand.XpandFacade(ctx).evaluate(templateName, target, arguments); } catch (AmbiguousDefinitionException e) { throw new EvaluationException(e); } return myOut.toString(); } public Map<Object, String> xpand(String templateName, Collection<?> target, Object... arguments) { // though it's reasonable to keep original order of input elements, // is it worth declaring in API? LinkedHashMap<Object, String> inputToResult = new LinkedHashMap<Object, String>(); boolean invokeForCollection = findDefinition(templateName, target, arguments) != null; if (invokeForCollection) { inputToResult.put(target, xpand(templateName, (Object)target, arguments)); return inputToResult; } myStreamsHolders = new HashMap<Object, StreamsHolder>(); for (Object nextInput: target) { if (nextInput == null) { continue; } String result = xpand(templateName, nextInput, arguments); inputToResult.put(nextInput, result); myStreamsHolders.put(nextInput, myBufferOut.getNamedStreams()); } return inputToResult; } /** * Returns names of named streams that were created during the most recent {@link #xpand(String, Object, Object...) operation and have non-empty contents. * @return */ public Collection<String> getNamedStreams() { assert myStreamsHolders == null; //if invoked for several elements separately, another version of this method should be used. if (myBufferOut == null) { return Collections.emptyList(); } return myBufferOut.getNamedStreams().getSlotNames(); } /** * Returns contents of the named stream that was created during the most recent {@link #xpand(String, Object, Object...) operation. * If the stream with the given name does not exist, the operation will throw an exception. * @param streamName * @return */ public String getNamedStreamContents(String streamName) { assert myStreamsHolders == null; //if invoked for several elements separately, another version of this method should be used. if (myBufferOut == null) { throw new UnsupportedOperationException("Stream with the given name does not exist", null); } return myBufferOut.getNamedStreams().getStreamContents(streamName); } /** * Returns names of non-empty named streams that were created during the most recent {@link #xpand(String, Collection, Object...)} operation for the given input. * @return */ public Collection<String> getNamedStreams(Object input) { if (myStreamsHolders == null) { //assume this is the input that was used during the last invocation, but do not enforce this. return getNamedStreams(); } StreamsHolder streamsHolder = myStreamsHolders.get(input); if (streamsHolder == null) { return Collections.emptyList(); } return streamsHolder.getSlotNames(); } /** * Returns contents of the named stream that was created during the most recent {@link #xpand(String, Collection, Object...) operation. * If the stream with the given name does not exist, the operation will throw an exception. * @param streamName * @return */ public String getNamedStreamContents(Object input, String streamName) { if (myStreamsHolders == null) { //assume this is the input that was used during the last invocation, but do not enforce this. return getNamedStreamContents(streamName); } StreamsHolder streamsHolder = myStreamsHolders.get(input); if (streamsHolder == null) { throw new UnsupportedOperationException("Stream with the given name does not exist", null); } return streamsHolder.getStreamContents(streamName); } private XpandDefinition findDefinition(String templateName, Object target, Object[] arguments) { EClassifier targetType = BuiltinMetaModel.getType(getXpandContext(), target); final EClassifier[] paramTypes = new EClassifier[arguments == null ? 0 : arguments.length]; for (int i = 0; i < paramTypes.length; i++) { paramTypes[i] = BuiltinMetaModel.getType(getXpandContext(), arguments[i]); } try { return getXpandContext().findDefinition(templateName, targetType, paramTypes); } catch (AmbiguousDefinitionException e) { return null; } } private void clearAllContexts() { myXpandCtx = null; } private void clearOut() { myOut.setLength(200); myOut.trimToSize(); myOut.setLength(0); //To clear streams, we have no other option but to reset the xpand context myXpandCtx = null; myBufferOut = null; } private ExecutionContext getXpandContext() { if (myXpandCtx == null) { BundleResourceManager rm = new BundleResourceManager(myLocations.toArray(new URL[myLocations.size()])) { @Override protected ResourceSet getMetamodelResourceSet() { return myResourceSet; } }; myBufferOut = new BufferOutput(myOut, myEnforceReadOnlyNamedStreamsAfterAccess); Scope scope = new Scope(rm, myGlobals, myBufferOut) { @Override public Registry createPackageRegistry(String[] metamodelURIs) { assert metamodelURIs != null; EPackage.Registry result = new EPackageRegistryImpl(); for (String namespace : metamodelURIs) { EPackage pkg; if (myMetamodelURI2LocationMap.containsKey(namespace)) { pkg = loadMainEPackage(myMetamodelURI2LocationMap.get(namespace)); } else if (EPackage.Registry.INSTANCE.containsKey(namespace)) { pkg = EPackage.Registry.INSTANCE.getEPackage(namespace); } else { URI metamodelURI = mySchemaLocations.get(namespace); Resource resource = myResourceSet.getResource(metamodelURI, true); if (resource.getContents().size() > 0 && resource.getContents().get(0) instanceof EPackage) { pkg = (EPackage) resource.getContents().get(0); } else { pkg = null; } } if (pkg != null) { result.put(namespace, pkg); } } return result; } }; myXpandCtx = new ExecutionContextImpl(scope); } return myXpandCtx; } private EPackage loadMainEPackage(URI uri) { Resource resource = myResourceSet.getResource(uri, true); if (resource.getContents().size() > 0 && resource.getContents().get(0) instanceof EPackage) { return (EPackage) resource.getContents().get(0); } return null; } }