Comportamientos

RECU-0052 (Recurso Manual)

Descripción

Desarrollo de comportamientos personalizados

Como ejemplo de como desarrollar comportamientos personalizados vamos a definir una funcionalidad para que los usuarios puedan asignar una puntuación a un contenido y mostrar la media de todas las puntuaciones insertadas para este dentro del modelo personalizado que definimos en el apartado dedicado a las extensiones en el modelo de contenidos:

uri: "mi.nuevo.modelo"
    prefijo: "mi"

Si se quiere guardar las puntuaciones en Alfresco, una manera de hacerlo podría ser crear un tipo personalizado puntuacion (rating) que podrá estar relacionado con los contenidos a través de una asociación hija.

<type name="mi:rating">
     <title>Someco Rating</title>
     <parent>sys:base</parent>
     <properties>
           <property name="mi:puntuacion">                                        
                  <type>d:int</type>
                  <mandatory>true</mandatory>
           </property>
           <property name="mi:explicacion">
                   <type>d:text</type>
                   <mandatory>true</mandatory>
           </property>
      </properties>
</type>

Se creará también un aspecto puntuable (rateable). Este aspecto contendrá una propiedad para guardar la media de puntuaciones y ademas definirá la relación hijo para las puntuaciones relacionadas con un contenido. Usar un aspecto hace que podamos añadir funcionalidad de puntuación a cualquier pieza de contenido del repositorio.

<aspect name="mi:rateable">
    <title>Puntuable</title>
    <properties>
        <property name="mi:puntuacionMedia">
            <type>d:double</type>
            <mandatory>false</mandatory>
        </property>
    </properties>
    <associations>
        <childassociation name="mi:puntuaciones">
            <title>Puntuacion</title>
            <source>
                <mandatory>false</mandatory>
                <many>true</many>
            </source>
            <target>
                <class>mi:rating</class>
                <mandatory>false</mandatory>
                <many>true</many>
            </target>
        </childassociation>
    </associations>
</aspect>

Esto en cuanto al modelo de datos, el siguiente paso es decidir como se va a ejecutar la lógica que calcule la media después de que se reciba cada puntuación. Una forma de hacerlo podría ser crear una acción que sea llamada por una regla. Cada vez que una puntuación es añadida a una carpeta, la regla desencadenara la acción que actualiza las medias. Esta opción tiene el problema de que cada vez que se quiera usar las puntuaciones de usuario se necesita configurar la regla en el espacio. También se podría implementar como una acción programada que buscará todos los contenidos con el aspecto puntuable y calcule su media, pero se perdería la posibilidad de conocer las puntuaciones en tiempo real.

La opción perfecta será implementar un comportamiento personalizado. Escribiremos la lógica necesaria para el calculo de las medias y los asociaremos a los eventos adecuados del tipo de contenido puntuación, que serán la creación y la eliminación del mismos que es cuando la puntuación afectará a la media.

Implementación usando Java

Escribir el comportamiento personalizado

Se implementará el comportamiento personalizado como una clase Java que implementará las interfaces que corresponden a los eventos con los que queremos enlazar: NodeServicePolicies.OnDeleteNodePolicy y NodeServicePolicies.OnCreateNodePolicy.

La declarción de la clase quedaría como sigue:

public class Rating implements NodeServicePolicies.OnDeleteNodePolicy, NodeServicePolicies.OnCreateNodePolicy {

La clase tendrá dos dependencias que debe manejar Spring. La primera es el NodeService que será usado en la lógica del calculo de la media y el otro será el PolicyComponent el cual será usado para ligar los comportamientos con los eventos:

// Dependencias
  private NodeService nodeService;
  private PolicyComponent policyComponent;
  // Comportamientos
  private Behaviour onCreateNode;
  private Behaviour onDeleteNode;

Es momento de crear un método init que se encargue de hacer saber a Alfresco de que el comportamiento debe ser ligado a un evento(policy). El metodo init será llamado cuando Spring cargue el bean.

public void init() {
    // Create comportamientos
    this.onCreateNode = new JavaBehaviour(this,"onCreateNode",NotificationFrequency.TRANSACTION_COMMIT);
    this.onDeleteNode = new JavaBehaviour(this,"onDeleteNode",NotificationFrequency.TRANSACTION_COMMIT);
    // Ligar comportamientos a eventos
    this.policyComponent.bindClassBehaviour(Qname.createQName(NamespaceService.ALFRESCO_URI,"onCreateNode"),
                                Qname.createQName("mi.nuevo.modelo","rating"),this.onCreateNode);
    this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI,"onDeleteNode"),
                                        Qname.createQName("mi.nuevo.modelo","rating"),this.onDeleteNode);
}

Se puede decidir cuando se invocará el comportamiento especificando el valor apropiado para NotificationFrequency. Ademas de TRANSACTION_COMMIT las otras opciones son FIRST_EVENT y EVERY_EVENT.

Tras la creación de los comportamientos llamamos la método bindClassBehaviour para establecer la relación. En este caso estamos ligando el Qname del comportamiento al Qname del tipo "rating" y diciendo a Alfresco que llame a los comportamientos onCreateNode y onDeleteNode de nuestro bean.

Para asociaciones (bindAssociationBehaviour) y para propiedades (bindPropertyBehaviour) existen métodos adicionales para establecer esta relación que se tendrán que usar dependiendo del tipo de evento con el que estemos intentando realizar la ligadura.

El siguiente paso es escribir los metodos requeridos por las interfaces de los eventos. Con independencia de que se este creando una puntuación o se este destruyendo debemos realizar de nuevo el calculo de la media así que en ambos casos lo único que se hará sera llamar a un método (calcularMedia()) que implementará la lógica del calculo pasando la referencia al nodo como parámetro.

public void onCreateNode(ChildAssociationRef childAssocRef) {
        calcularMedia(childAssocRef);
    }
   
    public void onDeleteNode(ChildAssociationRef childAssocRef, boolean isNodeArchived) {
        calcularMedia(childAssocRef);
    }

El método calcularMedia() obtendrá la referencia del padre del nodo que ha provocado el evento, a este le solicitará la lista de sus hijos y realizará una iteración sobre ellos calculando la media. Por último guardará el valor calculado en la propiedad media del contenido.

public void calcularMedia(ChildAssociationRef childAssocRef) {
    // obtener el nodo padre
    NodeRef padreRef = childAssocRef.getParentRef();
    // comprobamos el padre para asegurarnos que tiene el aspecto correcto
    if (nodeService.hasAspect(padreRef,Qname.createQName("mi.nuevo.modelo","rateable"))) {
        // obtener los nodos hijos del padre
        List<ChildAssociationRef> hijos = nodeService.getChildAssocs(parentRef);
   
        // iterar a traves de los hijos para obtener el total
        Double media = 0d;
        int total = 0;
        for (ChildAssociationRef hijo : hijos) {
            int puntuacion = (Integer)nodeService.getProperty(
                        hijo.getChildRef(),
                        Qname.createQName("mi.nuevo.modelo","puntuacion"));
            total += puntuacion;
        }
       
        // calcular la media
        media = total / (hijos.size() / 1.0d);
       
        // guardar la media en el nodo padre
        nodeService.setProperty(padreRef,
                    QName.createQName("mi.nuevo.modelo","puntuacionMedia"),
                    media);
    }
    return;
}

Para terminar solo quedaría establecer los métodos getter y setter para el Node Services y el Policy Component.

Configuar el bean de Spring

El último paso es configurar la clase del comportamiento como un bean de Spring. La configuración de este bean puede ponerse en cualquier fichero de configuración como por ejemplo el fichero de registro del modelo. Si tenemos varios comportamientos personalizados se podrían poner todos en un fichero de configuración propio. Para el comportamiento que se esta desarrollando como ejemplo la configuración sería:

<bean id="ratingBehavior" class="es.intecna.sad.behavior.Rating" initmethod="init">
    <property name="nodeService">
        <ref bean="nodeService" />
    </property>
    <property name="policyComponent">
        <ref bean="policyComponent" />
    </property>
</bean>

Implementación usando JavaScript

Escribir el comportamiento personalizado

Realmente se van a escribir tres scripts. Los scripts onCreateRating.js y onDeleteRating.js, que se ligaran respectivamente con los eventos onCreateNode y onDeleteNode, y el script rating.js, que contendrá la lógica del calculo de la media y tendra que ser importada por los otros dos usando la etiqueta .

Para hacer el despliegue de esta implementación hay dos opciones, desplegarlos como parte de la aplicación web o cargar los scripts en Alfresco. El primero tiene el inconveniente de que si queremos hacer algún cambio tendremos que volver a desplegar. En el segundo caso los scripts serán accesibles para otros usuarios de Alfresco. En este ejemplo se asumirá que se usa la primera opción así que deberemos desplegar dentro del directorio scripts un fichero onCreateRating.js:

<import resource="classpath:alfresco/extension/scripts/rating.js">
   var hayFallo = false;
   // Comprobar el objeto comportamiento que debía haber sido pasado
   if (behaviour == null) {
    logger.log("El objeto comportamiento no ha sido establecido.");
    hayFallo = true;
   }
   // Comprobar el nombre del comportamiento
   if (behaviour.name == null && behaviour.name != "onCreateNode") {
    logger.log("El objeto comportamiento no ha sido establecido adecuadamente.");
    hayFallo = true;
   } else {
    logger.log("Nombre del comportamiento: " + behaviour.name);
   }
   // Comprobar los argumentos
   if (behaviour.args == null) {
    logger.log("Los argumentos no han sido establecidos.");
    hayFallo = true;
   } else {
    if (behaviour.args.length == 1) {
        var assocHijo = behaviour.args[0];
        logger.log("Llamando al calculo de media");
        calcularMedia(assocHijo);
    } else {
        logger.log("El numero de argumentos es incorrecto.");
        hayFallo = true;
    }
   }

El código para el onDeleteRating.js es equivalente al anterior con la excepción del nombre del comportamiento y del número de argumentos esperados:

<import resource="classpath:alfresco/extension/scripts/rating.js">
   var hayFallo = false;
   // Comprobar el objeto comportamiento que debía haber sido pasado
   if (behaviour == null) {
    logger.log("El objeto comportamiento no ha sido establecido.");
    hayFallo = true;
   }
   // Comprobar el nombre del comportamiento
   if (behaviour.name == null && behaviour.name != "onDeleteNode") {
    logger.log("El objeto comportamiento no ha sido establecido adecuadamente.");
    hayFallo = true;
   } else {
    logger.log("Nombre del comportamiento: " + behaviour.name);
   }
   // Comprobar los argumentos
   if (behaviour.args == null) {
    logger.log("Los argumentos no han sido establecidos.");
    hayFallo = true;
   } else {
    if (behaviour.args.length == 2) {
        var assocHijo = behaviour.args[0];
        logger.log("Llamando al calculo de media");
        calcularMedia(assocHijo);
    } else {
        logger.log("El numero de argumentos es incorrecto.");
        hayFallo = true;
    }
   }

Para implementar el tercer script se usará la misma lógica que se uso en la implementación con Java pero siguiendo la sintaxis de la API de JavaScripts de Alfresco. Se creará el fichero rating.js con la siguiente estructura:

function calcularMedia(childAssocRef) {
    var padreRef = childAssocRef.parent;
    //ccomprobar el padré para asegurarnos de que tieneel aspecto correcto
    if (padreRef.hasAspect("{mi.nuevo.modelo}rateable")) {
        // obtener los nodos hijos del padre e iterar a traves de los hijos para obtener el total y la media
        var hijos = padreRef.children;
        var media = 0.0;
        var total = 0;
        if (hijos != null && hijos.length > 0) {
            for (i in hijos) {
                var hijo = hijos[i];
                var puntuacion = hijo.properties["{mi.nuevo.modelo}puntuacion"];
                total += puntuacion;
            }
            media = total / hijos.length;
        }
        // guardar la media en el nodo padre
        padreRef.properties["{mi.nuevo.modelo}puntuacionMedia"] = media;
        padreRef.save();
    }
    return;
}

Configurar el bean de Spring

En la implementación en Java se usaba el método init de la clase para realizar la ligadura de de los eventos con los comportamientos a traves de llamadas a los metodos de PolicyComponent. En la aproximación mediante JavaScript una vez escritos los scripts se necesita realizar una configuración de Spring para asociar estos a los eventos adecuados onCreateNode y onDeleteNode.

Así en un fichero de configuración de la aplicación introduciremos la siguiente configuración:

<bean id="onCreateRatingNode" 
    class="org.alfresco.repo.policy.registration.ClassPolicyRegistration"
    parent="policyRegistration">
    <property name="policyName">
        <value>{http://www.alfresco.org}onCreateNode</value>
    </property>
    <property name="className">
        <value>{mi.nuevo.modelo}rating</value>
    </property>
    <property name="behaviour">
        <bean class="org.alfresco.repo.jscript.ScriptBehaviour" parent="scriptBehaviour">
            <property name="location">
                <bean class="org.alfresco.repo.jscript.ClasspathScriptLocation">
                       <constructorarg>
                    <value>alfresco/extension/scripts/onCreateRating.js</value>
                       </constructorarg>
                </bean>
            </property>
        </bean>
    </property>
</bean>
<bean id="onDeleteRatingNode"
    class="org.alfresco.repo.policy.registration.ClassPolicyRegistration"
    parent="policyRegistration">
    <property name="policyName">
        <value>{http://www.alfresco.org}onDeleteNode</value>
    </property>
    <property name="className">
        <value>{mi.nuevo.modelo}rating</value>
    </property>
    <property name="behaviour">
        <bean class="org.alfresco.repo.jscript.ScriptBehaviour" parent="scriptBehaviour">
            <property name="location">
                <bean class="org.alfresco.repo.jscript.ClasspathScriptLocation">
                       <constructor-arg>
                    <value>alfresco/extension/scripts/onDeleteRating.js</value>
                       </constructor-arg>
                </bean>
            </property>
        </bean>
    </property>
</bean>