Custom validation Java procedure

Custom validation procedures provide a way for you to extend Distributed Marketing in-built validation.

A procedure is a custom or standard Java class hosted by Distributed Marketing that custom validates forms that are used in projects or instances of an On-demand Campaign or a List template. Procedures must be written in Java, and follow a simple programming model by using a well-defined API to validate On-demand and List components that are managed by Distributed Marketing. They are discovered through a simple lookup mechanism and XML-based definition file. Distributed Marketing runs the procedures, in wizard mode, while it creates or edits an On-demand campaign or List. Procedures run synchronously regarding their client; results are made available directly to the client and through a persisted auditing mechanism.

The following assumptions concern procedures.
  • The procedure implementation classes are packaged into a separate classes tree or JAR file and are made available to Distributed Marketing through a URL path. The procedure execution manager uses an independent class loader to load these classes as needed. By default, Distributed Marketing looks in the following directory: <Distributed_Marketing_Home>devkits/integration/examples/classes/.

    To change this default, set the integrationProcedureClasspathURL parameter under Settings > Configuration > Distributed Marketing > UDM Configuration Settings> Integration Services.

  • The procedure implementation class name follows the accepted Java naming conventions to avoid package collisions with Unica and classes from other vendors. In particular, you must not place procedures under the com.unica or com.unicacorp package tree.
  • The procedure implementation is coded to the Java runtime version used by Distributed Marketing on the application server (at least JRE 1.5).
  • Distributed Marketing provides a number of open source and third-party libraries. The application servers also use different versions of these libraries. Generally, this list changes from release to release.
    Note: To avoid possible compatibility problems, procedures should not use any open source, third-party, or application server-specific libraries. However, if such packages are used by a procedure, or by the secondary classes that are imported by the procedure, their use must agree exactly with the packages provided by Distributed Marketing and the application server. In this case, you might have to rework your procedure code, if a later version of Distributed Marketing upgrades or abandons a library.
  • The procedure implementation class is loaded by the class loading policy that is normally used by Distributed Marketing (typically parent-last). The application server might provide development tools and options to reload classes that would apply to Distributed Marketing procedures, but that is not required.
  • The procedure must be thread-safe regarding its own state; that is, its run method cannot depend on internal state changes from call to call.
  • A procedure cannot create threads on its own.
The design must focus on validating the form data that is submitted by the user while they create or edit an On-demand Campaign or List. The procedure implementation class uses an implementation of IExecutionContext, which provides vital information, such as user ID, locale, connection of the system data source, campaign ID of the campaign that is validated, and the current form name that is submitted. The implementation class is provided a map of FormName.AttributeName as a key and an object of IAttribute as value.
Note: The implementation procedure should be using the parameters that are provided in the run method only to validate the submitted data. The map contains all the attributes with their user-filled values or default values, if any, including summary tab attributes. After coded and compiled, the procedure implementation classes must be made available to Distributed Marketing. The build scripts that are supplied with the Distributed Marketing Integration Services place the compiled procedures in the default location. The final development step is to update the custom procedure plug-in definition file that is used by Distributed Marketing to discover the custom procedures. The procedure must implement the com.unica.publicapi.collaborate.plugin.procedure interface and have a parameter-less constructor (JavaBeans model). Coding and compilation of each procedure is done in a Java IDE of your choice, such as Eclipse, Borland JBuilder, Idea. Sample code is provided with Distributed Marketing in the developer toolkit, which is found in: <Distributed_Marketing_Home>/devkits/integration/examples/src/procedure.
Use the parameters under Settings > Configuration > Distributed Marketing > UDM Configuration Settings> Integration Services to configure the Distributed Marketing Integration Module as shown.
  • enableIntegrationServices: Enables/disables the custom form validation feature.
  • integrationProcedureDefintionPath: Specifies the location of the procedure-plugins.xml.

    Default Value: [udm-home]/devkits/integration/examples/src/procedure/procedure-plugins.xml

    Update the location according to your preference.
  • integrationProcedureClasspathURL: Specifies the location of the compiled binary files of the custom validation classes that are defined in the procedure-plugins.xml.

    Default Value: file://[udm-home]/devkits/integration/examples/classes/

    Update the location according to your preference.
    Note: The forward slash at the end of this path is mandatory.
The runtime lifecycle of a procedure:
  1. Discovery and initialization
  2. Selection (optional)
  3. Execution
Distributed Marketing must be made aware of all standard and custom procedures available for a particular installation instance. This process is called discovery. Custom procedures are defined in the procedure plug-in definition file. The Distributed Marketing plug-in manager reads this file during initialization. For each procedure found, the plug-in manager does the following action.
  1. Instantiate the procedure; transition its state to instantiated.
  2. Create a procedure audit record.
  3. If the procedure could be instantiated, its initialize() method is called with any initialization parameters found in its plug-in description file. If this method throws an exception, the status is logged and the procedure is abandoned. Otherwise, the procedure changes to the instantiated state. It is ready to execute.
  4. Create a procedure audit record.
  5. If the procedure could be initialized, its getKey() method is called to determine the key that is used by clients to reference the procedure. This key is associated with the instance and saved for later lookup.

Distributed Marketing might need to present a list of available procedures to you while it creates On-demand Campaign and List templates to associate the custom validation procedure with it. This is done only after the procedure is initialized. The procedure's getDisplayName() method are used for this purpose.

While it creates or edits On-demand Campaigns or Lists, and after the procedure is initialized, Distributed Marketing receives a request to run the procedure. This happens when the template that is used to create the campaigns is associated with a custom validation class. This might happen concurrently with other procedures, or the same procedure, running on other threads.

At execution time, the procedure execution manager does the following.
  1. Start a database transaction
  2. Set the procedure state to running.
  3. Create a procedure audit record.
  4. Call the procedure's execute() method with an execution context and any execute parameters that are provided by the client. This method uses the IExecutionContext and map of attributes in all forms of the On-demand Campaign or List (including summary tab attributes) to validate the data that is submitted by Distributed Marketing users. If the execute method throws an exception, the execution manager marks the transaction for rollback. If the execute method is successful it returns the result back to Distributed Marketing application. If the validation is successful user is allowed to continue with the operations or errors are shown on the top and the user is asked to rectify the values.
  5. Commit or rollback the transaction according to the execution results; set procedure state to executed.
  6. Create a procedure audit record.
    Note: The execute() method should not alter the procedure instance data.
The procedure execution manager automatically wraps execution of the procedure with a database transaction, committing or rolling it back as appropriate based on the outcome of the procedure execution. This guarantees that updates to the Distributed Marketing database are not visible to other users until committed and that the updates are atomic. The procedure writer still must acquire the necessary edit locks to ensure that other users cannot write changes to the database before the procedure execution completes.
Note: The connection that is provided as part of the IExecutionContext is for reading data from the system database of Distributed Marketing. Although, we do not restrict edits by using this connection, it is not recommended to use the connection to edit data in Distributed Marketing system tables.

The execute() method of a procedure returns an integer status code (error status code is -1) and zero or more messages, which are logged and persisted to the Distributed Marketing procedure audit table. The client must use the error status code as -1. You must create an object of ProcedureResult with status code and an array of ProcedureMessage. The error messages must follow this convention: <FormName>.<AttributeName><Separater><Error Message>. You may choose to set the separator by using the setSeparator() method of the class ProcedureResult. The default separator is "||".

The error messages are shown as they are on the user interface. For more information, see the section on the procedure example, which explains the setting of error messages, by using an example.

Distributed Marketing uses a system log for procedures as well: <Distributed_Marketing_home>\logs\system.log. Implementation classes of IProcedure must make sure that they create an object of the class UALogger. This class is available in the affinium_collaborate.jar, which is present in the <Distributed_Marketing_Home>\devkits\integration\examples\lib directory. For more information, see the procedure example to understand the usage.

The procedure execution manager logs the lifecycle of each procedure and creates audit records.
  1. logInfo(): write an informational message to the procedure log
  2. logWarning(): write a warning message to the procedure log
  3. logError(): write an error message to the procedure log
  4. logException(): dump the stack trace for the exception to the procedure log

The supplied integration development kit contains a set of Javadoc for the public Distributed Marketing API and supporting classes. For more information, see the IBM Distributed Marketing Public API Specification sections 3.2.11 and 3.2.12 for the classes, which have attribute names, values, and metadata that is submitted by users. These classes are not documented in the Javadoc mentioned.

IProcedure (com.unica.publicapi.collaborate.plugin.procedure) is an interface that all procedures must implement. Procedures go through a well-defined lifecycle and implement the following method that is mentioned.

/**
* Procedure's execute method.
* <p>
* If called from webservice, it is the responsibiliy of the caller to convert.
* NameValue parameters to their non-sequence map form, i.e.,
* <pre>
* "Form1.Attribute1", IAttribute1
* "Form1.Attribute2", IAttribute2
* ==>
* one map entry:
* entry.key = "Form1.Attribute1" 
* entry.value = IAttribute1
* </pre>
* </p>
* This work is done automatically by the ProcedureManager. *  
* @param context caller's execution context 
* @param parameters a map of procedure parameters.  
* @return result of procedure execution 
* @throws ProcedureExecutionException 
*/ ProcedureResult execute(IExecutionContext context, Map parameters)
					 throws ProcedureExecutionException;
IExecutionContext (com.unica.collaborate.common.api): interface of opaque context object that is handed to the procedure by the execution manager. Implementors of IProcedure must cast this object to CustomValidationExecutionContext, as shown in the procedure example, which provides vital information like user ID, locale, connection of the system data source, campaign ID of the campaign that is validated and the current form name that is submitted. Fields of this class are as below:
  • runAsLocale: locale of the logged in user.
  • runAsUser: User object of the logged in user.
  • connection: Connection to the System database.
  • projectId: Id of the On-demand or List Campaign being validated.
  • currentFormName: Tab that is being validated and submitted.

The following is an example of the Distributed Marketing procedure that attempts to validate data across two forms: Selection Form and Offer Assignment.

Following validations are carried out:
  1. Validates two text attributes against given date formats.
  2. Validates the total of two number attributes across both the forms.
    Note: This class is not shipped with the product, as it requires the forms and attributes with exact names.
This is a sample procedure and can be used by creating forms and attributes with names specified in the class constants.
package procedure;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang.StringUtils;

import com.unica.afc.logger.UALogger;
import com.unica.collaborate.common.api.CustomValidationExecutionContext;
import com.unica.collaborate.common.api.IExecutionContext;
import com.unica.collaborate.common.component.attribute.IAttribute;
import com.unica.collaborate.common.component.attribute.IntegerAttribute;
import com.unica.collaborate.common.component.attribute.TextAttribute;
import com.unica.publicapi.collaborate.plugin.PluginVersion;
import com.unica.publicapi.collaborate.plugin.procedure.IProcedure;
import com.unica.publicapi.collaborate.plugin.procedure.
ProcedureInitializationException;
import com.unica.publicapi.collaborate.plugin.procedure.ProcedureMessage;
import com.unica.publicapi.collaborate.plugin.procedure.ProcedureMessageTypeEnum;
import com.unica.publicapi.collaborate.plugin.procedure.ProcedureResult;

/**
* ValidateAttributesForCustomValidationExampleTemplate is an example of  
* Distributed Marketing procedure that attempts to validated data across 2 forms: 
 * Selection Form and Offer Assignment. Following validations are carried out: 
	  * 1. Validates 2 text attributes against given date formats.  
		* 2. Validates the total of 2 number attributes across both the forms.  * 
* <p>
* Expects the following initialization parameters:
* <ul>
* <li>debug: Boolean object, true or false, indicating if 
* debug tracing is enabled or not</li>
* </ul>
*
*<p>
* Epxects the following execute parameters:
* <ul>
*<li>parameters: Map of summary and custom tab attributes identified by
*'<FormName>.<Attribute Name>' as key and object of IAttribute as value</li>
* </ul>
*
*/ 

public final class ValidateAttributesForCustomValidationExampleTemplate
		implements IProcedure {

	// Logger object for audit logging.
	private static UALogger _logger = (UALogger) UALogger
			.getLogger(ValidateAttributesForCustomValidationExampleTemplate.class);

// our status codes
	private final static int STATUS_SUCCESS = 0;
	private static final int STATUS_FAILED = -1;
	// initialization parameters
	private final static String DEBUG_INITPARAMETER_NAME = "debug";
	// Date format to be validated against
	public static final String DATE_FORMAT = "MM/dd/yyyy";
	// Total of multiple fields across forms
	public static final String OFFER_Percentage = "ct_offer_percentage";

// Form Name and attribute constants for Selection Form
	private static final String Selection_Form = "Selection Form";
	/**Text attributes in New Group 14 which are to be treated as dates 
	 * and hence to be validated against the format specified above.
	*/
	public static final String New_GROUP_14_From = "inexp_from_date";
	public static final String New_GROUP_14_To = "inexp_end_date";

// Form Name and attribute constants for Offer Assignment Form
	private static final String Offer_Assignment_Form = "Offer Assignment Form";
	public static final String TIER_3_OFFER_Percentage = "offer_assignment_percentage";
	// Total of multiple attributes to be validated against.
	public static final int TOTAL_OF_OFFER_PERCENTS = 100;

	private boolean _debug = false;

	private boolean isDebug() {
		return _debug;
	}

	// simple name is unqualified class name
	public String getName() {
		return "ValidateAttributesForCustomValidationExampleTemplate";
	}

	// display name is always name
	public String getDisplayName(Locale locale) {
		return getName();
	}

	// description always in english
	public String getDescription(Locale locale) {
		// only do EN for now
		return "Example of Custom Validation Class for new "
				+ "feature introduced in UDM 8.6.0.3.";
	}

	// version we're coded to; must be 1.0.0 for now
	public PluginVersion getVersion() {
		return new PluginVersion(1, 0, 0);
	}

	// initialize instance from init parameters
	public void initialize(Map initParameters)
			throws ProcedureInitializationException {
		// the only init parameter we have is: debug, Boolean
		if (initParameters.containsKey(DEBUG_INITPARAMETER_NAME)) {
			try {
				_debug = ((Boolean) initParameters
						.get(DEBUG_INITPARAMETER_NAME)).booleanValue();
			} catch (Exception exception) {
				throw new ProcedureInitializationException("Problem using "

+ DEBUG_INITPARAMETER_NAME + " init parameter: "
						+ exception.getMessage());
			}
		}
	}

	/**
	 * Custom Validation Class's execute method.
	 * 
	 * This method is called for custom validation of the project forms.
	 * 
	 * @param context
	 *            caller's execution context
	 * @param parameters
	 *            a map of project parameters.
	 * @return result of procedure execution
	 */
	public ProcedureResult execute(IExecutionContext context, Map parameters) {	
		/**
		 * CustomValidationExecutionContext class provides the following information
		 * required to validate the attributes:
		 * 1. runAsLocale: locale of the logged in user.
		 * 2. runAsUser: User object of the logged in user.
		 * 3. connection: Connection to the System database.
		 * 4. projectId: Id of the On-Demand or List Campaign being validated.
		 * 5. currentFormName: Tab which is being validated and submitted.
		 */
		
CustomValidationExecutionContextcustomValidationContext 
									= (CustomValidationExecutionContext) context;
List<Procedure>errorMessages = newArrayList<ProcedureMessage>();
ProcedureResult result = null;
		if (Selection_Form.equalsIgnoreCase(customValidationContext
				.getCurrentFormName())) {
			if(isDebug()){
				_logger.debug(getName() + "::Validating Selection Form.");
			}
			//Get the text attributes to be validated as dates
			IAttribute dateFromAttr = (IAttribute) parameters
					.get(Selection_Form + "." + New_GROUP_14_From);

			IAttribute dateToAttr = (IAttribute) parameters.get(Selection_Form
					+ "." + New_GROUP_14_To);
			
			//Validate the text attributes for date format.
			errorMessages.addAll(validateDateForGivenFormat(Selection_Form,
					dateFromAttr, DATE_FORMAT));

			errorMessages.addAll(validateDateForGivenFormat(Selection_Form,
					dateToAttr, DATE_FORMAT));

			if (errorMessages.size() == 0)
				// Procedure result with 1 status shows successful validation of
				// the form/project.
				result = new ProcedureResult(STATUS_SUCCESS,
						new ProcedureMessage[] {});
			else {
				if(isDebug()){
					_logger.debug(getName() + "::Errors while validating Selection Form.");
				}
				// ProcedureResult with -1 status shows validation failure for
				// some of the attributes.
ProcedureMessage[] proMessages = new ProcedureMessage[errorMessages
						.size()];
				result = new ProcedureResult(STATUS_FAILED, errorMessages
						.toArray(proMessages), "||");
			}
		} else if (Offer_Assignment_Form
				.equalsIgnoreCase(customValidationContext.getCurrentFormName())) {
			
			if(isDebug()){
				_logger.debug(getName() + "::Validating Offer Assignment Form.");
			}
			
			String csFormOfferPercentage = Selection_Form + "."
					+ OFFER_Percentage;
			//get the Offer percentage attribute from Selection Form.
			IAttribute ctOfferPercentageAttr = (IAttribute) parameters
					.get(csFormOfferPercentage);

			String tier3OfferPercentage = Offer_Assignment_Form + "."
					+ TIER_3_OFFER_Percentage;
			//get the Offer percentage attribute from Offer Assignment Form.
			IAttribute tier3OfferPercentageAttr = (IAttribute) parameters
					.get(tier3OfferPercentage);
			
			//Add the attributes to a list.

ProcedureMessage[] proMessages = new ProcedureMessage[errorMessages
						.size()];
				result = new ProcedureResult(STATUS_FAILED, errorMessages
						.toArray(proMessages), "||");
			}
		} else if (Offer_Assignment_Form
				.equalsIgnoreCase(customValidationContext.getCurrentFormName())) {
			
			if(isDebug()){
				_logger.debug(getName() + "::Validating Offer Assignment Form.");
			}
			
			String csFormOfferPercentage = Selection_Form + "."
					+ OFFER_Percentage;
			//get the Offer percentage attribute from Selection Form.
			IAttribute ctOfferPercentageAttr = (IAttribute) parameters
					.get(csFormOfferPercentage);

			String tier3OfferPercentage = Offer_Assignment_Form + "."
					+ TIER_3_OFFER_Percentage;
			//get the Offer percentage attribute from Offer Assignment Form.
			IAttribute tier3OfferPercentageAttr = (IAttribute) parameters
					.get(tier3OfferPercentage);
			

//Add the attributes to a list.
List<IAttribute>listOfAttributesForTotal = newArrayList<IAttribute>();
listOfAttributesForTotal.add(ctOfferPercentageAttr);
			listOfAttributesForTotal.add(tier3OfferPercentageAttr);
			
			//Call method to find if total is 100
			boolean isTotalCorrect = validateTotalPercentageForGivenAttributes(
					listOfAttributesForTotal, TOTAL_OF_OFFER_PERCENTS);
			
			if (isTotalCorrect) {
				// Procedure result with 1 status shows successful validation of
				// the form/project.
				result = new ProcedureResult(STATUS_SUCCESS,
						new ProcedureMessage[] {});
			} else {
				result = new ProcedureResult(STATUS_FAILED,
						new ProcedureMessage[] { (new ProcedureMessage(
								ProcedureMessageTypeEnum.ERROR,
								tier3OfferPercentageAttr.getName() + "||"
										+ csFormOfferPercentage 											+ " and "
										+ tier3OfferPercentage
										+ "should add up to 100")) 											}, "||");
			}
		} else {
			result = new ProcedureResult(STATUS_SUCCESS,
					new ProcedureMessage[] {});
		}
		return result;

	}

	/**
	 * Validates if the number attribute is greater than 10
	 * @param numberAttribute
	 * @return
	 */

public static boolean validateNumber(IAttribute numberAttribute) {
		if (numberAttribute != null) {
			Long[] values = ((IntegerAttribute) numberAttribute).getValues();
			for (int i = 0; i < values.length; i++) {
				if (values[i] > 10) {
					return false;
				}
			}
		}
		return true;
	}
	
	/**
	 * Validates a given date against a given format.
	 * 
	 * @param selectionForm
	 * @param dateFromAttr
	 * @param dateFormat
	 * @return
	 */

public static List<ProcedureMessage> validateDateForGivenFormat(
	String selectionForm, IAttribute dateFromAttr,
			String dateFormat) {
		String dateString = null;
List<ProcedureMessage>errorMessages = newArrayList<ProcedureMessage>();
if (dateFromAttr != null) {
			if (dateFromAttr instanceof TextAttribute) {
				dateString = ((TextAttribute) dateFromAttr).getValue();
				if (StringUtils.isNotEmpty(dateString)) {
					SimpleDateFormat sdf = new SimpleDateFormat(
							DATE_FORMAT);
					try {
						sdf.parse(dateString);
					} catch (ParseException e) {
						// if the date is not in mentioned format, add an error
						// message
						errorMessages
								.add(new ProcedureMessage(
								ProcedureMessageTypeEnum.ERROR,
										dateFromAttr.getName()
									+ "||"
									+ selectionForm
									+ "."													+dateFromAttr.getName()
									+ "should be in "
									+ DATE_FORMAT
									+ " format "));
					}
				}
			} else {
				errorMessages.add(new ProcedureMessage(
						ProcedureMessageTypeEnum.ERROR, 											dateFromAttr.getName()
								+ "||" + "TextDate can not be empty."));
			}
		}
		return errorMessages;
	}
	
	/**
	 * Validates whether the total of the provided attributes is equal to 
	 * @param listOfAttributesForTotal

* @param totalOfOfferPercents
* @return
*/
public static boolean validateTotalPercentageForGivenAttributes(
		List<IAttribute>listOfAttributesForTotal, int totalOfOfferPercents) {
long total = 0;
		if (listOfAttributesForTotal != null
				&& listOfAttributesForTotal.size() > 0) {
			for (Iterator iterator = listOfAttributesForTotal.iterator(); iterator
					.hasNext();) {
				IntegerAttribute iAttribute = (IntegerAttribute) iterator
						.next();
				if (iAttribute != null && iAttribute.getValues() != null
						&& iAttribute.getValues().length > 0) {
					long percent = iAttribute.getValues()[0];
					total += percent;
				}
			}
		}
		return (total > 0 && total == totalOfOfferPercents)
				|| total == 0;
	}

	public void destroy() {
		// we don't need to do anything
	}

}

The procedure plug-in definition file defines implementation class, metadata, and other information about the custom procedures to be hosted in Distributed Marketing. By default, the procedure plug-in definition is assumed to be in the following path: <Distributed_Marketing_Home>\devkits/integration/examples/src/procedure/procedure-plugins.xml. This file is an XML document that contains the information that is described.

Procedures: a list of zero or more Procedure elements.

Procedure: an element that defines a procedure. Each procedure contains the following elements.
  • Key (optional): string that defines the lookup key for the procedure. This key must be unique regarding all standard (IBM-supplied) and custom procedures that are hosted by a particular Distributed Marketing instance. If not defined, defaults to the fully qualified version of the className element. Names starting with the string "uap" are reserved for use by Distributed Marketing.
  • className (required): fully qualified package name of the procedure class. This class must implement the IProcedure class (com.unica.publicapi.collaborate.plugin.procedure).
  • initParameters (optional): a list of zero or more initParameter elements. initParameter(optional): parameter to be passed to the initialize() method of the procedure. This element includes the nested parameter name, type, and value elements.
    • Name: string that defines the parameter name
    • Type: optional class name of the Java wrapper class that defines the type of the parameter value. Must be one of the following types:
      • java.lang.String (the default)
      • java.lang.Integer
      • java.lang.Double
      • java.lang.Calendar
      • java.lang.Boolean
    • Value: string form of the attribute value according to its type.