avatar

Xtext and more

  • Impressum

Xtext Model Visualization with PlantUML

April 12, 2013 · Christian Dietrich · Tags: PlantUML , UML , Visualization , Xtext

One of my Colleagues recently gave me a hint on PlantUML which is a nice tool to create Graphviz based UML diagrams from a textual input. This blogpost describes how to include PlantUML into Xtext to generate Visualizations from textual models on the fly.

Here is the DSL from

Domainmodel :
  elements += Type*
;
   
Type:
  DataType | Entity
;
   
DataType:
  'datatype' name = ID
;
  
Entity:
  'entity' name = ID ('extends' superType = [Entity])? '{'
     features += Feature*
  '}'
;
  
Feature:
  many?='many'? name = ID ':' type = [Type]
;

The target is to take an input model like

datatype String
 
entity A {
    many names : String
    c : C
}
 
entity B {
    something : String
    many myA : A
}
 
entity C {
     
} 

An generate a nice Diagram like

Plant UML Sample

To make the integration easy we generate the png using the existing Builder/Generator infrastructure

So here is the text input for PlantUML we need to generate

@startuml
class A {
    List<String> names
}
 
A o--  C : c
 
class B {
    String something
}
 
B o-- "*"  A : myA
 
class C {
}
 
 
@enduml

and here the generator that does the conversion and feeds Plantuml

class MyDslGenerator implements IGenerator {
 
    override void doGenerate(Resource resource, IFileSystemAccess fsa) {
        val filename = resource.URI.lastSegment
        for (dm : resource.contents.filter(typeof(Domainmodel))) {
            val plantUML = dm.toPlantUML.toString
            if (fsa instanceof IFileSystemAccessExtension3) {
                val out = new ByteArrayOutputStream()
                new SourceStringReader(plantUML).generateImage(out)
                (fsa as IFileSystemAccessExtension3).generateFile(filename + ".png",
                    new ByteArrayInputStream(out.toByteArray))
            } else {
                fsa.generateFile(filename + ".txt", plantUML)
            }
        }
    }
 
     
    def dispatch CharSequence toPlantUML(Domainmodel it) '''
    @startuml
    «FOR e : elements.filter(typeof(Entity))»
    «e.toPlantUML»
    «ENDFOR»
    @enduml
    '''
     
    def dispatch CharSequence toPlantUML(Entity it) '''
    class «name» {
        «FOR f : features.filter[type instanceof DataType]»
        «IF f.many»List<«f.type.name»>«ELSE»«f.type.name»«ENDIF» «f.name»
        «ENDFOR»
    }
     
    «FOR f : features.filter[type instanceof Entity]»
        «name» o-- «IF f.many»"*" «ENDIF» «f.type.name» : «f.name»
    «ENDFOR»
     
    ''' 
}

To get PlantUML into the Classpath we add the jar to the project and add it via the Manifest.MF file

Bundle-ClassPath: .,
 lib/plantuml.jar

Xtext: Referencing Elements of one DSL from another DSL

August 7, 2012 · Christian Dietrich · Tags: Cross-Reference , Import , Inter-Language , Xtext

This is a blog post on Inter-Language-Cross-References in Xtext. Let us asume we have a DSL that contains Definitions. Now we want to create another DSL that references (uses) the Definitions defined in the the first DSL.

So let us start with the first DSL: We create a new Xtext Project

New Project Wizard First DSL

And here is the (for demonstration purpose oversimplyfied) Grammar

grammar org.xtext.example.definitions.Definitions with org.eclipse.xtext.common.Terminals
 
generate definitions "http://www.xtext.org/example/definitions/Definitions"
 
Model:
    definitions+=Definition*;
 
Definition:
    'define' name=ID;

We run GenerateDefinitions.mwe2 to generate the language. This is all for the first DSL.

Now let us create a project for the second DSL.

New Project Wizard Second DSL

To be able to reference the first DSL from the second we add a bundle dependency to the Manifest of the second

Dependencies

Here the resulting manifest

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: org.xtext.example.usages
Bundle-Vendor: My Company
Bundle-Version: 1.0.0.qualifier
Bundle-SymbolicName: org.xtext.example.usages; singleton:=true
Bundle-ActivationPolicy: lazy
Require-Bundle: org.eclipse.xtext;visibility:=reexport,
 org.eclipse.xtext.xbase;resolution:=optional;visibility:=reexport,
 org.eclipse.xtext.generator;resolution:=optional,
 org.apache.commons.logging;bundle-version="1.0.4";resolution:=optional,
 org.eclipse.emf.codegen.ecore;resolution:=optional,
 org.eclipse.emf.mwe.utils;resolution:=optional,
 org.eclipse.emf.mwe2.launch;resolution:=optional,
 org.xtext.example.definitions;bundle-version="1.0.0"
Import-Package: org.apache.log4j
Bundle-RequiredExecutionEnvironment: J2SE-1.5

Now we can create the Grammar from the Usage DSL

grammar org.xtext.example.usages.Usages with org.eclipse.xtext.common.Terminals
 
generate usages "http://www.xtext.org/example/usages/Usages"
 
import "http://www.xtext.org/example/definitions/Definitions" as def
 
Model:
    usages+=Usage*;
     
Usage:
    'use' definition=[def::Definition];

With "http://www.xtext.org/example/definitions/Definitions" as def we import the metamodel of our Define DSL to our Usage DSL. Thus the Type Definition is available and can be used to define the cross reference definition=[def::Definition]

To get the thing running we have to do some adjustments to the workflow of the Usages language.

module org.xtext.example.usages.GenerateUsages
 
import org.eclipse.emf.mwe.utils.*
import org.eclipse.xtext.generator.*
import org.eclipse.xtext.ui.generator.*
 
var grammarURI = "classpath:/org/xtext/example/usages/Usages.xtext"
var file.extensions = "use"
var projectName = "org.xtext.example.usages"
var runtimeProject = "../${projectName}"
 
Workflow {
    bean = StandaloneSetup {
        scanClassPath = true
        platformUri = "${runtimeProject}/..";
        registerGeneratedEPackage = "org.xtext.example.definitions.definitions.DefinitionsPackage"
        registerGenModelFile = "platform:/resource/org.xtext.example.definitions/src-gen/org/xtext/example/definitions/Definitions.genmodel"
    }
     
        ...
}

we generate our Usage DSL GenerateUsages.mwe2

finally we start a new runtime eclipse and give it a try

Result

Update Xtext 2.9.2+:

Here is how the workflow looks like with the new Xtext 2.9.x generator

module org.xtext.example.usages.GenerateUsages
 
import org.eclipse.xtext.xtext.generator.*
import org.eclipse.xtext.xtext.generator.model.project.*
 
var rootPath = ".."
 
Workflow {
     
    component = XtextGenerator {
        configuration = {
            project = StandardProjectConfig {
                baseName = "org.xtext.example.usages"
                rootPath = rootPath
                runtimeTest = {
                    enabled = true
                }
                eclipsePlugin = {
                    enabled = true
                }
                eclipsePluginTest = {
                    enabled = true
                }
                createEclipseMetaData = true
            }
            code = {
                encoding = "UTF-8"
                fileHeader = "/*\n * generated by Xtext \${version}\n */"
            }
        }
        language = StandardLanguage {
            name = "org.xtext.example.usages.Usages"
            referencedResource = "platform:/resource/org.xtext.example.definitions/model/generated/Definitions.genmodel"
            fileExtensions = "use"
 
            serializer = {
                generateStub = false
            }
            validator = {
            }
        }
    }
}

Unittesting Xtend Generators

May 8, 2012 · hristian Dietrich · Tags: Generator , Unit Test , Xtext

Xtext offers nice Support for Unit Tests. But how to test a Xtend based Generator? This blogpost describes a simple approach for such a Test.

So let us take Xtext’s Hello World grammar as Starting point

Model:
    greetings+=Greeting*;
     
Greeting:
    'Hello' name=ID '!';

And following simple Generator

package org.xtext.example.mydsl.generator
 
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IFileSystemAccess
import org.eclipse.xtext.generator.IGenerator
import org.xtext.example.mydsl.myDsl.Greeting
 
class MyDslGenerator implements IGenerator {
     
    override void doGenerate(Resource resource, IFileSystemAccess fsa) {
        for (g : resource.allContents.toIterable.filter(typeof(Greeting))) {
            fsa.generateFile(g.name+".java", 
            '''
            public class «g.name» {
                 
            }
            ''')
        }
    }
}

And here the Test

import org.junit.Test
import org.junit.runner.RunWith
import org.eclipse.xtext.junit4.XtextRunner
import org.eclipse.xtext.junit4.InjectWith
import org.xtext.example.mydsl.MyDslInjectorProvider
import org.eclipse.xtext.generator.IGenerator
import com.google.inject.Inject
import org.eclipse.xtext.junit4.util.ParseHelper
import org.xtext.example.mydsl.myDsl.Model
import org.eclipse.xtext.generator.InMemoryFileSystemAccess
 
import static org.junit.Assert.*
import org.eclipse.xtext.generator.IFileSystemAccess
 
@RunWith(typeof(XtextRunner))
@InjectWith(typeof(MyDslInjectorProvider))
class GeneratorTest {
     
    @Inject IGenerator underTest
    @Inject ParseHelper<Model> parseHelper 
     
    @Test
    def test() {
        val model = parseHelper.parse('''
        Hello Alice!
        Hello Bob!
        ''')
        val fsa = new InMemoryFileSystemAccess()
        underTest.doGenerate(model.eResource, fsa)
        println(fsa.files)
        assertEquals(2,fsa.files.size)
        assertTrue(fsa.files.containsKey(IFileSystemAccess::DEFAULT_OUTPUT+"Alice.java"))
        assertEquals(
            '''
            public class Alice {
                 
            }
            '''.toString, fsa.files.get(IFileSystemAccess::DEFAULT_OUTPUT+"Alice.java").toString
        )
        assertTrue(fsa.files.containsKey(IFileSystemAccess::DEFAULT_OUTPUT+"Bob.java"))
        assertEquals(
            '''
            public class Bob {
                 
            }
            '''.toString, fsa.files.get(IFileSystemAccess::DEFAULT_OUTPUT+"Bob.java").toString)
         
    }
     
}

But how does that work?

Xtext offers a specific org.junit.runner.Runner. For Junit4 it is org.junit.runner.Runner. This Runner allows in combination with a org.eclipse.xtext.junit4.IInjectorProvide language specific injections within the test. Since we have fragment = junit.Junit4Fragment {} in our workflow Xtext already Generated the Class org.xtext.example.mydsl.MyDslInjectorProvider. If we would not use Xtext at all we would have to create such a InjectorProvider manually.

To wire these things up we annotate your Test with @RunWith(typeof(XtextRunner)) and @InjectWith(typeof(MyDslInjectorProvider))

Now we can write our Test. This Basically consists of 3 steps

  1. read a model
  2. call the Generator
  3. Capture the Result

We solve Step (1) using Xtext’s org.eclipse.xtext.junit4.util.ParseHelper and Step (3) by using a special kind of IFileSystemAccess that keeps the files InMemory and does not write them to the disk.

I hope this gives you a start writing you Xtext/Xtend Generator Tests.

Xtext: Calling the Generator from a Context Menu

October 15, 2011 · Christian Dietrich · Tags: Action , Command , Context Menu , Generator , Handler , Xtend

Xtext offers the user to implement an Xtend Class that implements the org.eclipse.xtext.generator.IGenerator Interface. By Default this Generator is called by the Builder through a org.eclipse.xtext.builder.IXtextBuilderParticipant when saving the file. But what to do if I want to call the Generator explicitely through a context menu entry on the file as shown in the screenshot below? This will be shown in the Following example.

Xtext Generate from Context Menu

Grammar & IGenerator

We first start with writing the Grammar and after generating the language we implement the Generator Stub Xtext created for us.

grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals
 
generate myDsl "http://www.xtext.org/example/mydsl/MyDsl"
 
Model:
    greetings+=Greeting*;
     
Greeting:
    'Hello' name=ID ('from' from=[Greeting])?'!';
package org.xtext.example.mydsl.generator
 
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.generator.IFileSystemAccess
import org.xtext.example.mydsl.myDsl.Greeting
 
import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*
 
class MyDslGenerator implements IGenerator {
     
    override void doGenerate(Resource resource, IFileSystemAccess fsa) {
        for (g : resource.allContentsIterable.filter(typeof(Greeting))) {
            fsa.generateFile(g.name+".txt",'''
                Hello «g.name» «IF g.from != null»from «g.from.name»«ENDIF»!
            ''')
        }
    }
}

Disabling the IBuilderParticipant

Then we go to the UI Projects plugin.xml and disable the default registration of an IXtextBuilderParticipant

<!--
   <extension
         point="org.eclipse.xtext.builder.participant">
      <participant
            class="org.xtext.example.mydsl.ui.MyDslExecutableExtensionFactory:org.eclipse.xtext.builder.IXtextBuilderParticipant">
      </participant>
   </extension>
-->

Setting up the Context Menu

Then we setup the context menu with Eclipse means

<extension
        point="org.eclipse.ui.handlers">
     <handler
           class="org.xtext.example.mydsl.ui.MyDslExecutableExtensionFactory:org.xtext.example.mydsl.ui.handler.GenerationHandler"
           commandId="org.xtext.example.mydsl.ui.handler.GenerationCommand">
     </handler>
      
  </extension>
   
  <extension
        point="org.eclipse.ui.commands">
        <command name="Generate Code"
              id="org.xtext.example.mydsl.ui.handler.GenerationCommand">
        </command>
  </extension>
   
  <extension point="org.eclipse.ui.menus">
    <menuContribution locationURI="popup:org.eclipse.jdt.ui.PackageExplorer">
        <command
            commandId="org.xtext.example.mydsl.ui.handler.GenerationCommand"
            style="push">
            <visibleWhen
                  checkEnabled="false">
                  <iterate>
       <adapt type="org.eclipse.core.resources.IResource">
          <test property="org.eclipse.core.resources.name" 
                value="*.mydsl"/>
       </adapt>
    </iterate>
            </visibleWhen>
        </command>
    </menuContribution>
    </extension>

Implementing the Handler / Calling the Generator

The last thing we have to do is to call the IGenerator from the handler class. this could look like

public class GenerationHandler extends AbstractHandler implements IHandler {
     
    @Inject
    private IGenerator generator;
 
    @Inject
    private Provider<EclipseResourceFileSystemAccess> fileAccessProvider;
     
    @Inject
    IResourceDescriptions resourceDescriptions;
     
    @Inject
    IResourceSetProvider resourceSetProvider;
     
    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {
         
        ISelection selection = HandlerUtil.getCurrentSelection(event);
        if (selection instanceof IStructuredSelection) {
            IStructuredSelection structuredSelection = (IStructuredSelection) selection;
            Object firstElement = structuredSelection.getFirstElement();
            if (firstElement instanceof IFile) {
                IFile file = (IFile) firstElement;
                IProject project = file.getProject();
                IFolder srcGenFolder = project.getFolder("src-gen");
                if (!srcGenFolder.exists()) {
                    try {
                        srcGenFolder.create(true, true,
                                new NullProgressMonitor());
                    } catch (CoreException e) {
                        return null;
                    }
                }
 
                final EclipseResourceFileSystemAccess fsa = fileAccessProvider.get();
                fsa.setOutputPath(srcGenFolder.getFullPath().toString());
                 
                URI uri = URI.createPlatformResourceURI(file.getFullPath().toString(), true);
                ResourceSet rs = resourceSetProvider.get(project);
                Resource r = rs.getResource(uri, true);
                generator.doGenerate(r, fsa);
                 
            }
        }
        return null;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
}

Give it a try

We start a runtime app, create a bunch of model files that refernce each other, try the genenerator – and hey – it works.

Result

Editor Context Menu

And how if i want to call the generator from the context menu of the open editor

Context Menu Editor

I register the command for the editor and write a Handler like this one

<extension point="org.eclipse.ui.menus">
    <menuContribution locationURI="popup:#TextEditorContext?after=additions">
        <command
            commandId="org.xtext.example.mydsl.ui.handler.GenerationCommand"
            style="push">
            <visibleWhen
                      checkEnabled="false">
                   <reference
                         definitionId="org.xtext.example.mydsl.MyDsl.Editor.opened">
                   </reference>
                </visibleWhen>
        </command>
    </menuContribution>
</extension>
public class GenerationHandler extends AbstractHandler implements IHandler {
     
    @Inject
    private IGenerator generator;
 
    @Inject
    private Provider<EclipseResourceFileSystemAccess> fileAccessProvider;
     
    @Inject
    IResourceDescriptions resourceDescriptions;
     
    @Inject
    IResourceSetProvider resourceSetProvider;
     
    @Override
    public Object execute(ExecutionEvent event) throws ExecutionException {
         
        IEditorPart activeEditor = HandlerUtil.getActiveEditor(event);
        IFile file = (IFile) activeEditor.getEditorInput().getAdapter(IFile.class);
        if (file != null) {
            IProject project = file.getProject();
            IFolder srcGenFolder = project.getFolder("src-gen");
            if (!srcGenFolder.exists()) {
                try {
                    srcGenFolder.create(true, true,
                            new NullProgressMonitor());
                } catch (CoreException e) {
                    return null;
                }
            }
     
            final EclipseResourceFileSystemAccess fsa = fileAccessProvider.get();
            fsa.setOutputPath(srcGenFolder.getFullPath().toString());
             
             
            if (activeEditor instanceof XtextEditor) {
                ((XtextEditor)activeEditor).getDocument().readOnly(new IUnitOfWork<Boolean, XtextResource>() {
                 
                    @Override
                    public Boolean exec(XtextResource state)
                            throws Exception {
                        generator.doGenerate(state, fsa);
                        return Boolean.TRUE;
                    }
                });
                 
            }
        }
        return null;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
}

Update: in newer Xtext Versions configure the File System Access as follows

fsa.setOutputPath("src-gen");
fsa.setProject(project);
fsa.setMonitor(new NullProgressMonitor());

Xtext Content Assist Auto Activation

September 19, 2011 · Christian Dietrich · Tags: Auto Activation , Content Assist , Xtext

Xtext offers nice Content Assist facilities. JDT offers a nice additional feature: Content assist is autoactivated if a certain character (.) is typed. To activate this feature in Xtext simply customize your UiModule

public class MyDslUiModule extends org.xtext.example.mydsl.ui.AbstractMyDslUiModule {
    public MyDslUiModule(AbstractUIPlugin plugin) {
        super(plugin);
    }
     
    @Override
    public void configure(Binder binder) {
        super.configure(binder);
    	binder.bind(String.class)
			.annotatedWith(com.google.inject.name.Names.named(
			(XtextContentAssistProcessor.COMPLETION_AUTO_ACTIVATION_CHARS)))
			.toInstance(".,:");
    }
}

In this case content assist is autoactivated on . , and :

© 2023 - Powered by Hugo with the Type Theme