Notifications

This page describes the Process notification feature, SDK documentation and sample implementation.

Global feature description

The role of the Process notification feature is to allow to easily define, emit and process asynchronously structured notification to recipient(s).

It is a generic notification system. We will be able to create specifics notification types for each specific use cases in the future.

List of features detailed in this page

  • Define a structured notification holding data
  • Emit instance(s) of this structured notification to recipient(s)
  • Define a notification processor in order to process emitted notification(s)

The Notification interfaces

Visiativ Process exposes some interfaces you will use in order to work with notifications

The INotificationData interface

This interface represents the bean which will transit through the Process notification system. This bean holds the data required to produce a message (mail, SMS, API call, etc…) when the notification will be process by a notification processor.

As a notification is generic regardless of the type of message (email, SMS, API call, …) that will be produced later in a processor, data it contains must be raw, unformatted and non-specific to a special type of message. For example, the INotificationData implementation should not hold a formatted html email content. This is responsibility of the notification processor to produce this html using the INotificationData’s raw data.

Please note that a INotificationData implementation have to be serializable and should be as light as possible.

package com.axemble.vdoc.sdk.interfaces.notification;

import java.io.Serializable;

/**
 * Represents notification data. It's used to transmit data in order to emit notification
 */
public interface INotificationData extends Serializable {
	
	/**
	 * This method return a human-readable representation of the notification data implementation. It is mainly used when logging and during
	 * notification data processing to be able to get an idea of the subject of the notification data.
	 *
	 * @return the human-readable representation of the message
	 */
	String getReadableRepresentation();
}

The INotificationRecipient interface

This interface represents a recipient for a notification. A notification can be emitted to one or more recipient.

None of its fields are mandatory, but it must have at least one of the following information: email, phone number, mobile phone number or externalID to be valid.

Please note that each field return Optional of the value instead of the value directly

The recipients instances are produced by the INotificationRecipientBuilder (explained later in this document).

package com.axemble.vdoc.sdk.interfaces.notification;

import java.io.Serializable;
import java.util.Locale;
import java.util.Optional;

import com.axemble.vdoc.sdk.interfaces.IUser;

/**
 * Represents a notification data's recipient
 */
public interface INotificationRecipient extends Serializable {
	
	/**
	 * Optional firstName of the recipient
	 *
	 * @return the optional firstName
	 */
	Optional<String> getFirstName();
	
	/**
	 * Optional lastName of the recipient
	 *
	 * @return the optional lastName
	 */
	Optional<String> getLastName();
	
	/**
	 * Optional fullName of the recipient
	 *
	 * @return the optional fullName
	 */
	Optional<String> getFullName();
	
	/**
	 * Optional title of the recipient
	 *
	 * @return the optional title
	 */
	Optional<String> getTitle();
	
	/**
	 * Optional sex of the recipient
	 *
	 * @return the optional sex
	 */
	Optional<String> getSex();
	
	/**
	 * Optional IUser of the recipient
	 *
	 * @return the optional IUser
	 */
	Optional<IUser> retrieveIUser();
	
	/**
	 * Optional login of the recipient
	 *
	 * @return the optional login
	 */
	Optional<String> getLogin();
	
	/**
	 * Optional externalID of the recipient
	 *
	 * @return the optional externalID
	 */
	Optional<String> getExternalID();
	
	/**
	 * Optional locale of the recipient
	 *
	 * @return the optional locale
	 */
	Optional<Locale> retrieveLocale();
	
	/**
	 * Optional e-mail of the recipient
	 *
	 * @return the optional e-mail String
	 */
	Optional<String> getEmail();
	
	/**
	 * Optional mobile phone number of the recipient
	 *
	 * @return the optional mobile phone number String
	 */
	Optional<String> getMobilePhoneNumber();
	
	/**
	 * Optional phone number of the recipient
	 *
	 * @return the optional phone number String
	 */
	Optional<String> getPhoneNumber();
}

The INotificationRecipientBuilder interface

This interface represents a builder used to produce an INotificationRecipient instance. It works like a standard builder. After retrieving a builder instance we can call the “with…(…)” methods to feed various data and finally use the “build()” method to retrieve a INotificationRecipient instance.

Using convenient the withIUser(iUser) method fills in all the information that can be found in the IUser passed in parameter into the builder.

Note that various checks are applied here:

  • A null or blank string parameter on “with…(String)” methods throws an IllegalArgumentException.
  • An invalid email address passed to the “withEmail(String)” method throws an IllegalArgumentException too.
  • A call on “build()” method throws an NotificationRecipientException if none of the following information is filled in the builder: email, phone number, mobile phone number and externalID.

Please note that a new builder instance have to be fetched to build each recipient.

The builder instances are produced by the INotificationEmitter (explained later in this document).

package com.axemble.vdoc.sdk.interfaces.notification;

import com.axemble.vdoc.sdk.interfaces.IUser;

/**
 * Builder tool used to produce notification recipient.
 * <p>
 * A builder instance can only produce one recipient instance safely. So, to produce multiple recipient instance you need to retrieve
 * multiple builder instances.
 * </p>
 */
public interface INotificationRecipientBuilder {
	
	/**
	 * Feeds the firstName and returns the builder
	 *
	 * @param firstName the firstName to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withFirstName(String firstName);
	
	/**
	 * Feeds the lastName and returns the builder
	 *
	 * @param lastName the lastName to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withLastName(String lastName);
	
	/**
	 * Feeds the fullName and returns the builder
	 *
	 * @param fullName the fullName to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withFullName(String fullName);
	
	/**
	 * Feeds the title and returns the builder
	 *
	 * @param title the title to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withTitle(String title);
	
	/**
	 * Feeds the sex and returns the builder
	 *
	 * @param sex the sex to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withSex(String sex);
	
	/**
	 * Feeds the IUser's protocolURI and returns the builder
	 *
	 * @param iUserProtocolURI the IUser's protocolURI to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withIUserProtocolURI(String iUserProtocolURI);
	
	/**
	 * Feeds the login and returns the builder
	 *
	 * @param login the login to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withLogin(String login);
	
	/**
	 * Feeds the externalID and returns the builder
	 *
	 * @param externalID the externalID to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withExternalID(String externalID);
	
	/**
	 * Feeds the languageCode and returns the builder
	 *
	 * @param languageCode the languageCode to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withLanguageCode(String languageCode);
	
	/**
	 * Feeds the e-mail and returns the builder
	 *
	 * @param email the e-mail to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withEmail(String email);
	
	/**
	 * Feeds the phone number and returns the builder
	 *
	 * @param phoneNumber the phone number to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withPhoneNumber(String phoneNumber);
	
	/**
	 * Feeds the mobile phone number and returns the builder
	 *
	 * @param mobilePhoneNumber the mobile phone number to feed
	 * @return the builder
	 */
	INotificationRecipientBuilder withMobilePhoneNumber(String mobilePhoneNumber);
	
	/**
	 * Feeds every data that can be found in the IUser and returns the builder
	 *
	 * @param iUser the IUser to use to feed data
	 * @return the builder
	 */
	INotificationRecipientBuilder withIUser(IUser iUser);
	
	/**
	 * Produces a notification recipient with the information previously fed using "with..." methods
	 *
	 * @return the produced notification recipient
	 * @throws UnsupportedOperationException - If builder hasn't enough data to produce a notification recipient (use setters before)
	 */
	INotificationRecipient build() throws UnsupportedOperationException;
}

The INotificationEmitter interface

This interface represents the tool used to emit notifications. An INotificationEmitter instance can only emit a specific type of INotificationData it is created for.

This interface is closable, each instance have to be closed and works in a transaction. This way, if we emit several INotificationData instance with a same emitter instance, the INotificationData will be really emitted at the call on the emitter’s close() method and when the transaction is validated and committed.

We can retrieve an INotificationEmitter instance through an INotificationController (explained later in this document).

package com.axemble.vdoc.sdk.interfaces.notification;

import java.io.Closeable;

/**
 * Represents a notification emitter for an {@link INotificationData} type.
 */
public interface INotificationEmitter<T extends INotificationData> extends Closeable {
	
	/**
	 * Produce and return a new instance of {@link INotificationRecipientBuilder}
	 *
	 * @return a new {@link INotificationRecipientBuilder} instance
	 */
	INotificationRecipientBuilder createRecipientBuilder();
	
	/**
	 * Add a {@link INotificationRecipient} to the recipient list which will be used to emit the next {@link INotificationData}.
	 *
	 * @param recipient the {@link INotificationRecipient} to add
	 */
	void addRecipient(INotificationRecipient recipient);
	
	/**
	 * Emit the {@link INotificationData} with the previously added {@link INotificationRecipient}
	 * ({@link #addRecipient(INotificationRecipient)}) and clear the previously added {@link INotificationRecipient}(s)
	 *
	 * @param notificationData the {@link INotificationData} to emit
	 */
	void emit(T notificationData);
	
	/**
	 * Emit the {@link INotificationData} with the previously added {@link INotificationRecipient}
	 * ({@link #addRecipient(INotificationRecipient)})
	 *
	 * @param notificationData the {@link INotificationData} to emit
	 * @param clearRecipients  if true the previously added {@link INotificationRecipient} will be clear after emit the
	 *    {@link INotificationData} data, otherwise the previously added recipients won't be changed.
	 */
	void emit(T notificationData, boolean clearRecipients);
	
	/**
	 * Clear the previously added {@link INotificationRecipient}(s)
	 */
	void clear();
}

The INotificationController interface

This interface represents the SDK entry point tool and can be retrieved through an IBasePortalModule instance (see example later in this document).

For the moment it only allows the retrieve of an INotificationEmitter instance for a specific INotificationData implementation type.

package com.axemble.vdoc.sdk.interfaces.notification;

import com.axemble.vdoc.sdk.interfaces.IController;

public interface INotificationController extends IController {
	
	/**
	 * Create and return an {@link INotificationEmitter} instance for a specific {@link INotificationData} type
	 *
	 * @param notificationDataClass the {@link INotificationData} implementation class
	 * @return the {@link INotificationEmitter} instance
	 */
	<T extends INotificationData> INotificationEmitter<T> createNotificationEmitter(Class<T> notificationDataClass);
}

The INotificationProcessor interface

Implementing this interface allows to define a treatment to process on a specific INotificationData implementation type. A processor implementation have to be declared through SPI. This way, the Visiativ Process core product will be able to find all INotificationProcessor matching a specific INotificationData implementation type and run them on it.

package com.axemble.vdoc.sdk.interfaces.notification;

import java.util.Collection;

/**
 * Represents a processor for an {@link INotificationData} type.
 * <p>
 * An INotificationProcessor implementation must be
 * declared using Service Provider Interface ( @see <a href="https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html">SPI</a> ).
 * </p>
 */
public interface INotificationProcessor<T extends INotificationData> {
	
	/**
	 * Process an {@link INotificationData} with the {@link INotificationRecipient} list.
	 *
	 * @param notificationData the {@link INotificationData} instance
	 * @param recipients       the {@link INotificationRecipient} list
	 */
	void process(T notificationData, Collection<INotificationRecipient> recipients);
}

A case study

Here is an implementation example of the Process Notification feature.

Definition of the case’s context

Let’s imagine a Process agent that iterate on a collection of IWorkflowInstance and has to send notifications to recipients during its processing. These notifications will contain a title and a content. At the end of the chain, we will send these notifications by email and cell phone.

Define the new INotificationData implementation

As we said just before, the notification will contain a title and a content. So we can write the following new implementation of INotificationData.

package com.moovapps.sample.notification.data;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationData;

public class SampleNotificationData implements INotificationData {
	
  private final String title;
  private final String description;
  
  public SampleNotificationData(String title, String description) {
    this.title = title;
    this.description = description;
  }
  
  public String getTitle() {
    return title;
  }
  
  public String getDescription() {
    return description;
  }
  
  @Override
  public String getReadableRepresentation() {
    return title + "-" + description;
  }
}

Emitting the SampleNotificationData in the Process agent

Here is the code of the agent.

Please note the usage of a “try” surrounding the recipient building. The reason is that an IUser may not have any of the following information: email, mobile phone, phone number or externalID. At least one of them is required to produce a valid recipient. So the builder can throw an exception in this case. Using this try, an IUser without any of this information will simply be ignored and won’t break the for loop.

package com.moovapps.sample.notification.agents;
import com.axemble.vdoc.sdk.agent.base.BaseAgent;
import com.axemble.vdoc.sdk.exceptions.ModuleException;
import com.axemble.vdoc.sdk.interfaces.IContext;
import com.axemble.vdoc.sdk.interfaces.IUser;
import com.axemble.vdoc.sdk.interfaces.IWorkflow;
import com.axemble.vdoc.sdk.interfaces.IWorkflowInstance;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationEmitter;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationRecipientBuilder;
import com.moovapps.sample.notification.data.SampleNotificationData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;

public class SampleNotificationAgent extends BaseAgent {
  private static final Logger LOGGER = LoggerFactory.getLogger(SampleNotificationAgent.class);
  private static final String PROCESS_URI = "uril://vdoc/workflow/DefaultOrganization/SampleApplication/SampleGroup:0/SampleProcess/SampleProcess_1.0";
  
  @Override
  protected void execute() {
    // Retrieve a notification emitter for the desired notification data type
    // The try-with-resources statement ensures that the closable emitter is closed.
    try (INotificationEmitter<SampleNotificationData> notificationEmitter = getPortalModule().getNotificationController()
      .createNotificationEmitter(SampleNotificationData.class)) {
      IContext sysadminContext = getDirectoryModule().getSysadminContext();
      IWorkflow iWorkflow = (IWorkflow)getWorkflowModule().getElementByProtocolURI(PROCESS_URI);
      Collection<? extends IWorkflowInstance> instances = getWorkflowModule().getWorkflowInstances(sysadminContext, iWorkflow);
      for (IWorkflowInstance instance : instances) {
        String title = (String)instance.getValue("sys_Title");
        String description = (String)instance.getValue("sys_Reference");
        IUser createdBy = instance.getCreatedBy();
        getReport().addInfo("sending notifications for document " + title);
        // Create the new SampleNotificationData instance holding the titre and the content
        SampleNotificationData data = new SampleNotificationData(title, description);
        try {
          // Retrieve a new recipient builder
          INotificationRecipientBuilder recipientBuilder = notificationEmitter.createRecipientBuilder();
          // Produce the recipient using the builder and add it to the emitter
          notificationEmitter.addRecipient(recipientBuilder.withIUser(createdBy).build());
          // Emit the notification data through the emitter
          notificationEmitter.emit(data);
        } catch (Exception e) {
          String errorMessage = "Unable to create recipient for user " + createdBy.getLogin() + " (" + e.getMessage() + ")";
          this.getReport().addError(errorMessage);
          LOGGER.error(errorMessage, e);
        }
      }
    } catch (ModuleException | IOException e) {
      String errorMessage = "Failed to execute agent (" + e.getMessage() + ")";
      getReport().addError(errorMessage);
      LOGGER.error(errorMessage, e);
    }
  }
}

Writing and declaring the two processors

Now we can write two INotificationProcessor implementation, one to send email and another to deals with cell phone notification.

When the Visiativ Process core product will receive a SampleNotificationData emitted through an INotificationEmitter, it will asynchronously look for all INotificationProcessors defined to work on this SampleNotificationData type using SPI (explained later in this document). Then create a new instance of these processors and make them process the notification data calling their process( notificationData, recipients) method.

The processor to deals with email

package com.moovapps.sample.notification.processors;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationProcessor;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationRecipient;
import com.axemble.vdoc.sdk.utils.Logger;
import com.moovapps.sample.notification.data.SampleNotificationData;
import java.util.Collection;
import java.util.Optional;

public class SampleMailNotificationProcessor implements INotificationProcessor<SampleNotificationData> {
  private static final Logger LOGGER = Logger.getLogger(SampleMailNotificationProcessor.class);
  
  @Override
  public void process(SampleNotificationData sampleNotificationData, Collection<INotificationRecipient> recipients) {
    for (INotificationRecipient recipient : recipients) {
      Optional<String> login = recipient.getLogin();
      
      if (login.isPresent()) {
        Optional<String> email = recipient.getEmail();
        
        if (email.isPresent()) {
          LOGGER.info("sending Mail notification " + sampleNotificationData.getReadableRepresentation() + " to " + email.get());
          // Your Mail notification code goes here....
        } else {
          LOGGER.error(
            "unable to send EMail notification " + sampleNotificationData.getReadableRepresentation() + " (recipient " + login.get()
              + " has no email address)");
        }
      } else {
        LOGGER.error(
          "unable to send EMail notification " + sampleNotificationData.getReadableRepresentation() + " (recipient has no login)");
      }
    }
  }
}

The processor to deals with cell phone number

package com.moovapps.sample.notification.processors;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationProcessor;
import com.axemble.vdoc.sdk.interfaces.notification.INotificationRecipient;
import com.axemble.vdoc.sdk.utils.Logger;
import com.moovapps.sample.notification.data.SampleNotificationData;
import java.util.Collection;
import java.util.Optional;

public class SampleSMSNotificationProcessor implements INotificationProcessor<SampleNotificationData> {
  private static final com.axemble.vdoc.sdk.utils.Logger LOGGER = Logger.getLogger(SampleMailNotificationProcessor.class);
  
  @Override
  public void process(SampleNotificationData sampleNotificationData, Collection<INotificationRecipient> recipients) {
    for (INotificationRecipient recipient : recipients) {
      Optional<String> login = recipient.getLogin();
      
      if (login.isPresent()) {
        Optional<String> mobilePhoneNumber = recipient.getMobilePhoneNumber();
        
        if (mobilePhoneNumber.isPresent()) {
          LOGGER.info("sending SMS notification " + sampleNotificationData.getReadableRepresentation() + " to " + mobilePhoneNumber.get());
          // Your Cell phone notification code goes here....
        } else {
          LOGG*ER.error(
            "unable to send SMS notification " + sampleNotificationData.getReadableRepresentation() + " (recipient " + login.get()*
              + " has no valid mobile phone number)");
        }
      } else {
        LOGGER.error("unable to send SMS notification " + sampleNotificationData.getReadableRepresentation() + " (recipient has no login)");
      }
    }
  }
}

Declare the processors

Processors have to be declared using the SPI norm. This way, Visiativ Process will be able to find and use them to process notification data.

The project’s jar must contain a new file in the following path “META-INF/services/”. The file name have to match the service interface it implements “com.axemble.vdoc.sdk.interfaces.notification.INotificationProcessor”. The file contains the list of classes that implements the service:

com.moovapps.sample.notification.processors.SampleMailNotificationProcessor
com.moovapps.sample.notification.processors.SampleSMSNotificationProcessor

Please note that in case of a Maven project, the “META-INF” directory takes place in the following path “src/main/resources”.

Writing processor for INotificationData defined in a project we depend on

This feature allows an addon to declare INotificationProcessor implementation that works on INotificationData type declared in Visiativ Process or another addon on which it depends. For example, addons or other SDK development will be able to add processors on standard Visiativ Process notification such as the workflow intervention notification.