Skip to content

Latest commit

 

History

History
359 lines (260 loc) · 14.1 KB

00-workflow-framework-intro.markdown

File metadata and controls

359 lines (260 loc) · 14.1 KB
header-id
liferays-workflow-framework

Liferay's Workflow Framework

[TOC levels=1-4]

Enabling your application's entities to support workflow is so easy, you could do it in your sleep (but don't try). Workflow enabled entities require a few things:

  • A workflow handler class to interact with Liferay's workflow back end and the entity's service layer.

  • Some extra fields in their database table that help keep track of their status.

    In most Liferay applications, Service Builder will be used to create those fields.

  • Updates to the service layer.

    The service layer needs code to populate the new fields when entities are added to the database.

    The service layer needs to send the entity through Liferay's workflow, and it needs to handle the workflow status of the entity when it's returned by the workflow.

    The service layer needs getters that return entities of the desired workflow status (usually approved).

  • The View layer should account for the workflow status of displayed entities.

The code for most Liferay applications spans multiple modules, so where should you implement the workflow handler? It should go in the module with your service implementations. It's nice to keep your back end code separate from your view layer and controller (in the MVC pattern).

Creating a Workflow Handler

First create a Component class. It should extend BaseWorkflowHandler<T>, an abstract class that provides a default implementation of the WorkflowHandler<T> service. Pass the interface for your model as the type parameter for the class.

FooEntity WorkflowHandler extends BaseWorkflowHandler<FooEntity>

Since you're publishing a service to be consumed in the OSGi runtime, your workflow handler class needs to be registered. If you're using Declarative Services, make it a Component class, using the @Component annotation.

@Component(
    property = {"model.class.name=com.my.app.package.model.FooEntity"},
    service = WorkflowHandler.class
)

It needs one property, to set model.class.name to the fully qualified class name of the class you passed as the type parameter. It also needs to declare the type of service being implemented (WorkflowHandler.class).

What methods do you need to override in your workflow handler? Just three:

@Override
public String getClassName() {

@Override
public String getType(Locale locale) {

@Override
public FooEntity updateStatus(int status, Map<String, Serializable> workflowContext) {

The first two are pretty boilerplate. Most of the heavy lifting is being done in the updateStatus method. It returns a call to a local service method of the same name, so the status returned from the workflow back end can be persisted to the entity table in the database.

The updateStatus method should take a user ID, the primary key for the class (for example, fooEntityId), the workflow status, the service context, and the workflow context. The status and the workflow context can be obtained from the workflow back end. You'll need to define the rest of the parameters, which can be obtained from the workflow context.

@Override
public FooEntity updateStatus(
        int status, Map<String, Serializable> workflowContext)
    throws PortalException {

    long userId = GetterUtil.getLong(
        (String)workflowContext.get(WorkflowConstants.CONTEXT_USER_ID));
    long classPK = GetterUtil.getLong(
        (String)workflowContext.get(
            WorkflowConstants.CONTEXT_ENTRY_CLASS_PK));

    ServiceContext serviceContext = (ServiceContext)workflowContext.get(
        "serviceContext");

    return _fooEntityLocalService.updateStatus(
        userId, classPK, status, serviceContext, workflowContext);
}

Now your entity can be handled by Liferay's workflow framework. Next, update the service methods to account for workflow status, and add a new method to update the status of an entity in the database.

Updating the Service Layer

Make sure your entity database table has status, statusByUserId, statusByUserName, and statusDate fields. If you're using service builder, add this to your service.xml if you haven't already:

<column name="status" type="int" />
<column name="statusByUserId" type="long" />
<column name="statusByUserName" type="String" />
<column name="statusDate" type="Date" />

Wherever you're setting the other database fields in your persistence code, set the workflow status as a draft and set the other fields.

fooEntity.setStatus(WorkflowConstants.STATUS_DRAFT);
fooEntity.setStatusByUserId(userId);
fooEntity.setStatusByUserName(user.getFullName());
fooEntity.setStatusDate(serviceContext.getModifiedDate(null));

With Service Builder driven Liferay applications, this will be in the local service implementation class (-LocalServiceImpl).

Whenever an entity is added to the database you need to detect whether workflow is installed and active. If not, you need to automatically mark the entity as approved so it appears in the UI. If it is, you want to leave it in draft status and send it to the workflow back end where it can be properly handled. Thankfully, this whole process is easily done with a single call to WorkflowHandlerRegistryUtil.startWorkflowInstance. There are several methods of this name which take a different parameter set, so inspect the WorkflowHandlerRegistryUtil class and decide which is right for your case.

WorkflowHandlerRegistryUtil.startWorkflowInstance(fooEntity.getCompanyId(),
        fooEntity.getGroupId(), fooEntity.getUserId(), FooEntity.class.getName(),
        fooEntity.getPrimaryKey(), fooEntity, serviceContext);

Once you've set the database fields for workflow status and started the workflow instance, implement the updateStatus method that you need to call in the workflow handler. The workflow handler gets the entity's status from the workflow back end and passes it to your service layer, which persists the updated entity to the database.

fooEntity.setStatus(status);
fooEntity.setStatusByUserId(user.getUserId());
fooEntity.setStatusByUserName(user.getFullName());
fooEntity.setStatusDate(serviceContext.getModifiedDate(now));

fooEntityPersistence.update(fooEntity);

After setting the workflow fields for the entity, think about the specifics of your situation and whether any additional logic should be added to this method. For instance, if your entities are Liferay Assets already, you'll want to change the visibility of the asset depending on its workflow status. You don't want the Asset Publisher displaying entities that haven't yet been approved in the workflow process.

if (status == WorkflowConstants.STATUS_APPROVED) {

    assetEntryLocalService.updateEntry(
        FooEntity.class.getName(), fooEntityId, fooEntity.getDisplayDate(),
        null, true, true);
}

else {

    assetEntryLocalService.updateVisible(
        fooEntity.class.getName(), entryId, false);
}

If approved, the asset is updated, with the publication date, a listable boolean, and a visible boolean being updated to reflect the current state of the asset. If the workflow status is anything other than approved, its visibility is set to false.

For an example of a fully implemented updateStatus method, see the com.liferay.portlet.blogs.service.impl.BlogsEntryLocalServiceImpl class in portal-impl.

Before leaving the service layer, add a call to deleteWorkflowInstanceLinks in the deleteEntity method. Here's what it looks like:

workflowInstanceLinkLocalService.deleteWorkflowInstanceLinks(
    fooEntity.getCompanyId(), fooEntity.getGroupId(),
    FooEntity.class.getName(), fooEntity.getFooEntityId());

When you send an entity to the workflow framework via the startWorkflowInstance call, it creates an entry in the workflowinstancelink database table. This delete call ensures there are no orphaned entries in the workflowinstancelinks table.

Note, to get the WorkflowInstanceLocalService injected into your *LocalServiceBaseImpl so you can call its methods in the LocalServiceImpl, add this to your entity declaration in service.xml:

	<reference entity="WorkflowInstanceLink" package-path="com.liferay.portal" />

Save your work and run Service Builder. Once you've accounted for workflow status in your service layer, there's only one thing left to do: update the user interface.

Workflow Status and the View Layer

If you have an application with database entities, you're likely displaying them. If you're sending entities through a workflow process, you only want to display approved entities to your end users.

This often involves the following steps:

  • Create a finder for your entities that accounts for the status field in your database table.

  • Expose the finder in a getter method of your service layer.

  • Update the view layer to use the new getter for displaying entities (e.g., in a Search Container).

If you're using Service Builder, define your finder in your application's service.xml and let Service Builder generate it for you.

<finder name="G_S" return-type="Collection">
    <finder-column name="groupId"></finder-column>
    <finder-column name="status"></finder-column>
</finder>

Then make sure you have a getter in your service layer that uses the new finder.

public List<FooEntity> getFooEntities(long groupId, int status)
       throws SystemException {
    return fooEntityPersistence.findByG_S(groupId,
         WorkflowConstants.STATUS_APPROVED);
}

Now all you need to do is update your JSP to use the appropriate getter.

<liferay-ui:search-container-results
    results="<%=FooEntityLocalServiceUtil.getFooEntities(scopeGroupId,
              fooEntityId(), Workflowconstants.STATUS_APPROVED, searchContainer.getStart(),
              searchContainer.getEnd())%>"
    ...

In an administrative type application (in other words, one that's displayed in the Site Menu's Content section) you might want to display all the entities, with their current workflow status (for example, include workflow status as a column in the search container). To do so, use the <aui:worklfow-status> tag.

    <aui:workflow-status markupView="lexicon" showIcon="<%= false %>" showLabel="<%= false %>" status="<%= fooEntity.getStatus() %>" />

Figure 1: You can display the workflow status of your entities. This is useful in administrative applications.

You only needed one new class, one new method in the service layer, and some updates to your view, and workflow is fully implemented and ready to use in your Liferay application.