The Acceleo Query Language (AQL) is a language used to navigate and query an EMF model. In this document, you will find the description of the syntax, all the services and the standard library of AQL.
AQL as a query engine is small, simple, fast, extensible and it brings a richer validation than the MTL interpreter.
For those looking for a simple and fast interpreters for your EMF models, AQL can provide you with a lot of features, including:
The AQL interpreter is used in Sirius with the prefix «aql:».
The syntax is very similar to the OCL syntax. An expression always starts with a variable
aVariable
The variable named
self represent the current object (think of it as the
this
in Java).
Let’s consider the following metamodel :
From a variable one can access field or reference values using the
.
separator.
With
self
being an instance of
Person,
self.name
returns the value of the attribute
name and
self.father
return the father of the person.
If the attribute or the reference is multi-valued, then
self.parents
will return a collection.
Calls can be chained, as such
self.parents.name
will return a collection containing the names of the parents.
If one want to access the collection itself, then the separator
->
must be used, as such
self.parents.name->size()
will return the number of elements in the collection whereas
self.parents.name.size()
will return a collection containing
the sizes of each name.
AQL can also call methods modeled as EOperations or defined through Java services. The syntax denoting such a call is
@. @self.someCall()
will call the
someCall method and return the result.
Filtering a collection is generaly done using either
->filter(..)
to keep elements of a given type or
->select(..)
to keep elements which are validating a given condition.
With
self
being an instance of
Family,
self.members->filter(family::Man)
will return all the members of the family which are mens and
self.members->select( p | p.name.startsWith('A'))
will return all the members of the family which have a name starting by the letter ‹A›.
To access an element at a particular index you can use the operation
->at(..)
;
self.members->at(1)
will return the first person which is a member of the family (in that specific case it is probably better to use
self.members->first()
AQL has two kinds of collections, a
Sequence
which is a list, or an
OrderedSet
which does not allow doubles. You can convert a
Sequence
to an
OrderedSet
by as such :
self.members->asSet()
You can also define a collection by extension using the following syntax:
OrderedSet{self}
which returns a set containing the current EObject.
Sequence{self, self.eContainer()}
returns a sequence containing the current EObject and its parent.
AQL provides operations out of the box to browse the model. Most notably :
self.eContainer()
returns the parent of the current object if there is one.
self.eAllContents(some::Type)
returns all direct and indirect children matching the given type.
self.eContents()
return all the direct children.
self.eInverse('father')
returns the cross reference of the reference named ‹father›. In this case it will return all the persons which have the current object (self) as a father.
AQL provides an If but it has to be an expression and not a statement. As such one has to define the else. Here is the syntax
if
self.name.startsWith('a')
then
self
else
self.eContainer()
endif
variable_name | a reference to a variable | myVariable |
expression . feature_name | implicit collect | eClass.name |
expression . service_name ( ( expression ( , expression ) * ) ? ) | implicit collect | myVariable.toString() |
expression -> service_name ( ( expression ( , expression ) * ) ? ) | call on the collection itself if the expression is not a collection it will be wrapped into an ordered set | mySequence->sep(‹,›) |
not expression | call the not service | not eClass.interface |
- expression | call the unaryMin service | -3 |
expression + expression | call the add service | 2 + 2 |
expression - expression | call the sub service | 2 – 2 |
expression * expression | call the mult service | 2 * 2 |
expression / expression | call the divOp service | 2 / 2 |
expression <= expression | call the lessThanEqual service | 2 <= 2 |
expression >= expression | call the greaterThanEqual service | 2 >= 2 |
expression < expression | call the lessThan service | 1 < 2 |
expression > expression | call the greaterThan service | 2 > 1 |
expression <> expression | call the differs service | 1 <> 2 |
expression != expression | call the differs service | 1 != 2 |
expression = expression | call the equals service | 1 = 1 |
expression and expression | call the and service | eClass.interface and eClass.abstact |
expression or expression | call the or service | eClass.interface or eClass.abstact |
expression xor expression | call the xor service | eClass.interface xor eClass.abstact |
expression implies expression | call implies service | eClass.interface implies eClass.abstact |
( expression ) | parenthesis are used to change priority during evaluation | (2 + 2 ) * 3 |
if expression then expression else expression endif | conditional expression | if eClass.abstract then ‹blue› else ‹red› endif |
let new_variable_name ( : type_literal)? ( , new_variable_name ( : type_literal)?)* in expression | let allows to define variables in order to factorise expression | let container = self.eContainer() in container.eAllContents() |
' escaped_string ' | you can use java style escape sequence \u0000 \x00 \\ \' \b \t \n ... | 'TODO list:\n\t- walk the dog\n\t- make diner' |
[ 0 - 9]+ | an integer | 100 |
[ 0 - 9]+ . [ 0 - 9]+ | a real | 3.14 |
true | the boolean value true | true |
false | the boolean value false | false |
null | the null value | null |
Sequence{ ( expression ( , expression) * ) ? } | a sequence defined in extension | Sequence{1, 2, 3, 3} |
OrderedSet{ ( expression ( , expression) * ) ? } | an ordered set defined in extension | OrderedSet{1, 2, 3} |
epackage_name :: eenum_name :: eenum_literal_name | an EEnumLiteral | art::Color::blue |
String | the string type | String | |
Integer | the integer type | Integer | |
Real | the real type | Real | |
Boolean | the string type | Boolean | |
Sequence( type_litral ) | a sequence type | Sequence(String) | |
OrderedSet( type_litral ) | an ordered set type | OrderedSet(String) | |
epackage_name :: eclassifier_name | an eclassifier type | ecore::EPackage | |
{ epackage_name :: eclassifier_name (* | * epackage_name :: eclassifier_name) * } | a set of eclassifiers | {ecore::EPackage | ecore::EClass} |
These sections are listing all the services of the standard library of AQL.
As languages, AQL and MTL are very close yet there are some notable differences:
There is no implicit variable reference. With this change, you can easily find out if you are using a feature of an object or a string representation of said object. As a result, instead of using
@, you must use @self.something
if you want to access the feature named «something» of the current object or «something» if you want to retrieve the object named something.
In a lambda expression, you must now define the name of the variable used for the iteration in order to easily identify which variable is used by an expression. In Acceleo MTL, you can write
Sequence{self}->collect(eAllContents(uml::Property))
and Acceleo will use the implicit iterator as a source of the operation eAllContents.
The problem comes when using a lambda like
Sequence{self}->collect(something)
, we can’t know if «something» is a feature of «self» or if it is another variable.
Using AQL, you will now have to write either
collect(m | m.eAllContents(uml::Property))
or
collect(m: uml::Model | eAllContents(uml::Property))
.
When a call or a feature acces is done on a collection the result is flattened for the first level. For instance a service returning a collection called on a collection will return a collection of elements and not a collection of collection of elements.
Type literals can’t be in the form someEPackage::someSubEPackage::SomeEClass but instead someSubEPackage::SomeEClass should be directly used. Note that the name of the EPackage is mandatory. Type literals are handled just like any other type.
Calls like
self.eAllContents(self.eClass())
are possible and will return all the children of type compatible with “self”.
Furthermore if you need a type literal as a parameter in your own service, you just have to have a first parameter with the type :
Set<EClass>
. Yes, that’s an important point, any type in AQL is possibly a union of several existing types, hence the collection here. As such the syntax for creating Sets or collections can be used as a substitute for type literals.
Enumeration literal should be prefixed with the name of the containing EPacakge for instance «myPackage::myEnum::value».
You can only have Sequences or OrderedSets as collections and as such the order of their elements is always deterministic. In Acceleo MTL, you had access to Sets, which are now OrderedSets and Bags, which are now Sequences. Those four kinds of collections were motivated by the fact that Sequence and OrderedSet were ordered contrary to Sets and Bags. On another side, OrderedSets and Sets did not accept any duplicate contrary to Bags and Sequences.
By careful reviewing the use of those collections in various Acceleo generators and Sirius Designers we have quickly found out that the lack of determinism in the order of the collections Sets and Bags was a major issue for our users. As a result, only two collections remain, the Sequence which can contain any kind of element and the OrderedSet which has a similar behavior except that it does not accept duplicates.
Previously in Acceleo MTL, you could transform a literal into a collection by using the operator
->
on the literal directly. In Acceleo MTL, the collection created was a Bag which is not available anymore. It is recommended to use the extension notation like
Sequence{self}
or
OrderedSet{self}
. By default in AQL the created collection is an OrderedSet.
Some operations have been renamed. As such «addAll» and «removeAll» have been renamed «add» and «sub» because those two names are used by AQL in order to provide access to the operator «+» and «-». As a result we can now write in AQL «firstSequence + secondSequence» or «firstSet - secondSet».
AQL is way smarter than MTL regarding to the types of your expressions. As a result, you can combine expressions using multiple types quite easily. For example, this is a valid AQL expression
self.eContents(uml::Class).add(self.eContents(ecore::EClass)).name
. In Acceleo MTL, we could not use this behavior because Acceleo MTL had to fall back to the concept EObject which does not have a feature «name» while AQL knows that the collection contains objects that are either «uml::Class» or «ecore::EClass» and both of those types have a feature named «name».
AQL handles null (OclVoid) differently from ocl, a null value will not cause a failure but will be silently handled.
For example,
null.oclIsKindOf(ecore::EClass)
would have returned true for MTL/OCL, forcing users to use
not self.oclIsUndefined() and self.oclIsKindOf(ecore::EClass)
instead. This is no longer true in AQL, where «null» doesn’t conform to any type, so
null.oclIsKindOf(ecore::EClass)
will return false. Note that it’s still possible to «cast» null in any given classifier.
null.oclAsType(ecore::EClass)
will not fail at runtime.
Furthermore
oclIsUndefined() does not exist in AQL and should be replaced by a
... <> null
expression.
All operations referencing a type are now using a type literal with the name of the EPackage and the name of the type instead of a string with the name of the type. As a result,
eObject.eAllContents('EClass')
would be translated using
eObject.eAllContents('ecore::EClass')
. This allows AQL to now in which EPackage to look for the type and as such, it improves the quality of the validation.
In order to test the type of an EObject, a common pattern in Acceleo 2 was to treat the EObject as a collection and filter said collection on the type desired to see if the size of the collection changed. In AQL, you have access to the operations oclIsTypeOf and oclIsKindOf. You can thus test the type of an EObject with the expression «eObject.oclIsKindOf(ecore::EStructuralFeature)» or «eObject.oclIsTypeOf(ecore::EAttribute)». You can use the operation oclIsKindOf to test if an object has the type of the given parameter or one of its subtype. On the other hand, you can use the operation oclIsTypeOf to test if an object has exactly the type of the given parameter.
Casting in AQL is useless, since AQL is very understandable when it comes to types, it will always tries its best to evaluate your expression.
Since AQL is very close to Acceleo MTL, you can find some additional documentation using the Acceleo equivalence documentation in the Acceleo documentation.
In Acceleo2
self.eContainer("TypeName")
actually had the behavior of returning self if it was matching the TypeName. As such, when migrating from an eContainer(..) call you should either make sure that this behavior is not needed or use the
compatibility method provided by AQL :
self.eContainerOrSelf(some::Type)
This section provide information and code snippet. It will help you to integrate AQL in your own tool.
Simple overview of AQL:
For each node of the AST we create a set of possible types as follow:
A special type NothingType is used to mark a problem on a given node of the AST. Those NothingTypes are then used to create validation messages. If an AST node has only NothingTypes validation messages will be set as errors for this node, otherwise they are set as warnings.
The completion rely on the AST production and the type validation.
The identifier fragments preceding (prefix) and following (remaining) the cursor position are removed from the expression to parse. The prefix and remaining are used later to filter the proposals. Many filters can be implemented: filter only on prefix, filter on prefix and remaining, same strategies with support for camel case, ...
Completion on the AST:
To get a fresh environment you can use one of the following snippet:
IQueryEnvironment queryEnvironment = Query.newEnvironmentWithDefaultServices(null);
To get an environment with predefined services.
or
IQueryEnvironment queryEnvironment = Query.newEnvironment(null);
To get an environment with no predefined services. It can be useful to create your own language primitives.
Note that you can also provide a CrossReferenceProvider to define the scope of cross references in your environment. See CrossReferencerToAQL for more details.
You can register new services Class as follow:
ServiceRegistrationResult registrationResult = queryEnvironment.registerServicePackage(MyServices.class);
The registration result contains information about services overrides.
You can also register your EPackages. Only registered EPackages are used to validate and evaluate AQL expression.
queryEnvironment.registerEPackage(MyEPackage.eINSTANCE);
In some cases you might also want to create custom mappings between an EClass and its Class. A basic case is the use of EMap:
queryEnvironment.registerCustomClassMapping(EcorePackage.eINSTANCE.getEStringToStringMapEntry(), EStringToStringMapEntryImpl.class);
By default the EClass is mapped to Map.Entry which is not an EObject. This prevents using services on EObject.
The first step is building your expression from a String to an AST:
QueryBuilderEngine builder = new QueryBuilderEngine(queryEnvironment);
AstResult astResult = builder.build("self.name");
To evaluate an AQL expression you can use the QueryEvaluationEngine
QueryEvaluationEngine engine = new QueryEvaluationEngine(queryEnvironment);
Map<String, Object> variables = Maps.newHashMap();
variables.put("self", EcorePackage.eINSTANCE);
EvaluationResult evaluationResult = engine.eval(astResult, variables);
Here we only use one variable for demonstration purpose.
This step is optional for evaluation. You can evaluate an AQL expression without validating it in the first place.
Map<String, Set<IType>> variableTypes = new LinkedHashMap<String, Set<IType>>();
Set<IType> selfTypes = new LinkedHashSet<IType>();
selfTypes.add(new EClassifierType(queryEnvironment, EcorePackage.eINSTANCE.getEPackage()));
variableTypes.put("self", selfTypes);
AstValidator validator = new AstValidator(queryEnvironment, variableTypes);
IValidationResult validationResult = validator.validate(astResult);
To do this use the QueryCompletionEngine, it will build the query and validate it for you. It will also compute needed prefix and suffix if any:
Map<String, Set<IType>> variableTypes = new LinkedHashMap<String, Set<IType>>();
Set<IType> selfTypes = new LinkedHashSet<IType>();
selfTypes.add(new EClassifierType(queryEnvironment, EcorePackage.eINSTANCE.getEPackage()));
variableTypes.put("self", selfTypes);
QueryCompletionEngine engine = new QueryCompletionEngine(queryEnvironment);
ICompletionResult completionResult = engine.getCompletion("self.", 5, variableTypes);
List<ICompletionProposal> proposals = completionResult.getProposals(new BasicFilter(completionResult));
Here 5 is the offset where the completion should be computed in the given expression.