Difference between revisions of "Custom operations"

From Complete Cyclos documentation wiki
Jump to: navigation, search
(MemberInfoCommand.executeCommand method)
(MemberInfoCommand.getRequiredI18NKeys method)
Line 334: Line 334:
 
# Generar (y envíar) el mensaje de respuesta: <code>facade.asyncNotifyToMember()</code>
 
# Generar (y envíar) el mensaje de respuesta: <code>facade.asyncNotifyToMember()</code>
  
====MemberInfoCommand.getRequiredI18NKeys method====
+
====<code>MemberInfoCommand.getRequiredI18NKeys</code> method====
Para la operación de MemberInfo se requiere disponer de las siguientes claves en cyclosInstance_en_US_MINFO.properties:
+
Para la operación de MemberInfo se requiere disponer de las siguientes claves en <code>cyclosInstance_en_US_MINFO.properties</code>:
 +
<code>
 
# command.mInfo.regularExpression
 
# command.mInfo.regularExpression
 
# command.mInfo.help.withConf
 
# command.mInfo.help.withConf
Line 341: Line 342:
 
# command.mInfo.response
 
# command.mInfo.response
 
# command.mInfo.confirmation
 
# command.mInfo.confirmation
 +
</code>
  
La primera de estas claves, ''command.mInfo.regularExpression'' define la expresión regular 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.
+
La primera de estas claves, <code>command.mInfo.regularExpression</code> define la expresión regular para identificar los mensajes de texto que hacen referencia a la operación de comando. La clave <code>command.<commandName>.regularExpression</code> es exigida por el controllador para todos los comandos definidos en su configuración.
  
Desde el segundo punto en adelante se especifican claves puntuales de este comando que (al igual que ''.regularExpression'') será obligatorio definir en los archivos de internacionalización.  
+
Desde el segundo punto en adelante se especifican claves puntuales de este comando que (al igual que <code>.regularExpression</code>) será obligatorio definir en los archivos de internacionalización.  
  
 
Si alguna de las claves listadas anteriormente no se encuentra definida en el archivo cyclosInstance_en_US_MINFO.properties el módulo de SMS no se inicializa, generándose un error describiendo la clave faltante.
 
Si alguna de las claves listadas anteriormente no se encuentra definida en el archivo cyclosInstance_en_US_MINFO.properties el módulo de SMS no se inicializa, generándose un error describiendo la clave faltante.

Revision as of 20:34, 20 January 2012

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 but you want to modify or add some SMS operations. Building from SMS Module would avoid having to program all the SMS operations from scratch.
  2. You use Cyclos with the SMS module but need some additional functionality and you decide to create a custom command. 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 code 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.

Determina que el presente comando utiliza PIN. Los comandos pueden o no requerir un PIN de autenticacion del miembro que envía el mensaje. Para el caso de que el comando utilice PIN (situación por defecto), se han de considerar los siguientes puntos:

  1. El PIN debe ser el primer parámetro del comando.
  2. Para el caso de un comando que utiliza PIN y es configurado con Security confirmation (ver cyclosInstance -> commands -> requestConfirmation) el PIN será tomado de la confirmación no siendo necesario su envío en el mensaje original del comando.

getRequiredConfigParameters

protected CommandConfigParameter[] getRequiredConfigParameters()

  • Implementation optional.

Allows to define a series of parameters which would be required for the execution of a command. In case a parametrization not existing in Config.xml file is required, the SMS module will not be initialized, indicating the missing parameter as error cause.

Clase de comando en el ejemplo

Source MemberInfoCommand

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

?? porque aqui coloca 'method' y en los otros encima no? No es una lista con methodes. Porque no tiene arugmento si es obligatorio o no como los de encima?
Durante el procesado del comando se ejecutan los siguientes pasos:

  1. Validar el texto del mensaje (correspondiente a un alias del comando y sus parámetros): commandHelper.checkParametersCount()
  2. Validar la existencia del miembro remitente del mensaje y su autenticación: checkMemberCredential()
  3. Obtener el custom field por defecto a utilizar de la configuracion (config.xml).
  4. Verificar y considerar un posible nombre de custom field pasado como parámetro en el mensaje de texto.
  5. Obtiener el valor de custom field para el miembro a consultar: memberCustomFieldValue()
  6. Generar (y envíar) el mensaje de respuesta: facade.asyncNotifyToMember()

MemberInfoCommand.getRequiredI18NKeys method

Para la operación de MemberInfo se requiere disponer de las siguientes claves en 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

La primera de estas claves, command.mInfo.regularExpression define la expresión regular 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 se especifican claves puntuales de este comando que (al igual que .regularExpression) será obligatorio definir en los archivos de internacionalización.

Si alguna de las claves listadas anteriormente no se encuentra definida en el archivo cyclosInstance_en_US_MINFO.properties el módulo de SMS no se inicializa, generándose un error describiendo la clave faltante.

Error handling class

Cada comando posee (obligatoriamente) una clase de manejo de errores que extiende a nl.strohalm.cyclos.controller.command.handler.CommandErrorHandler. El manejador de errores tiene la funcion de tomar acciones (en general de notificación) cuando se producen exepciones de la Clase CommandException, durante la ejecución de su comando asociado.

ATENCION: El objeto que se crea (de una Clase de manejo de errores) es SINGLETON dentro de una cyclosInstance (será único para la aplicacion completa, ver Configuración de Spring). Por tanto NO debe contener variables de Objeto que cambien su valor entre distintos llamados de sus métodos (sin considerar los métodos SET que puedan ser utilizados durante el inicializado del controller en la Configuracion de Spring)

Existen dos tipos de manejo de exepciones en el el errorHandler:

  • Exepciones ocasionadas por un parámetro particular del mensaje.
  • Exepciones que el propio comando genera en funcion de capturar eventos u otras situaciones de error, o por la propia lógica del mismo.

Métodos de la clase de manejo de errores

doHandleException

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

  • Implementación opcional.

Este metodo es llamado cada vez que el comando asociado a la clase de error genera una exception de la Clase CommandException (salvando las exepciones capturadas en la implementación por defecto de handleException(CommandException) ), su responsabilidad principal es tomar las acciones necesarias para tratar el error.

En general la acción que se toma en este método es la notificación al usuario que ha enviado el mensaje responsable de la ejecución del comando, pero esto no es excluyente y podría (además o en su lugar) realizar llamados a Web service de aplicaciones externas o cualquier otra accion que sea requerida. La accion por defecto notifica al miembro con un texto obtenido de una clave de internacionalización correspondiente al cyclos instance con el cual se ha procesado el mensaje (para obtener la clave se utiliza el método que se describe en Error translation keys).

Para el caso de trabajar con una Clase de exepción personalizada) 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 es definido en el momento de lanzar la excepción, es decir que es definido por el comando que se ha intentado ejecutar.

Parametros de doHandleException
  • CommandException e este parametro es la excepción propiamente dicha (ver también Clase de exepción personalizada)
    • String[] commandParameters contiene las distintas palabras del texto del mensaje, excluyendo el alias de comando (representa los parámetros del comando)
    • Command command - un objeto representando el comando que se ha ejecutado.
  • CyclosInstanceConfig cyclosInstance - toda la configuración del cyclos instance contra el cual se intentó ejecutar el comando.

handleException(CommandException)

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

  • Implementación opcional

Este método es llamado antes de llamar a doHandleException, a quien llama únicamente si las excepciones que se han producido no son de tipo NoCodedError.ORIGINATE_MEMBER_NOT_FOUND o NoCodedError.ORIGINATE_MEMBER_INVALID_CHANNEL. Esta salvedad es hecha ya que en estos casos no tiene sentido generar mensajes de respuesta, y lo unico que se hace es loguear información de trazado.

En caso de querer disponer de un control total de las exepciones pertenecientes a la Clase CommandException generadas en el comando implemente este método (en lugar de doHandleException).

handleException(CommandParameterException)

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

  • Implementación opcional.

Este metodo se comporta en forma similar a handleException(CommandException) pero para instancia de exepciones de la clase CommandParameterException.

CommandParameterException extiende a la Clase CommandException y es lanzada por un comando en ejecucion cuando se produce un error de validacion de uno de los parámetros del comando. Esta exepcion anexa informacion de la posición del parámetro que ha sido el responzable de su lanzamiento (este dato es seteado en el comando al momento de lanzarce la exepción).

getErrorSmsTypeCode

protected String getErrorSmsTypeCode()

  • Implementación obligatoria.

Este método retorna el tipo bajo el cual se clasificarán los mensajes enviados en Cyclos, en consecuencia de errores que produzcan durante la ejecución del comando (ver Nuevo SmsType).

Send SMS messages

Desde una CommandErrorHandler se pueden enviar mensajes haciendo uso de los metodos que se listan a continuación.

  • Envio de mensajes pasando por Cyclos
  • Envío de mensajes directamente mediante el Driver - Se utiliza en el caso de enviar o contestar mensajes a números de teléfonos móviles que no tienen miembro asociado en Cyclos o que los mismos no se encuentran habilitados (por cualquier razón) para el envío de mensajes.
    • facade.notifyErrorToDriver
    • facade.sendMsgToDriver

NOTA: En el caso de que un mensaje sea enviado directamente mediante el Driver cyclos no registrará el mismo, ni podrá realizar operaciones de descuento de crédito por el despacho de dicho mensaje. ?? como?

Los mismos metodos listados anteriormente son válidos para el envío de mensajes de respuesta desde una Clase de comando.

Clase de manejo de errores en el ejemplo

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

Una clase de manejo de errores personalizada debe extender a CommandException, y es la responsable de transportar toda la informacion necesaria para que la Clase de manejo de errores pueda actuar. 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

Paquete:nl.strohalm.cyclos.controller.exception

La clase command exception transporta informacion de un error durante la ejecucion de un comando (desde el comando que la lanza, hacia el manejador de errores quien la procesa):

  • 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

En general, el nuevo comando personalizado requerirá manejar códigos de error particulares (que no estan comprendidos dentro de la clase nl.strohalm.cyclos.controller.exception.CodeSupportedError) y que sean tratados de forma particular en el Manejador de errores.

El error que se puede lanzar desde un comando personalizado es CodeSupportedError.CUSTOM_COMMAND_ERROR, pero se pueden crear tantos códigos para este comando como sea necesario. Make sure to use enumerates. El enumerado de códigos de error es una forma práctica de mantener los enteros de códigos de error a una constante perteneciente a un enumerado.

Enumerado de códigos de error en el ejemplo

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

Se require anexar configuración a Spring, necesaria para instanciar los objetos de la operación personalizada (comando y manejador de errores). Spring debe instanciar por una parte el bean correspondiente al comando personalizado propiamente dicho, su configuracion sera similar a la siguiente:

<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>

Es importante detenerse en el valor <commandID> ya que el mismo será el identificador de referencia para la configuración del comando en el config.xml (ver Cyclos instances->commands->name)

Por otra parte tambien es necesario instanciar un bean para el manejador de errores del comando personalido, su configuración sera similar a la siguiente:

<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>

Configuración de Spring en el ejemplo

myInfoCommand.xml

En nuestro comando personalido de ejemplo hemos puesto este archivo en el paquete /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

El archivo /WebContent/WEB-INF/web.xml dentro del proyecto AIO debe ser modificado de forma tal que agregue la ruta al archivo XML de spring que instanciará los beans correspondientes al comando peronalizado.

El archivo web.xml debe agregar un texto similar al siguiente:

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


web.xml en el ejemplo

Hemos montado nuestro comando personalizado de ejemplo sobre un AIO Gateway (ver Gateway http), por tanto nuestro web.xml ha quedado de la siguiente forma:

<?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>

Anexos a config.xml

El archivo de configuracion del controller WebContent/WEB-INF/classes/config.xml fue modificado para soportar el comando customizado desarrollado. Se debe cosiderar dos pequeñas secciones:

  1. El atributo variant para el elemento cyclosInstance - Este cambio permite agregar las customizaciones necesarias del comando en el archivo Webcontent/WEB-INF/classes/i18n/cyclosInstance_en_US_MINFO sin necesidad de afectar el archivo de internacionalización de ingles incluido en la distribución original.
  2. Se agregó un elemento <command ...> - Esta es la configuración del comando peronalizado con su atributo, se observa el parámetro defaultCustomFieldName siendo su valor el campo personalizado por defecto que se utilizará.


config.xml en el ejemplo

<?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>

Anexos?? a cyclosInstance.properties

Los textos de mensajes (o componentes de textos de mensajes) que utilizará el comando se internacionalizan mediante el archivo cyclosInstance.properties.

Deben especificarse en este archivo (o archivos) todas las claves definidas en el método Command. getRequiredI18NKeys.

cyclosInstance.properties en el ejemplo

cyclosInstance_en_US_MINFO.properties

Para nuestro ejemplo se internacionalizan las siguientes claves:

  • Claves generales de comando
    • command.mInfo.regularExpression
  • Claves especificadas en MemberInfocommand.getRequiredI18NKeys()
    • command.minfo.response
    • command.mInfo.confirmation
    • command.mInfo.help.withConf
    • command.mInfo.help.withoutConf
  • Para ajuste de la ayuda general del sistema se personaliza la clave:
    • 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}

Anexos a error.properties

Los textos de errores (o componentes de textos de errores) que el comando notificara se internacionalizan mediante el archivo errors.properties.

Deben especificarse en este archivo (o archivos) todas las claves con codigos que pueden ser lanzadas durante el intento de ejecución de la operación personalizada.

errors.properties en el ejemplo

Por mayor detalle, ver las secciones Clase de manejo de errores y Clase de exepción personalizada.

errors_en_US_MINFO.properties

Además de los errores codificados específicos que se lanzan en el comando MINFO se ha personalizado también la clave error.COMMAND_NOT_FOUND para el caso de que se utilice la variante de configuración notifyNotRegisteredUser="true" (ver 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.

Configuraciones necesarias en Cyclos

Nuevo SmsType

A fin de que cyclos pueda clasificar los mensajes MT que el sistema envía (hacia un teléfono móvil) se deben realizar las siguientes configuraciones en Cyclos:

  • Agregar manualmente en la tabla sms_types de la base de datos de cyclos, los registros necesarios que reflejen los tipos de mensajes de texto que serán enviados por el sistema (en relacion al comando personalizado).
  • Configurar la internacionalizacion para los tipos creados en base de datos. En cyclos->adminMenu->Translation->Aplication definir los textos para las claves:
    • sms.type.MEMBER_INFO.description
    • sms.type.MEMBER_INFO_ERROR.description
  • Agregar registro en tabla cyclos3.sms_types
  • Definir claves en AdminMenu->Transaction->Applications->key=sms.types

SmsType en el ejemplo

Para soportar nuestro ejemplo se han ejecutados las siguientes instrucciones sobre la base de datos de Cyclos:

  • 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"

Por otra parte se ha accedido a cyclos->adminMenu->Translation->Aplication y se han definido las siguientes claves:

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