Xtext ermöglicht das Referenzieren von Elementen in DSLs auf mehrere Arten. Eine Möglichkeit sieht den Import von Elementen über Namensräume vor. Dies geschieht über die Verwendung des ImportedNamespaceAwareLocalScopeProvider
und erlaubt den "Import" einzelner oder, unter Einsatz von Wildcards (.*), aller Elemente eines Namensraumes.
Es kann aber Sprachen geben, in denen dieses Verhalten nicht gewünscht ist. In diesen Sprachen importiert der Nutzer explizit eine oder mehrere Ressource-Dateien, um auf deren Inhalte zugreifen zu können.
Eine DSL mit einem solchen Import-Verhalten lässt sich mit Xtext recht einfach erstellen, indem man eine Parser-Regel mit dem speziellen Attributnamen importURI
in die DSL einbaut. Das folgende Beispiel stellt eine einfache DSL dar, die es erlaubt, in beliebigen Ressourcen Namen zu definieren und diese in Grußbotschaften zu verwenden.
grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals generate myDsl "http://www.xtext.org/example/mydsl/MyDsl" Model: includes+=Include* names+=Name* greetings+=Greeting*; Include: 'import' importURI=STRING ; Name: 'def' name=ID ; Greeting: 'Hallo' name=[Name] '!' ;
Wir möchten Kollegen aus unserer Firma Grußbotschaften schicken. Da die Firma aber groß ist und aus vielen Kollegen besteht, die in unterschiedlichen Bereichen arbeiten, möchten wir für jeden Firmenbereich eine eigene Datei erstellen, die die Namen der jeweiligen Kollegen enthält. Dies erhöht die Übersicht und Wartbarkeit.
Nur durch einen expliziten Import einer Ressource wollen wir die enthaltenen Namensdefinitionen in den Scope aufnehmen. Dabei soll dies möglichst schnell und ressourcenschonend erfolgen.
Der Ansatz ist hierbei, die Verwendung des Index, die das unnötige und (bei großen Modellen) zeitaufwendige Laden von Ressourcen überflüssig macht. Als ersten Schritt müssen wir die Informationen bzgl. der importierten Ressourcen in den Index schreiben. Dazu implementieren wir eine Klasse MyDslResourceDescriptionStrategy
, die von DefaultResourceDescriptionStrategy
ableitet. Die Strings mit den URIs, der in der Parser-Regel Model importierten Ressourcen, werden in einen durch Kommas getrennten String zusammengeführt und unter dem Schlüssel includes in der userData Map der Objektbeschreibung im Index gespeichert.
package org.xtext.example.mydsl import com.google.inject.Inject import java.util.HashMap import org.eclipse.xtext.naming.QualifiedName import org.eclipse.xtext.resource.EObjectDescription import org.eclipse.xtext.resource.IEObjectDescription import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy import org.eclipse.xtext.scoping.impl.ImportUriResolver import org.eclipse.xtext.util.IAcceptor import org.xtext.example.mydsl.myDsl.Model import org.eclipse.emf.ecore.EObject class MyDslResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { public static final String INCLUDES = "includes" @Inject ImportUriResolver uriResolver override createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) { if(eObject instanceof Model) { this.createEObjectDescriptionForModel(eObject, acceptor) return true } else { super.createEObjectDescriptions(eObject, acceptor) } } def void createEObjectDescriptionForModel(Model model, IAcceptor<IEObjectDescription> acceptor) { val uris = newArrayList() model.includes.forEach[uris.add(uriResolver.apply(it))] val userData = new HashMap<string,string> userData.put(INCLUDES, uris.join(",")) acceptor.accept(EObjectDescription.create(QualifiedName.create(model.eResource.URI.toString), model, userData)) } }
ResourceDescriptionStrategy
nutzen zu können, müssen wir sie noch im MyDslRuntimeModule
binden.
package org.xtext.example.mydsl
import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy
import org.eclipse.xtext.scoping.IGlobalScopeProvider
import org.xtext.example.mydsl.scoping.MyDslGlobalScopeProvider
class MyDslRuntimeModule extends AbstractMyDslRuntimeModule {
def Class<? extends IDefaultResourceDescriptionStrategy> bindIDefaultResourceDescriptionStrategy() {
MyDslResourceDescriptionStrategy
}
}
Bisher haben wir nur Informationen gesammelt und im Index gespeichert. Um sie verwenden zu können, benötigen wir zusätzlich einen eigenen IGlobalScopeProvider
. Dazu implementieren wir eine Klasse MyDslGlobalScopeProvider
, die von ImportUriGlobalScopeProvider
ableitet, und überschreiben die Methode getImportedUris(Resource resource)
. Diese Methode liefert ein LinkedHashSet
zurück, das letztendlich alle URIs enthält, die in der Ressource importiert werden sollen.
Das Auslesen der importierten Ressourcen aus dem Index wird von der Methode collectImportUris
erledigt. Die Methode fragt den IResourceDescription.Manager
nach der IResourceDescription
der Ressource. Aus dieser wird für jedes Model-Element aus der userData Map der unter dem Schlüssel includes gespeicherte String mit den URIs der importierten Ressourcen ausgelesen, zerlegt und die einzelnen URIs in einem Set gespeichert.
package org.xtext.example.mydsl.scoping import com.google.common.base.Splitter import com.google.inject.Inject import com.google.inject.Provider import java.util.LinkedHashSet import org.eclipse.emf.common.util.URI import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.EcoreUtil2 import org.eclipse.xtext.resource.IResourceDescription import org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider import org.eclipse.xtext.util.IResourceScopeCache import org.xtext.example.mydsl.MyDslResourceDescriptionStrategy import org.xtext.example.mydsl.myDsl.MyDslPackage class MyDslGlobalScopeProvider extends ImportUriGlobalScopeProvider { private static final Splitter SPLITTER = Splitter.on(','); @Inject IResourceDescription.Manager descriptionManager; @Inject IResourceScopeCache cache; override protected getImportedUris(Resource resource) { return cache.get(MyDslGlobalScopeProvider.getSimpleName(), resource, new Provider<LinkedHashSet<URI>>() { override get() { val uniqueImportURIs = collectImportUris(resource, new LinkedHashSet<URI>(5)) val uriIter = uniqueImportURIs.iterator() while(uriIter.hasNext()) { if (!EcoreUtil2.isValidUri(resource, uriIter.next())) uriIter.remove() } return uniqueImportURIs } def LinkedHashSet<URI> collectImportUris(Resource resource, LinkedHashSet<URI> uniqueImportURIs) { val resourceDescription = descriptionManager.getResourceDescription(resource) val models = resourceDescription.getExportedObjectsByType(MyDslPackage.Literals.MODEL) models.forEach[ val userData = getUserData(MyDslResourceDescriptionStrategy.INCLUDES) if(userData !== null) { SPLITTER.split(userData).forEach[uri | var includedUri = URI.createURI(uri) includedUri = includedUri.resolve(resource.URI) if(uniqueImportURIs.add(includedUri)) { collectImportUris(resource.getResourceSet().getResource(includedUri, true), uniqueImportURIs) } ] } ] return uniqueImportURIs } }); } }
Um unseren MyDslGlobalScopeProvider
nutzen zu können, müssen wir diesen wiederum im MyDslRuntimeModule
binden.
package org.xtext.example.mydsl import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy import org.eclipse.xtext.scoping.IGlobalScopeProvider import org.xtext.example.mydsl.scoping.MyDslGlobalScopeProvider class MyDslRuntimeModule extends AbstractMyDslRuntimeModule { def Class<? extends IDefaultResourceDescriptionStrategy> bindIDefaultResourceDescriptionStrategy() { MyDslResourceDescriptionStrategy } override Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() { MyDslGlobalScopeProvider; } }
Wir starten den Editor für unsere kleine Sprache und beginnen die Modell-Dateien zu erstellen. Dabei haben wir die Idee, die Ressourcen der unterschiedlichen Firmenbereiche nicht einzeln zu importieren, sondern eine Ressource zu erstellen, die alle Importe enthält, und diese dann zu importieren. Dazu erstellen wir folgende Ressourcen:
Beim Erstellen der Ressource mit den Grußbotschaften stellen wir fest, dass die Namen nicht aufgelößt werden können.
Woran liegt das? Wir haben doch alle importierten Ressourcen in den Index geschrieben.
Das ist soweit richtig. Alle direkt importieren Ressourcen werden in den Index geschrieben. Die Importe in einer importierten Ressource jedoch werden ignoriert. Das von uns gewünschte Feature bezeichnet man als transitive Importe. Mit dem Import einer Ressource werden implizit alle von ihr importierten Ressourcen mit importiert.
Um in unserer Sprache transitive Importe zu ermöglichen, müssen wir unseren MyDslGlobalScopeProvider
anpassen. Statt die URI einer importierten Ressource nur in dem Set zu speichern, rufen wir zusätzlich die Methode collectImportUris
auf und übergeben die URI als Parameter, sodass deren importierte Ressourcen ebenfalls verarbeitet werden.
package org.xtext.example.mydsl.scoping import com.google.common.base.Splitter import com.google.inject.Inject import com.google.inject.Provider import java.util.LinkedHashSet import org.eclipse.emf.common.util.URI import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.EcoreUtil2 import org.eclipse.xtext.resource.IResourceDescription import org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider import org.eclipse.xtext.util.IResourceScopeCache import org.xtext.example.mydsl.MyDslResourceDescriptionStrategy import org.xtext.example.mydsl.myDsl.MyDslPackage class MyDslGlobalScopeProvider extends ImportUriGlobalScopeProvider { private static final Splitter SPLITTER = Splitter.on(','); @Inject IResourceDescription.Manager descriptionManager; @Inject IResourceScopeCache cache; override protected getImportedUris(Resource resource) { return cache.get(MyDslGlobalScopeProvider.getSimpleName(), resource, new Provider<LinkedHashSet<URI>>() { override get() { val uniqueImportURIs = collectImportUris(resource, new LinkedHashSet<URI>(5)) val uriIter = uniqueImportURIs.iterator() while(uriIter.hasNext()) { if (!EcoreUtil2.isValidUri(resource, uriIter.next())) uriIter.remove() } return uniqueImportURIs } def LinkedHashSet<URI> collectImportUris(Resource resource, LinkedHashSet<URI> uniqueImportURIs) { val resourceDescription = descriptionManager.getResourceDescription(resource) val models = resourceDescription.getExportedObjectsByType(MyDslPackage.Literals.MODEL) models.forEach[ val userData = getUserData(MyDslResourceDescriptionStrategy.INCLUDES) if(userData !== null) { SPLITTER.split(userData).forEach[uri | var includedUri = URI.createURI(uri) includedUri = includedUri.resolve(resource.URI) if(uniqueImportURIs.add(includedUri)) { collectImportUris(resource.getResourceSet().getResource(includedUri, true), uniqueImportURIs) } ] } ] return uniqueImportURIs } }); } }
Wenn wir nach dieser kleinen Anpassung unsere Ressource mit den Grußbotschaften erneut öffnen sehen wir, dass die Namen durch die transitiven Importe aufgelöst werden können.
Das Beispielprojekt kann hier heruntergeladen werden.