| /******************************************************************************* |
| * Copyright (c) 2015, 2021 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.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import org.eclipse.acceleo.query.ast.Call; |
| import org.eclipse.acceleo.query.ast.Error; |
| import org.eclipse.acceleo.query.parser.CombineIterator; |
| import org.eclipse.acceleo.query.runtime.AcceleoQueryValidationException; |
| import org.eclipse.acceleo.query.runtime.IReadOnlyQueryEnvironment; |
| import org.eclipse.acceleo.query.runtime.IService; |
| import org.eclipse.acceleo.query.runtime.IValidationResult; |
| import org.eclipse.acceleo.query.validation.type.ClassLiteralType; |
| import org.eclipse.acceleo.query.validation.type.ClassType; |
| import org.eclipse.acceleo.query.validation.type.EClassifierLiteralType; |
| import org.eclipse.acceleo.query.validation.type.EClassifierType; |
| import org.eclipse.acceleo.query.validation.type.ICollectionType; |
| import org.eclipse.acceleo.query.validation.type.IType; |
| import org.eclipse.acceleo.query.validation.type.NothingType; |
| import org.eclipse.acceleo.query.validation.type.SequenceType; |
| import org.eclipse.acceleo.query.validation.type.SetType; |
| import org.eclipse.emf.common.util.Diagnostic; |
| import org.eclipse.emf.ecore.EClass; |
| import org.eclipse.emf.ecore.EClassifier; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EcorePackage; |
| |
| /** |
| * Implementation of the elementary language validation services like variable typing and service call typing. |
| * |
| * @author <a href="mailto:yvan.lussaud@obeo.fr">Yvan Lussaud</a> |
| */ |
| public class ValidationServices extends AbstractLanguageServices { |
| |
| /** |
| * Log message used when an internal validation error is encountered. |
| */ |
| public static final String INTERNAL_ERROR_MSG = "An internal error occured during validation of a query"; |
| |
| /** |
| * Log message used when a variable is present in the types map but has no types. |
| */ |
| private static final String VARIABLE_HAS_NO_TYPES = "The %s variable has no types"; |
| |
| /** |
| * Should never happen message. |
| */ |
| private static final String SHOULD_NEVER_HAPPEN = "should never happen"; |
| |
| /** |
| * Constructor. |
| * |
| * @param queryEnv |
| * the {@link IReadOnlyQueryEnvironment} to use during evaluation |
| */ |
| public ValidationServices(IReadOnlyQueryEnvironment queryEnv) { |
| super(queryEnv); |
| } |
| |
| /** |
| * 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 a nothing {@link NothingValidationStatus}. |
| */ |
| public NothingType nothing(String message, Object... msgArgs) { |
| final String formatedMessage = String.format(message, msgArgs); |
| return new NothingType(formatedMessage); |
| } |
| |
| /** |
| * Returns the type of the specified variable in the specified map. Returns {@link Nothing} class when the |
| * variable is not found. |
| * |
| * @param variableTypes |
| * the set of variable types definition in which to lookup the specified variable. |
| * @param variableName |
| * the name of the variable to lookup in the specified map. |
| * @return Returns types of the specified variable in the specified map or {@link Nothing} class. |
| */ |
| public Set<IType> getVariableTypes(Map<String, Set<IType>> variableTypes, String variableName) { |
| try { |
| final Set<IType> res = new LinkedHashSet<IType>(); |
| final Set<IType> types = variableTypes.get(variableName); |
| if (types != null) { |
| if (types.size() > 0) { |
| res.addAll(types); |
| } else { |
| res.add(nothing(VARIABLE_HAS_NO_TYPES, variableName)); |
| } |
| } else { |
| res.add(nothing(VARIABLE_NOT_FOUND, variableName)); |
| } |
| return res; |
| } catch (NullPointerException e) { |
| throw new AcceleoQueryValidationException(INTERNAL_ERROR_MSG, e); |
| } |
| } |
| |
| /** |
| * Gets the {@link ServicesValidationResult} for the given {@link Call} and {@link IType} of parameters. |
| * |
| * @param call |
| * the {@link Call} |
| * @param validationResult |
| * the {@link IValidationResult} |
| * @param argTypes |
| * the {@link IType} of parameters |
| * @return the {@link ServicesValidationResult} for the given {@link Call} and {@link IType} of parameters |
| */ |
| public ServicesValidationResult call(Call call, IValidationResult validationResult, |
| final List<Set<IType>> argTypes) { |
| final ServicesValidationResult servicesValidationResult; |
| final String serviceName = call.getServiceName(); |
| switch (call.getType()) { |
| case CALLSERVICE: |
| servicesValidationResult = callType(call, validationResult, serviceName, argTypes); |
| break; |
| case CALLORAPPLY: |
| servicesValidationResult = callOrApplyTypes(call, validationResult, serviceName, argTypes); |
| break; |
| case COLLECTIONCALL: |
| servicesValidationResult = collectionServiceCallTypes(call, validationResult, serviceName, |
| argTypes); |
| break; |
| default: |
| throw new UnsupportedOperationException(SHOULD_NEVER_HAPPEN); |
| } |
| return servicesValidationResult; |
| } |
| |
| /** |
| * Gets the {@link ServicesValidationResult} for the given {@link IService#getName() service name} and |
| * {@link IType} of parameters. |
| * |
| * @param call |
| * the {@link Call} |
| * @param validationResult |
| * the {@link IValidationResult} being constructed |
| * @param serviceName |
| * the {@link IService#getName() service name} |
| * @param argTypes |
| * the {@link IType} of parameters |
| * @return the {@link ServicesValidationResult} |
| */ |
| public ServicesValidationResult callType(Call call, IValidationResult validationResult, |
| String serviceName, List<Set<IType>> argTypes) { |
| if (argTypes.size() == 0) { |
| throw new AcceleoQueryValidationException( |
| "An internal error occured during validation of a query : at least one argument must be specified for service " |
| + serviceName + "."); |
| } |
| try { |
| final ServicesValidationResult result = new ServicesValidationResult(queryEnvironment, this); |
| CombineIterator<IType> it = new CombineIterator<IType>(argTypes); |
| final Map<IService<?>, Map<List<IType>, Set<IType>>> typesPerService = new LinkedHashMap<IService<?>, Map<List<IType>, Set<IType>>>(); |
| boolean serviceFound = false; |
| boolean emptyCombination = !it.hasNext(); |
| List<String> notFoundSignatures = new ArrayList<String>(); |
| while (it.hasNext()) { |
| List<IType> currentArgTypes = it.next(); |
| IService<?> service = queryEnvironment.getLookupEngine().lookup(serviceName, currentArgTypes |
| .toArray(new IType[currentArgTypes.size()])); |
| if (service != null) { |
| Map<List<IType>, Set<IType>> typeMapping = typesPerService.get(service); |
| if (typeMapping == null) { |
| typeMapping = new LinkedHashMap<List<IType>, Set<IType>>(); |
| typesPerService.put(service, typeMapping); |
| } |
| Set<IType> serviceTypes = service.getType(call, this, validationResult, queryEnvironment, |
| currentArgTypes); |
| typeMapping.put(currentArgTypes, serviceTypes); |
| serviceFound = true; |
| } else { |
| notFoundSignatures.add(serviceSignature(serviceName, currentArgTypes)); |
| } |
| } |
| |
| if (!emptyCombination) { |
| if (serviceFound) { |
| for (Entry<IService<?>, Map<List<IType>, Set<IType>>> entry : typesPerService |
| .entrySet()) { |
| final IService<?> service = entry.getKey(); |
| final Map<List<IType>, Set<IType>> types = entry.getValue(); |
| result.addServiceTypes(service, types); |
| } |
| } else { |
| final StringBuilder builder = new StringBuilder(); |
| for (String signature : notFoundSignatures) { |
| builder.append(String.format(SERVICE_NOT_FOUND, signature) + "\n"); |
| } |
| result.addServiceNotFound(nothing(builder.substring(0, builder.length() - 1))); |
| } |
| } |
| |
| return result; |
| // CHECKSTYLE:OFF |
| } catch (Exception e) { |
| // CHECKSTYLE:ON |
| throw new AcceleoQueryValidationException(INTERNAL_ERROR_MSG, e); |
| } |
| } |
| |
| /** |
| * The callOrApply method validates 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 call |
| * the {@link Call} |
| * @param validationResult |
| * the {@link IValidationResult} being constructed |
| * @param serviceName |
| * the name of the service to call |
| * @param argTypes |
| * the arguments to pass to the called service |
| * @return the {@link ServicesValidationResult} |
| */ |
| public ServicesValidationResult callOrApplyTypes(Call call, IValidationResult validationResult, |
| String serviceName, List<Set<IType>> argTypes) { |
| try { |
| ServicesValidationResult result = new ServicesValidationResult(queryEnvironment, this); |
| final List<Set<IType>> argTypesNoReceiver = new ArrayList<Set<IType>>(argTypes); |
| final Set<IType> receiverTypes = argTypesNoReceiver.remove(0); |
| for (IType receiverType : receiverTypes) { |
| if (receiverType instanceof SequenceType) { |
| result.merge(validateCallOnSequence(call, validationResult, serviceName, |
| (SequenceType)receiverType, argTypesNoReceiver)); |
| } else if (receiverType instanceof SetType) { |
| result.merge(validateCallOnSet(call, validationResult, serviceName, (SetType)receiverType, |
| argTypesNoReceiver)); |
| } else { |
| final List<Set<IType>> newArgTypes = new ArrayList<Set<IType>>(argTypesNoReceiver); |
| final Set<IType> newReceiverTypes = new LinkedHashSet<IType>(); |
| newReceiverTypes.add(receiverType); |
| newArgTypes.add(0, newReceiverTypes); |
| result.merge(callType(call, validationResult, serviceName, newArgTypes)); |
| } |
| } |
| return result; |
| // CHECKSTYLE:OFF |
| } catch (Exception e) { |
| // CHECKSTYLE:ON |
| throw new AcceleoQueryValidationException(INTERNAL_ERROR_MSG, e); |
| } |
| } |
| |
| /** |
| * Validates a service call on a sequence of objects. |
| * |
| * @param call |
| * the {@link Call} |
| * @param validationResult |
| * the {@link IValidationResult} being constructed |
| * @param serviceName |
| * the name of the service to be called. |
| * @param receiverType |
| * the receiver type on which elements to validate the service |
| * @param argTypesNoReceiver |
| * the argument types to pass to the service |
| * @return the {@link ServicesValidationResult} |
| */ |
| private ServicesValidationResult validateCallOnSequence(Call call, IValidationResult validationResult, |
| String serviceName, SequenceType receiverType, List<Set<IType>> argTypesNoReceiver) { |
| try { |
| final List<Set<IType>> newArgTypes = new ArrayList<Set<IType>>(argTypesNoReceiver); |
| final Set<IType> newReceiverTypes = new LinkedHashSet<IType>(); |
| newReceiverTypes.add(receiverType.getCollectionType()); |
| newArgTypes.add(0, newReceiverTypes); |
| ServicesValidationResult result = callOrApplyTypes(call, validationResult, serviceName, |
| newArgTypes); |
| flattenSequence(result); |
| return result; |
| // CHECKSTYLE:OFF |
| } catch (Exception e) { |
| // CHECKSTYLE:ON |
| throw new AcceleoQueryValidationException("empty argument array passed to callOrApply " |
| + serviceName, e); |
| } |
| } |
| |
| /** |
| * Flatten {@link List} on the given {@link ServicesValidationResult}. |
| * |
| * @param result |
| * the {@link ServicesValidationResult} |
| */ |
| protected void flattenSequence(ServicesValidationResult result) { |
| result.flattenSequence(); |
| } |
| |
| /** |
| * Validates a service call on a set of objects. |
| * |
| * @param call |
| * the {@link Call} |
| * @param validationResult |
| * the {@link IValidationResult} being constructed |
| * @param serviceName |
| * the name of the service to be called. |
| * @param receiverType |
| * the receiver type on which elements to validate the service |
| * @param argTypesNoReceiver |
| * the argument types to pass to the service |
| * @return the {@link ServicesValidationResult} |
| */ |
| private ServicesValidationResult validateCallOnSet(Call call, IValidationResult validationResult, |
| String serviceName, SetType receiverType, List<Set<IType>> argTypesNoReceiver) { |
| try { |
| final List<Set<IType>> newArgTypes = new ArrayList<Set<IType>>(argTypesNoReceiver); |
| final Set<IType> newReceiverTypes = new LinkedHashSet<IType>(); |
| newReceiverTypes.add(receiverType.getCollectionType()); |
| newArgTypes.add(0, newReceiverTypes); |
| ServicesValidationResult result = callOrApplyTypes(call, validationResult, serviceName, |
| newArgTypes); |
| flattenSet(result); |
| return result; |
| // CHECKSTYLE:OFF |
| } catch (Exception e) { |
| // CHECKSTYLE:ON |
| throw new AcceleoQueryValidationException("empty argument array passed to callOrApply " |
| + serviceName, e); |
| } |
| } |
| |
| /** |
| * Flatten {@link Set} on the given {@link ServicesValidationResult}. |
| * |
| * @param result |
| * the {@link ServicesValidationResult} to flatten |
| */ |
| protected void flattenSet(ServicesValidationResult result) { |
| result.flattenSet(); |
| } |
| |
| /** |
| * Calls a collection's service. |
| * |
| * @param call |
| * the {@link Call} |
| * @param validationResult |
| * the {@link IValidationResult} being constructed |
| * @param serviceName |
| * the {@link IService#getName() the service name} |
| * @param argTypes |
| * {@link IService#getParameterTypes(IReadOnlyQueryEnvironment) service parameter types} |
| * @return the {@link ServicesValidationResult} |
| */ |
| public ServicesValidationResult collectionServiceCallTypes(Call call, IValidationResult validationResult, |
| String serviceName, List<Set<IType>> argTypes) { |
| List<Set<IType>> newArguments = new ArrayList<Set<IType>>(argTypes); |
| try { |
| final Set<IType> receiverTypes = newArguments.remove(0); |
| final Set<IType> newReceiverTypes = new LinkedHashSet<IType>(); |
| for (IType receiverType : receiverTypes) { |
| if (receiverType instanceof ClassType && receiverType.getType() == null) { |
| // Call on the NullLiteral |
| newReceiverTypes.add(new SetType(queryEnvironment, nothing( |
| "The Collection was empty due to a null value being wrapped as a Collection."))); |
| } else if (!(receiverType instanceof ICollectionType) |
| && !(receiverType instanceof NothingType)) { |
| // implicit set conversion. |
| newReceiverTypes.add(new SetType(queryEnvironment, receiverType)); |
| } else { |
| newReceiverTypes.add(receiverType); |
| } |
| } |
| newArguments.add(0, newReceiverTypes); |
| return callType(call, validationResult, serviceName, newArguments); |
| // CHECKSTYLE:OFF |
| } catch (Exception e) { |
| // CHECKSTYLE:ON |
| throw new AcceleoQueryValidationException(INTERNAL_ERROR_MSG, e); |
| } |
| } |
| |
| /** |
| * Build up the specified service's signature for reporting. |
| * |
| * @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, List<IType> argumentTypes) { |
| StringBuilder builder = new StringBuilder(); |
| builder.append(serviceName).append('('); |
| boolean first = true; |
| for (IType argType : argumentTypes) { |
| if (!first) { |
| builder.append(','); |
| } else { |
| first = false; |
| } |
| builder.append(argType.toString()); |
| } |
| return builder.append(')').toString(); |
| } |
| |
| /** |
| * Gets the {@link Error} types. |
| * |
| * @param validationResult |
| * the {@link IValidationResult} |
| * @param error |
| * the {@link Error} |
| * @return the {@link Error} types |
| */ |
| public Set<IType> getErrorTypes(IValidationResult validationResult, Error error) { |
| final Set<IType> result = new LinkedHashSet<IType>(); |
| |
| for (Diagnostic diagnostic : validationResult.getAstResult().getDiagnostic().getChildren()) { |
| if (diagnostic.getData().contains(error)) { |
| result.add(nothing(diagnostic.getMessage())); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Gets the lower {@link IType} from the two given {@link IType} if they are in the same |
| * {@link IType#isAssignableFrom(IType) hierarchy} ({@link EClassifierLiteralType} are converted to |
| * {@link EClassifierType}). |
| * |
| * @param type1 |
| * the first {@link IType} |
| * @param type2 |
| * the second {@link IType} |
| * @return the lower {@link IType} from the two given {@link IType} if they are in the same |
| * {@link IType#isAssignableFrom(IType) hierarchy}, <code>null</code> otherwise |
| */ |
| public IType lower(IType type1, IType type2) { |
| final IType result; |
| |
| if (type1 == null || type2 == null) { |
| result = null; |
| } else { |
| if (type1.isAssignableFrom(type2) || type1.getType() == EcorePackage.eINSTANCE.getEObject() |
| || type1.getType() == EObject.class) { |
| if (type2 instanceof EClassifierLiteralType) { |
| result = new EClassifierType(queryEnvironment, ((EClassifierLiteralType)type2).getType()); |
| } else if (type2 instanceof ClassLiteralType) { |
| result = new ClassType(queryEnvironment, ((ClassLiteralType)type2).getType()); |
| } else { |
| result = type2; |
| } |
| } else if (type2.isAssignableFrom(type1) || type2.getType() == EcorePackage.eINSTANCE.getEObject() |
| || type2.getType() == EObject.class) { |
| if (type1 instanceof EClassifierLiteralType) { |
| result = new EClassifierType(queryEnvironment, ((EClassifierLiteralType)type1).getType()); |
| } else if (type1 instanceof ClassLiteralType) { |
| result = new ClassType(queryEnvironment, ((ClassLiteralType)type1).getType()); |
| } else { |
| result = type1; |
| } |
| } else { |
| result = null; |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Gets the {@link Set} of the higher {@link EClass} in the super types hierarchy inheriting from both |
| * given {@link EClass}. |
| * |
| * @param eCls1 |
| * the first {@link EClass} |
| * @param eCls2 |
| * the second {@link EClass} |
| * @return the {@link Set} of the higher {@link EClass} in the super types hierarchy inheriting from both |
| * given {@link EClass} |
| */ |
| public Set<EClass> getSubTypesTopIntersection(EClass eCls1, EClass eCls2) { |
| final Set<EClass> result = new LinkedHashSet<EClass>(); |
| |
| final Set<EClass> subTypes1 = queryEnvironment.getEPackageProvider().getAllSubTypes(eCls1); |
| final Set<EClass> subTypes2 = queryEnvironment.getEPackageProvider().getAllSubTypes(eCls2); |
| |
| final Set<EClass> intersection = new LinkedHashSet<EClass>(subTypes1); |
| intersection.retainAll(subTypes2); |
| |
| for (EClass eCls : intersection) { |
| final boolean isTopEClass = Collections.disjoint(eCls.getEAllSuperTypes(), intersection); |
| if (isTopEClass) { |
| result.add(eCls); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Gets the {@link Set} of {@link IType} that are both type1 and type2. |
| * |
| * @param type1 |
| * the first {@link IType} |
| * @param type2 |
| * the second {@link IType} |
| * @return the {@link Set} of {@link IType} that are both type1 and type2 |
| */ |
| public Set<IType> intersection(IType type1, IType type2) { |
| final Set<IType> result = new LinkedHashSet<IType>(); |
| |
| final IType lowerType = lower(type1, type2); |
| if (lowerType != null) { |
| result.add(lowerType); |
| } else if (type1 != null && type2 != null) { |
| Set<EClass> eClasses1 = getEClasses(type1); |
| Set<EClass> eClasses2 = getEClasses(type2); |
| for (EClass eCls1 : eClasses1) { |
| for (EClass eCls2 : eClasses2) { |
| for (EClass eCls : getSubTypesTopIntersection(eCls1, eCls2)) { |
| result.add(new EClassifierType(getQueryEnvironment(), eCls)); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Gets the {@link Set} of {@link EClass} form the given {@link IType}. |
| * |
| * @param type |
| * the {@link IType} |
| * @return the {@link Set} of {@link EClass} form the given {@link IType} |
| */ |
| public Set<EClass> getEClasses(IType type) { |
| final Set<EClass> result = new LinkedHashSet<EClass>(); |
| |
| if (type.getType() instanceof EClass) { |
| result.add((EClass)type.getType()); |
| } else if (type.getType() instanceof Class) { |
| final Set<EClassifier> eClassifiers = queryEnvironment.getEPackageProvider().getEClassifiers( |
| (Class<?>)type.getType()); |
| if (eClassifiers != null) { |
| for (EClassifier eClassifier : eClassifiers) { |
| if (eClassifier instanceof EClass) { |
| result.add((EClass)eClassifier); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| } |