6.11.2. Example Plugins

This section provides some examples of various types of plugins. All classes must be deployed on the Workflow Server in order to become functional.

Expression

The first example show how to create a reusable expression for performing queries in the Content Server. The expression starts by extracting the Unified API connection from the argument workflow object. (Alternatively, the connection could have been injected by implementing the CapConnectionAware interface)

public Object evaluate(WorkflowObject wo,
  Map<String,Object> localVariables)
{
  CapConnection connection = wo.getRepository().getConnection();
  QueryService queryService = connection.getContentRepository()
    .getQueryService();

Afterwards, all subexpressions are evaluated. Note that the localVariables are passed to the subexpression unchanged.

Object[] parameters = new Object[expressions.size()];
  for (int i = 0; i < parameters.length; i++) {
    Expression expression = expressions.get(i);
    parameters[i] = expression.evaluate(object, localVariables);
  }

Lastly, you can pose the actual query.

return new ArrayList<Content>(queryService.
    poseContentQuery(query, parameters));
}

In the XML definition, the subexpressions occur as XML subelements and the query as an attribute of the <Expression> element.

<Expression class="com.coremedia.examples.plugin.QueryExpression"
  query="REFERENCED BY ?0">
  <Get variable="document"/>
</Expression>

The query string is passed into the QueryExpression object by means of a specific setter setQuery(String). The subexpression Get is parsed and handed to the example expression and stored in a list named expressions by means of the following method:

public void add(Object o) {
  if (o instanceof Expression) {
    expressions.add((Expression)o);
  } else {
    throw new RuntimeException("don't know how to add "+o);
  }
}

Even if you do not intend to use subexpressions, you might want to implement a similar method when requiring a highly flexible configuration mechanism. Every nested XML element that cannot be handled by a more specific setter method is passed to the set(Object) method.

Action

The next example introduces a custom action that is capable of moving and renaming a content atomically. The class is named MoveAndRenameAction. It is derived from the base class SimpleAction, which further reduces its complexity.

The arguments for the action are taken from three process variables, whose names are configured in the XML definition and stored in three fields in the action. The action is configured as follows:

<Action class="com.coremedia.examples.plugin.MoveAndRenameAction"
  contentVariable="content"
  targetVariable="target"
  nameVariable="name" />

Except for the three fields and the setters, the implementation consists of a single method.

public boolean doExecute(Process process) {
  Content content = process.getLink(contentVariable);
  Content target = process.getLink(targetVariable);
  String name = process.getString(nameVariable);
  content.moveTo(target, name);  
  return true;
}

By returning true, the action indicates that it completed normally.

Another example of an action implemented as SimpleAction that sends emails is listed in Section 6.11.3, “Example Code of the Mail Action”. Because the mail server is an external component that might take long to respond this action is a good candidate to be implemented as a LongAction as described below.

LongAction

In the following, an action that sends a mail is implemented. Because the mail server is an external component that might not respond immediately, a long action is created. You omit the definition of various string fields that hold configuration values for the action and skip immediately the methods for executing the action.

During the first phase the receiver, subject and body text of the mail are determined.

public class MailAction implements LongAction {
  public Object extractParameters(Task task) {
    com.coremedia.cap.workflow.Process process =
      task.getContainingProcess();
    String receiver = process.getString(receiverVariable);
    String subject = process.getString(subjectVariable);
    String text = process.getString(textVariable);
    return new Object[]{receiver, subject, text};
  }
  ...

Afterwards, the mail is actually sent outside of a DB transaction.

public Object execute(Object params) {
    Object[] paramArr = (Object[]) params;
    String receiver = (String) paramArr[0];
    String subject = (String) paramArr[1];
    String text = (String) paramArr[2];
    boolean result = false;
    try {
      result = send(host, user, password, 
        from, receiver, subject, text);
    } catch (Exception e) {
      return e;
    }
    return result;
  }
 
  protected boolean send(String host, String username,
    String password, String from,
    String receiver, String subject, String text)
  throws MessagingException, AddressException {
    ...
  }
}

Please see the full source code for details of the mail delivery, which is outside the scope of this manual. Finally, the result is converted into an action result.

public ActionResult storeResult(Task task, Object result) {
    if (result instanceof Boolean) {
      return new ActionResult(((Boolean)result).booleanValue());
    } else {
      return new ActionResult((Exception)result);
    }
  }
}

Assuming there are process variables receiver, subject, and text, the LongAction could be used in a process definition as follows:

<AutomatedTask name="SendMail" final="true">
  <Action class="com.coremedia.examples.plugin.MailAction"
    host="smtp.company.com"
    user="automailer"
    password="secret"
    from="noreply@company.com"
    receiverVariable="receiver"
    subjectVariable="subject"
    textVariable="text"/>
</AutomatedTask>

PerformersPolicy

One example of a performer policy is the DefaultPerformersPolicy, which is distributed together with the Unified API sources. The main method of that class will be discussed here.

First, the users that may execute the task are calculated.

public Performers calculatePerformers(Task task, 
  Collection permittedUsers)
{
  Set<User> users = new HashSet<User>();
  users.addAll(permittedUsers);
  users.removeAll(getExcludedUsers(task));

If the task is forced to a user, that user is chosen.

User forcedUser = getForcedUser(task);
  if(forcedUser != null) {
    if(users.contains(forcedUser)) {
      return new Performers(forcedUser, true);
    }
  }

Otherwise, you look for users who are preferred, but have to rejected the task.

users.removeAll(getRejectedUsers(task));
  Set<User> preferredUsers =
    new HashSet<User>(getPreferredUsers(task));
  preferredUsers.retainAll(users);
  if(preferredUsers.size() > 0) {
    return new Performers(preferredUsers, false);
  }

If you failed due to rejections, those rejections are cleared before recomputing the set of users. Note that this is a side effect that is explicitly allowed during the calculatePerformers method.

if(users.size() == 0 && getRejectedUsers(task).size() > 0) {
    removeAllRejections(task);
    return calculatePerformers(task, permittedUsers);
  }

If there are no rejections to be cleared, you have to go with the users that are not preferred.

return new Performers(users, false);
}
RightsPolicy

In the following, you will see the Unified API half of a custom rights policy. That policy assigns rights to exactly that user who created a process and grants rights for the creation of new processes to all members of a single group.

The server half, as presented in the Workflow Manual, is only sufficient for use in the Workflow Server and for the editor. The Unified API needs its own implementation.

First, you need some code to deal with serialization and configuration.

public class OnlyOwnerRightsPolicy implements RightsPolicy {
  private static final long serialVersionUID = 
   7465148942676430339L;

  private Group group = null;

  public void setGroup(Group group) {
    this.group = group;
  }

  public Group getGroup() {
    return group;
  }

  public void setGroup(String groupAtDomain) throws WfException {
    UserRepository userRepository = WfServer.getConnection().
     getUserRepository();
    Group group = userRepository.getGroupByName(groupAtDomain);

    if (group == null) {
      throw new RuntimeException("Could not find 
      group "+groupAtDomain);
    }
    setGroup(group);
  }
  ...

The last method is called only in the Workflow Server while an XML process definition using the new policy is parsed. You are therefore allowed to obtain the server's Unified API connection through the WfServer singleton.

Now you can look at some of the methods that compute the rights of individual users.

private User getOwner(WorkflowObject workflowObject) {
    if (workflowObject instanceof Task) {
      workflowObject = 
       ((Task)workflowObject).getContainingProcess();
    }
    return ((Process)workflowObject).getOwner();
  }
  public boolean mayPerform(WorkflowObject workflowObject,
    Right right, User user)
  {
    if (user.isSuperUser()) return true;
    User owner = getOwner(workflowObject);
    return owner != null && owner.equals(user);
  }
  public boolean mayPerform(WorkflowObjectDefinition
    definition, Right right, User user)
  {
    return user.isMemberOf(group);
  }
  ...

Skipping some parts of the code that are very similar to the server-side code as presented in the Workflow Manual, you observe that there is also a weight method that estimates the main memory size of the policy in bytes. It is used for caching policies. Here 12 bytes for the policy and 16 bytes for the referenced group are estimated.

public int getWeight() {
    return 28;
  }

Finally, there is the unmarshalling process that is needed to create a policy instance in the client VM.

public RightsPolicyMarshaller getMarshaller() {
    return new OnlyOwnerRightsPolicyMarshaller();
  }
}

The marshaller itself resides in yet another class. Let us look at the unmarshal method, only.

public class OnlyOwnerRightsPolicyMarshaller 
implements RightsPolicyMarshaller {
  ...
  public RightsPolicy unmarshal(CapConnection connection, 
    byte[] data)
  {
    OnlyOwnerRightsPolicy result = new OnlyOwnerRightsPolicy();
    if (data[4] == 1) {
      int groupId = (data[0] & 0x000000ff) +
        (data[1]<<8 & 0x0000ff00) +
        (data[2]<<16 & 0x00ff0000) +
        (data[3]<<24);
      result.setGroup(connection.getUserRepository().
        getGroup(IdHelper.formatGroupId(groupId)));
    }
    return result;
  }
  ...
}

Notice how a connection is passed into the unmarshaller, so that it can be used to build Unified API objects for use in the policy.