Implementing a Repository Connection Extension Point

Infinity Process Platform provides you with the option to implement your own repository connection extension points. This chapter covers the following topics:


Providing Extension Information in the plugin.xml

For each connection you have to implement at least two extensions: a handler for the repository connection and one (ore more) property page(s) for the connection.

The handler is defined as following:

<extension point="org.eclipse.stardust.modeling.repository.common.connections">
   <connection
      handler="<implementation class name>"
      icon="<the icon of the connection type>"
      id="<unique identifier>"
      name="< connection type name>"/>
</extension>

The handler attribute represents the fully qualified class name of an org.eclipse.stardust.modeling.repository.common.ConnectionHandler interface implementation, for example org.eclipse.stardust.modeling.repository.file.FileConnectionHandler.

The implementation class must have a public default constructor to allow instantiation in the OSGI bundle context.

The icon attribute describe a path to a plug-in resource of an actual image file, for example icons/draw_transition.gif.

The id attribute is the unique identifier of the connection type. To ensure uniqueness, a good practice is to qualify the id with the plug-in id. An example would be org.eclipse.stardust.modeling.repository.file.connection.

The name attribute is the human readable name of the connection type. It is used in all the labels presented to the end user.

The property page is defined as below:

<extension point="org.eclipse.ui.propertyPages">
   <page
      class="<page class name>"
      id="<unique id>"
      name="<property page name>"
      objectClass="org.eclipse.stardust.modeling.repository.common.Connection">
      <filter
         name="feature:type"
         value="<id of the connection type>"/>
   </page>
</extension>

The class attribute represents the fully qualified class name of a standard org.eclipse.ui.IWorkbenchPropertyPage interface implementation. The implementation class must have a public default constructor to allow instantiation in the OSGI bundle context.

The id attribute is the unique identifier of the property page. To ensure uniqueness, a good practice is to qualify the id with the plug-in id. An example would be org.eclipse.stardust.modeling.repository.file.fileConnection.

The name attribute is the label in the property pages tree and is used also as the title of the property page itself.

The objectClass attribute represents a fully qualified class or interface name that the element to which the property page will be displayed must extend or implement. It must be org.eclipse.stardust.modeling.repository.common.Connection or a subclass of it.

The filter tag is optional, but if it is missing, the property page will be displayed for all repository connection types. To ensure that the property page is displayed only for a specific connection type, a filter must be provided with the name feature:type and the value containing the id of the connection type.

An example plugin.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="4.3"?>
<plugin>
  <extension point="org.eclipse.stardust.modeling.repository.common.connections">
    <connection
      handler="org.eclipse.stardust.test.TestConnectionHandler"
      icon="icons/draw_transition.gif"
      id="org.eclipse.stardust.test"
      name="Test Connection"/>
  </extension>
  <extension point="org.eclipse.ui.propertyPages">
    <page
      class="org.eclipse.stardust.test.TestConnectionPropertyPage"
      id="org.eclipse.stardust.test.testConnection"
      name="Test Connection Property Page"
      objectClass="org.eclipse.stardust.modeling.repository.common.Connection">
      <filter
        name="feature:type"
        value="org.eclipse.stardust.test"/>
    </page>
  </extension>
</plugin>

Implementing the Property Page

The property page must implement the org.eclipse.ui.IWorkbenchPropertyPage interface. The scope of the property page is to allow the user to specify the set of attributes needed to open the connection. When implementing the page, a good choice is to extend org.eclipse.ui.dialogs.PropertyPage class.

The user must implement at least the Control createContents(Composite parent) method. The page can get and set attributes to the corresponding Connection object. The Connection attached to the page can be obtained via (Connection) getElement().getAdapter(EObject.class).

Once you have the Connection object, you can get or set the id, name, or type (although it is not recommended to change the type of the connection). Every connection has two sets of properties: persistent attributes that will be stored in the model file and transient properties that will be available in the context of the current eclipse session.

You can get or set the persistent attributes using List Connection.getAttributes().

For transient properties, getProperty, setProperty and removeProperty methods are provided.

Implementing the Connection Handler

The actual connection handler must implement the following methods:

void open(Connection connection) throws CoreException;

This method is responsible for actually opening a connection resource to the external repository. Failing of opening the connection must be signaled by throwing a CoreException. For opening the connection, the implementer may use both the persistent attributes and the transient properties. If necessary he can ask the user for further input via dialogs. No queries will be performed on the connection by the Infinity Process Platform modeler if the connection is not successfully opened.

void close(Connection connection) throws CoreException;

This method is responsible for closing a connection resource to the external repository and releasing any resources acquired since opening.

List select(IFilter[] filters) throws CoreException;

The method returns a list of IObjectDescriptors that will be used to generate the outline tree. The filters (if present) may be used to restrict the elements to be returned.

void importObject(ModelType model, IObjectDescriptor[] descriptor, boolean asLink);

This method is responsible for creating and adding the model elements corresponding to the array of descriptors to the provided model. The parameter asLink specifies if the objects will be generated as links or will be fully imported into the model. It is possible and allowed that for a specific object descriptor more than one model element is added to the model.

Example Implementation

The following shows an example implementation:

public abstract void createData(boolean asLink);

public void importObject(ModelType model, IObjectDescriptor[] descriptors, 
	boolean asLink)
{
   for (int i = 0; i < descriptors.length; i++) 
   {
      IObjectDescriptor descriptor = descriptors[i];
      if ("process".equals(descriptor.getURI().segment(0)))
      {
         createData(asLink);
         DataTypeType primitiveType = (DataTypeType) 
ModelUtils.findIdentifiableElement(
               model.getDataType(), "primitive");
         data.setType(primitiveType);
         
         model.getProcessDefinition().add(process);
         model.getRole().add(role);
         model.getData().add(data);
      }
   }   
}

Object resolve(ModelType model, Object proxy);

This method is responsible for replacing proxy objects that are created during model load with actual linked elements. This method is invoked only once per each proxy object at the end of a successful model load. The returned value is the resolved object or null if the proxy was not resolved. This method is supposed to perform all necessary changes in the model.

Example Implementation

A typical implementation is provided below:

public abstract EObject findObjectByUri(URI uri);

public Object resolve(ModelType model, Object object)
{
   URI uri = ((EObjectImpl) object).eProxyURI();

   EObject element = findObjectByUri(uri);
   if (element != null)
   {
      EcoreUtil.replace(eObject, element);
      for (EContentsEList.FeatureIterator featureIterator =
          (EContentsEList.FeatureIterator) eObject.eCrossReferences()
.iterator();featureIterator.hasNext(); )
      {
         EObject target = (EObject)featureIterator.next();
         EReference eReference = (EReference)featureIterator.feature();
         unset(eObject, target, eReference);
         set(element, target, eReference);
      }
   }
   return element;
}

private void set(EObject object, EObject target, EReference reference)
{
   if (reference.isMany())
   {
      List list = (List) object.eGet(reference);
      list.add(target);
   }
   else
   {
      object.eSet(reference, target);
   }
}

private void unset(EObject object, EObject target, EReference reference)
{
   if (reference.isMany())
   {
      List list = (List) object.eGet(reference);
      list.remove(target);
   }
   else if (reference.isUnsettable())
   {
      object.eUnset(reference);
   }
   else
   {
      object.eSet(reference, null);
   }
}

User Interaction with Outline Nodes

There are situations where the developer may want to give the user the possibility to interact with the outline nodes. This is possible via two extension points:

The property pages must be implemented in the exact way as described for the connections, with the only change that the objectClass attribute to match the class of the implementation node and the filter removed or adapted to the specific class.

Context menu actions are specified in the plugin.xml file as follows:

<extension point="org.eclipse.stardust.modeling.core.contextMenuAction">
  <contextMenuAction
    actionClass="<class name of the action>"
    group="rest"
    id="<unique identifier>"
    targetEObjectType="<class name or superclass/interface of the node>"/>
</extension>

The action class must implement IAction interface, but it is recommended to implement ISpiAction interface too to get access to the actual selected element, configuration element of the action (the data described in the plugin.xml for that action) and the WorkflowModelEditor that have invoked the action.

If the result of the action has generated a change in the node data (either label/icon or children) the developer must ensure that a corresponding notification is sent by the node to all adapters so that the change will be reflected in the outline tree.

Example action:

public class GetCategoryDetailsAction extends Action implements ISpiAction
{
   private IStructuredSelection selection;

   public boolean isEnabled()
   {
      return !selection.isEmpty() && isMyOwnCategory();      
   }

   private boolean isMyOwnCategory ()
   {
      Object selection = this.selection.getFirstElement();
      if (selection instanceof IAdaptable)
      {
         Object model = ((IAdaptable) selection).getAdapter(EObject.class);
         if (model instanceof CategoryDescriptor)
         {
            return !((CategoryDescriptor) model).isFetched();
         }
      }
      return false;
   }
   
   public void run()
   {
      Object selection = this.selection.getFirstElement();
      CategoryDescriptor descriptor = (CategoryDescriptor)
         ((IAdaptable) selection).getAdapter(EObject.class);
      descriptor.fetchChildren();
   }

   public void setConfiguration(IConfigurationElement config,
         WorkflowModelEditor editor, IStructuredSelection selection)
   {
      setId(config.getAttribute(SpiConstants.ID));
      setText("Get Details");
      this.selection = selection;
   }
}

The CategoryDescriptor is the actual implementation class of the node. The fetchChildren() method is responsible for sending the change notification, as below:

public void fetchChildren()
{
   doAction();
   if (eNotificationRequired())
   {
      if (labelOrIconChanged())
      {
         eNotify(new ENotificationImpl(this, Notification.SET, null, null, null));
      }
      if (childrenChanged())
      {
         eNotify(new ENotificationImpl(this, Notification.ADD_MANY, null, null, null));
      }
   }
}

Quick Start using XPDL Documents

If the plug-in developer has already the elements that he wants to expose in the form of XPDL documents, he can reuse the Infinity Process Platform loading functionality to populate the outline tree. The XPDL documents can be retrieved either from the file system, a database or generated on the fly.

On connection open(), the developer should prepare the connection handler to be able to retrieve the XPDL document and parse it into the relevant model elements. In the example below, we will read the XPDL document and store the contained model for later usage:

public void open(Connection connection) throws CoreException
{
   InputStream is = getInputStreamOfTheXpdlDocument(connection);
   if (is != null)
   {
      connectionUri = ConnectionManager.makeURI(connection);
      CarnotWorkflowModelPackageImpl.init();
      Resource.Factory factory =
         new CarnotWorkflowModelResourceFactoryImpl();
      Resource resource = factory.createResource(
         URI.createFileURI("dummy.xpdl"));
      Map options = new HashMap();
      options.put(XMLResource.OPTION_RECORD_UNKNOWN_FEATURE, Boolean.TRUE);
      try
      {
         resource.load(is, options);
      }
      catch (IOException e)
      {
         throw new CoreException(new Status(IStatus.ERROR,
               ObjectRepositoryActivator.PLUGIN_ID, 0,
               "Error parsing input stream.", null));
      }
      finally
      {
         try
         {
            is.close();
         }
         catch (IOException e)
         {
         }
      }
      EList l = resource.getContents();
      Iterator i = l.iterator();
      while (i.hasNext())
      {
         Object o = i.next();
         if (o instanceof DocumentRoot)
         {
            model = ((DocumentRoot) o).getModel();
            break;
         }
      }
   }
   else
   {
      throw new CoreException(new Status(IStatus.ERROR,
            ObjectRepositoryActivator.PLUGIN_ID, 0,
            "Cannot access resource.", null));
   }
}

After successful execution of open() we will have two values stored in the handler fields:

The outline nodes are retrieved via the select method. In this example we will ignore the filters and provide nodes for participants and applications found in the model. We will create two categories, one for the participants and one for the applications, and for each category we will have to add the IObjectDescriptors corresponding to each model element added.

In order to keep the example simple, we will reuse another Infinity class - EObjectDescriptor - that works as an adapter between IObjectDescriptors and EObjects.

public List select(IFilter[] filters) throws CoreException
{
   ArrayList participants = new ArrayList();
   addElements(participants, "role", model.getRole());
   addElements(participants, "organization", model.getOrganization());
   
   ArrayList applications = new ArrayList();
   addElements(applications, "application", model.getApplication());
   
   ArrayList result = new ArrayList();
   result.add(createCategory("participants", "Participants",
      (IObjectDescriptor[]) participants.toArray(
         new IObjectDescriptor[participants.size()])));
   result.add(createCategory("applications", "Applications",
      (IObjectDescriptor[]) applications.toArray(
         new IObjectDescriptor[applications.size()])));
   return Collections.unmodifiableList(result);      
}

private IObjectDescriptor createCategory(String id, String name,
      final IObjectDescriptor[] children)
{
   return new EObjectDescriptor(connectionUri.appendSegment(id),
         null, id, name, null, null, null)
   {

      public IObjectDescriptor[] getChildren()
      {
         return children;
      }

      public boolean hasChildren()
      {
         return children.length > 0;
      }
   };
}

private void addElements(List descriptors, String type, List elements)
{
   for (int i = 0; i < elements.size(); i++)
   {
      IIdentifiableModelElement identifiable =
         (IIdentifiableModelElement) elements.get(i);
      URI elementUri = connectionUri.appendSegment(type)
         .appendSegment(identifiable.getId());
      EObjectDescriptor descriptor = new EObjectDescriptor(elementUri, 
            // classifier
            ((EObject) identifiable).eClass(),
            // id, name, description
            identifiable.getId(), 
            identifiable.getName(), 
            ModelUtils.getDescriptionText(identifiable.getDescription()),
            // Icon
            null, 
            null);
      descriptors.add(descriptor);
   }
}

For importing, we create a command for each imported model element. First we search the actual model elements that we want to link. For each model element we must set the proxy URI so that Infinity Process Platform modeler and the EMF framework will know that the new model element is a linked element. After that we add the element to the parent using the SetValueCmd that has as arguments the parent, the EMF containing feature of the element to add and the last argument is the element we want to add.

public void importObject(ModelType origmodel, IObjectDescriptor[] 
descriptors, boolean asLink)
{      
   for (int i = 0; i < descriptors.length; i++) 
   {
      IObjectDescriptor descriptor = descriptors[i];
      URI uri = descriptor.getURI();
      IIdentifiableModelElement identifiable = findIdentifiableByUri(model, 
Uri, asLink);
      if (identifiable != null)
      {
         if (identifiable != null)
         {
            new SetValueCmd(origmodel,
               identifiable.eContainingFeature(), identifiable).execute();
         }
      }
   }   
}

public IIdentifiableModelElement findIdentifiableByUri(ModelType model,
URI uri, boolean asLink)
{
   String type = uri.segment(0);
   String id = uri.segment(1);
   
   List searchList = null;
   
   if ("role".equals(type))
   {
      searchList = model.getRole();
   }
   else if ("organization".equals(type))
   {
      searchList = model.getOrganization();
   }
   else if ("application".equals(type))
   {
      searchList = model.getApplication();
   }
   
   if (searchList != null)
   {
      IIdentifiableModelElement identifiable = (IIdentifiableModelElement)
         ModelUtils.findIdentifiableElement(searchList, id);
	if (asLink)
	{
         // the proxy uri is needed for resolving
         ((EObjectImpl) identifiable).eSetProxyURI(uri);
}
else
{
   AttributeUtil.setAttribute(identifiable, 
      ConnectionManager.URI_ATTRIBUTE_NAME,
            uri);
   
}
      return identifiable;
   }
   
   return null;
}

Programmatically Creating Model Elements

There are cases when the plug-in developers do not have XPDL documents but some other proprietary solution management that cannot be easily converted to Infinity Process Platform formats. In this case, the developer may want to create "on the fly" the model elements that he wants to link based on the information that he has.

Creating Infinity Process Platform model elements is done always via the Infinity Process Platform factory. The factory is a singleton and has methods to create each of the Infinity Process Platform model elements (createXXXType()). For example, if you want to create a Role and an Application, you can use:

CarnotWorkflowModelFactory factory = CarnotWorkflowModelFactory.eINSTANCE;
RoleType role = factory.createRoleType();
ApplicationType application = factory.createApplicationType();

The full set of Infinity model elements is comprised of the following classes:

Each of this classes follows the java bean pattern, so that for each property there are setters and getters with the exception of aggregate structures that have only a getter that retrieves a java.util.List where the developer can directly add or remove elements.

We will describe below how to create a Process Definition containing two activities, a transition, one role and a diagram. This process definition can be presented, linked and resolved as described in the previous sections.

For the beginning, we will create the process and both activities:

CarnotWorkflowModelFactory factory = CarnotWorkflowModelFactory.eINSTANCE;

ProcessDefinitionType process = factory.createProcessDefinitionType();
process.setId("sample");
process.setName("Sample Process");
process.setDescription(ModelUtils.createDescription(
"Sample process created programmatically"));
((EObjectImpl) process).eSetProxyURI(
connectionUri.appendSegment("process"));

ActivityType route = factory.createActivityType();
route.setId("route");
route.setName("Route Activity");
route.setImplementation(ActivityImplementationType.ROUTE_LITERAL);
process.getActivity().add(route);

ActivityType manual = factory.createActivityType();
manual.setId("manual");
manual.setName("Manual Activity");
manual.setImplementation(ActivityImplementationType.MANUAL_LITERAL);
process.getActivity().add(manual);

The manual activity, needs a performer, so we will create a new role and set it as the activity performer.

RoleType role = factory.createRoleType();
role.setId("role");
role.setName("Role ");
((EObjectImpl) role).eSetProxyURI(connectionUri.appendSegment("role"));
 manual.setPerformer(role);

Next, we will create a Data object and a data mapping to the manual activity. The Data object will have the Infinity meta type primitive. Since the meta type should be normally retrieved from the model, we will not set yet the type of the data, and will defer that to the time of linking or resolving. As a primitive data, we must set two dynamic attributes that defines the specifics of the data object:

DataType data = factory.createDataType();
data.setId("primitive_string");
data.setName("Primitive String Data");
AttributeUtil.setAttribute(data, "carnot:engine:type",
"org.eclipse.stardust.engine.core.pojo.Type", "String");
AttributeUtil.setAttribute(data, "carnot:engine:defaultValue", "Hello!");
((EObjectImpl) data).eSetProxyURI(connectionUri.appendSegment("data"));

DataMappingType mapping = factory.createDataMappingType();
mapping.setId("in_mapping");
mapping.setDirection(DirectionType.IN_LITERAL);
mapping.setContext("default");
mapping.setData(data);
manual.getDataMapping().add(mapping);

Now we create a transition between the route and manual activities:

TransitionType transition = factory.createTransitionType();
transition.setId("transition");
transition.setName("Transition");
transition.setCondition("TRUE");
transition.setFrom(route);
transition.setTo(manual);
process.getTransition().add(transition);

This process can be used already as it is, but we want to add a diagram too:

DiagramType diagram = factory.createDiagramType();
diagram.setName("Diagram");
diagram.setOrientation(OrientationType.HORIZONTAL_LITERAL);
process.getDiagram().add(diagram);

ActivitySymbolType routeSymbol = factory.createActivitySymbolType();
routeSymbol.setActivity(route);
routeSymbol.setXPos(300);
routeSymbol.setYPos(200);
diagram.getActivitySymbol().add(routeSymbol);

ActivitySymbolType manualSymbol = factory.createActivitySymbolType();
manualSymbol.setActivity(manual);
manualSymbol.setXPos(100);
manualSymbol.setYPos(400);
diagram.getActivitySymbol().add(manualSymbol);

RoleSymbolType roleSymbol = factory.createRoleSymbolType();
roleSymbol.setRole(role);
roleSymbol.setXPos(450);
roleSymbol.setYPos(300);
diagram.getRoleSymbol().add(roleSymbol);

DataSymbolType dataSymbol = factory.createDataSymbolType();
dataSymbol.setData(data);
dataSymbol.setXPos(400);
dataSymbol.setYPos(50);
diagram.getDataSymbol().add(dataSymbol);

TransitionConnectionType transitionConnection = factory.createTransitionConnectionType();
transitionConnection.setTransition(transition);
transitionConnection.setSourceAnchor("center");
transitionConnection.setTargetAnchor("center");
transitionConnection.setSourceActivitySymbol(routeSymbol);
transitionConnection.setTargetActivitySymbol(manualSymbol);
diagram.getTransitionConnection().add(transitionConnection);

PerformsConnectionType performsConnection = factory.createPerformsConnectionType();
performsConnection.setActivitySymbol(manualSymbol);
performsConnection.setParticipantSymbol(roleSymbol);
diagram.getPerformsConnection().add(performsConnection);

DataMappingConnectionType mappingConnection = factory.createDataMappingConnectionType();
mappingConnection.setActivitySymbol(manualSymbol);
mappingConnection.setDataSymbol(dataSymbol);
diagram.getDataMappingConnection().add(mappingConnection);

For our example the presentation will deliver a single element, the process definition. For that, the select method looks fairly simple:

public List select(IFilter[] filters) throws CoreException
{
   EObjectDescriptor descriptor = new EObjectDescriptor(
connectionUri.appendSegment("process"),            CarnotWorkflowModelPackage.eINSTANCE
         .getProcessDefinitionType(),
      "sample", "Sample Process", null, null, null);
   return Collections.singletonList(descriptor);
}

In contrast, the import operation is a composite, because we need to add not only the process, but the role and the data too, then again we must set the proper type of the data.

public void importObject(ModelType model, IObjectDescriptor[]
descriptors, boolean asLink)
{
   for (int i = 0; i < descriptors.length; i++) 
   {
      IObjectDescriptor descriptor = descriptors[i];
      if ("process".equals(descriptor.getURI().segment(0)))
      {
         updateOids(model);
         DataTypeType primitiveType = (DataTypeType)
ModelUtils.findIdentifiableElement(model.getDataType(),
   "primitive");
         
	   model.getProcessDefinition().add(process);
	   model.getRole().add(role);
	   model.getData().add(data);
	   data.setType(primitiveType);
      }
   }   
}

private void updateOids(ModelType model)
{
   long oid = ModelUtils.getElementOid(process, model);
   process.setElementOid(oid++);
   route.setElementOid(oid++);
   manual.setElementOid(oid++);
   role.setElementOid(oid++);
   data.setElementOid(oid++);
   mapping.setElementOid(oid++);
   transition.setElementOid(oid++);
   diagram.setElementOid(oid++);
   routeSymbol.setElementOid(oid++);
   manualSymbol.setElementOid(oid++);
   roleSymbol.setElementOid(oid++);
   dataSymbol.setElementOid(oid++);
   transitionConnection.setElementOid(oid++);
   performsConnection.setElementOid(oid++);
   mappingConnection.setElementOid(oid++);
}