Monday, November 19, 2012

Xtext tip: How do I get the Guice Injector of my language?

This post is about one of the most frequent mistakes people are making when starting to use Xtext beyond the basic functionality. I stumble across this in our newsgroup about three times a week.

The problem usually starts by the necessity to load a file in your language in the UI, lets say in a custom action. You already know that in Xtext everything is wired up with dependency injection (DI), and that you need a DI configured XtextResourceSet to load a model. So you start looking for an Injector to create your XtextResourceSet and find that one is returned by the generated [DSL]StandaloneSetup.createInjectorAndDoEMFRegistration(). This seems to work fine first, but in the followoing you'll get a lot of strange errors. Things that worked before are suddenly broken.

A variant of the error – more popular among former oAW users – is to run a generator MWE2 workflow containing such a [DSL]StandaloneSetup from the UI.

Never ever use the [DSL]StandaloneSetup for your language within Eclipse!


Why not? Xtext relies on EMF and EMF uses on a bunch of global registries to work. E.g. the EPackage.Registry for the implementation classes for EPackages or the Resource.Factory.Registry for parsers for model files. These are global singletons, and they are usually populated by means of Eclipse extension points, e.g. org.eclipse.emf.ecore.generated_package or org.eclipse.emf.ecore.extension_parser. If you run a standalone Java application – the term standalone means without equinox here – there is no concept of extension points. That means you have to register the DSL to EMF's registries yourself. This is what the [DSL]StandaloneSetup is meant for. If you're using it within Eclipse, you'll overwrite the contributions from the extension points with new instances from the standalone setup. This will crash your application. 

It gets even worse: Xtext has a couple of singletons, too, but they are not global but scoped within the (singleton) Injector of your language. If you just create a new Injector, you'll end up with two instances for all of these singletons. This will likely hit you badly.

Finally, some Xtext components have different implementations for Eclipse and for standalone mode. E.g., Xtext's Java reflection layer (aka JVM model) uses JDT's Java model in Eclipse and Java Reflection in standalone mode. The Injector from the standalone setup will register different implementation classes, thus also breaking functionality.

So how do I use dependency injection in Eclipse properly then?


The Equinox aware Injector you're looking for can be obtained from the Activator of your language (in the UI plug-in). But it would be bad advise to just tell you to go there. You should rather use DI properly. If you think about where to get the Injector you're most often already on the wrong track.

A central aspect of DI is not to care about object creation at all, but just declare dependencies (via @Inject in Guice). Every dependency that is injected will also be created by the Injector so its own dependencies will be created on-demand and injected recursively. As a result, in a well-designed application using DI, there is only one entry class that is explicitly created or populated by the Injector and the rest is automatically DI aware as it is injected somewhere.

IDE components in Eclipse are usually registered by contributing to an extension point with an executable extension, usually an attribute with name class, for example

   <extension point="org.eclipse.ui.editorActions">
      
      <editorContribution targetID="..." id="...">

         <action
 class="[some class extending IActionDelegate]"
  
   ... 

Xtext generates a so called executable extension factory for you that will instantiate the given class using the Injector, so use

  ... class="[DSL]ExecutableExtensionFactory:[some class name]"

and the class will be generated with all its dependencies injected. This is also covered in the documentation.

If there is no such extension point, and your class is not yet instantiated using DI, you might have to access the Injector via the Activator and use it to inject all dependencies, i.e.

   [DSL]Activator.getInstance().getInjector("").injectMembers(this);

Note that you should only have to do this for the entry class, from which all other components are injected. If you have to use the Injector, make sure it is only once.

How do I use DI in tests?


There is a whole section on using DI in tests in the documentation. I am not going to repeat this here. The base configuration requires using JUnit4 and is automatically generated for your language has been created with a recent version of Xtext. If not, make sure to add

   pathTestProject = "[DSL].tests" 

in the section on paths in your MWE2 workflow and

   // generates junit test support classes into Generator#pathTestProject
   fragment = junit.Junit4Fragment {}
 


to the fragments and regenerate. This will create the preconfigured test plug-in. Use the InjectorProvider for standalone tests and the UiInjectorProvider for plug-in tests. Once again, your code should not show any references to an Injector.

And the Injector of another language?


Sometimes, you want to switch to a service implementation from another language, e.g. when your DSL allows cross references to another. Usually, you are in the DI context of one language and have an URI to a resource or element of the other language at hand. In this case use the IResourceServiceProvider.Registry:

   @Inject IResourceServiceProvider.Registry reg;

and use that to get the component, e.g. to get the default ILabelProvider

   ILabelProvider otherLangLabelProvider = 
      reg.getResourceServiceProvider(otherLangURI)
           .get(ILabelProvider.class);

7 comments:

Michael Vorburger said...

Hallo Jan, this post is VERY timely... I was just pulling my hair out today trying to find a way to get an IQualifiedNameProvider & IQualifiedNameConverter in a kind of generic helper we have which has to work across different languages, including cross references in a language to another (@see https://github.com/vorburger/xtext-sandbox/blob/master/Xtext-XML/ch.vorburger.xtext.xml/src/ch/vorburger/xtext/xml/NameURISwapperImpl.java if interested) ...

... and using IResourceServiceProvider.Registry.INSTANCE.getResourceServiceProvider(resource.getURI()).get(IQualifiedNameProvider.class) did the magic!

I do realize @Inject IResourceServiceProvider.Registry reg; would be much cleaner than IResourceServiceProvider.Registry.INSTANCE... but DI isn't always easily possible in existing code. And INSTANCE appears to work.. both in IDE as well as in a standalone @RunWith(XtextRunner.class) test.

Sp this IResourceServiceProvider API is VERY useful - I wish I knew about this months ago. May be you guys could feature it more prominently in the Doc, e.g. straight in http://www.eclipse.org/Xtext/documentation.html#dependencyInjection ?

Thanks a ton!!!

Jan Köhnlein said...

Yes, IResourceServiceProvider.Registry.INSTANCE works fine if you cannot get into the DI context. Nevertheless it is much cleaner to try to get into the DI context at the first attempt: The whole idea about DI is that clients don't worry about the creation of their dependencies at all and that the code should be clean and readable. Along the boundaries of the DI context it's the complete opposite. In that sense, DI is contageous: If your code builds upon other DI based code, you should really try to use DI, too.

Danny said...

Hi Jan, in this moment I'm development a plugin that allows users convert a defined xtext language to another.
The method that I use in my plugin is:
First: Extract the info of the model and initialize an instance of the initial language something like that:
new ColegioStandaloneSetup().createInjectorAndDoEMFRegistration();
System.out.println(" HAGA ALGO");
}

ResourceSet rs = new ResourceSetImpl();
System.out.println("OTRAAAAAAA");
org.eclipse.emf.common.util.URI nuevillo=org.eclipse.emf.common.util.URI.createFileURI("/Users/------/Documents/runtime-New_configuration/pruebaColegio/src/prueba.colegio");
System.out.println("ALGO");

Second: Convert this information with a tree parser help and convert to another languge model.



My problem is that Eclipse doesn't read this line "new ColegioStandaloneSetup().createInjectorAndDoEMFRegistration();" . I'm looking many information about that but I couldn't make nothing with it.

I am not sure if your post is about it but I need help from you if you know something of this topic.

Jan Köhnlein said...
This comment has been removed by the author.
Jan Köhnlein said...

Danny, please post such questions in the Xtext forum.

The fact that you are building a plug-in and use the standalone setup makes me wonder if you read this post.

Jörn Guy Süß said...

Hello Jan, I am trying to run an MWE2 workflow and the MWE2Runner class also requires injection. Can you shed light on how this is best done correctly within Eclipse?

Jan Köhnlein said...

@Jörn I guess that's discussed widely in https://bugs.eclipse.org/bugs/show_bug.cgi?id=318721