Adaptando plugins Liferay

En el desarrollo web con Liferay, es bastante común la personalización de componentes (JSP’s, servicios de Portal, traducciones, etc.) del core de Liferay mediante el uso de hooks pero, ¿que ocurre cuando el componente no se encuentra en el core de Liferay, sino que se trata de un plugin?. En estos casos, no es factible la implementación de hooks, pero Liferay proporciona mecanismos alternativos (documentados en el Liferay Portal Developer’s Guide).

En este artículo, describimos el proceso de personalización de plugins, basándonos en un caso práctico, en el que personalizaremos el plugin Calendar.

Obtener el código fuente de los plugins

El primer paso es descargar el código fuente de los plugins (teniendo en cuenta la versión de nuestra instalación de Liferay). Para ello, clonaremos el repositorio github en el que se encuentran los plugins.

git clone https://github.com/liferay/liferay-plugins.git

En nuestro caso, implementaremos el ejemplo para Liferay 6.2-ga3, por lo que accederemos al tag 6.2.x disponible en el repositorio que hemos clonado:

git checkout origin/6.2.x

El código fuente del plugin que vamos a personalizar se encuentra en el directorio $LIFERAY_PLUGINS/portlets/calendar-portlet.

Creación del proyecto

En el IDE, creamos un plugin project en el que seleccionaremos el mismo gestor de proyectos (Build type) con el que ha sido implementado el plugin original, que en el caso concreto del calendario es ANT.

Una vez creado el proyecto, se eliminará el contenido incluido en el directorio docroot. En este momento, el proyecto debe contener los archivos de configuración generados por eclipse (esto es, .settings, .classpath, . project) y un archivo build.xml vacío, como el que se muestra a continuación:

< ?xml version="1.0">
< !DOCTYPE project>


	

postExtendPlugin1-241x300

Para poder realizar la personalización del plugin, es preciso empaquetar un war a partir del código original. Para crear el empaquetado, podemos optar por lanzar ant directamente en el directorio donde se encuentran los fuentes o, desplegar directamente el plugin original sobre el IDE.

Un vez esté disponible el war, lo copiamos en el directorio raíz del proyecto, e incluimos en el archivo build.xml, una referencia al archivo que acabamos de copiar, mediante la propiedad original.war.file:

< ?xml version="1.0">
< !DOCTYPE project>;


		
		

Para vincular la nueva versión del plugin con la versión original y, copiar los archivos necesarios en el proyecto, ejecutaremos ant seleccionando el target «merge». Para lanzar esta ejecución, marcamos el nombre del proyecto y desplegamos el menú contextual (generalmente haciendo click con el botón derecho) y, seleccionamos la opción «Liferay -> SDK -> merge». Como resultado de esta operación, se creará una carrera tmp con el código fuente del plugin original.

Si el plugin original cuenta con algún servicio, será preciso crear una copia de la clase «com.liferay.calendar.service.ClpSerializer» en el nuevo proyecto y, modificar el valor de la variable «_servletContextName» por el nombre del nuevo proyecto:

if (Validator.isNull(_servletContextName)) {
     //_servletContextName = "calendar-portlet";
     _servletContextName = "custom-calendar-portlet";
}

Personalización del Plugin

Una vez preparado el proyecto, podemos realizar los ajustes necesarios para adaptarlo a nuestras necesidades. Como hemos mencionado anteriormente, realizaremos una pequeña modificación el plugin, con el objetivo de mostrar todos los pasos del proceso. En el siguiente ejemplo, modificaremos la vista mensual del calendario para marcar los domingos con un color distinto.

Puesto que el calendario es generado mediante AlloyUI Scheduler (y no vamos a modificar la distribución de este framework incluida en Liferay), aplicaremos los cambios mediante el uso de jQuery. Para ello, crearemos una copia de view_calendar.jsp en el docroot del nuevo proyecto (es decir, realizamos los mismo pasos que en el caso de sobreescritura de una jsp en un hook).

En este caso, para conseguir nuestro objetivo, bastará con incluir el siguiente código al final de view_calendar.jsp :

A partir de este momento, ya es posible empaquetar y realizar el despliegue del componente en el servidor Liferay.

Como se puede ver, la sobreescritura de plugins Liferay requiere una preparación previa del proyecto y, una vez realizada, el proceso es muy similar al seguido en el desarrollo de hooks.

Sistemas recomendadores con Apache Mahout

Apache Mahout

En este post incluimos una breve introducción a una de las librerías (software libre) más utilizadas en la actualidad, que proporcionan implementación de algoritmos machine learning: Apache Mahout.

Mahout incluye algoritmos de clasificación, sistemas de recomendación, clustering (agrupación de vectores en base a ciertos criterios).

Desde nuestro punto de vista, es una buena solución para realizar filtrado colaborativo, por ejemplo para la implementar recomendadores pero, el resto de funcionalidades, principalmente las de clasificación y agrupación están muy enfocadas a clasificación/análisis de textos y, se echa en falta la implementación de algoritmos necesarios para soluciones de análisis productivo.

Recomendación basada en filtrado colaborativo

Los algoritmos de filtrado colaborativo, se basan en localizar puntos de similitud entre los elementos, generando conjuntos de elementos «similares». La recomendación se realiza a partir de puntos conocidos en los elementos del conjunto.

En la imagen de la derecha, podemos ver un modelo de datos sencillo, en el que se incluyen las valoraciones que realizan los usuarios sobre ciertos elementos (que podrían ser contenidos, libros, hoteles …).

Un paso importante a la hora de generar los conjuntos de elementos similares, es definir el umbral para considerar que una valoración es similar. Por ejemplo, en el gráfico vemos que el usuario 3 y el 1 han valorado el ítem 4, pero la opinión que tienen del mismo es muy distinta. Para salvar esta situación, definimos por ejemplo un umbral de 0.3, motivo por el cual, se han seleccionado los usuarios que han valorado el ítem 1 con un valor 5.1±03.

Para hacer una pequeña demostración de uso de Apache Mahout, implementaremos ejemplo de recomendación colaborativa (existen otras opciones basadas en contenidos, en utilidad, demográfica o basadas en conocimiento), por ser una de las opciones más fáciles para iniciarse en este tipo de sistemas. En concreto, utilizaremos el algoritmo de lo K vecinos más cercanos (k-NN Nearest Neighbour), que encaja perfectamente en el proceso descrito hasta el momento.

postmahout_imggrupos

Supongamos que queremos incorporar un recomendador para una página de reservas hoteleras, que tomará como parámetro de entrada el nombre de una ciudad (introducida por el usuario en el buscador) y que se basará en las valoraciones de hoteles realizadas por otros usuarios.

Para simplificar el ejemplo, no tendremos en cuenta muchos de los parámetros y fuentes de datos disponibles en este tipo de webs, como por ejemplo, búsquedas y compras anteriores realizadas por el usuario, así como muchos de los parámetros que se ofrecen en los buscadores (nº de habitaciones, tipo de servicios, etc.). En una implementación real, habría que diseñar soluciones para poder realizar recomendaciones de calidad a usuarios nuevos, que han interactuado poco con el sistema y, para los que no se cuenta con información suficiente para generar el conjunto de elementos «similares». Ocurre lo mismo con los nuevos hoteles que sean incorporados (¿cómo recomendar un hotel que nadie ha valorado?).

Para facilitar la prueba, utilizaremos un conjunto de datos bastante reducido. En el siguiente gráfico se muestra el flujo de trabajo esperado con el ejemplo:

usuarios similares

Para la implementación de este ejemplo, crearemos un objeto scala que obtendrá la información de valoraciones de los usuarios y, la información detallada de los hoteles.

 
//Identificador del usuario para el que se solicitan las recomendaciones
val userId: Int = args(1).toInt
//Número de elementos solicitados
val numeroRecomendaciones: Int = args(2).toInt
//Umbral
val threshold: Float = args(3).toFloat
//Localización del hotel
val localizacion: String = args(4)
 
lazy val conf = new SparkConf()

val context = new SparkContext(conf)
	.setMaster("local")
	.setAppName("Recomendador de Hoteles")
	.set("spark.executor.memory", "1g")
	.setJars(Array("hdfs://localhost:8020/apps/lib/spark-assembly-1.1.0-hadoop2.4.0.jar"))

//Fichero con las valoraciones de los usuarios
val modelo: DataModel = new FileDataModel(new File("/tmp/valoraciones.csv"))

/*
 * Al tratarse de información cuantitativa, utilizamos el coeficiente de correlación de Pearson
 * como medida de similaridad.
 */			
val similarity: UserSimilarity = new CachingUserSimilarity(new PearsonCorrelationSimilarity(modelo), modelo)

/*
 * Obtenemos el conjunto de usuarios que ha valorado algunos de los hoteles valorados por el usuario
 * para el que se han solicitado las recomendaciones y, cuyas valoraciones están dentro del umbral
 * especificado.
 */			
val usuariosSimilares: UserNeighborhood = new ThresholdUserNeighborhood(threshold, similarity, modelo)

val recomendador: UserBasedRecommender = new GenericUserBasedRecommender(modelo, usuariosSimilares, similarity)
val recommendaciones: List[RecommendedItem] = recomendador.recommend(userId, numeroRecomendaciones)

/*
 * Información de cada hotel (hotelId, nombre y ubicación) y realizamos el
 * filtro para obtener aquellos que coinciden con la población solicitada
 * por el usuario.
 * 
 * Convertimos cada línea del fichero (un CSV) de hoteles en un 
 * mapa: (K=hotelID,V=(nombre,ubicación))
 */
val hoteles = context.textFile("hdfs://localhost:8020/samples/hoteles")
        .filter(line => line.contains(localizacion))
        .map(line => line.split(","))
	.map(item => (item(0).toInt, (item(1), item(2))))

if (recommendaciones.isEmpty()) {
    println("No existen recomendaciones");
} else {
    for (recomendacion < - recommendaciones) { 	hoteles.filter(_._1 == recomendacion.getItemID()) 	.foreach(hotel => println("Hotel recomendado: " + hotel._2._1 + ", ubicado en: " + hotel._2._2))
    }
}

Ejecutamos el código, solicitando 1 hotel recomendado para el usuario 1, con un umbral de 0.1 y, ubicado en Sevilla:

spark-submit --name "Recomendador Mahout" --class com.metadology.bigdata.core.Launcher ./bigdataSamples_2.10-1.0.0.jar ManoutRec 1 1 0.1 SEVILLA
.
.
.
INFO storage.MemoryStore: ensureFreeSpace(3416) called with curMem=166036, maxMem=278302556
INFO storage.MemoryStore: Block broadcast_1 stored as values in memory (estimated size 3.3 KB, free 265.2 MB)
INFO scheduler.DAGScheduler: Submitting 1 missing tasks from Stage 0 (FilteredRDD[5] at filter at ManoutRec.scala:85)
INFO scheduler.TaskSchedulerImpl: Adding task set 0.0 with 1 tasks
INFO scheduler.TaskSetManager: Starting task 0.0 in stage 0.0 (TID 0, localhost, ANY, 1270 bytes)
INFO executor.Executor: Running task 0.0 in stage 0.0 (TID 0)
INFO executor.Executor: Fetching hdfs://localhost:8020/apps/lib/spark-assembly-1.1.0-hadoop2.4.0.jar with timestamp 1412261626822
INFO executor.Executor: Adding file:/var/folders/ck/qgwmfj951mbdbk22kn8q4zpm0000gn/T/spark-161c1d3d-c140-45b2-b838-73b25fa15e62/sp
INFO rdd.HadoopRDD: Input split: hdfs://localhost:8020/samples/hoteles:0+240
Hotel recomendado: "Sevilla La Posada", ubicado en: "SEVILLA"
INFO scheduler.TaskSchedulerImpl: Removed TaskSet 1.0, whose tasks have all completed, from pool 

Como hemos comentado anteriormente, este ejemplo no pretende ser más que una muestra de las funcionalidades de Mahout. Por lo general, salvo que en procesamiento por lotes, los datos estarían almacenados en bases de datos NoSQL o serían obtenidos desde un stream y, requerirían otro tipo de soluciones de procesamiento distribuido.

Uso de cookies

Las cookies son importantes para el correcto funcionamiento de este sitio. Para mejorar su experiencia, usamos cookies tanto propias como de terceros con la finalidad de permitir al usuario su navegación en esta página web y el uso de sus servicios así como para recordar las preferencias de usuario y recopilar estadísticas con el fin de optimizar la funcionalidad del sitio. Haga clic en Aceptar y continuar para aceptar las cookies e ir directamente al sitio. política de cookies

ACEPTAR
Aviso de cookies