blob: 7fdf121691204a7bc0db1f9305ff28971429f0c9 [file] [log] [blame]
/*
* 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;
}
}