| /* |
| * 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; |
| } |
| |
| } |