Skip to content

rALF API Usage

Zoltán Ujhelyi edited this page Jul 31, 2015 · 9 revisions

#rALF API Usage

Introduction

The EMDW-MC project uses a simplified version of the ALF language to define the dynamic behavior of xtUML models. The rALF parser and generator API can be used to parse these action code segments, and generate C++ code fragments based off of them.

The usage of these API classes, similar to Xtext, relies on the Google Guice dependency injection framework. This framework is used to define the UML model used as context for resolution; the API tries to hide it.

rALF API classes

Tha rALF API at this stage, consists of two main main interfaces. Each one of these interfaces has one reference implementation. Naturally, custom implementations can be defined by the user.

IReducedAlfParser

The IReducedAlfParser interface enables the user to create an ALF AST (Abstract Syntax Tree) based on a given ALF code segment, or behavioral unit containing the aforementioned code segment. The reference implementation (ReducedAlfParser.class) creates a synthetic resource for the rALF AST, containing the rALF AST. This approach is similar to the way Xtext based editors parse domain specific languages.

public interface IReducedAlfParser {
	/**
	 * Creates a rALF AST based on the specified rALF code. This AST cannot
	 * refer to UML types except the primitive types.
	 * 
	 * @param behavior
	 * @return
	 */
	ParsingResults parse(String behavior);

	/**
	 * Creates a rALF AST based on the specified rALF code and an UML context
	 * provider
	 * 
	 * @param behavior
	 * @param contextProvider
	 * @return
	 */
	ParsingResults parse(String behavior, IUMLContextProvider contextProvider);

	/**
	 * Extracts the rALF code from a specified OpaqueBehavior instance, and
	 * creates the corresponding rALF AST. The UML context provider is
	 * initialized based on the behavior parameter.
	 * 
	 * @param behavior
	 * @return
	 */
	ParsingResults parse(OpaqueBehavior behavior);

}

The recommended parse method to use is the third one, that receives directly an opaque behavior. The other two approaches are mostly for parser testing.

The returned ParsingResults class stores both the created AST (if it could be created), and a set of diagnostics (errors, warnings).

IReducedAlfGenerator

The IReducedAlfGenerator interface provides support for the creation of C++ code snippets based on rALF ASTs or AST fragments. It also has methods for generating C++ snippets from rALF snippets in one step, using a specified IReducedALFParser instance.

public interface IReducedAlfGenerator {
    
    /**
     * Creates a C++ snippet based on the defined rALF code using the provided rALF parser.
     * @param behavior String containing the rALF code
     * @param parser Parser used for parsing the rALF code
     * @return
     */
    Snippet createSnippet(String behavior, IReducedAlfParser parser) throws SnippetCompilerException;
    
    /**
     * Creates a C++ snippet based on the the rALF code, which is contained by the specified opaque behavior 
     * using the provided rALF parser.
     * @param behavior Opaque behavior containing the rALF code
     * @param parser Parser used for parsing the rALF code
     * @return
     */
    Snippet createSnippet(OpaqueBehavior behavior, IReducedAlfParser parser) throws SnippetCompilerException;
    
    /**
     * Creates a C++ snippet based on a given rALF AST.
     */
    Snippet createSnippet(ParsingResults result) throws SnippetCompilerException;

    /**
     * Creates a C++ snippet from a string representation and a context provider.
     */
    Snippet createSnippet(String behavior, IUMLContextProvider contextProvider, IReducedAlfParser parser)
			throws SnippetCompilerException;
}

Examples

During the usage of the rALF API the following main steps need to be undertaken:

  • Define injector provider.
  • This injector provider should create an injector consisting of the following modules:
    • ReducedAlfLanguageRuntimeModule: This module is responsible for the initialization of the rALF parser among other language framework elements.
    • A new module that defines the binding rules of the API classes (Note that the introduction of a module class that does this is planned, this way less boilerplate code is needed). A specific binding of the interface IUMLContextProvider is used to connect the parsed text to a UML model instance.
  • Create an injector using the injector provider.
  • Create API class instances using the injector.
  • Use the API classes.

The usage of this programming interface, is demonstrated via two example JUnit tests located in the following project: Example test location. Each one of these unit tests aims to show different scenarios the API can be used in.

  • JUnitPrimitiveTypeExampleTest: This basic JUnit test shows the usage of the API, without connecting it to an existing UML model. Because of this, only primitive types (Integer, Real, String, Boolean) can be used.
  • PluginUMLTypeExampleTest: This JUnit Plug-In test uses UML types, classes and signals specified by an example uml model located in its /model folder.

Usage with Primitive Types Only

According to the above-specified points, in this case the following classes need to be implemented:

Injector and Context Provider

Google Guice injectors can be defined using modules, sets of binding rules. If the injector receives a request to instantiate a certain class or interface, it consults its binding definitions, and based on them, instantiates the correct instance. In the following section, the injector provider required to use the API is shown. As in this basic case only primitive types are used, the injector provider and UML context provider locatied in the rALF test project can utilized.

public class ReducedAlfLanguageJUnitInjectorProvider extends ReducedAlfLanguageInjectorProvider {

    @Override
    protected Injector internalCreateInjector() {

        // register default ePackages
        if (!Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().containsKey("ecore"))
            Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("ecore",
                    new org.eclipse.emf.ecore.xmi.impl.EcoreResourceFactoryImpl());
        if (!Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().containsKey("xmi"))
            Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("xmi",
                    new org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl());
        if (!Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().containsKey("xtextbin"))
            Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("xtextbin",
                    new org.eclipse.xtext.resource.impl.BinaryGrammarResourceFactoryImpl());
        if (!EPackage.Registry.INSTANCE.containsKey(org.eclipse.xtext.XtextPackage.eNS_URI))
            EPackage.Registry.INSTANCE.put(org.eclipse.xtext.XtextPackage.eNS_URI,
                    org.eclipse.xtext.XtextPackage.eINSTANCE);

        //Create the base rALF module
        Module runtimeModule = (Module) new com.incquerylabs.uml.ralf.ReducedAlfLanguageRuntimeModule();

        //create a new module that binds the API classes
        Module customizations = new Module() {
            @Override
            public void configure(Binder binder) {
                //UML Context Provider
                binder.bind(IUMLContextProvider.class).toInstance(new TestUMLContextProvider());
                binder.bind(ReducedAlfSnippetCompiler.class).toInstance(new ReducedAlfSnippetCompiler());
                binder.bind(IReducedAlfGenerator.class).to(ReducedAlfGenerator.class);
                binder.bind(IReducedAlfParser.class).to(ReducedAlfParser.class);
            }
        };

        //create a new injector based off the modules
        return Guice.createInjector(runtimeModule, customizations);
    }

}
//The only responsibility of this context provider is to return the four basic primitive types.
//This functionality is already implemented in the AbstractUMLContextProvider, so it only implements its abstract methods and adds no further functionality.
class TestUMLContextProvider extends AbstractUMLContextProvider {

	val Resource resource

	new() {
		val set = new ResourceSetImpl()
		UMLResourcesUtil.init(set)
		resource = set.getResource(URI.createURI(UMLResource.UML_PRIMITIVE_TYPES_LIBRARY_URI), true)
	}

	override getPrimitivePackage() {
		EcoreUtil.getObjectByType(resource.getContents(), UMLPackage.Literals.PACKAGE) as Package
	}
	
	override getContainerResource() {
		resource
	}
}

Usage in Test Cases

Once the injector provider is created, it is time to use the API classes. As shown in the example test case below, create an injector using the created injector provider. Use this injector to instantiate the parser and generator classes. Finally, use the API to parse a rALF code fragment, and the generator to generate the C++ code snippets.

class JUnitPrimitiveTypeExampleTest {
	// Injector that creates the parser and the generator
	Injector injector;
	IReducedAlfGenerator generator
	IReducedAlfParser parser

	@Before
	def void init() {
		// Get an injector instance from the provider defined by the user
		// In this provider, we can define that certain which interface is implemented by which class. 
		// If the specified interface is required, the injector will then create an instance of the specified class. 
		var provider = new ReducedAlfLanguageJUnitInjectorProvider();
		injector = provider.injector
		generator = injector.getInstance(IReducedAlfGenerator)
		parser = injector.getInstance(IReducedAlfParser)
	}

	@Test
	def exampleTestCase() {
		val input = '''
		Integer x = (1 + 2) * 3 + -4;
		++x;
		Integer y = x;
		y = x - 15;
		if ((x > 3) && !(y < -5)) {
			x--;
		}'''
		
		val expected = '''
		PrimitiveTypes::Integer x = (1 + 2) * 3 + -4;
		++x;
		PrimitiveTypes::Integer y = x;
		y = x - 15;
		if ((x > 3) && !(y < -5)) {
			x--;
		}'''
		
		//create AST
		val ast = parser.parse(input)
		//generate snippet from AST
		val snippet = generator.createSnippet(ast)
		assertEquals("The created snippet does not match the expected result", expected, snippet)
	}
}

Usage with UML models

According to the above-specified points, in this case the following classes need to be implemented:

Injector and Context Provider

In this case, the UML context provider needs to be connected to an existing UML model. Therefore, the Injector provider needs to be modified in a manner that it specifies the location of the aforementioned UML model.

// This class is responsible for creating an injector for JUnit Plugin tests that use an UML model.
// Instead of a primitive context provider, it uses a context provider that returns UML Classes, Types, signals, properties and associations 
// based on a given UML model.
public class ReducedAlfLanguagePluginInjectorProvider extends ReducedAlfLanguageInjectorProvider {
    private static final String LANGUAGE_NAME = "rALF";
    
    @Override
    protected Injector internalCreateInjector() {
        // register default ePackages
        if (!Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().containsKey("ecore"))
            Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("ecore",
                    new org.eclipse.emf.ecore.xmi.impl.EcoreResourceFactoryImpl());
        if (!Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().containsKey("xmi"))
            Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("xmi",
                    new org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl());
        if (!Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().containsKey("xtextbin"))
            Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("xtextbin",
                    new org.eclipse.xtext.resource.impl.BinaryGrammarResourceFactoryImpl());
        if (!EPackage.Registry.INSTANCE.containsKey(org.eclipse.xtext.XtextPackage.eNS_URI))
            EPackage.Registry.INSTANCE.put(org.eclipse.xtext.XtextPackage.eNS_URI,
                    org.eclipse.xtext.XtextPackage.eINSTANCE);
        //Create the base rALF module
        Module runtimeModule = (Module) new com.incquerylabs.uml.ralf.ReducedAlfLanguageRuntimeModule();
        //Create a new module that binds the API classes
        Module customizations = new Module() {

            @Override
            public void configure(Binder binder) {
                //Here the path of a given UML model is specified.
                TestModelUMLContextProvider provider = new TestModelUMLContextProvider("/com.incquerylabs.uml.ralf.tests.examples/model/model.uml");
                binder.bind(IUMLContextProvider.class).toInstance(provider);
                binder.bind(TestModelUMLContextProvider.class).toInstance(provider);
                binder.bind(IReducedAlfParser.class).toInstance(new ReducedAlfParser(LANGUAGE_NAME));
                binder.bind(IReducedAlfGenerator.class).to(ReducedAlfGenerator.class);
            }
        };
        return Guice.createInjector(runtimeModule, customizations);
    }

}

As for the uml context provider:

/**
 * This context provider loads an UML model from a .uml file, returns the known classes, 
 * types, signals of a given UML model.
 * 
 */
@Singleton
class TestModelUMLContextProvider extends UMLContextProvider {

    var Model model
    val ResourceSet resourceSet

    new(String location) {
        resourceSet = new ResourceSetImpl
        resourceSet.getResource(URI.createURI(UMLResource.UML_PRIMITIVE_TYPES_LIBRARY_URI),
                true) => [
                    load(#{})    
                ]
        val resource = resourceSet.createResource(URI.createPlatformPluginURI(location, true)) => [
            load(#{})
        ]
        model =  resource.allContents.filter(typeof(Model)).findFirst[true]
    }
    
    public def setDefinedOperation(String elementFQN) {
        definedOperation = model.allOwnedElements.filter(Operation)
           .findFirst[qualifiedName == elementFQN]
    }
    
    override protected getContextObject() {
        getDefinedOperation()
    }
    
    override protected doGetEngine() {
        IncQueryEngine.on(new EMFScope(resourceSet));
    }
    
}

Usage in Test Cases

The usage of these classes in the test cases is similar to the previous case. This time however, if a 'this' operator is used, we need to specify to which UML element the action code to be parsed belongs to. This can be done, via handing the fully qualified name (FQN) of the given element to the uml context provider.

class PluginUMLTypeExampleTest {
	// Injector that creates the parser, the generator and the UML context provider
	Injector injector;
	IReducedAlfGenerator generator
	IReducedAlfParser parser
	//The UML context provider informs the parser about classes, types and signals defined in the UML model,
	//and the "this" object as well. (The object the action code under parsing belongs to.) 
	TestModelUMLContextProvider context
	
	@Before
	def void init() {
		// Get an injector instance from the provider defined by the user
		// In this provider, we can define that certain which interface is implemented by which class. 
		// If the specified interface is required, the injector will then create an instance of the specified class. 
		var provider = new ReducedAlfLanguagePluginInjectorProvider();
		injector = provider.injector
		generator = injector.getInstance(IReducedAlfGenerator)
		parser = injector.getInstance(IReducedAlfParser)
		context = injector.getInstance(TestModelUMLContextProvider)
	}
	
	@Test
	def sendSignalExampleTest(){
		//This example test case uses the model of the Ping-Pong example. 
		//It parses the action code describing a ping signal being sent to a new Pong object
		
		val input = '''
			Pong p = new Pong();
			ping_s s = new ping_s();
			send s to p->ping;'''
			
		val expected = '''
			model::Comp::Pong p = new model::Comp::Pong();
			model::Comp::Pong::ping_s s = new model::Comp::Pong::ping_s();
			p->ping->generate_event(s);'''
	
	    //Set context by adding an qualified name for a behavior
	    context.definedOperation = "model::Comp::Pong::sendPong"
		//create AST
		val ast = parser.parse(input, context)
		//generate snippets
		val snippet = generator.createSnippet(ast)
		val serializer = new ReducedAlfSnippetTemplateSerializer
        val serializedSnippet = serializer.serialize(snippet)
		//compare results
		assertEquals("The created snippet does not match the expected result",expected,serializedSnippet)
	}
	
	@Test
	def sendSignalExampleTest_This(){
		//This example test case uses the model of the Ping-Pong example. 
		//It parses the action code describing a ping signal being sent to the "ping" attribute (association end) of the current object.
		
		val input = '''
			send new ping_s() to this->ping;'''
			
		val expected = '''
			this->ping->generate_event(new model::Comp::Pong::ping_s());'''
		
		//As in this test case there is no editor attached to the UML model, the qualified name of the current type needs to be specified.
		//Hand the name of the current type to the context provider
		context.definedOperation = "model::Comp::Pong::sendPong"
		
		//create AST
		val ast = parser.parse(input, context)
		//generate snippets
		val snippet = generator.createSnippet(ast)
		val serializer = new ReducedAlfSnippetTemplateSerializer
		val serializedSnippet = serializer.serialize(snippet)
		//compare results
		assertEquals("The created snippet does not match the expected result",expected,serializedSnippet)
	}
}

Project Locations

Clone this wiki locally