blob: b26aa17481029d0c9617a5e4b3ab12370c9e69a7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 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.query.runtime.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.acceleo.query.parser.AstBuilderListener;
import org.eclipse.acceleo.query.runtime.AcceleoQueryEvaluationException;
import org.eclipse.acceleo.query.runtime.IReadOnlyQueryEnvironment;
import org.eclipse.acceleo.query.runtime.IService;
import org.eclipse.acceleo.query.validation.type.ClassType;
import org.eclipse.acceleo.query.validation.type.EClassifierType;
import org.eclipse.acceleo.query.validation.type.IType;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.DiagnosticChain;
import org.eclipse.emf.ecore.EObject;
/**
* Implementation of the elementary language services like variable access and service call.
*
* @author <a href="mailto:romain.guider@obeo.fr">Romain Guider</a>
*/
public class EvaluationServices extends AbstractLanguageServices {
/**
* Log message used when an internal evaluation error is encountered.
*/
private static final String INTERNAL_ERROR_MSG = "An internal error occured during evaluation of a query";
/**
* Creates a new {@link EvaluationServices} instance given a lookupEngine and logging flag.
*
* @param queryEnv
* the {@link IReadOnlyQueryEnvironment} to use during evaluation
*/
public EvaluationServices(IReadOnlyQueryEnvironment queryEnv) {
super(queryEnv);
}
/**
* Returns the value of the specified variable in the specified map. Returns NOTHING when the variable is
* not found.
*
* @param variableDefinitions
* the set of variable definition in which to lookup the specified variable.
* @param variableName
* the name of the variable to lookup in the specified map.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return Returns the value of the specified variable in the specified map or nothing.
*/
public Object getVariableValue(Map<String, Object> variableDefinitions, String variableName,
Diagnostic diagnostic) {
try {
Object result = variableDefinitions.get(variableName);
if (result == null && !variableDefinitions.containsKey(variableName)) {
Nothing placeHolder = nothing(VARIABLE_NOT_FOUND, variableName);
addDiagnosticFor(diagnostic, Diagnostic.ERROR, placeHolder);
result = placeHolder;
}
return result;
} catch (NullPointerException e) {
throw new AcceleoQueryEvaluationException(INTERNAL_ERROR_MSG, e);
}
}
/**
* returns the nothing value and logs the specified error message.
*
* @param message
* the message to log.
* @param msgArgs
* the object arguments used to format the log message.
* @return an instance of nothing initialized with the formatted error message.
*/
private Nothing nothing(String message, Object... msgArgs) {
String formatedMessage = String.format(message, msgArgs);
return new Nothing(formatedMessage);
}
/**
* Build the argument {@link IType} array that corresponds to the specified arguments array.
*
* @param arguments
* the argument from which types are required
* @return the argument {@link IType} array that corresponds to the specified arguments array
*/
private IType[] getArgumentTypes(Object[] arguments) {
IType[] argumentTypes = new IType[arguments.length];
for (int i = 0; i < arguments.length; i++) {
if (arguments[i] == null) {
argumentTypes[i] = new ClassType(queryEnvironment, null);
} else if (arguments[i] instanceof EObject) {
argumentTypes[i] = new EClassifierType(queryEnvironment, ((EObject)arguments[i]).eClass());
} else {
argumentTypes[i] = new ClassType(queryEnvironment, arguments[i].getClass());
}
}
return argumentTypes;
}
/**
* Do a service call.
*
* @param service
* the specification of the service to call.
* @param arguments
* the arguments of the service call.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return the value produced by the service execution.
*/
private Object callService(IService service, Object[] arguments, Diagnostic diagnostic) {
try {
return service.invoke(arguments);
} catch (AcceleoQueryEvaluationException e) {
Nothing placeHolder = new Nothing(e.getMessage(), e);
addDiagnosticFor(diagnostic, Diagnostic.WARNING, placeHolder);
return placeHolder;
}
}
/**
* Evaluate a service call.
*
* @param serviceName
* the @link IService#getName() service name}.
* @param arguments
* the arguments to pass to the service evaluation.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return the value resulting from evaluating the service given the specified arguments.
*/
public Object call(String serviceName, Object[] arguments, Diagnostic diagnostic) {
final Object result;
if (arguments.length == 0) {
throw new AcceleoQueryEvaluationException(
"An internal error occured during evaluation of a query : at least one argument must be specified for service "
+ serviceName + ".");
}
try {
IType[] argumentTypes = getArgumentTypes(arguments);
IService service = queryEnvironment.getLookupEngine().lookup(serviceName, argumentTypes);
if (service == null) {
Nothing placeHolder = nothing(SERVICE_NOT_FOUND, serviceSignature(serviceName, argumentTypes));
addDiagnosticFor(diagnostic, Diagnostic.WARNING, placeHolder);
result = placeHolder;
} else {
result = callService(service, arguments, diagnostic);
}
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
throw new AcceleoQueryEvaluationException(INTERNAL_ERROR_MSG, e);
}
return result;
}
/**
* The callOrApply method evaluates an expression of the form "<exp>.<service name>(<exp>*)" The first
* argument in the arguments array is considered the receiver of the call. If the receiver is a collection
* then callOrApply is applied recursively to all elements of the collection thus returning a collection
* of the result of this application(nothing values not being added).
*
* @param serviceName
* the name of the service to call.
* @param arguments
* the arguments to pass to the called service.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return the result of evaluating the specified service on the specified arguments.
*/
@SuppressWarnings("unchecked")
public Object callOrApply(String serviceName, Object[] arguments, Diagnostic diagnostic) {
try {
Object result;
if (arguments[0] instanceof List) {
List<Object> list = (List<Object>)arguments[0];
result = applyCallOnSequence(serviceName, list, arguments, diagnostic);
} else if (arguments[0] instanceof Set) {
Set<Object> set = (Set<Object>)arguments[0];
result = applyCallOnSet(serviceName, set, arguments, diagnostic);
} else {
result = call(serviceName, arguments, diagnostic);
}
return result;
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
throw new AcceleoQueryEvaluationException(INTERNAL_ERROR_MSG, e);
}
}
/**
* Calls a collection's service.
*
* @param serviceName
* the name of the service.
* @param arguments
* the service's arguments.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return the result of evaluating the specified service on the specified arguments.
*/
public Object collectionServiceCall(String serviceName, Object[] arguments, Diagnostic diagnostic) {
Object[] newArguments;
try {
Object receiver = arguments[0];
if (!(receiver instanceof Collection) && !(receiver instanceof Nothing)) {
// implicit set conversion.
Set<Object> newReceiver = new LinkedHashSet<Object>();
// treat "null" as a non existing value. The auto-boxed set will then be empty.
if (receiver != null) {
newReceiver.add(receiver);
}
receiver = newReceiver;
newArguments = arguments.clone();
newArguments[0] = newReceiver;
} else {
newArguments = arguments;
}
return call(serviceName, newArguments, diagnostic);
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
throw new AcceleoQueryEvaluationException(INTERNAL_ERROR_MSG, e);
}
}
/**
* Applies a service call on a sequence of objects.
*
* @param serviceName
* the name of the service to be called.
* @param origin
* the sequence on which elements to apply the service
* @param arguments
* the arguments to pass to the service.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return a sequence made of the result of calling the specified service on the specified arguments on
* the origin list elements.
*/
private Object applyCallOnSequence(String serviceName, List<Object> origin, Object[] arguments,
Diagnostic diagnostic) {
try {
List<Object> result = new ArrayList<Object>(origin.size());
Object[] innerArguments = arguments.clone();
for (Object obj : origin) {
innerArguments[0] = obj;
Object newResult = callOrApply(serviceName, innerArguments, diagnostic);
// flatten
if (!(newResult instanceof Nothing)) {
if (newResult instanceof Collection) {
result.addAll((Collection<?>)newResult);
} else if (newResult != null) {
result.add(newResult);
}
}
}
return result;
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
throw new AcceleoQueryEvaluationException("empty argument array passed to callOrApply "
+ serviceName, e);
}
}
/**
* Applies a service call on a set of objects.
*
* @param serviceName
* the name of the service to be called.
* @param origin
* the sequence on which elements to apply the service
* @param arguments
* the arguments to pass to the service.
* @param diagnostic
* The status to update in case of warnings or errors during this call.
* @return a sequence made of the result of calling the specified service on the specified arguments on
* the origin set elements.
*/
private Object applyCallOnSet(String serviceName, Set<Object> origin, Object[] arguments,
Diagnostic diagnostic) {
try {
Set<Object> result = new LinkedHashSet<Object>(origin.size());
Object[] innerArguments = arguments.clone();
for (Object obj : origin) {
innerArguments[0] = obj;
Object newResult = callOrApply(serviceName, innerArguments, diagnostic);
// flatten
if (!(newResult instanceof Nothing)) {
if (newResult instanceof Collection) {
result.addAll((Collection<?>)newResult);
} else if (newResult != null) {
result.add(newResult);
}
}
}
return result;
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
throw new AcceleoQueryEvaluationException(INTERNAL_ERROR_MSG, e);
}
}
/**
* Build up the specified service's signature for reporting. Only use this when the service is not
* available directly, otherwise use {@link IService#getShortSignature() short signature}.
*
* @param serviceName
* the name of the service
* @param argumentTypes
* the service's call argument types
* @return the specified service's signature.
*/
protected String serviceSignature(String serviceName, IType[] argumentTypes) {
StringBuilder builder = new StringBuilder();
builder.append(serviceName).append('(');
boolean first = true;
for (IType argType : argumentTypes) {
if (!first) {
builder.append(',');
} else {
first = false;
}
if (argType == null) {
builder.append("Object=null");
} else {
builder.append(argType.toString());
}
}
return builder.append(')').toString();
}
/**
* Adds a child to the given diagnostic chain with the information from that Nothing instance.
*
* @param chain
* The parent chain.
* @param severity
* Severity to give that issue.
* @param nothing
* The nothing we've obtained as a result of an evaluation failure.
*/
private void addDiagnosticFor(Diagnostic chain, int severity, Nothing nothing) {
if (chain instanceof DiagnosticChain) {
Diagnostic child = new BasicDiagnostic(severity, AstBuilderListener.PLUGIN_ID, 0, nothing
.getMessage(), new Object[] {nothing.getCause(), });
((DiagnosticChain)chain).add(child);
}
}
}