Custom operations

From Complete Cyclos documentation wiki
Revision as of 19:36, 20 February 2012 by Fabio (talk | contribs) (Configuraciones necesarias en Cyclos)
Jump to: navigation, search

Documentation under construction

The SMS module has been developed for the Cyclos software, as explained in the page Architecture. It is however possible to create 'custom commands' (operations). There can be two main reasons who you would want to create custom commands:

  1. You have your own payment system and want to offer similar SMS services and do not want to develop an SMS module from scratch.
  2. You use Cyclos with the SMS module but need some additional functionality. The custom command can either connect to Cyclos or/and third party software. For example, one organization enhanced the authentication by SMS with external GPS checking. (In the example below the custom command connects to Cyclos for simplicity reasons)

This page will describe in detail how to create custom commands.

Note: In order to develop custom operations you will need solid knowledge of SMS messaging, Java, Cyclos, and the SMS module.


Contents

Example

The code used with the examples can be downloaded at the section: Example code.

The example command permits retrieving information from other members registered in Cyclos. In the example you can search for a profile field of a specific member. This is just as example and might not be of use for live systems.

The format of the command consists of four words divided by spaces:

  1. Alias of the command. In the example the word MINFO is used
  2. PIN the PIN of the user that is sending the command
  3. Member This is the user that you want to retrieve the profile information from. Depending on the configuration this can be the Cyclos username or mobile phone number. See Installation steps. In the example the mobile phone number is used.
  4. Custom profile field - This is the profile field you want to retrieve from the member. This parameter is optional. If not given the address will be returned.

Operation flow

When the Driver will receive a message from the number 098654321 with message text Minfo 7410 099123456 the following will happen:

  1. The command MINFO will be invoked. Which will in its turn:
    1. Verify if the number 098654321 belongs to a valid member in Cyclos
    2. Verify if the credentials (PIN) of the member with phone number 098654321 is indeed "7410"
    3. Fetch the profile information of the member which mobilePhone value is "099123456"
    4. Generate a message to mobile phone number 098654321 with text address for 099123456: Minas 1486 (the address is retrieved because no custom field argument is passed in the initial message)

Development of custom command

In oder to create a custom command the following steps need to be done:

  1. Create a Command class that will contain the required functionality.
  2. Create a Error handling class that can handle (and notify if necessary) the possible errors generated during the execution of the command.
  3. Define the Spring configuration that will instantiate the command and auxiliary components.
  4. SMS module configuration define the custom command to be included.

The error handling can be implemented with the following classes:

  1. Error codes: Permits adding new error codes that will be handled by the Error handling class.
  2. Custom exception class: The exception that will be thrown from the custom command. It can use the Error codes and provide information about the environment and error. This is the class that is passed as an argument to the Error handling class.

Packages dependencies & deploy

The development interface will need the following libraries (all available in the distribution AIO of the SMS module):

  • controller-driver-xx.xx.xx.jar
  • controller-xx.xx.xx.jar
  • cyclos_3.6_client.jar (Web Service interfaces for Cyclos)

Example code

Command class

The command class will need to extend the class nl.strohalm.cyclos.controller.command.Command which is available in the library controller-xx.xx.xx.jar.

Note: The command object is a SINGLETON within a the scope of the cyclosInstance ( see Spring_configuration). Because of the fact that the class is singleton it cannot contain any variables that can change their value when receiving method calls (with the exception of the SET methods that can be used during the initialization of the controller, (see also Spring_configuration).

If you want to send or receive SMS messages from a command class please refer to the section Send SMS messages. Make sure the methods of the command class are implemented correctly (see next section)

Command class methods

executeCommand

protected void executeCommand(String[] commandParameters, DriverMessage driverMessage, CyclosInstanceConfig cyclosInstance)

  • Implementation required.

This method will be called by the Controller when a method is received which first word(s) match with one of the aliases configured in the configuration of the custom command. The method will receive the following parameters:

  • commandParameters - The words (aliases) received that define the command, (e.g. 'pay').
  • driverMessage - All data concerning the received message (message text, origin and destination number, Driver identification, indentificador de traza??, etc.)
  • cyclosInstance - The configuration of the cyclos instance that processes the command (permitting access to internationalization keys, Web Service clients etc.).

getRequiredI18NKeys

protected String[] getRequiredI18NKeys()

  • Implementation required.

This method will return the internationalization keys that are related to the operation command. For the implementataiton the following points are important:

  • Internationalization keys command.<command_name>.regularExpression, command.memberNotRegistered, and error.template are used for all commands and therefore it is not necessary to specify them in this list.
  • If you want the build-in Help command to return keys you will need to define the keys command.<command_name>.help.withConf, and command.<command_name>.help.withoutConf.
  • If you want to allow the option to enable the Security confirmation for the command you will have to define the key: command.<command_name>.confirmation.
  • More details about internationalization keys can be found at [#Anexos_a_cyclosInstance.properties|Anexos a cyclosInstance.properties]].??

getErrorCodes

public Set<Integer> getErrorCodes(CodeSupportedError error) -

  • Implementation required.

When an CodeSupportedError is generated it will return all possible error codes that were generated by it. It will return one of the error codes defined in Enumerado de códigos de error (TODO:change link) when it receives the value CodeSupportedError.CUSTOM_COMMAND_ERROR as parameter.

getHelpParameters

public String[] getHelpParameters(CyclosInstanceConfig cyclosInstance)

  • Implementation required.

Returns a list of parameters which can be used to substitute in the internationalization the keys command.<command_name>.help.withConf, y command.<command_name>.help.withoutConf (for more details please see Operations translation keys). Generally, the first element of the returned list is the shortest alias to the command.

isUsePin

  • public boolean isUsePin().
  • Implementation optional.

Defines if the command requires a PIN. PIN is enabled by default. The following points need to be considered.

  1. If PIN is used it will need to be first parameter after the command.
  2. If both PIN and OTP (Security confirmation) are enabled (see cyclosInstance -> commands -> requestConfirmation) the PIN will not be required in the original message.

getRequiredConfigParameters

protected CommandConfigParameter[] getRequiredConfigParameters()

  • Implementation optional.

Allows to define extra parameters required for the execution of a command. In case the extra parameters, or configured parameters in Config.xml file are missing, the SMS module will not be initialized, indicating the missing parameter as error cause.

Command class example

MemberInfoCommand (source)

package nl.strohalm.cyclos.controller.command.minfo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import nl.strohalm.cyclos.controller.command.Command;
import nl.strohalm.cyclos.controller.command.CommandConfigParameter;
import nl.strohalm.cyclos.controller.config.CyclosInstanceConfig;
import nl.strohalm.cyclos.controller.exception.CodeSupportedError;
import nl.strohalm.cyclos.controller.exception.CommandException;
import nl.strohalm.cyclos.driver.DriverMessage;
import nl.strohalm.cyclos.webservices.access.CheckCredentialsParameters;
import nl.strohalm.cyclos.webservices.access.CredentialsStatus;
import nl.strohalm.cyclos.webservices.members.MemberResultPage;
import nl.strohalm.cyclos.webservices.members.MemberSearchParameters;
import nl.strohalm.cyclos.webservices.model.FieldValueVO;
import nl.strohalm.cyclos.webservices.model.MemberVO;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Text message syntax: COMMAND <PIN> <memberPrincipal> [<customField>]
 * Response with the Cyclos member memberPrincipal customField value
 */
public class MemberInfoCommand extends Command {
    private static final Log logger   = LogFactory.getLog(MemberInfoCommand.class); 

    static final String      SMS_TYPE = "MEMBER_INFO";

    static {
        logger.info("****** MEMBER INFO SAMPLE CUSTOM COMMAND. VERSION: 1.0 ******");
    }

    /**
     * @return the code assigned to a (supported) built-in error.
     */
    private static Integer getCode(final CodeSupportedError error) {
        switch (error) {
            case INVALID_PARAMETERS_COUNT:
                return 901;
            case UNKNOWN_COMMAND_ERROR:
                return 902;
            case ORIGINATE_MEMBER_BLOCKED_CREDENTIALS:
                return 903;
            case ORIGINATE_MEMBER_INVALID_CREDENTIALS:
                return 904;
            default:
                return null;
        }
    }

    /**
     * All returned codes must exist as error.<ERROR_TYPE>.code key in 
     * errors__<language>_<country>_<variant>.properties file
     * @see  nl.strohalm.cyclos.controller.exception.IErrorCodeDescriptor#getErrorCodes(nl.strohalm.cyclos.controller.exception.CodeSupportedError)
     */
    @Override
    public Set<Integer> getErrorCodes(final CodeSupportedError error) {
        if (CodeSupportedError.CUSTOM_COMMAND_ERROR == error) {
            final Set<Integer> codes = new HashSet<Integer>();
            for (final MemberInfoCommandErrorCode customCode : MemberInfoCommandErrorCode.values()) {
                codes.add(customCode.code());
            }
            return codes;
        } else {
            final Integer code = getCode(error);
            if (code != null) {
                return Collections.singleton(code);
            } else {
                return Collections.emptySet();
            }
        }
    }

    @Override
    public String[] getHelpParameters(final CyclosInstanceConfig cyclosInstance) {
        return new String[] { getShortestAlias(cyclosInstance) };
    }

    /**
     * Command implementation
     * @see nl.strohalm.cyclos.controller.command.Command#executeCommand(java.lang.String[], nl.strohalm.cyclos.driver.DriverMessage,
     * nl.strohalm.cyclos.controller.config.CyclosInstanceConfig)
     */
    @Override
    protected void executeCommand(final String[] commandParameters, final DriverMessage message, final CyclosInstanceConfig cyclosInstance) {
        commandHelper.checkParametersCount(getCode(CodeSupportedError.INVALID_PARAMETERS_COUNT), commandParameters.length, 2, 3);
        checkMemberCredential(cyclosInstance, message.getMessage().getFrom(), commandParameters[0]);
        String customFieldToReturn =  getConfig(cyclosInstance.getName()).getParameter(MemberInfoCommandConfigParameter.DEFAULT_CUSTOM_FIELD_NAME);
        if (commandParameters.length > 2) {
            // we receive a specific custom field name (do not use the default)
            customFieldToReturn = commandParameters[2];
        }

        final String customFieldValue = memberCustomFieldValue(commandParameters[1], customFieldToReturn, cyclosInstance); 

        final String[] msgTextParam = new String[] { commandParameters[1], customFieldToReturn, customFieldValue };
        facade.asyncNotifyToMember(message.getMessage().getFrom(), message.getMessage().getTraceData(), cyclosInstance, SMS_TYPE,  "command." + name + ".response", msgTextParam);
    }

    @Override
    protected CommandConfigParameter[] getRequiredConfigParameters() {
        return MemberInfoCommandConfigParameter.values();
    }

    /**
     * The specified key must exist in cyclosInstance_<language>_<country>_<variant>.properties file
     * @see nl.strohalm.cyclos.controller.command.Command#getRequiredI18NKeys()
     */
    @Override
    protected String[] getRequiredI18NKeys() {
        return new String[] {
                "command." + name + ".help.withConf", // response to help message if you are using requestConfirmation
                "command." + name + ".help.withoutConf", // response to help message if you are not using requestConfirmation
                "command." + name + ".response", // message to response this command
                "command." + name + ".confirmation" // message to response this command
        };
    }

    /**
     * Check if given credentials are valid for CyclosInstance Member with phoneCustonFields equals originatorPhone.
     * if not, then exit trough CommandException or IllegalStateException
     * @throws CommandException, IllegalStateException
     */
    private void checkMemberCredential(final CyclosInstanceConfig cyclosInstance, final String originatorPhone, final String credentials) {
        CodeSupportedError errorType;
        final String searchByPrincipalType = cyclosInstance.getMemberSettings().getPhoneCustomField();
        final CheckCredentialsParameters params = new CheckCredentialsParameters();
        params.setPrincipalType(searchByPrincipalType);
        params.setPrincipal(originatorPhone);
        params.setCredentials(credentials);
        final CredentialsStatus status = cyclosWsManager.checkCredentials(cyclosInstance.getName(), params);
        switch (status) {
            case VALID: // ok
                break;
            case BLOCKED:
                errorType = CodeSupportedError.ORIGINATE_MEMBER_BLOCKED_CREDENTIALS;
                throw new CommandException(errorType, getCode(errorType), String.format("The credentials are BLOCKED for member  (originatePhone: %1$s).", originatorPhone));
            case INVALID:
                // check if member is registered (is not good idea give this detail, but we are making a command example)
                final MemberSearchParameters mSearchParam = new MemberSearchParameters();
                final List<FieldValueVO> searchFields = new ArrayList<FieldValueVO>();
                searchFields.add(new FieldValueVO(searchByPrincipalType, originatorPhone));
                mSearchParam.setFields(searchFields);
                final MemberResultPage searchResult = cyclosWsManager.searchMember(cyclosInstance.getName(), mSearchParam);
                if (searchResult.getTotalCount() == 0) {
                    throw new CommandException(NoCodedError.ORIGINATE_MEMBER_NOT_FOUND, String.format("Member (originatePhone:  %1$s) not found.", originatorPhone));
                } else {
                    // the member exist then the problem is the credential
                    errorType = CodeSupportedError.ORIGINATE_MEMBER_INVALID_CREDENTIALS;
                    throw new CommandException(errorType, getCode(errorType), String.format("The credentials are INVALID for member (originatePhone: %1$s).", originatorPhone));
                }
            default:
                throw new IllegalStateException(String.format("Unsupported credentials status result: %1$s", status));
        }
    }

    /**
     * @return the customFieldToReturn value for member with principal
     * @throws MemberInfoCommandException
     */
    private String memberCustomFieldValue(final String principal, final String customFieldToRetun, final CyclosInstanceConfig  cyclosInstance) {
        final String msgParamPrincipalType = cyclosInstance.getMemberSettings().getMsgParamPrincipalType();
        final MemberSearchParameters memberSearchParam = new MemberSearchParameters();
        memberSearchParam.setShowCustomFields(true);
        final FieldValueVO fieldValue = new FieldValueVO(msgParamPrincipalType, principal);
        final List<FieldValueVO> fieldValuesVO = new ArrayList<FieldValueVO>();
        fieldValuesVO.add(fieldValue);
        memberSearchParam.setFields(fieldValuesVO);
        final MemberResultPage result = cyclosWsManager.searchMember(cyclosInstance.getName(), memberSearchParam); 

        if (CollectionUtils.isNotEmpty(result.getMembers())) {
            final MemberVO member = result.getMembers().get(0);
            final FieldValueVO clientCustomFieldValue = (FieldValueVO) CollectionUtils.find(member.getFields(), new Predicate() {
                @Override
                public boolean evaluate(final Object arg0) {
                    final FieldValueVO fieldValue = (FieldValueVO) arg0;
                    return fieldValue.getField().equals(customFieldToRetun);
                }
            }); 

            if (clientCustomFieldValue == null) {
                // (check if the custom field is hidden)
                final String exceptionMessage = "Member was found but it doesn't have a custom field with name: " +  customFieldToRetun + ". Member principal: " + principal;
                throw new MemberInfoCommandException(MemberInfoCommandErrorCode.CUSTOM_FIELD_NOT_FOUND, exceptionMessage);
            }
            return clientCustomFieldValue.getValue();
        } else { // it does not find a member
            final String exceptionMessage = "Can not find a member for " + msgParamPrincipalType + ": " + principal;
            throw new MemberInfoCommandException(MemberInfoCommandErrorCode.MEMBER_NOT_FOUND, exceptionMessage);
        }
    }

}

SMS_TYPE constant

This constant defines the type of error code. See getErrorSmsTypeCode.

MemberInfoCommand.executeCommand method

During the execution process of the example command the following steps will be performed.

  1. Validation of the SMS text (will have to correspond with an alias of the command and its parameters): commandHelper.checkParametersCount()
  2. Validate if the sending member is registered and check authentification: checkMemberCredential()
  3. Obtener el custom field por defecto a utilizar de la configuracion (config.xml). ??, what arugment in config.xml what custom field? mobile phone? what configuration??
  4. Verify the parameter that identifies the custom field.
  5. Retrieve the value of the custom field memberCustomFieldValue()
  6. Generate and send response message: facade.asyncNotifyToMember()

MemberInfoCommand.getRequiredI18NKeys method

For the MemberInfo operations the following keys are required in the file: cyclosInstance_en_US_MINFO.properties:

  1. command.mInfo.regularExpression
  2. command.mInfo.help.withConf
  3. command.mInfo.help.withoutConf
  4. command.mInfo.response
  5. command.mInfo.confirmation

The command.mInfo.regularExpression key defines the command para identificar los mensajes de texto que hacen referencia a la operación de comando.?? La clave command.<commandName>.regularExpression es exigida por el controllador para todos los comandos definidos en su configuración.??

Desde el segundo punto en adelante (?? please be specific) se especifican claves puntuales de este comando que (al igual que .regularExpression) será obligatorio definir en los archivos de internacionalización.

If during the initialization process some keys are missing in the file cyclosInstance_en_US_MINFO.properties the SMS module will not start and generate an error indicating what key is missing.

Error handling class

Every command has an (obligatory) error handling class that extends the class nl.strohalm.cyclos.controller.command.handler.CommandErrorHandler. The error handling class will handle errors (normally sending notifications) when exceptions in the class CommandException of the executed command occur.

Note: The command object is a SINGLETON within a the scope of the cyclosInstance ( see Spring_configuration). Because of the fact that the class is singleton it cannot contain any variables that can change their value when receiving method calls (with the exception of the SET methods that can be used during the initialization of the controller, (see also Spring_configuration).

There are two types of errorHandler exceptions:

  • Exceptions generated by a particular message parameter.
  • Exceptions generated by the command because of specific (non desired) events within the command flow.

Error class handling methods

doHandleException

protected void handleException(final CommandException e, final CyclosInstanceConfig cyclosInstance)

  • Implementation optional.

This method will be called every time the associated command generates an exception of the type Clase CommandException (and not the standard exceptions that are handled by the handleException(CommandException) classs). The doHandleException method will be responsible to take necessary error handling actions. Generally this action will be to send a notification to the user indicating that the operation could not be processed and informing an error code. But the method could take other actions like a call to an external application. When sending a notification the text will be retrieved from an internationalization key, see Error translation keys.

If a Clase de exepción personalizada) is used , se tiene el error?? CodeSupportedError.CUSTOM_COMMAND_ERROR, en consecuencia las claves de internacionalización de las cuales se toman los mensajes de respuesta tienen el siguiente formato:

  • error.CUSTOM_COMMAND_ERROR.<codeNumber>

CodeNumber is defined when the exception thrown, that means it is defined by the command that was trying to be executed.

doHandleException parameters
  • CommandException this parameter is the very exception (see Clase de exepción personalizada)
    • String[] commandParameters contains the words (parameters) that make up the text (excluding the alias of the command).
    • Command command - this object represents the command that has been executed.
  • CyclosInstanceConfig cyclosInstance - contains the configuration parameters of the cyclos instance of the command that was executed.

handleException(CommandException)

public void handleException(final CommandException e, final CyclosInstanceConfig cyclosInstance)

  • Implementation optional

This method is called before the doHandleException is called, it only handles exceptions of the type NoCodedError.ORIGINATE_MEMBER_NOT_FOUND or NoCodedError.ORIGINATE_MEMBER_INVALID_CHANNEL. These errors do not need to generate a response message and are just used for logging.

In the case you want full control over the errors you can implement Clase CommandException in stead of doHandleException.

handleException(CommandParameterException)

public void handleException(final CommandParameterException e, final CyclosInstanceConfig cyclosInstance)

  • Implementation optional

This method has a similar behavior as handleException(CommandException) but related to instance exceptions (class CommandParameterException).

The exception CommandParameterException extends the class Clase CommandException and is launched when a command generates a validation error related to wrong/missing paremetros. This exception will provide information about the position of the parameter that caused the exception.

getErrorSmsTypeCode

protected String getErrorSmsTypeCode()

  • Implementation optional

This method returns the message code type (see Nuevo SmsType).

Send SMS messages

It is possible to send messages from a CommandErrorHandler with the following methods:

  • Sending message via Cyclos
  • Sending message directly via the Driver.
    • If it is required to send messages to unregistered members you can use:
    • facade.notifyErrorToDriver
    • facade.sendMsgToDriver

The same methods can be used to send response message from a Clase de comando.

Note: If a message is sent directly via de Driver it will not be logged in Cyclos. This means that it won't be possible to charge a member for these messages.

Error handling class of the example

Source MemberInfoCommandErrorHandler

package nl.strohalm.cyclos.controller.command.minfo;

import nl.strohalm.cyclos.controller.command.handler.CommandErrorHandler;
import nl.strohalm.cyclos.controller.config.CyclosInstanceConfig;
import nl.strohalm.cyclos.controller.exception.CommandException;

import org.apache.commons.lang.ArrayUtils;

public class MemberInfoCommandErrorHandler extends CommandErrorHandler {

    /**
     * Custom the handler for CodeSupportedError.CUSTOM_COMMAND_ERROR
     * @see nl.strohalm.cyclos.controller.command.handler.CommandErrorHandler#doHandleException(nl.strohalm.cyclos.controller.exception.CommandException,
     * nl.strohalm.cyclos.controller.config.CyclosInstanceConfig)
     */
    @Override
    protected void doHandleException(final CommandException e, final CyclosInstanceConfig cyclosInstance) {
        if (e instanceof MemberInfoCommandException) {
            // customize the way to process a CUSTOM_COMMAND_ERROR type
            String msgParam = null;
            if (e.getErrorCode() == MemberInfoCommandErrorCode.MEMBER_NOT_FOUND.code()) {
                msgParam = e.getCommandParameters()[1]; // the member principal value (given in text message)
            } else if (e.getErrorCode() == MemberInfoCommandErrorCode.CUSTOM_FIELD_NOT_FOUND.code()) {
                msgParam = e.getCommandParameters()[2]; // the principal customField (given in text message or configured as  default)
            }
            facade.notifyErrorToMember(e.getDriverMessage(), cyclosInstance, getErrorSmsTypeCode(), e.getError(), e.getErrorCode(), msgParam == null ? ArrayUtils.EMPTY_STRING_ARRAY : new String[] { msgParam });
        } else {
            super.doHandleException(e, cyclosInstance);
        }
    }

    @Override
    protected String getErrorSmsTypeCode() {
        return MemberInfoCommand.SMS_TYPE + "_ERROR";
    }
}

MemberInfoCommandErrorHandler.doHandleException method

Se observa?? la utilización de la Clase de exepción personalizada (MemberInfoCommandException) para poder acceder a la información extra que esta aporta. En especial se muestra el manejo de dos códigos de error propios del comando customizado (ver Enumerado de códigos de error):

  • MemberInfoCommandErrorCode.MEMBER_NOT_FOUND
  • MemberInfoCommandErrorCode.CUSTOM_FIELD_NOT_FOUND

Custom exception class

A custom exception class will need to extend a CommandException. The custom class is responsible to pass the necessary information to the Clase de manejo de errores. Ademas de los datos ya existentes en una CommandException, esta?? clase puede incluir información extra referente al comando para el cual es construida.

CommandException

Package:nl.strohalm.cyclos.controller.exception

The command class exception passes information about the error during the execution of the command (from the point where the command is launched to the error handling that processes it):

  • DriverMessage driverMessage objeto que representa el mensaje causante de la ejecución del comando. Posee datos de:
    • Identificador del driver a través del cual fue recibido
    • el número telefónico del móvil que lo enviado
    • el número al cual fue enviado
    • el texto completo del mensaje
  • String[] commandParameters un array de string conteniendo las distintas palabras del texto mensaj, excluyendo el alias de comando (representa los parámetros del comando)
  • Command command - un objeto representando el comando que se ha ejecutado.

Clase de exepción personalizada en el ejemplo

Source MemberInfoCommandException

package nl.strohalm.cyclos.controller.command.minfo;

import nl.strohalm.cyclos.controller.exception.CodeSupportedError;
import nl.strohalm.cyclos.controller.exception.CommandException;

/**
 * Base command exception for Member info custom command errors
 */
public class MemberInfoCommandException extends CommandException { 

    private static final long          serialVersionUID = 1L;
    private MemberInfoCommandErrorCode errorCode;

    public MemberInfoCommandException(final MemberInfoCommandErrorCode errorCode) {
        this(errorCode, null, null);
    }

    public MemberInfoCommandException(final MemberInfoCommandErrorCode errorCode, final String detailedMessage) {
        this(errorCode, detailedMessage, null);
    }

    public MemberInfoCommandException(final MemberInfoCommandErrorCode errorCode, final String detailedMessage, final Throwable  cause) {
        super(CodeSupportedError.CUSTOM_COMMAND_ERROR, errorCode.code(), detailedMessage, cause);
        this.errorCode = errorCode;
    }

    public MemberInfoCommandException(final MemberInfoCommandErrorCode errorCode, final Throwable cause) {
        this(errorCode, null, cause);
    }

    public MemberInfoCommandErrorCode getCustomCommandErrorCode() {
        return errorCode;
    }
}

Error codes

Generally, the new personalized command requires the handling of specific error codes (which are not contained in the classnl.strohalm.cyclos.controller.exception.CodeSupportedError) in a particular form in the Error handler.

A command which can be triggered from a personalized command is CodeSupportedError.CUSTOM_COMMAND_ERROR, but as many error codes as needed can be created. Make sure to use enumerates. An enumeration of error codes is a pragmatic way of keeping error codes mapped to an enumerated constant.

Enumeration of error codes in the example

Source MemberInfoCommandErrorCode
package nl.strohalm.cyclos.controller.command.minfo;

/**
 * The followings are the codes assigned to the CodeSupportedError.CUSTOM_COMMAND_ERROR
* The intent of codifying the custom command error is to differentiate each internal error
* and allow the handler work properly. This command doesn't use the codes in the same way as the built-in commands does. * */ public enum MemberInfoCommandErrorCode { CUSTOM_FIELD_NOT_FOUND(910), // The member does not have a custom field with specified name (or default name) MEMBER_NOT_FOUND(911); // Can not find (on Cyclos) a member with msgPrincipalType value received (in text message) public static MemberInfoCommandErrorCode error(final Integer code) { if (code == null) { return null; } for (final MemberInfoCommandErrorCode error : values()) { if (error.code == code) { return error; } } throw new IllegalArgumentException("Can't retrieve CustomCommandErrorCode. Unknown integer code: " + code); } private int code; private MemberInfoCommandErrorCode(final int code) { this.code = code; } public int code() { return code; } }

Spring configuration

A configuration is required to be supplied to Spring, for the instantiation of objects for the personalized operation (command and error handler). Spring needs to instantiate the bean correspoding to the proper personalized command, its configuration would be similar to the following:

<bean id="<IdCommandBean>"
	class="<path to command class>">
	<property name="facade" ref="facade"/>
	<property name="langManager" ref="langManager"/>
	<property name="sessionHandler" ref="sessionHandler"/>
	<property name="name" value="<commandID>"/>
	<property name="errorHandler" ref="<IdErrorHandlerBean>"/>
	<property name="logUtils" ref="logUtils"/>
	<property name="commandHelper" ref="commandHelper"/>
</bean>

The value of <commandID> is quite important, as this is the ID referenced for the configuration of the command in the config.xml (see Cyclos instances->commands->name)

Also, it is important to instantiate a bean for the error handling of the personalized command, its configuration would be similar to the following:

<bean id="<IdErrorHandlerBean>" 
	class="<path to Error handler class>">
	<property name="facade" ref="facade"/>
	<property name="command" ref="<IdCommandBean>"/>
	<property name="logUtils" ref="logUtils"/>
</bean>

Spring configuration in the example

myInfoCommand.xml

In our personalized command we put this file into the package /nl/strohalm/cyclos/controller/command/minfo/spring

<?xml version="1.0" encoding="UTF-8"?>

<beans	default-autowire="byName"
		xmlns="http://www.springframework.org/schema/beans"	
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"	
		xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

	<bean id="mInfo"
		class="nl.strohalm.cyclos.controller.command.minfo.MemberInfoCommand">
		<property name="facade" ref="facade"/>
		<property name="langManager" ref="langManager"/>
		<property name="sessionHandler" ref="sessionHandler"/>
		<property name="name" value="mInfo"/>
		<property name="errorHandler" ref="mInfoErrorHandler"/>
		<property name="logUtils" ref="logUtils"/>
		<property name="commandHelper" ref="commandHelper"/>
	</bean>
	<bean id="mInfoErrorHandler"
		class="nl.strohalm.cyclos.controller.command.minfo.MemberInfoCommandErrorHandler">
		<property name="facade" ref="facade"/>
		<property name="command" ref="mInfo"/>
		<property name="logUtils" ref="logUtils"/>
	</bean>
</beans>

SMS module configuration

web.xml

The file /WebContent/WEB-INF/web.xml inside the project AIO has to be modified to include the path to the Spring XML file instantiating the beans corresponding to the personalized command.

The web.xml file should include entries like following:

<web-app .....>
   ....................................
   <context-param>
     ..................................
     <param-value>
          	classpath:
			.................................................
			<rutaAConfiguracionSpringComandoPersonalizado>
     </param-value>
   </context-param>
   ....................................
   ....................................
</web-app>


web.xml in the example

We configured our example personalized command on an AIO gateway (see Gateway http), therefore, our web.xml looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 
                    xmlns:jsp="http://java.sun.com/xml/ns/javaee/jsp"
                    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
                    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                                       http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
                    id="WebApp_ID"
                    version="2.5">
  <display-name>Cyclos SMS Module (All-in-One: Http)</display-name>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        	classpath:META-INF/cxf/cxf.xml 
			classpath:META-INF/cxf/cxf-extension-soap.xml
			classpath:META-INF/cxf/cxf-servlet.xml
			classpath:/nl/strohalm/cyclos/sms/common/spring/commonBeans.xml
    		classpath:/nl/strohalm/cyclos/controller/spring/core.xml
			classpath:/nl/strohalm/cyclos/controller/spring/commands.xml
			classpath:/nl/strohalm/cyclos/controller/spring/dao.xml
			classpath:/nl/strohalm/cyclos/controller/spring/web-beans.xml
			classpath:/nl/strohalm/cyclos/driver/spring/driverCore.xml
			classpath:/nl/strohalm/cyclos/driver/spring/dao.xml	
			classpath:/nl/strohalm/cyclos/driver/spring/db.xml
			classpath:/nl/strohalm/cyclos/driver/monitor/log/monitor.xml
			classpath:/driverContext.xml
			classpath:/nl/strohalm/cyclos/controller/command/minfo/spring/mInfoCommands.xml
	</param-value>
  </context-param>
  <servlet>
    <servlet-name>cxf</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>cxf</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping> 
  <servlet>
    <servlet-name>monitorServlet</servlet-name>
    <servlet-class>nl.strohalm.cyclos.driver.monitor.http.DriverMonitorServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>monitorServlet</servlet-name>
    <url-pattern>/restricted/monitor/monitorServlet/*</url-pattern>
  </servlet-mapping>
  
  <servlet>
    <servlet-name>httpReceiverServlet</servlet-name>
    <servlet-class>nl.strohalm.cyclos.driver.http.HttpGatewayReceiverServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>httpReceiverServlet</servlet-name>
    <url-pattern>/restricted/http/*</url-pattern>
  </servlet-mapping>
  
  <servlet>
		<servlet-name>dwr-invoker</servlet-name>
		<servlet-class>org.directwebremoting.spring.DwrSpringServlet</servlet-class>	
		<init-param>
			<param-name>debug</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>activeReverseAjaxEnabled</param-name>
			<param-value>true</param-value>
		</init-param>		
  </servlet>
  <servlet-mapping>
		<servlet-name>dwr-invoker</servlet-name>
		<url-pattern>/restricted/dwr/*</url-pattern>
  </servlet-mapping>
  
  <welcome-file-list>
	<welcome-file>restricted/monitor/index.jsp</welcome-file>
  </welcome-file-list>
	
  <jsp-config>
    <taglib>
      <taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri>
      <taglib-location>/WEB-INF/taglibs/c.tld</taglib-location>
    </taglib>
  </jsp-config>
  
  <security-constraint>
    <display-name>Security Constraint</display-name>
    <web-resource-collection> 
      <web-resource-name>Restricted Area</web-resource-name>
      <url-pattern>/restricted/monitor/*</url-pattern>
      <http-method>DELETE</http-method>
      <http-method>GET</http-method>
      <http-method>POST</http-method>
      <http-method>PUT</http-method>
    </web-resource-collection>
    <auth-constraint>
      <role-name>ADMIN</role-name>
    </auth-constraint>
  </security-constraint>

  <login-config>
    <auth-method>FORM</auth-method>
    <realm-name>SMS Administrator</realm-name>
    <form-login-config>
      <form-login-page>/restricted/login.jsp</form-login-page>
      <form-error-page>/jsp/error.jsp</form-error-page>
    </form-login-config>
  </login-config>
        
  <security-role>
    <role-name>ADMIN</role-name>
  </security-role>
  
  <filter>
    <display-name>Access Control Filter</display-name>
    <filter-name>accessControlFilter</filter-name>
    <filter-class>nl.strohalm.cyclos.sms.common.http.AccessControlFilter</filter-class>
    <init-param>
      <param-name>gateway</param-name>
      <param-value>/restricted/http</param-value>
    </init-param>
    <init-param>
      <param-name>monitor</param-name>
      <param-value>/restricted/monitor, /restricted/login.jsp, /restricted/logout.jsp, /restricted/dwr</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>accessControlFilter</filter-name>
    <url-pattern>/restricted/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
  </filter-mapping>
  
  <error-page>
    <error-code>404</error-code>
    <location>/jsp/error.jsp</location>
  </error-page>
</web-app>

Attachments to config.xml

The controller configuration file WebContent/WEB-INF/classes/config.xml has been modified to support the new customized command. Two little sections need to be considered:

  1. The attribute variant for the element cyclosInstance - This change allows to add the customizations of the command needed in the file Webcontent/WEB-INF/classes/i18n/cyclosInstance_en_US_MINFO without having to change the English internationalization file included in the original distribution.
  2. An element <command ...> has been added - This is the configuration of the customized command with its attribute, please observe the parameter defaultCustomFieldName, being its value the personalized field which will be used by default.


config.xml in the example

<?xml version="1.0" encoding="UTF-8"?>
<controller language="es" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="config.xsd">
  <cyclosInstances>
    <cyclosInstance name="cyclos" language="en" country="US" variant="MINFO">
      <currencies>
        <currency symbol="units" default="true">
          <alias name="U"/>
          <alias name="Un"/>
          <alias name="Unit"/>
          <alias name="Units"/>
        </currency>
      </currencies>
      <commands>
		<command name="confirm" requestConfirmation="false"/>
		<command name="requestPayment" requestConfirmation="false"/>
		<command name="accountDetails" requestConfirmation="false">
			<parameter name="nameLen" value="0"/>
			<parameter name="pageSize" value="3"/>
		</command> 
		<command name="performPayment" requestConfirmation="false"/>
		<command name="help" requestConfirmation="false"/>
		<command name="registration" requestConfirmation="false">
                   <parameter name="notifyErrorByDriver" value="false"/>
	 	    <parameter name="useLoginName" value="true"/>
        	<parameter name="defaultInitialGroup" value="6"/>
        	<parameter name="groupPrefix" value="."/>
        	<paramGroup name="groupAliases">
            	<parameter name="full" value="5"/>
            	<parameter name="new" value="6"/>
        	</paramGroup> 
		</command>
		<command name="infoText" requestConfirmation="false"/>
		<command name="mInfo" requestConfirmation="false"> 
			<parameter name="defaultCustomFieldName" value="address" />
		</command>
      </commands>
      <driverRouting>
        <route fromProvider="*" toDriver="aioDriver" usedFromNumber="9999" default="true"/>
      </driverRouting>
      <connectionSettings rootUrl="http://localhost:8080/cyclos" disableCNCheck="true" connectionTimeout="120000" readTimeout="120000" trustAllCert="true"/> 
      <memberSettings phoneCustomField="mobilePhone" msgParamPrincipalType="mobilePhone" providerCustomField="provider"  notifyNotRegisteredUser="false">
      	<principalSettings regexp="^(\d){7,9}$"/>
      </memberSettings>
    </cyclosInstance>
  </cyclosInstances>
  <driverInstances>
    <driverInstance name="aioDriver">
      <cyclosRouting>
        <route fromTargetNumber="9999" toCyclos="cyclos"/>        
      </cyclosRouting>
      <connectionSettings connectionTimeout="120000" readTimeout="120000" disableCNCheck="true" trustAllCert="true"/>
    </driverInstance>
  </driverInstances>
 
 <globalSettings responseInvalidMessages="false"/>
   
  <databaseSettings username="root" password="">
  	<url>jdbc:mysql://localhost/cyclos3_sms_aio</url>
  	<driverClassName>com.mysql.jdbc.Driver</driverClassName>
  	<querydslTemplatesClassName>com.mysema.query.sql.MySQLTemplates</querydslTemplatesClassName>
  </databaseSettings>
  <sessionSettings removeExpiredDelayInSeconds="120">
  	<control timeoutInSeconds="300" maxWrongTries="10"/>  		
  	<confirmation timeoutInSeconds="300" maxWrongTries="3" maxWrongPinTries="4" useKeyFromDictionaryFirst="true" keyLength="4" pendingsByCommand="3"/>
  </sessionSettings>
</controller>

Additions to cyclosInstance.properties

The message texts (or components of the message texts) the command will be used are internationalized through the cyclosInstance.properties file.

In this file(s), all keys defined in the Command. getRequiredI18NKeys method need to be added.

cyclosInstance.properties in the example

cyclosInstance_en_US_MINFO.properties

In our example, the following keys are internationalized:

  • General command keys
    • command.mInfo.regularExpression
  • Keys specified in MemberInfocommand.getRequiredI18NKeys()
    • command.minfo.response
    • command.mInfo.confirmation
    • command.mInfo.help.withConf
    • command.mInfo.help.withoutConf
  • To configure the general help system, customize the key:
    • command.help.help
command.help.help=For information about SMS operations. Send a message with the word help followed by one of the following operation commands\: acinfo, minfo, pay, rq, reg, info.
command.mInfo.regularExpression=(\\s*)(minfo)(\\s+.*)*$
command.mInfo.response={1} for {0}: {2}
command.mInfo.help.withoutConf=Member info command. Send a message with\: {0} pin principal [field_name]
command.mInfo.help.withConf=Member info command. Send a message with\: {0} principal [field_name]
command.mInfo.confirmation=Please confirm request for member info\: {0}

Additions to error.properties

The error texts (or components of error texts) the command is issuing are to be internationalized through the errors.properties file.

In this file(s), all keys with codes which can be triggered during the execution of the customized commands need to be specified.

errors.properties in the example

For more details, see the sections Clase de manejo de errores and Clase de exepción personalizada.

errors_en_US_MINFO.properties

On top of the specific error codes triggered with the MINFO command, also the key error.COMMAND_NOT_FOUND has been customized in the case that the configuration variation notifyNotRegisteredUser="true" has been set (see Cyclos instances->memberSettings->notifyNotRegisteredUser).

error.COMMAND_NOT_FOUND=Invalid command. Send a message with the word help followed by one of the following\: acinfo, minfo, pay, rq, reg, info.
error.CUSTOM_COMMAND_ERROR.910=Can not find the custom field {0}, or field value is empty. Member info command failed 
error.CUSTOM_COMMAND_ERROR.911=Can not find the member for principal {0}. Member info command failed
error.INVALID_PARAMETERS_COUNT.901=Invalid message format. Member info command failed.
error.ORIGINATE_MEMBER_BLOCKED_CREDENTIALS.903=Your password is blocked. Member info command failed.
error.ORIGINATE_MEMBER_INVALID_CREDENTIALS.904=Invalid password. Member info command failed.
error.UNKNOWN_COMMAND_ERROR.902=Unknown error processing message. Member info command failed.

Necessary configurations in Cyclos

New SmsType

To allow cyclos to classify the MT messages the system sends (to a mobile phone), in cyclos the following configurations need to be done:

  • Manually add in the sms_types table of the cyclos database the entries reflecting the text message type which will be sent by the system (of course in relationship to the personalized command).
  • Configure the internationalization for the types created in the database. In cyclos->adminMenu->Translation->Application define the texts for the keys:
    • sms.type.MEMBER_INFO.description
    • sms.type.MEMBER_INFO_ERROR.description
  • Add an entry in the cyclos3.sms_types table
  • Define keys in AdminMenu->Transaction->Applications->key=sms.types

SmsType in the example

To support our example, the following instructions have been issued on the cyclos database:

  • insert into sms_types set id="13", code="MEMBER_INFO", order_index="12"
  • insert into sms_types set id="14", code="MEMBER_INFO_ERROR", order_index="13"

Also, at cyclos->adminMenu->Translation->Aplication the following keys have been defined:

  • sms.type.MEMBER_INFO.description - Member info
  • sms.type.MEMBER_INFO_ERROR.description - Member info error