Building Applications with Eclipse, EMF, GMF and XMF, Part 2


Summary

This tutorial builds upon a previous article describing building applications with Eclipse, EMF, GMF and XMF to show how more complex applications can be built using this array of technologies.  Two applications are described.  Firstly, a route applications calculates the optimum route through a graph of node and edges.  Secondly, an application that calculates the critical path through a similar graph structure.

Contents


Introduction

Armed with the knowledge of using Eclipse, EMF, GMF and XMF in to develop rich applications, this tutorial describes how this technology arsenal can be used to develop more complex applications.  These applications center around the graph structure of nodes and edges commonly found in a wide spectrum of applications.  It is often useful to ask questions or to analyze properties of graph structures.  The first application in this tutorial calculates the optimum route between two nodes in a graph.  The second application identifies the critical path between two nodes in a graph assuming that each edge is given a weighting, this type of application is often found in project planning software.  This application also shows how XMF can be used to add constraints to a graph model to analyze whether key properties are observed.

To top.

Optimum Route Application

The application presented in this first section calculates the optimum route through a graph of nodes and edges.  This type of application has many uses including, for example, train journey planning and calculating which route involves the least number of station stops.  The screenshot below shows the diagram editor for a configuration of train stations which are represented as nodes and train station connections which are represented as edges (note that these are not accurate to reality - please do not use it as a basis for travel plans!!)

Running the route finder

This route application supports the user in indicating their boarding station and their destination station via menus on the station nodes.  The user is then able to ask the application which route between the two stations involves the least number of stops.  The screenshot below shows the result of this analysis when the start station is York and the destination station is Hull.

Calculating path

The EMF model for the optimum route application is shown below, this is used as a basis for the GMF editor (show above) which supports the diagramming of nodes and edges. The EMF model is also used to generate the code stubs for writing the XMF processing,  one of these is partially shown in the lower editor below.  The process for generating XMF code stubs from EMF models  is described here.

Defining the model

As described in the introductory tutorial here, the relationship between Eclipse code written in Java and XMF code is service based.  This involves XMF offering parameterized services which can then be invoked in Java code.  The route application offers three services setStart and setTerminal node and getPath which calculates the optimum path.  The first two services simply involve making a record of the start and terminal node in a known place.

@Service setStart(node)
Root::start := node
end;

The getPath service is more involved.

@Service getPath(graph)

// Define a local recursive operation that
// calculates a path ending in the terminal
// node...

@Letrec findPath(path) =

// If the path ends with the terminal node
// then produce the path as the result of
// findPath...

if path->last = terminal
then @Result path end
else

// Otherwise, select an edge such that the
// source of the edge is the last node in
// the path and the target of the edge
// has not been visited before...

@Select edge

// If a subsequent selection ever fails
// then control returns here and tries
// another edge...

from graph.getEdges()

when edge.getSource() = path->last() and
not path->includes(edge.getTarget())


do
// Add the edge and the target node to the
// path and call findPath again...

findPath(path + Seq{edge,edge.getTarget()})
end
end
in
// Get a path...

let path = @Start findPath(Seq{start}) end
in
// Display the path in a text dialog...

xmf.textArea("info",pathLabel(path),displayPath(path))
end
end
end;

The services are invoked in a menu handler, the invocation of the setStart service is shown below. 

public void setStartNode(Node node) {
getMachine().invokeService("setStart", new Object[] { node }, new ClientResult() {
public void result(Object value) {
}

public void error(String message) {
Shell shell = Display.getCurrent().getActiveShell();
MessageDialog.openError(shell, "Error", message);
}
},getClassLoader());
}
The service is parameterized with the selected node, the service invocation is also parameterized with:

To top.

Critical Path Application

Critical Path (CP) Applications  have many uses including project planning and knowledge base systems.  As in the previous application, they are based on a graph shaped structure although in this case they have a numeric value assigned to an edge which represents a weighting.  The higher the value of this weighting, the more it costs to navigate the edge.  For example, in project planning applications the edge represents time to complete a particular task.  The screenshot below shows a graph in the CP application describing a software development project which consists of five projects each node represents a stage in the project, for example the completion of feature E, and each edge represents the transition to reaching a stage with some associated time taken, for example the transition from B to E takes 5 units.  Some of the five features, such as A and B, can be developed in parallel; but there are dependencies between other features, D can only be developed on completion of feature A.

Critical Path


The screenshot below shows the same diagram, but this time the critical path has been calculated and marked, and features are annotated with their start and end time.  The critical path is the most expensive route through the graph, in a project this is the path which needs to be analyzed if the project length needs to be shortened by reducing linear dependencies or simply the effort required on specific components of the project.

Critical path after calculation

The EMF model for the above application is shown below.

Critical Path Model

Once the EMF model has been exported as an XMF model, the classes can be extended with operations. For example:

Extending Activity

Note that the operations getStartEvent() and getEventEvent() are implemented by EMF in Java. Java-defined methods can be called from XOCL defined operations. In this way the XOCL defined class Activity can be viewed as extending the Java defined class for Activity.

Events are also extended:

Event

All of the application code is added to the XOCL classes in this way. For documentation purposes, the rest of the tutorial will show individual definitions as:

context C
// some definition
which is to be understood to mean that the source code for class C includes the definition as shown above for Event and Activity.

The CPM tool provides a service called getPath that calculates the critical path for the CPM network. This service is to be offered as a menu item from the GMF-generated CPM network editor. To do this we include a popup menu item in the plugin:
<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="ID" objectClass="cpm.diagram.edit.parts.CPM_ModelEditPart">
<action id="getPath" label="Get Path" menubarPath="additions" class="cpm.xmf.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>
and an implementation in the action handler:
public void run(IAction action) {	

Object selected = selection.getFirstElement();

if(selected instanceof GraphicalEditPart) {

GraphicalEditPart gep = (GraphicalEditPart)selected;
View view = (View)gep.getModel();
EObject eobject = view.getElement();

if (action.getId().equals("getPath"))
calculatePath((CPM_Model)eobject);

}
}
The service is implemented in XMF as:
@Service getPath(model)
model.performPasses()
end;
The class CPM_Model is extended to include an operation that performs the passed to calculate the critical path:

@Operation performPasses()
self.forwardPass();
self.backwardPass();
self.showCriticalPath()
end

The class CPM_Model requires three operations in order to calculate and display the critical path. First it performs a forward pass that calculates the earliest start dates for the events. Then it performs a pass that calculates the lastest end dates for the events. Finally it calculates the critical path and displays it  by adding labels to each of the activities on the path.

All of the CPM_Model critical path operations require a collection of helper-operations. We will show some key features of the implementation. The complete source code can be downloaded here.

   context CPM_Model
@Operation forwardPass()

// Start off at the roots and visit each event setting
// the earliest start time...

let roots = self.roots() then
visited = roots->asSet
in
// While there are some unvisited events...
@While visited <> self.getEvents()->asSeq->asSet do
@For event in self.getEvents() do

// If all the predecessors of the selected event
// have been visited...

if self.predecessors(event)->forAll(n |
visited->includes(n)) and
not visited->includes(event)
then

// Calculate the earliest time that the event could
// occur based on the duration of the activities...

let preds = self.predecessors(event);
earliest = 0
in @For pred in preds do
@For activity in self.activitiesBetween(pred,event) do
earliest := earliest.max(pred.getEarliestStart() + activity.getDuration())
end
end;

// Set the earliest start time...
event.setEarliestStart(earliest);

// Mark this event as having been visited...
visited := visited->including(event)
end
end
end
end
end
end
The backward pass is similar to the forward pass except that it calculates the lastest end time for the events.

The critical path is calculated by selecting and marking activities where the activities join events that have 0 slack. The smount of slack in an event determines the time over which it can occur. An event with 0 slack cannot be moved and therefore is critical to the plan. A sequence of events with 0 slack is a critical path.

To calculate a critical path, we simply select a sequence of joined up activities that start and end with events with 0 slack. Once selected, each activity is marked by changing its label as shown below:
context CPM_Model
@Operation showCriticalPath()
let path = @Start self.calculateCriticalPath() end
in @For activity in self.getActivities() do
activity.setLabel("")
end;
@For activity in path do
activity.setLabel("critical")
end
end
end

context CPM_Model
 @Operation calculateCriticalPath()
@Select event from self.roots() do
self.critical(event,Seq{})
end
end

context CPM_Model
 @Operation critical(event,path)
if self.terminals()->includes(event)
then @Result path end
else
@Select activity from self.getActivities()
when activity.getStartEvent() = event and
activity.isCritical()
do self.critical(activity.getEndEvent(),path + Seq{activity})
end
end
end

To top.

Adding Constraints to the CP Application

A CPM network must be well formed in order to support the critical path calculations described above. The labels on events must be unique and the network should have only one terminal event (the end of the project). XMF allows you to easily add constraints to classes and to check that the constraints hold. This section describes how to add constraints to the CPM network and to define a service that performs the check and displays the results.

The CPM_Model can be extended with constraints as follows:
context CPM_model
@Constraint UniqueEventLabels

// Since ->asSet will remove any duplicates, the following
// expression checks that there are no duplicate label names...

self.events().label->size = self.events().label->asSet->size
fail

// A fail-clause in a constraint is used to calculate a
// message-string that is used to report the reason when
// the constraint is not satisfied...

let names = self.events().label then
duplicateNames = names->asSet->select(n | names->select(nn | nn = n)->size > 1)
in formats("The following names are duplicated: ~{,~;~S~}",Seq{duplicateNames})
end
end

context CPM_Model
@Constraint UniqueTerminalEvent
self.terminals()->size <= 1
fail
formats("Can only have a single terminal event: ~{,~;~S~}",Seq{self.terminals().label})
end
The constraint check is to be offered as a serice via a menu. The plugin.xml file is modified to include:
<extension point="org.eclipse.ui.popupMenus">
<objectContribution id="ID" objectClass="cpm.diagram.edit.parts.CPM_ModelEditPart">
<action id="check" label="Check" menubarPath="additions" class="actions.HandleAction" enablesFor="1">
</action>
</objectContribution>
</extension>
The action handler is modified to call the following service:
@Service check(model)

// Each EMFClass can be requested to 'eclipseClassify' an
// EMF object. This will run any constraints that have been
// defined for the class and report the failures using the
// Problem View in Eclipse...

CPM_Model.eclipseClassify(model)
end;

Now the constraints can be run on the CP:

OK Constraints

No problems are reported because the constraints are all satisfied. Now supposing that netowkr is modified so that one of the event labels is duplicated. Re-checking the constraints produces the following problems:

Duplicate Labels

Adding an extra terminal node causes the following problem to be added when the network is re-checked:

Two Terminals
To top.

Summary

The tutorial has described how more complex applications can be developed using XMF and Eclipse based technologies using two graph based examples.  The final tutorial in this series shows how a complex and realistic testing framework is developed using the same approach.

To top.