I'm trying to get cross-references to work in my DSL. Here's a stripped down version of the grammar (a modified version of the standard example DSL):
grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals
generate myDsl "http://www.xtext.org/example/mydsl/MyDsl"
Releases:
releases+=Release*
;
terminal VERSION : ('0'..'9')+'.'('0'..'9')+('.'('0'..'9'|'x')+)?;
Release:
'release' version = VERSION ('extends' parent = [Release|VERSION])?
;
Since I'm not using the standard name = ID pattern, I followed this blog post about how to create my own IQualifiedNameProvider:
public class MyDslQNP extends DefaultDeclarativeQualifiedNameProvider {
QualifiedName qualifiedName(Release e) {
Package p = (Package) e.eContainer();
return QualifiedName.create(p.getName(), e.getVersion());
}
}
From another answer on SO, I gathered that I should implement my own scope provider:
public class MyDslScopeProvider extends AbstractDeclarativeScopeProvider {
IScope scope_Release_parent(Release release, EReference ref) {
Releases releases = (Releases) release.eContainer();
return Scopes.scopeFor(releases.getReleases());
}
}
I've also bound these in the runtime module:
public class MyDslRuntimeModule extends
org.xtext.example.mydsl.AbstractMyDslRuntimeModule {
#Override
public Class<? extends IQualifiedNameProvider> bindIQualifiedNameProvider() {
return MyDslQNP.class;
}
#Override
public Class<? extends IScopeProvider> bindIScopeProvider() {
return MyDslScopeProvider.class;
}
}
When running the generated editor I create a file which looks like this:
release 1.2.3
release 1.2.2 extends 1.2.3
The problem is that (1) the editor won't autocomplete on the 'extends' clause, and (2) the editor shows an error message Couldn't resolve reference to Release '1.2.3'.
What am I missing?
Your QualifiedNameProvider seems to create bogus qualified names, e.g. the name for the release 1.2.2 would have two segments [release][1.2.2] where the lookup will search for [release][1][2][2].
Please try to create a proper qualified name like this:
QualifiedName qualifiedName(Release e) {
Package p = (Package) e.eContainer();
QualifiedName release = getConverter().toQualifiedName(e.getVersion());
return QualifiedName.create(pack.getName()).append(releaseSuffix);
}
Scopes.scopeFor is a static method and therefore doesn't use the configured IQualifiedNameProvider.
You need to pass it in explicitly using
Scopes.scopeFor(Iterable, Function, IScope)
But in your case you don't need the special handling in the scope provider at all, since local elements with a qualified name provider are being put on the scope automatically.
Related
When having two Xtext models in the same project but in different folders using the same names (ID) for different objects, the scoping does not work how I want it to. How can I restrict the scoping to inside one folder and not the whole project?
Example:
grammar:
Model:
persons+=Person*
greetings+=Greeting*;
Greeting:
'Hello' name=[Person] '!';
Person:
'person' name=ID;
folder structure:
project
|-folder1
|-person1.mydsl
|-folder2
|-greeting.mydsl
|-person2.mydsl
person1.mydsl contains a Person ("Jane"), person2.mydsl also contains a Person ("Jane") and greeting.mydsl contains a Greeting ("Hello Jane!") referencing the person in person1.mydsl instead of the person in person2.mydsl.
The documentation tells me to use the StateBasedContainerManager but I don't understand where and how.
Thanks to #Christian and this I found a solution. So first adding a filter to the DefaultGlobalScopeProvider
public class MyGlobalScopeProvider extends DefaultGlobalScopeProvider {
#Override
public IScope getScope(Resource resource, EReference reference, Predicate<IEObjectDescription> filter) {
filter = new Predicate<IEObjectDescription>() {
#Override
public boolean apply(IEObjectDescription input) {
// implement here
}
};
return super.getScope(resource, reference, filter);
}
}
and then telling my runtime module to use my provider instead
public class MyDslRuntimeModule extends AbstractMyDslRuntimeModule {
#Override
public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() {
return MyGlobalScopeProvider.class;
}
}
In Xtext, how do I follow a reference from grammar B to grammar A, within a validator of grammar B (which is in the ui-plugin)? Consider the following example.
Grammar A is org.xtext.people.People
grammar org.xtext.people.People with org.eclipse.xtext.common.Terminals
generate people "http://www.xtext.org/people/People"
People:
people+=Person*;
Person:
'person' name=ID ';';
and an instance
person Alice {citizenship "MN"; id "12345"; }
person Bob {citizenship "CH"; id "54321";}
person Malice {citizenship "XXX"; id "66666"; }
At an airport, entries of people are recorded.
enter Alice;
enter Bob;
enter Malice;
Entries are modelled with a second grammar B org.xtext.entries.Entries
grammar org.xtext.entries.Entries with org.eclipse.xtext.common.Terminals
generate entries "http://www.xtext.org/entries/Entries"
import "http://www.xtext.org/people/People"
Entries:
entries+=Entry*;
Entry:
'enter' person=[Person] ';';
After ensuring that the Eclipse project org.xtext.entries has the project org.xtext.people on it's classpath, and ensuring that the org.xtext.entries plugin has the org.xtext.people as a dependency, all works as expected.
There is a travel ban on people from country XXX, although certain deserving people are excluded. Only the CIA knows who is excluded from the ban. Entries must not be allowed for people from XXX unless excluded.
The updated grammar is
grammar org.xtext.entries.Entries with org.eclipse.xtext.common.Terminals
generate entries "http://www.xtext.org/entries/Entries"
import "http://www.xtext.org/people/People"
Entries:
entries+=Entry*;
Entry:
travelBanOverride=TravelBanOverride?
'enter' person=[Person] ';';
TravelBanOverride: '#TravelBanOverride' '(' code=STRING ')';
with validator
package org.xtext.entries.validation
import org.eclipse.xtext.validation.Check
import org.xtext.entries.entries.EntriesPackage
import org.xtext.entries.entries.Entry
import org.xtext.entries.CIA
class EntriesValidator extends AbstractEntriesValidator {
public static val BAN = 'BAN'
public static val ILLEGAL_OVERRIDE = 'ILLEGAL_OVERRIDE'
#Check
def checkBan(Entry entry) {
if (entry.person.citizenship == "XXX") {
if (entry.travelBanOverride === null) {
error('Violation of Travel Ban', EntriesPackage.Literals.ENTRY__PERSON, BAN)
}
else {
val overridecode = entry.travelBanOverride.code;
val valid = CIA.valid(entry.person.name, entry.person.id, overridecode)
if (!valid) {
error('Illegal override code', EntriesPackage.Literals.ENTRY__TRAVEL_BAN_OVERRIDE, ILLEGAL_OVERRIDE)
}
}
}
}
}
where the driver for the external CIA web-app is modelled for example by
package org.xtext.entries;
public class CIA {
public static boolean valid(String name, String id, String overrideCode) {
System.out.println("UNValid["+name+","+overrideCode+"]");
return name.equals("Malice") && id.equals("66666") && overrideCode.equals("123");
}
}
The validations work as expected.
I now wish to provided a quick-fix for BAN, that checks for an override code from the CIA.
package org.xtext.entries.ui.quickfix
import org.eclipse.xtext.ui.editor.quickfix.DefaultQuickfixProvider
import org.eclipse.xtext.ui.editor.quickfix.Fix
import org.xtext.entries.validation.EntriesValidator
import org.eclipse.xtext.validation.Issue
import org.eclipse.xtext.ui.editor.quickfix.IssueResolutionAcceptor
import org.xtext.entries.entries.Entry
import org.xtext.entries.Helper
class EntriesQuickfixProvider extends DefaultQuickfixProvider {
#Fix(EntriesValidator.BAN)
def tryOverride(Issue issue, IssueResolutionAcceptor acceptor) {
acceptor.accept(issue, 'Try override', 'Override if CIA says so.', 'override.png')
[element ,context |
val entry = element as Entry
// val person = entry.person // no such attribute
//val person = Helper.get(entry); // The method get(Entry) from the type Helper refers to the missing type Object
]
}
}
The first commented line does not compile: there is no attribute person. The second commented line is an attempt to solve the problem by getting a helper class in org.xtext.entries to get the person, but this does not compile either, giving a "The method get(Entry) from the type Helper refers to the missing type Object" error message.
For completeness, here is that helper.
package org.xtext.entries
import org.xtext.people.people.Person
import org.xtext.entries.entries.Entry
class Helper {
static def Person get(Entry entry) {
return entry.person;
}
}
Further, entry.travelBanOverride compiles fine, but entry.person does not. Clicking on Entry in Eclipse takes one to the expected code, which has both travelBanOverride and person.
The issue does not occur with a Java class in the same project and package.
package org.xtext.entries.ui.quickfix;
import org.xtext.entries.entries.Entry;
import org.xtext.people.people.Person;
public class Test {
public static void main(String[] args) {
Entry entry = null;
Person p = entry.getPerson();
}
}
Rewriting the quickfix in Java solves the problem.
package org.xtext.entries.ui.quickfix;
import org.eclipse.xtext.ui.editor.quickfix.DefaultQuickfixProvider;
import org.eclipse.xtext.ui.editor.quickfix.Fix;
import org.xtext.entries.validation.EntriesValidator;
import org.eclipse.xtext.validation.Issue;
import org.eclipse.xtext.ui.editor.quickfix.IssueResolutionAcceptor;
import org.xtext.entries.entries.Entry;
import org.xtext.entries.Helper;
import org.eclipse.xtext.ui.editor.model.edit.IModificationContext;
import org.eclipse.xtext.ui.editor.model.edit.ISemanticModification;
import org.eclipse.emf.ecore.EObject;
import org.xtext.entries.entries.Entry;
import org.xtext.people.people.Person;
public class EntriesQuickfixProvider extends DefaultQuickfixProvider {
#Fix(EntriesValidator.BAN)
public void tryOverride(final Issue issue, IssueResolutionAcceptor acceptor) {
acceptor.accept(issue,
"Try to override",
"Override",
"override.gif",
new ISemanticModification() {
public void apply(EObject element, IModificationContext context) {
Entry entry = (Entry) element;
System.out.println(entry.getPerson());
}
}
);
}
}
How do I follow a reference from grammar B (Entries) to grammar A (People), within a validator of grammar B?
My mistake is the following.
After ensuring that the Eclipse project org.xtext.entries has the
project org.xtext.people on it's classpath, and ensuring that the
org.xtext.entries plugin has the org.xtext.people as a dependency, all
works as expected.
The org.xtext.entries.ui ui-plugin must also have the org.xtext.people on its Java (Eclipse project) build path. Exporting and making a plugin-dependency it not enough.
Note that this setting should be made early, before crafting the quick-fix, because the Xtend editor has refreshing issues.
Not sure if I'm thinking right about this, I'm looking[in CDI] for something similar to what we have in Spring - #ConditionalOnMissingBean - that allows you tell spring - create only if the bean specified is missing.
I've tried using extensions, looks like one can tap several events, and use those to VETO beans. One way might be to have BeanManager at this stage, and look for already present beans, and if it contains the one you're about to inject, VETO this one. BUT, this would only work when we HAVE LOOKED AT ALL the beans.
AfterBeanDiscovery looks suitable, however, before it is invoked, validation fails, complaining of multiple beans of the same type.
Would be great if I could get some help here.
Your question is interesting and can be solved using a CDI extension (almost as you describe, actually), see below for a naive, working, proof-of-concept implementation. It is naive because it does not handle e.g. producer methods/fields and may be missing more.
CDI extensions are really great and powerful, but can be rather technical, so let's discuss other alternatives first.
Specialization: Maybe it is enough for your use case to document explicitly that you provide the default implementation of SomeService through, say, public class SomeServiceDefaultImpl and in order to override it the developer should do:
#Specializes
public class SomeServiceSpecialImpl extends SomeServiceDefaultImpl {...}
Also consider the alternatives, as mentioned in the comment from John Ament.
Qualifiers: If this service is used only in one place/a few places and only inside your code, you could qualify your SomeServiceDefaultImpl with a custom qualifier, say #MyDefaultImpl. Then inject an Instance<SomeService>, look for an unqualified instance first and, if that is not satisfied, look for the qualified - something along the lines of:
private SomeService someService;
#Inject
void setSomeServiceInstance(Instance<SomeService> s) {
// not tried, please adapt as needed
if( s.isUnsatisfied() ) {
someService = s.select(new MyDefaultImplAnnotation()).get();
}
else {
someService = s.get();
}
}
Provide a default implementation that is #Vetoed so as to force the client of your code to provide an implementation. If the client wants to use the default, they can simply use a producer.
Having said the above, the implementation below is a proof of concept that:
Requires the following annotation to be present on the default implementation:
#Target({ TYPE, METHOD, FIELD })
#Retention(RUNTIME)
#Documented
public #interface ConditionalOnMissingBean {
Class<?> value();
}
The value() is required and denotes the bean type that is "defaulted". Your implementation can be smarter, i.e. detect the bean type from the actual default implementation, but, hey, that's only a proof of concept!
Blatantly ignores producers!
Is lightly tested, so there are probably evil corner cases, so BEWARE!
In addition to the code you need all the choreography of an extension (META-INF/services/javax.enterprise.inject.spi.Extension, beans.xml).
import java.util.HashMap;
import java.util.Map;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanAttributes;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.InjectionTargetFactory;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
public class ConditionalOnMissingBeanExtension implements Extension {
private Map<Class<?>, AnnotatedType<?>> map = new HashMap<>();
<T> void processAnnotatedType(#Observes ProcessAnnotatedType<T> pat) {
AnnotatedType<?> annotatedType = pat.getAnnotatedType();
ConditionalOnMissingBean annotation = annotatedType.getAnnotation(ConditionalOnMissingBean.class);
if( annotation != null ) {
map.put(annotation.value(), annotatedType);
pat.veto();
}
}
void afterBeanDiscovery(#Observes AfterBeanDiscovery abd, BeanManager beanManager) {
map.entrySet().stream()
.filter(e -> doesNotHaveBeanOfType(beanManager, e.getKey()))
.map(e -> defineBean(beanManager, e.getValue()))
.forEach(abd::addBean);
map = null;
}
private boolean doesNotHaveBeanOfType(BeanManager beanManager, Class<?> type) {
return beanManager.getBeans(type).isEmpty();
}
private <T> Bean<T> defineBean(BeanManager beanManager, AnnotatedType<T> annotatedType) {
BeanAttributes<T> beanAttributes = beanManager.createBeanAttributes(annotatedType);
InjectionTargetFactory<T> injectionTargetFactory = beanManager.getInjectionTargetFactory(annotatedType);
return beanManager.createBean(beanAttributes, annotatedType.getJavaClass(), injectionTargetFactory);
}
}
An example of a default implementation of a service interface would be:
#ApplicationScoped
#ConditionalOnMissingBean(SomeService.class)
public class SomeServiceDefaultImpl implements SomeService {
#Override
public String doSomeCalculation() {
return "from default implementation";
}
}
The error: The argument type 'AdvformBaseComponent(advform/base.dart)' can't be assigned to the parameter type 'AdvformBaseComponent(advform/base.dart)'.
The analyzer is warning that the argument type can't be AdvformBaseComponent, but the function is expecting that exact class as argument:
void addControl(AdvformBaseComponent baseComponent, String fieldName);
the below snippet is within a ngOnInit of AdvformBaseComponent.
objector.addControl(this, name);
It only hides the warning if I do a "addControl(this as dynamic", but that is so hacky and non performant...
sdk 1.22.1
EDIT:
The AdvformBaseComponent is an abstract class that is implemented by other components. It is essentially a base class for form components.
The AdvformObjectComponent is a panel group of AdvformBaseComponent based components. It is mandatory that any AdvformBaseComponent have a parent of AdvformObjectComponent.
They are in the same project and same directory.
I can share these files if you find it useful, they are browser components anyway.
abstract class AdvformBaseComponent implements OnInit, AfterViewInit,
OnDestroy {
AdvformObjectComponent objector;
AdvformBaseComponent(this.objector, #Optional() this._submitter, this.translator, #Optional() this._group) {
if (objector == null)
throw new Exception(
'Advform inputs must have a advform-object as parent.');
}
}
class AdvformObjectComponent implements OnInit {
void addControl(AdvformBaseComponent baseComponent, String fieldName) {
...
}
}
the error stopped after the 1.23.0
I am in the process of converting some tests from Hamcrest to AssertJ. In Hamcrest I use the following snippet:
assertThat(list, either(contains(Tags.SWEETS, Tags.HIGH))
.or(contains(Tags.SOUPS, Tags.RED)));
That is, the list may be either that or that. How can I express this in AssertJ? The anyOf function (of course, any is something else than either, but that would be a second question) takes a Condition; I have implemented that myself, but it feels as if this should be a common case.
Edited:
Since 3.12.0 AssertJ provides satisfiesAnyOf which succeeds is one of the given assertion succeeds,
assertThat(list).satisfiesAnyOf(
listParam -> assertThat(listParam).contains(Tags.SWEETS, Tags.HIGH),
listParam -> assertThat(listParam).contains(Tags.SOUPS, Tags.RED)
);
Original answer:
No, this is an area where Hamcrest is better than AssertJ.
To write the following assertion:
Set<String> goodTags = newLinkedHashSet("Fine", "Good");
Set<String> badTags = newLinkedHashSet("Bad!", "Awful");
Set<String> tags = newLinkedHashSet("Fine", "Good", "Ok", "?");
// contains is statically imported from ContainsCondition
// anyOf succeeds if one of the conditions is met (logical 'or')
assertThat(tags).has(anyOf(contains(goodTags), contains(badTags)));
you need to create this Condition:
import static org.assertj.core.util.Lists.newArrayList;
import java.util.Collection;
import org.assertj.core.api.Condition;
public class ContainsCondition extends Condition<Iterable<String>> {
private Collection<String> collection;
public ContainsCondition(Iterable<String> values) {
super("contains " + values);
this.collection = newArrayList(values);
}
static ContainsCondition contains(Collection<String> set) {
return new ContainsCondition(set);
}
#Override
public boolean matches(Iterable<String> actual) {
Collection<String> values = newArrayList(actual);
for (String string : collection) {
if (!values.contains(string)) return false;
}
return true;
};
}
It might not be what you if you expect that the presence of your tags in one collection implies they are not in the other one.
Inspired by this thread, you might want to use this little repo I put together, that adapts the Hamcrest Matcher API into AssertJ's Condition API. Also includes a handy-dandy conversion shell script.