-
Notifications
You must be signed in to change notification settings - Fork 1
rALF API Usage
#rALF API Usage
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.
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.
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).
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;
}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.
According to the above-specified points, in this case the following classes need to be implemented:
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
}
}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)
}
}According to the above-specified points, in this case the following classes need to be implemented:
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));
}
}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)
}
}