blob: 434b1b51d713048e363cedca25a0ab771505a3e4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2020 Obeo.
* 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:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.acceleo.aql;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import org.eclipse.acceleo.ErrorMetamodel;
import org.eclipse.acceleo.Import;
import org.eclipse.acceleo.Metamodel;
import org.eclipse.acceleo.Module;
import org.eclipse.acceleo.ModuleElement;
import org.eclipse.acceleo.ModuleReference;
import org.eclipse.acceleo.OpenModeKind;
import org.eclipse.acceleo.Query;
import org.eclipse.acceleo.Template;
import org.eclipse.acceleo.aql.evaluation.AcceleoCallStack;
import org.eclipse.acceleo.aql.evaluation.AcceleoEvaluator;
import org.eclipse.acceleo.aql.evaluation.AcceleoQueryEnvironment;
import org.eclipse.acceleo.aql.evaluation.GenerationResult;
import org.eclipse.acceleo.aql.evaluation.QueryService;
import org.eclipse.acceleo.aql.evaluation.TemplateService;
import org.eclipse.acceleo.aql.evaluation.writer.IAcceleoGenerationStrategy;
import org.eclipse.acceleo.aql.evaluation.writer.IAcceleoWriter;
import org.eclipse.acceleo.aql.resolver.IQualifiedNameResolver;
import org.eclipse.acceleo.query.runtime.IQueryEnvironment;
import org.eclipse.acceleo.query.runtime.IService;
import org.eclipse.acceleo.query.runtime.ServiceUtils;
import org.eclipse.acceleo.query.runtime.impl.EPackageProvider;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EPackage;
/**
* This environment will keep track of Acceleo's evaluation context. TODO doc.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class AcceleoEnvironment implements IAcceleoEnvironment {
/** maps the modules registered against this environment with their qualified name. */
private Map<String, Module> qualifiedNameToModule;
/**
* Maps a {@link IAcceleoEnvironment#registerModule(String, Module) registered} {@link Module} to its
* qualified name.
*/
private final Map<Module, String> moduleToQualifiedName;
/**
* The mapping from the module qualified name to its extends.
*/
private final Map<String, String> moduleExtends;
/**
* The mapping from the module qualified name to its imports.
*/
private final Map<String, LinkedList<String>> moduleImports;
/** Keeps track of the services each qualified name provides, mapped to their names. */
private Map<String, Map<String, Set<IService>>> qualifiedNameServices;
/** The AQL environment that will be used to evaluate aql expressions from this Acceleo context. */
private IQueryEnvironment aqlEnvironment;
/** This will hold the writer stack for the file blocks. */
private final Deque<IAcceleoWriter> writers = new ArrayDeque<IAcceleoWriter>();
/** The current generation strategy. */
private final IAcceleoGenerationStrategy generationStrategy;
/**
* The destination {@link URI}.
*/
private final URI destination;
/**
* Keeps track of the module elements we've called in order. Template and queries we call will be pushed
* against this depending on "how" they were called.
* <p>
* Module elements called because they're the "main" template (starting point of an evaluation) or through
* regular calls within the extends hierarchy of a module will be pushed atop the latest stack, whereas
* those called because they're "imported" from the module of a previous call will be pushed atop a new
* stack.
* </p>
* <p>
* This will allow us to always know where the "current" evaluated point is according to the previous
* calls, which in turn will help us properly configure the lookup engine of the underlying
* {@link #aqlEnvironment}.
* </p>
*/
private final Deque<AcceleoCallStack> callStacks;
/**
* The {@link GenerationResult}.
*/
private final GenerationResult generationResult = new GenerationResult();
// TODO without a default value, the environment will not be able to resolve imported or extended modules.
// Can we set a default with the information we have at creation time?
/**
* The resolver for this environment.
* <p>
* This will be used whenever a module tries to access a qualified name, such as import or extends.
* </p>
*/
private IQualifiedNameResolver resolver;
/**
* The {@link AcceleoEvaluator}.
*/
private AcceleoEvaluator evaluator;
/**
* Constructor.
*
* @param generationStrategy
* the {@link IAcceleoGenerationStrategy}
* @param destination
* the destination {@link URI}
*/
public AcceleoEnvironment(IAcceleoGenerationStrategy generationStrategy, URI destination) {
this.generationStrategy = generationStrategy;
this.destination = destination;
this.qualifiedNameToModule = new LinkedHashMap<>();
this.moduleToQualifiedName = new LinkedHashMap<>();
this.moduleExtends = new LinkedHashMap<>();
this.moduleImports = new LinkedHashMap<>();
this.qualifiedNameServices = new LinkedHashMap<>();
this.callStacks = new ArrayDeque<>();
this.aqlEnvironment = new AcceleoQueryEnvironment(new EPackageProvider(), this);
/* FIXME we need a cross reference provider, and we need to make it configurable */
org.eclipse.acceleo.query.runtime.Query.configureEnvironment(aqlEnvironment, null, null);
}
@Override
public void pushImport(String importModuleQualifiedName, ModuleElement moduleElement) {
final AcceleoCallStack currentStack = new AcceleoCallStack(importModuleQualifiedName);
callStacks.addLast(currentStack);
currentStack.push(moduleElement);
}
@Override
public void push(ModuleElement moduleElement) {
final AcceleoCallStack currentStack = callStacks.peekLast();
currentStack.push(moduleElement);
}
@Override
public void popStack(ModuleElement moduleElement) {
AcceleoCallStack currentStack = callStacks.peekLast();
if (currentStack == null || (!currentStack.pop().equals(moduleElement) && currentStack.isEmpty())) {
// TODO this module wasn't on the top of our current stack. we're out of turn on our push/pop
// cycle. throw exception?
// this probably need to be an assert since it's a developer concern
}
if (currentStack.isEmpty()) {
callStacks.pollLast();
}
}
/* TODO Make this package protected? there are a few things on this class we don't want to expose. */
/**
* Returns the latest (current) call stack known to this environment.
*
* @return The latest (current) call stack known to this environment.
*/
public AcceleoCallStack getCurrentStack() {
return callStacks.peekLast();
}
private Module resolveModule(String qualifiedName) {
if (resolver == null) {
return qualifiedNameToModule.get(qualifiedName);
}
Module module;
try {
module = resolver.resolveModule(qualifiedName);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
module = null;
}
if (module != null) {
registerModule(qualifiedName, module);
}
return module;
}
@Override
public String getModuleQualifiedName(Module module) {
return moduleToQualifiedName.get(module);
}
@Override
public URL getModuleURL(Module module) {
return resolver.getModuleURL(getModuleQualifiedName(module));
}
@Override
public String getExtend(String qualifiedName) {
String extended = moduleExtends.get(qualifiedName);
if (extended != null && !hasQualifiedName(extended)) {
// TODO log runtime error? This should happen at evaluation time if the extended module cannot be
// resolved
}
return extended;
}
@Override
public Collection<String> getImports(String qualifiedName) {
Collection<String> imported = moduleImports.getOrDefault(qualifiedName, new LinkedList<>());
for (String importedName : imported) {
if (!hasQualifiedName(importedName)) {
// TODO log runtime error? would happen at evaluation time if an import cannot be resolved
}
}
return imported;
}
/**
* Returns all IServices with the given {@code name} provided by the given qualified name.
*
* @param qualifiedName
* The qualified name which services we're looking up.
* @param moduleElementName
* Name of the service(s) we're searching for.
* @return All IServices with the given {@code name} provided by the given module, <code>null</code> if
* none.
*/
public Set<IService> getServicesWithName(String qualifiedName, String moduleElementName) {
final Module module = getModule(qualifiedName);
if (module == null) {
resolveClass(qualifiedName);
}
return qualifiedNameServices.getOrDefault(qualifiedName, new LinkedHashMap<>()).getOrDefault(
moduleElementName, new LinkedHashSet<IService>());
}
/**
* Resolves the {@link Class} with the given qualified name.
*
* @param qualifiedName
* the qualified name
* @return the resolved {@link Class}
*/
private Class<?> resolveClass(String qualifiedName) {
Class<?> res = null;
if (this.resolver != null) {
try {
res = resolver.resolveClass(qualifiedName);
final Map<String, Set<IService>> servicesMap = qualifiedNameServices.computeIfAbsent(
qualifiedName, key -> new LinkedHashMap<String, Set<IService>>());
for (IService service : ServiceUtils.getServices(aqlEnvironment, res)) {
final Set<IService> services = servicesMap.computeIfAbsent(service.getName(),
key -> new LinkedHashSet<IService>());
services.add(service);
}
} catch (ClassNotFoundException e) {
// the class doesn't exist
res = null;
}
}
return res;
}
@Override
public void registerModule(String qualifiedName, Module module) {
qualifiedNameToModule.put(qualifiedName, module);
moduleToQualifiedName.put(module, qualifiedName);
if (module.getExtends() != null && module.getExtends().getQualifiedName() != null) {
moduleExtends.put(qualifiedName, module.getExtends().getQualifiedName());
}
for (Import imp : module.getImports()) {
final ModuleReference moduleRef = imp.getModule();
if (moduleRef != null && moduleRef.getQualifiedName() != null) {
moduleImports.computeIfAbsent(qualifiedName, key -> new LinkedList<>()).add(moduleRef
.getQualifiedName());
}
}
for (Metamodel metamodel : module.getMetamodels()) {
if (!(metamodel instanceof ErrorMetamodel)) {
EPackage referredPackage = metamodel.getReferencedPackage();
aqlEnvironment.registerEPackage(referredPackage);
ServiceUtils.registerServices(aqlEnvironment, ServiceUtils.getServices(referredPackage));
}
}
Map<String, Set<IService>> services = qualifiedNameServices.computeIfAbsent(qualifiedName,
key -> new LinkedHashMap<>());
for (ModuleElement element : module.getModuleElements()) {
if (element instanceof Template) {
String name = ((Template)element).getName();
services.computeIfAbsent(name, key -> new LinkedHashSet<>()).add(new TemplateService(this,
(Template)element));
} else if (element instanceof Query) {
String name = ((Query)element).getName();
services.computeIfAbsent(name, key -> new LinkedHashSet<>()).add(new QueryService(this,
(Query)element));
}
}
}
@Override
public IQueryEnvironment getQueryEnvironment() {
return aqlEnvironment;
}
@Override
public boolean hasQualifiedName(String qualifiedName) {
return qualifiedNameServices.containsKey(qualifiedName) || resolveModule(qualifiedName) != null
|| resolveClass(qualifiedName) != null;
}
@Override
public Module getModule(String qualifiedName) {
final Module res;
final Module cachedModule = qualifiedNameToModule.get(qualifiedName);
if (cachedModule != null) {
res = cachedModule;
} else {
res = resolveModule(qualifiedName);
}
return res;
}
@Override
public Module getModule(URL url) {
final Module res;
final String qualifiedName = resolver.getQualifierName(url);
if (qualifiedName != null) {
res = getModule(qualifiedName);
} else {
res = null;
}
return res;
}
@Override
public void openWriter(URI uri, OpenModeKind openMode, Charset charset, String lineDelimiter)
throws IOException {
final IAcceleoWriter writer = generationStrategy.createWriterFor(uri, openMode, charset,
lineDelimiter);
writers.addLast(writer);
generationResult.getGeneratedFiles().add(uri);
}
@Override
public void closeWriter() throws IOException {
final IAcceleoWriter writer = writers.removeLast();
writer.close();
}
@Override
public void write(String text) throws IOException {
IAcceleoWriter writer = writers.peekLast();
writer.append(text);
}
@Override
public URI getDestination() {
return destination;
}
@Override
public GenerationResult getGenerationResult() {
return generationResult;
}
@Override
public void setModuleResolver(IQualifiedNameResolver nameResolver) {
this.resolver = nameResolver;
}
@Override
public void setEvaluator(AcceleoEvaluator evaluator) {
this.evaluator = evaluator;
}
@Override
public AcceleoEvaluator getEvaluator() {
return evaluator;
}
}