Library Bundles for your Xtext DSL

Xtext offers powerful cross-reference mechanisms to resolve elements inside the current resource or even from imported resources. External resources can either be imported explicitly or implicitly. My goal was to provide a library for my DSL containing a number of elements which should be referencable from “everywhere”. Consequently the “header” files from my library must be imported implicitly on a global level.

Creating a Library Bundle

To create a bundle for your library, use the New Project Wizard in Eclipse and choose Plug-In Development -> Plug-In Project. In this way as OSGi-based bundle will be created. Create a new folder for your resources to be imported globally (e.g. headers) and copy your header files (written in your DSL) to this folder.

Registering Implicit Imports in your DSL Bundle

Global implicit import behaviour is achievable using a custom ImportUriGlobalScopeProvider. Create your class in your DSL bundle in the package <your DSL package prefix>.dsl.scoping and extend org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.LinkedHashSet;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider;
 
public class MyDSLImportURIGlobalScopeProvider extends ImportUriGlobalScopeProvider
{
    public static final URI HEADER_URI = URI.createURI("platform:/plugin/my.dsl.library/headers/myHeader.ext");
    
    @Override
    protected LinkedHashSet<URI> getImportedUris(Resource resource)
    {
        LinkedHashSet<URI> importedURIs = super.getImportedUris(resource);
        importedURIs.add(HEADER_URI);
        return importedURIs;
    }
 
}

The method getImportedUris() is overwritten and extends the set of imports retrieved from the super implementation. Note that I used a platform:/plugin URI here, which is actually resolved sucessfully in an Eclipse Runtime Workspace. In the itemis blog article about global implicit imports classpath-based URIs are used. A disadvantage of classpath-based library resolving is that it does not work out-of-the-box. In fact you have to create a plug-in project in the runtime workspace and add a dependency to your library bundle in MANIFEST.MF manually. I successfully avoided this problem by using platform:/plugin URIs which are resolved as soon as the library bundle is present in the run configuration of the runtime Eclipse instance.

Now your global scope provider has to be bound in the runtime module of your workspace. Open MyDSLRuntimeModule and add the following binding:

1
2
3
4
5
@Override
public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider()
{
    return MyDSLImportURIGlobalScopeProvider.class;
}

If you start your runtime eclipse with the library bundle now, your global implicit imports should be resolved in the editor for your DSL.

 Resolving Implicit Global Imports in Standalone Mode

If you rely on the global implicit imports in standalone mode (e.g. in unit tests executed in your development Eclipse instance) the platform:/plugin URIs can not be resolved. But this can easily be fixed by using URI mappings in a ResourceSet. The following example shows how to create a standalone parser by injecting a ResourceSet and creating an URI mapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class StandaloneXtextParser
{    
    @Inject
    private XtextResourceSet resourceSet;
 
    private boolean initialized;
    
    public StandaloneXtextParser()
    {
        super();
    }
 
    public EObject parse(URI uri) throws IOException
    {
        initializeIfApplicable();
        Resource resource = resourceSet.getResource(uri, true);
        return resource.getContents().get(0);
    }
 
    private void initializeIfApplicable()
    {
        // Note: this is not the most elegant way, just for demonstration purposes
        // Just make sure that setupParser() is called once before you parse
	if(!initialized)
        {
            setupParser();
	    initialized = true;
        }
    }
 
    protected void setupParser()
    {
        resourceSet.addLoadOption(XtextResource.OPTION_RESOLVE_ALL, Boolean.TRUE);
        registerURIMappingsForImplicitImports(resourceSet);
    }
    
    private static void registerURIMappingsForImplicitImports(XtextResourceSet resourceSet)
    {
        final URIConverter uriConverter = resourceSet.getURIConverter();
        final Map<URI, URI> uriMap = uriConverter.getURIMap();
        registerPlatformToFileURIMapping(MyDSLImportURIGlobalScopeProvider.HEADER_URI, uriMap);
    }
 
    private static void registerPlatformToFileURIMapping(URI uri, Map<URI, URI> uriMap)
    {
        final URI fileURI = createFileURIForHeaderFile(uri);
        final File file = new File(fileURI.toFileString());
        Preconditions.checkArgument(file.exists());
        uriMap.put(uri, fileURI);
        
    }
 
    private static URI createFileURIForHeaderFile(URI uri)
    {
        return URI.createFileURI(deriveFilePathFromURI(uri));
    }
 
    private static String deriveFilePathFromURI(URI uri)
    {
        return ".." + uri.path().substring(7);
    }
}

The trick is to add an URI mapping which resolves the platform:/plugin URI to a relative file URI. Then the library resources are resolved by means of a relative path in the workspace and the global implicit imports can also be used in unit tests. For more information on standalone parsing please read this blog article.

Other useful resources:

 

7 thoughts on “Library Bundles for your Xtext DSL

  1. I like this idea of adding it as resource. However I’m using a DeclarativeScopeProvider. Any clue how to do this?

  2. Great idea. Thanks for sharing this. It works like a charm for a library I created for my DSL. However, because MyDSLImportURIGlobalScopeProvider extends ImportUriGlobalScopeProvider and not the DefaultGlobalScopeProvider, I now lose the ability to import and reference objects defined in other files in my project. The DefaultGlobalScopeProvider actually provides a project based scope, which is quite useful for this purpose in combination with a ImportedNamespaceAwareLocalScopeProvider. How did you get around this problem?

  3. Thanks for your article, I have exactly that problem, i.e. cannot load my library for unit tests.

    Which baseclass are you subclassing your StandaloneXtextParser from? It overrides setupParser() but where is that?

    • Thanks for your comments! I think I accidentally copied the @Override because I had subclasses of StandaloneXtextParser and combined everything in one class for the blog post. I removed it now.

  4. I just added some more code that includes the parsing and the setup itself, so it’s more understandable when setupParser() should be called. It must be called once before you start parsing.

Leave a Reply

Your email address will not be published. Required fields are marked *