Minimizando conflictos de aui en nuestros Themes de Liferay

Fecha de publicación 28/09/16 18:03

Cuando queremos crear un Theme en Liferay en muchas ocasiones nos podemos encontrar con que los estilos aui entran en conflicto con los estilos que nosotros queremos aplicar. Un caso típico es el choque producido entre el bootstrap que utiliza aui y bootstrap 3.

¿Cómo podemos mantener los estilos Liferay para el propio Liferay sin que afecte a los estilos de nuestra web?

Pues no existe una fórmula mágica, pero podemos mitigar el impacto y que al menos desde fuera no se aprecien los conflictos.

Para ello haremos que los estilos aui no se apliquen a partir de la class "aui" (que se encuentra al inicio, en el <html>) sino a un nivel más bajo.

Esto lo podemos lograr mediante el Theme, modificando el fichero "aui.css" y añadiéndolo a "_diffs/css/":

 

_diffs\css\aui.css:

.aui .lfr-styles {
    ...
}

 

Cambiando .aui {…} por .aui .lfr-styles {…} provocamos que los estilos aui no se apliquen hasta que se encuentre algún elemento con la class lfr-styles, de este modo podemos controlar qué bloques queremos que usen estos estilos.

Como he dicho anteriormente esto sirve para mitigar los conflictos, pero no es perfecto y en el proceso podemos perder estilos genéricos (aui). Por ejemplo, si tenemos un “font-family” que antes aplicaba a “.aui body”, ahora aplicaría a “.aui .lfr-styles body”, si donde colocamos ese “.lfr-styles” está por debajo del body (que como veremos más adelante será lo más normal) ese estilo no se aplicaría.

Con este sistema algún estilo aui propio de Liferay podría verse afectado pero nuestra web y nuestros estilos custom estarían libres de conflictos.

Ahora, para poder seguir utilizando aui en lo que a Liferay se refiere (los portlets propios) podemos usar el mismo Theme y el Hook.

Desde el Theme, si queremos ahorrarnos trabajo del Hook, podemos hacer que aplique también los estilos directamente a los popup generados por Liferay donde se incluiría: el menú de configuración de los portlet, el seleccionar web contents, exportar/importar un portlet, seleccionar estructura o plantilla, etc.

Esto se puede conseguir añadiendo a la línea anterior otra class que está presente en este tipo de contenidos:

 

_diffs\css\aui.css:

.aui .lfr-styles, .aui .portal-popup{
    ...
}

 

Por otro lado, desde el Hook podemos incluir la class antes mencionada a los portlets propios de Liferay que queremos que continúen usando la clase aui. A continuación listamos los más típicos, y que archivo necesitaríamos modificar:

  • Menu Liferay (Dockbar):  \html\portlet\dockbar\view.jsp
  • Menu Liferay: add, edit, preview (Dockbar): \html\js\liferay\dockbar.js
  • Add WebContent, Edit WebContent (Journal): \html\portlet\journal\edit_article.jsp
  • Look and Feel (Portlet CSS): \html\portlet\portlet_css\view.jsp

El proceso para los jsp suele ser el mismo, usaremos uno de ejemplo:

 

\html\portlet\dockbar\view.jsp:

<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util" %>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<liferay-theme:defineObjects />

<liferay-util:buffer var="html">
       <liferay-util:include page="/html/portlet/dockbar/view.portal.jsp" useCustomPage="false"  />
</liferay-util:buffer>

<div class="lfr-styles">
       <%= html %>
</div>

 

De esta forma incluimos un div por encima del contenido que queremos que use aui con nuestra nueva class.

En el caso del dockbar.js lo usamos por simplificar, ya que añadiendo nuestra class a tres líneas de código nos ahorramos hookear tres jsp.

 

\html\js\liferay\dockbar.js:

var TPL_ADD_CONTENT = '<div class="lfr-add-panel lfr-admin-panel lfr-styles" id="{0}" />';
var TPL_EDIT_LAYOUT_PANEL = '<div class="lfr-admin-panel lfr-edit-layout-panel lfr-styles" id="{0}" />';
var TPL_PREVIEW_PANEL = '<div class="lfr-admin-panel lfr-device-preview-panel lfr-styles" id="{0}" />';

 

Se pueden buscar atajos o simplificaciones, pero la cuestión es lograr incluir nuestra clase para que aui afecte sólo al contenido que nosotros queremos que afecte y no provoque conflictos en lo que ve el usuario, que al fin y al cabo, es para el que está pensado nuestro diseño.

Espero que os sirva de ayuda. Como he comentado al principio no es la solución perfecta, pero cuando tienes que forzar 25 estilos para que un simple botón se vea como tú quieres (estilos por defecto, estilos hover, active , focus, responsive, shadows, …) el reducir estos conflictos te pueden ahorrar mucho tiempo y dolores de cabeza.

Saludos.

Liferay Dynamic Queries

Fecha de publicación 22/07/16 15:03

Liferay define una serie de procedimientos (API) para obtener datos de la base de datos. Liferay Dynamic es una de ellos. Esta forma puede añadir complejidad a las búsquedas que hagamos en la base de datos respecto a las finders que podemos encontrar en los servicios locales de las entidades.

Unos casos de uso (especificados por el equipo de Liferay) pueden ser los siguientes:

  • Hacer agregados como MAX, MIN y AVG entre otros.
  • Necesitas ciertas columnas en vez de objetos tipificados.
  • Añadir complejidad con OR/AND.
  • Otros.

Con un ejemplo se ve mejor: disponemos de una entidad Proyecto creada con el Service Builder de Liferay. Tenemos un campo fecha de creación (creation_date) y queremos encontrar los proyectos creados en un mes especificado. Necesitaremos una query que haga la siguiente acción:

  SELECT *
  FROM Proyecto
  WHERE [date_ini] <= creation_date
    AND creation_date <= [date_fin]

Al no poder realizarla con los finders de la entidad, tenemos que buscar una alternativa. Como las custom sql o las Dynamic Query. Aqui tenéis un ejemplo:

  /*
  * Creamos un objeto dynamicQuery para la clase Proyecto (que hemos generado)
  * Si la Dynamic Query fuera sobre una clase propia de Liferay (User por ejemplo) se debería
  * añadir el campo PortalClassLoaderUtil.getClassLoader()
  */

  DynamicQuery dynamicQuery = DynamicQueryFactoryUtil.forClass(Proyecto.class);

 

Ahora podemos realizar 2 acciones:

  • Añadir criterios de búsqueda.
  • Añadir criterios de ordenación.

Criterios de búsqueda

Aqui tenemos que crear objetos con la Factoria RestrictionsFactoryUtil, que nos devuelve objetos de tipo Criterion. Esto nos sirve para construir lógicas como las siguientes:

  • ilike eq (igual)
  • gt (más grande estricto)
  • ge (más grande o igual)
  • métodos AND y OR
  • etc

Criterios de ordenación

Aqui usaremos la factoria OrderFactoryUtil, que nos devuelve objetos de tipo Order. Aqui define dos métodos:

  • Order asc(String nombrePropiedad)
  • Order desc(String nombrePropiedad)

Una vez hechos los Criterios y las Ordenaciones se añaden al objeto dynamic query así de fàcil:

  • dynamicQuery.add(criterio)
  • dynamicQuery.addOrder(order)

Finalmente, solo nos faltará ejecutar la dynamic query, pero aqui volvemos al ejemplo con todo lo aprendido:

  Criterion criterion1 = RestrictionsFactoryUtil.ge("date_ini", searchdate);
  Criterion criterion2 = RestrictionsFactoryUtil.le("date_fin", searchdate);
  Criterion criterion = RestrictionsFactoryUtil.and(criterion1, criterion2);
  dynamicQuery.add(criterion);
  List<proyecto> list = ProyectoLocalServiceUtil.dynamicQuery(dynamicQuery);

¡Espero que os sirva de ayuda!

Controlar meta tags en Liferay de forma dinámica

Fecha de publicación 15/07/16 13:37

Liferay permite añadir meta tags desde el apartado Site Pages > Page > SEO del panel de control, pero estos meta tags se aplican a la página sin importar lo que contenga y sus opciones son bastante limitadas.

Esta funcionalidad en según qué casos no es suficiente.

Vamos a poner un ejemplo para entender mejor lo que queremos hacer.

Tenemos un portlet que genera las fichas de todas las personas que pertenecen o han pertenecido a una empresa y Google, por defecto, nos las va a indexar todas.

Lo que queremos es mantener la ficha de todas las personas en nuestra web pero, por otro lado, no queremos que Google indexe las de las personas que se den de baja. Por tanto, lo que necesitamos es añadir (solo a esas fichas) el meta tag que le indica al robot que no indexe ese contenido.

Añadiendo el siguiente código dentro del jsp que genera la ficha de la persona podemos condicionar el meta tag para especificar cómo queremos que trate Google a esa página.

 

if (persona.dadaDeBaja()) {
    Element metaElement = portletResponse.createElement("meta");

    metaElement.setAttribute("name", "robots");
    metaElement.setAttribute("content", "noindex");

    portletResponse.addProperty(MimeResponse.MARKUP_HEAD_ELEMENT, metaElement);
}

 

Con esto, cuando la persona que queramos mostrar haya sido dada de baja, mostrará la ficha normalmente, pero dentro del <head> de la página habrá un nuevo meta tag que indicará a los buscadores que no se quiere indexar ese contenido.

 

<head>
    …
    <meta content="noindex" name="robots">
    …
</head>

 

Con este sistema se pueden manejar los elementos de la página al gusto. Nosotros hemos puesto tan sólo un ejemplo, pero hay muchas más posibilidades.

Aquí se puede encontrar más información sobre las distintas opciones que puede interpretar el robot de Google.

Espero que os sirva. Un saludo.

Liferay Security Manager con PACL

Fecha de publicación 20/06/16 13:46

¡Muy buenas!

 

Hace unas semanas tuvimos la necesidad de añadir una lista de control de acceso al portal o Portal Access Control List (PACL) a uno de nuestros portlets. La razón es que algunos portales Liferay pueden tener activado el Security Manager. Básicamente lo que hace es controlar todas las acciones que hacen los portlets i que no ejecuten código malicioso. El Security Manager distinguirá un código malicioso (no tiene porqué ser malicioso per se) si éste hace un acceso no registrado en la lista PACL.

 

 

Una vez sabido ésto, vamos a explicar como generar el listado para tu portlet. Primero deberemos activar tu portal en desarrollo en modo security, se ha de añadir la siguiente línea en el portal-ext.properties i reiniciar el tomcat:

 

portal.security.manager.strategy=liferay

 

Modificamos el archivo liferay-plugin-package.properties del portlet y añadimos la siguiente línea:

 

security-manager-enabled=generate

 

Compilamos y hacemos deploy en el portal, entonces podremos ver cómo se genera una carpeta a la altura del tomcat y contendrá los accesos que haga nuestro portlet:

 

[liferay.home]/pacl-policy/[servletContextName].policy

 

Para éste punto es importante, ejecutar todas las funcionalidades que tenga tu portlet para registrar todos los accesos que haga. Los accesos pueden ser des de llamadas a Local Services de Liferay, llamadas a apis externas o accesos a carpetas en el mismo servidor.

 

Una vez hecho esto, tendremos que copiar el contenido generado en el archivo .policy y añadirlo a el liferay-plugin-package.properties.

 

Para comprobar que hemos hecho bien el procedimiento, modificamos la propiedad security-manager-enabled a true en el liferay-plugin-package.properties:

 

security-manager-enabled=true

    

#Reglas generadas PACL

security-manager-get-bean-property[portal]=\
           com.liferay.portal.kernel.exception.SystemException,\
           com.liferay.portal.kernel.language.LanguageUtil,\
           com.liferay.portal.kernel.util.PrefsPropsUtil,\
           jodd.util.ReflectUtil

    security-manager-hook-language-properties-locales=\
           ca,\
           en,\
           es

#etc

 

Y, ejecutamos las funcionalidades a falta para ver si el Security Manager encuentra algo no especificado en el PACL.

 

Con esto tenemos que para tener un control de seguridad podemos activarlo en nuestro portal (recomendable si tienes portlets de terceros) y gracias a la lista PACL podremos comprobar a qué accede el portlet.


¡Un saludo!

Compartir la Capa de Servicios

Fecha de publicación 27/05/16 12:17

¡Buenas!

 

Hoy os explico diferentes maneras de compartir la capa de servicios que hayáis creado con el service builder entre diferentes portlets.

 

Primero, tenemos que contar que tenéis un portlet base base-portlet, el cuál habéis creado entidades y servicios con el service builder. Si quisieramos que un segundo portlet y un hook, por ejemplo, accedieran a estos servicios, replicarlos para cada plugin sería ineficiente y causante d’errores de inconsistencia en el futuro.

 

La mejor opción siempre es que todos los plugin accedan a la misma capa:


Untitled.png

 

 

Propondremos dos opciones:

 
  • Crear los diferentes portlets y hooks en un mismo plugin.

  • Compartir la capa de servicios a nivel de tomcat.

 

Crear los diferentes portlets y hooks en un mismo plugin

 

La idea es crear diferentes portlets en un mismo plugin de Liferay y tener unos servicios creados con el Service Builder. De esta manera, estos servicios seran disponibles y accesibles por cada portlet o hook dentro del plugin.

 

Compartir la capa de servicios a nivel de tomcat

 

Si la propuesta anterior no fuera posible, también podriamos exportar la capa de servicios mediante un archivo jar. Cuando ejecutas el service builder, este archivo se genera en: “your-portlet/docroot/WEB-INF/lib/your-portlet-service.jar”.

 

Este jar nos da acceso a los local service que hayas programado. Para que todos los plugin de tu instancia puedan usarlo este jar lo tienes que añadir en este directorio del servidor: “<<Liferay-tomcat>>tomcat-x.x.x/lib/ext”.

 

Recuerda, para cualquier actualización de los servicios, se tendrá que reemplazar el jar y, además, reiniciar el tomcat.


¡Espero que os haya ayudado!

Bug de regresión - Liferay 6.2 CE GA5

Fecha de publicación 20/05/16 11:24

Trabajando con la versión 6.2 CE GA5 hemos detectado un bug de regresión relacionado con la gestión de los idiomas, concretamente la incidencia de Liferay LPS-60313. Este bug solo afecta si se trabaja con entornos multi-idioma. En esta entrada de blog vamos a explicar los pasos que hemos seguido para resolver este problema.

Primero de todo, nos aseguramos que la incidencia detectada no esté relacionada con nuestros propios desarrollos. Se vio claramente ya que apareció al hacer el upgrade a la versión 6.2 CE GA5, aún así se revisaron los desarrollos relacionados para estar del todo seguros.

En este punto se empieza a investigar en los foros de Liferay y en el resto de sites relacionados; con esta incidencia en concreto se llegó a preguntar directamente en el foro Liferay y como casi siempre se nos orientó sobre el origen del bug.

Aquí es donde se empieza a aclarar el problema, los comentarios recibidos te permiten hacer un estudio más exhaustivo y llegamos a la incidencia reportada en el sistema de incidencias de Liferay.

Liferay utiliza JIRA para gestionar sus incidencias, con un simple vistazo tienes toda la información necesaria, con la posibilidad de profundizar en los detalles. 

El siguiente paso lógico, es buscar si existe solución por parte de Liferay para evitar realizar trabajos innecesarios. Pero revisando la incidencia JIRA se ve claramente que solo hay solución para las versiones Enterprise (6.2.X EE). En el detalle de la incidencia se explica la causa del problema, de todas formas al no existir fix para la versión CE nos obliga a crear un plugin EXT para su resolución.

Así que el último paso es la creación de un plugin EXT que modificará el CORE de Liferay para cambiar el código que provoca este problema en cuestión. Este EXT en si no es mucho, concretamente solo se modifica una línea del CORE.

Como vemos, la solución (a nivel de programación) ha sido muy sencilla, pero tiene graves implicaciones para la continuidad de la versión que se está utilizando actualmente (6.2.4 CE GA5):

  • La inclusión de un plugin EXT en el sistema. Opción no recomendada por Liferay pero sin fix no queda otra opción.
  • Tener en cuenta la existencia de este nuevo EXT en el caso de hacer futuros upgrade de la instancia de Liferay. Si es necesario su continuación en el caso que no se haya solucionado el bug o si por el contrario se tiene que eliminar.

Espero que os haya servido de ayuda.

Modificar el tiempo de sesión de Liferay

Fecha de publicación 29/04/16 9:30

La duración de la sesión de Liferay por defecto es de 30 minutos. Hoy os mostraremos como modificar este valor.

Podemos encontrar este valor dentro del portal.properties de Liferay.

  • portal.properties (Liferay src): /portal/portal-impl/classes
  • portal.properties (Server): /webapps/ROOT/WEB-INF/lib/portal-impl.jar
    #
    # Specify the number of minutes before a session expires. This value is
    # always overridden by the value set in web.xml.
    #
    session.timeout=30

 

Al estar como propiedad de portal podemos sobrescribirla des del portal-ext.properties o el portal-setup-wizard.properties

  • portal-ext.properties: /webapps/ROOT/WEB-INF/classes/
  • portal-setup-wizard.properties: /liferay-portal-x.x-xx-xxx/
    session.timeout=240

 

Tal y como indican en el propio portal.properties, este cambio no es suficiente, ya que prevalece el valor que se encuentra en el archivo web.xml

  • web.xml (Liferay src): /portal-web/docroot/WEB-INF/
  • web.xml (Server): /webapps/ROOT/WEB-INF/
<session-config>
    <session-timeout>30</session-timeout>
</session-config>

 

La forma recomendada de modificar este tipo de propiedades es mediante un Ext Plugin.

Al crear un nuevo Ext Plugin, se nos generarán diversos directorios y archivos. El que queremos modificar nosotros ya se encuentra en (/docroot/WEB-INF/ext-web/docroot/WEB-INF/web.xml ). Solamente es necesario editarlo y añadir únicamente el tag que queremos modificar. No es necesario como en otros casos poner todo el contenido, ya se encargará de hacer el merge con el archivo original.

<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <session-config></session-config>
</web-app>

En nuestro caso eliminamos el tag <session-timeout> para que tome como válido el valor que pongamos en el portal-ext.properties, siendo más sencillo controlar dicho valor.

Una vez aplicado el EXT podremos observar que el archivo web.xml de nuestro servidor que antes contenía <session-timeot> dentro de <session-config>, ahora ya no tiene esa propiedad. Desde este momento ya podremos modificar el tiempo de sesión desde las propiedades del portal.

Recordad que para aplicar un EXT es necesario reiniciar después de hacer el deploy, y si ya existía un EXT anterior, según la situación, puede ser necesario limpiar Liferay antes de aplicar de nuevo un Ext Plugin (más información aquí).

Un saludo.

Filtros de búsqueda en sesión

Fecha de publicación 11/04/16 11:28

Todos nos hemos encontrado alguna vez, al programar portlets en Liferay, con la necesidad de presentar al usuario un listado de elementos y un pequeño formulario con una serie de campos para filtrar los resultados de dicho listado. Si estamos usando el search container de Liferay, nos encontraremos con que tenemos que guardar los valores de los filtros si queremos mantenerlos al clicar en algún botón del search container. Por ejemplo, si tenemos un listado de casas, y filtramos por año de construcción, si el resultado tiene varias páginas de registros, nos encontraremos que al clicar en el botón de siguiente página, habremos perdido el filtro de año de construcción.

La manera "Estándard" como se hace esto en Liferay es guardando los valores de los filtros como preferencias del portlet, de manera que siempre que vayamos al listado nos encontraremos con los filtros que teníamos puestos en la última búsqueda que hicimos. Esto es así incluso si nuestra sesión ha expirado o si la hemos cerrado. Al volver a entrar nuestras credenciales e ir al listado, veremos los filtros tal y como estaban la última vez.

Hay situaciones en las que queremos que la funcionalidad sea similar a esta, pero que al cerrar sesión, o cuando la sesión expire, los filtros se borren y aparezcan los valores por defecto de los mismos al volver a entrar al listado. Para este caso, lo que habría que hacer sería guardar los filtros en la sesión.

Para conseguir esto, tendremos que tener, en el jsp de formularios de filtro, algo similar a esto:

...
String keywordFilter = SessionParamUtil.getString(request, "keywordFilter""");
String descriptionFilter = SessionParamUtil.getString(request, "descriptionFilter""");
String commentsFilter = SessionParamUtil.getString(request, "commentsFilter""");
...

 

Aquí definimos las variables donde guardaremos los filtros y, en vez de usar la clase ParamUtil, como es habitual, usaremos SessionParamUtil, que busca el parámetro también en la sesión (no sólo en la request).

Además, en nuestro controlador tendremos que poner el código encargado de recoger los filtros de la request y de guardarlos en la sesión:

public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException{
        PortletSession portletSession = renderRequest.getPortletSession();
        Enumeration<String> params = renderRequest.getParameterNames();
        while(params.hasMoreElements()){
            String param = params.nextElement();
            if(!param.endsWith("Filter")) continue;
            portletSession.setAttribute(param, renderRequest.getParameter(param), PortletSession.PORTLET_SCOPE);
        }
        super.render(renderRequest, renderResponse);
}

 

En este caso hemos modificado la función render de nuestro controlador para que guarde todos los parámetros de la request cuyo nombre acabe en "Filter".

voilà, ya tenemos nuestros filtros guardados en sesión!

 

Esperamos que os haya sido de ayuda, hasta la próxima!!

Localizar Entidades Custom

Fecha de publicación 30/03/16 12:55

Hoy explicaré cómo localizar nuestras entidades creadas con el Service Builder de Liferay. Para hacer esto tendremos que modificar el archivo service.xml para la entidad que queramos localizar:

 

<entity name="Entidad" local-service="true" remote-service="true" 
     uuid="true">
     <!-- PK fields -->

     <column name="entidadId" type="long" primary="true" />
        
     ...
        
     <column name="nombre" type="String" localized="true" />

</entity>        …

Asi cuando agamos la acción build service, en vez de generarnos un campo nombre como un objeto String, serà un objeto Map<Locale,String>.

 

Ahora, tendremos que tener en cuenta que tenemos que tratar siempre un objeto Map des de la vista, pasando por el controlador hasta la persistencia. Por ejemplo, el campo nombre se guardará como un xml en base de datos, por lo tanto crear un finder en el service.xml por la columna nombre no sería viable, se debería crear una custom sql que parseará el xml por locale.

 

Des de la vista tendremos la etiqueta liferay-ui:input-localized que nos servirá para añadir campos localizados:

 
<label class="aui-field-label" for="<portlet:namespace />nombre"><liferay-ui:message key="name" /></label>
<liferay-ui:input-localized *xml="" name="nombre" />
* el campo xml es dónde podemos introducir los valores del input, en formato xml.
 
 

Los idiomas disponibles son los especificados en el site (Configurarlo en Site Administration). En la acción podremos recoger el dato de la siguiente manera:

 

Map<Locale,String> nombre = LocalizationUtil.getLocalizationMap(actionRequest, "nombre");

 

Y finalmente, nos faltará modificar los servicios para añadir nuevos objetos localizados:

 

public Entidad addEntidad(Map<Locale,String> nombre, ….) throws SystemException, PortalException {
        Entidad entidad = null;
        
        long entidadId = 
               counterLocalService.increment(Entidad.class.getName());
        
        entidad = entidadPersistence.create(entidadId);
        
        entidad.setCreateDate(DateUtil.newDate());
        entidad.setUserId(userId);
        entidad.setUserName(userName);
 
        entidad.setNombreMap(nombre);
                      …
 
        entidad = entidadPersistence.update(entidad);
}

 

Y ya tendríamos la entidad localizada. Tened en cuenta que para cualquier cosa que hagáis con esta entidad, por ejemplo un search container, ahora tendréis un xml.


¡Espero que os sea útil!

Indexación customizada

Fecha de publicación 1/03/16 11:35

Buenas,

Hoy nos centraremos en explicar como indexar contenido propio creando una clase Indexer custom.

 

1. Crear la clase CustomIndexer.java que extiende a BaseIndexer

Crearemos una nueva clase java que extenderá a la clase de Liferay com.liferay.portal.kernel.search.BaseIndexer.

package com.sonicon.index.util;

import java.util.Locale;
import javax.portlet.PortletURL;
import com.liferay.portal.kernel.search.BaseIndexer;
import com.liferay.portal.kernel.search.Document;
import com.liferay.portal.kernel.search.SearchContext;
import com.liferay.portal.kernel.search.Summary;

public class CustomIndexer extends BaseIndexer{

}

 

2. Implementar los métodos necesarios

Es necesario implementar los siguientes métodos para el nuevo indexer. Puedes ver ejemplos en los indexer propios de Liferay como AssetCategoryIndexer, BlogsIndexer, JournalArticleIndexer, etc. (ejemplo: JournalArticleIndexer)

public static final String[] CLASS_NAMES = { Custom.class.getName() };
public static final String PORTLET_ID = "custom-portlet";

@Override
public String[] getClassNames() {
	return CLASS_NAMES;
}

@Override
public String getPortletId() {
	return PORTLET_ID;
}

@Override
protected void doDelete(Object obj) throws Exception {
	// TODO Auto-generated method stub
}

@Override
protected Document doGetDocument(Object obj) throws Exception {
	// TODO Auto-generated method stub
}

@Override
protected Summary doGetSummary(Document document, Locale locale, String snippet, 
		PortletURL portletURL) throws Exception {
	// TODO Auto-generated method stub
}

@Override
protected void doReindex(Object obj) throws Exception {
	// TODO Auto-generated method stub
}

@Override
protected void doReindex(String[] ids) throws Exception {
	// TODO Auto-generated method stub
}

@Override
protected void doReindex(String className, long classPK) throws Exception {
	// TODO Auto-generated method stub
}

@Override
protected String getPortletId(SearchContext searchContext) {
	// TODO Auto-generated method stub
}

 

3. Registrar el nuevo indexer

Añadir dentro del liferay-portlet.xml el siguiente elemento. Normalmente se sitúa después del elemento <icon></icon>, mirar el DTD para más información:

<liferay-portlet-app>
  <portlet>
    <portlet-name>custom</portlet-name>
    <icon>/icon.png</icon>
    <indexer-class>com.sonicon.index.util.CustomIndexer</indexer-class>
    ...
  </portlet>
  ...
</liferay-portlet-app>

Nota: el indexer se registra al deployar el portlet.

 

4. Comprobando el registro del indexer

Podemos printar el listado de indexers registrados con el siguiente código.

import com.liferay.portal.kernel.search.IndexerRegistryUtil;
import com.liferay.portal.kernel.search.Indexer;
import java.util.List;

List indexers = IndexerRegistryUtil.getIndexers();

for (Indexer indexer : indexers) {
  String[] classnames = indexer.getClassNames();
  String portletId = indexer.getPortletId();

  for (String classname : classnames) {
    System.out.println("PortletId ["+portletId+"] - classname: "+classname);
  }
}

Nota: se puede cargar en modo script desde el Control Panel > Server Administration > Script > Groovy y ver el resultado en el catalina.out.

 

5. Utilizando el indexer

Para añadir un documento al indexer se realiza con el siguiente código. Lo más habitual es ejecutarlo al realizar un add o update del objeto desde su LocalServiceImpl.

Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Custom.class);
indexer.reindex(custom_obj);

 

Del mismo modo, también se debería eliminar el documento del indexer al realizar un delete del objeto.

Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Custom.class);
indexer.delete(custom_obj);

 

6. Comprobando el contenido del indexer

Para ver que documentos contiene un indexer podemos utilizar un código de este estilo:

SearchContext searchContext = new SearchContext();
Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Custom.class);
Hits hits = indexer.search(searchContext);
List docs = hits.toList();
int i = 1;
for (Document doc : docs) {
  System.out.println("Documento ["+i+"]: "+doc.getField(Field.TITLE).getValue());
  i++;
}

Nota: también existe un programa que te permite analizar los documentos indexados por Lucene (Luke).

 

Espero que este post os resulte útil. Un saludo.

jQuery Validator

Fecha de publicación 8/02/16 17:17

¡Muy buenas!

 

En esta entrada de blog me gustaría introducir, a quién no conozca o no lo haya usado nunca, la librería jQuery validator.

 

Es una extensión de jquery que sirve para la validación (¡qué sorpresa!) de formularios en el lado del cliente.

 

Para empezar aquí tenéis la web la qual nos podremos descargar el código y, además, podremos encontrar diferentes ejemplos de uso.

 

Los métodos principales que añade esta librería son los siguientes:

  • validate(): Inicializa la validación del formulario y, además, lo valida.

  • valid(): Se usa para validar el formulario. No acepta ningún argumento, se usa una vez hecho el método validate.

  • rules(): Añade, modifica y elimina normas de validación.

 

Las principales validaciones que podemos hacer a los campos son las siguientes:

  • required – Hace que el elemento sea obligatorio.

  • remote – Solicita un recurso para validar el elemento.

  • minlength – Marca el número de carácteres mínimos para que sea válido.

  • maxlength – Marca el número de carácteres máximos para que sea válido.

  • rangelength – Marca el número de carácteres mínimos y máximos para que sea válido.

  • min – (Número) Marca un número mínimo para que el elemento sea válido.

  • max – (Número) Marca un número máximo para que el elemento sea válido.

  • range – (Número) Marca un número mínimo y máximo para que el elemento sea válido.

  • email – Valida si el email introducido tiene un formato correcto.

  • url – Valida si la url introducida tiene un formato correcto.

  • date – Valida si la fecha introducida tiene un formato correcto.

  • dateISO – Valida si la fecha introducida tiene un formato ISO correcto.

  • number – Valida si el elemento es un número decimal.

  • digits – Valida si todos los carácteres del elemento son números.

  • creditcard – Valida si el elemento es un número de targeta de crédito.

  • equalTo – Valida si el elemento es igual a otro elemento especificado.

 

Si ninguna de estas validaciones encajan con tu formulario podrias añadir tus propias funciones, por ejemplo un validador de email más complejo que el de por defecto:

$.validator.addMethod("emailCustomValidation", function(value, element, arg){
        var patt = new RegExp("^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
                + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$");
        return patt.test(value);
         }, “El correo electrónico introducido no tiene un formato correcto”);

 

Para finalizar, vamos a listar las principales opcions de la función validate y pondré un ejemplo:

  • rules: Añade normas en los elementos del formulario.

  • messages: Puedes añadir mensajes personalizados para las validaciones (si no estan aparece el de por defecto).

  • highlight: Cómo se remarca un elemento cuando es inválido.

  • unhighlight: Cómo se se desmarca un elemento cuando es válido.

  • onclick: Valida los checkboxes y los radio buttons cuando se pulsan.

  • errorClass: Nombre de la clase que se añade cuando es inválido.

  • validClass: Nombre de la clase que se añade al elemento cuando es válido.

  • Todas las demás opciones las encontraréis aquí.

 

Ejemplo:

var messageRequiredName = "El campo nombre es obligatorio";
...

 

$('#contactform').validate({
    rules: {

      //Normas en los campos
        name :{required:true, maxlength:75, minlength:2},
        email :{required:true, maxlength:75, emailValidation: true},
        comment :{required:true, maxlength:200, minlength: 10}
    },
    messages: {

      //Mensajes de error
        name : {required:messageRequiredName, minlength: messageLeastName},
        email : {required:messageRequiredEmail, emailValidation: messageInvalidEmail},
        comment : {required:messageRequiredComments, minlength: messageLeastComments}
    },
    highlight: function(element) {

       //Resalta cuando sea inválido
        $(element).parent().addClass("has-error");
               $(element).parent().find('.form-control-feedback').css('display','initial');
    },
    unhighlight: function(element) {

      //Desmarca cuando sea válido
        $(element).parent().removeClass("has-error");
               $(element).parent().find('.form-control-feedback').css('display','none');
    }
});

 

¡Espero que os sea útil!

 

Cómo gestionar permisos en nuestros portlets

Fecha de publicación 3/02/16 14:58

Algo realmente importante a tener en cuenta cuando creamos nuestros portlets con Liferay es cómo gestionar el acceso a los diferentes recursos. La gestión de permisos es algo básico si queremos que algunos roles puedan realizar ciertas acciones pero no otras. En este post vamos a hablar de como integrar los permisos y recursos de nuestros portlets con el sistema de permisos de Liferay.

Primero de todo tenemos crear el archivo default.xml en la carpeta docroot/WEB-INF/src/resource-actions. En este archivo definiremos, para nuestro modelo de datos, que permisos aplicaremos a qué recursos.

Este archivo tiene este aspecto:

<?xml version="1.0"?>
<!DOCTYPE resource-action-mapping PUBLIC "-//Liferay//DTD Resource Action Mapping 6.2.0//EN" 
"http://www.liferay.com/dtd/liferay-resource-action- mapping_6_2_0.dtd">

<resource-action-mapping>
    <portlet-resource>
        <portlet-name>guestbook</portlet-name>
        <permissions>
            <supports>
                <action-key>ADD_TO_PAGE</action-key>
                ...
            </supports>
            <site-member-defaults>
                <action-key>VIEW</action-key>
            </site-member-defaults>
            <guest-defaults>
                <action-key>VIEW</action-key>
            </guest-defaults>
            <guest-unsupported />
        </permissions>
    </portlet-resource>

    <model-resource>
        <model-name>com.liferay.docs.guestbook.model</model-name>
        <portlet-ref>
            <portlet-name>guestbook</portlet-name>
        </portlet-ref>
        <permissions>
        <supports>
            <action-key>ADD_GUESTBOOK</action-key>
            ...
        </supports>
        <site-member-defaults>
            <action-key>ADD_ENTRY</action-key>
        </site-member-defaults>
        <guest-defaults />
        <guest-unsupported>
            <action-key>ADD_GUESTBOOK</action-key>
            ...
        </guest-unsupported>
        </permissions>
    </model-resource>
</resource-action-mapping>

En este archivo definimos las acciones soportadas por cada uno de nuestros portlets (bloques <portlet-resource/>, pueden existir varios) y de nuestras entidades (bloques <model-resource/>, pueden existir varios), así como los permisos por defecto de los miembros del site o del usuario invitado, etc.

Además de crear este archivo, deberemos insertar esta linea en el archivo docroot/WEB-INF/src/portlet.properties (crearlo si aun no existe):

resource.actions.configs=resource-actions/default.xml

voilà! Una vez compilado y desplegado en nuestro servidor de aplicaciones ya podemos acceder al panel de control de Liferay y dar o quitar permisos a lo roles que creamos conveniente.

Solo nos faltaría comprobar antes de realizar una acción (por ejemplo, añadir un Guestbook) que alguno de los roles del usuario contienen el permiso para realizar dicha acción... pero eso mejor lo dejamos para otro dia  ;-)

Search Container

Fecha de publicación 22/01/16 13:10

Search container es una herramienta AUI de Liferay que permite paginar datos de forma sencilla. El objetivo de este post es mostrar la estructura de Search container haciendo un ejemplo sencillo para aplicarlo.

 

Primero de todo tendremos que añadir la siguiente declaración:

<%@taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %>

 

Cuando queremos hacer una paginación empezamos con la etiqueta ‘liferay-ui:search-container’:

 

<liferay-ui:search-container delta="10" orderByCol="firstName" orderByType="asc" emptyResultsMessage="no-data-were-found">

</liferay-ui:search-container>

 

Los parámetros son:

  • Delta: Número de elementos por página.
  • emptyResultsMessage: Como su nombre indica, mensaje que se muestra cuando no se encuentran datos.

(Además, si queremos hacer ordenación por columnas añadiremos los siguientes parámetros)

  • orderByCol: Especifica la columna por la cuál se ordenará.
  • orderByType: Especifica el tipo de ordenación, hay disponible la ascendente (‘asc’) y descendente (‘desc’).

Etiquetas internas del Search Container

Dentro deberemos añadir las siguientes etiquetas:

liferay-ui:search-container-results

Dentro de esta etiqueta tendremos la lista de objetos, los cuáles se les hará la paginación (results) y la cantidad que hay de estos objetos (total).

 

<liferay-ui:search-container-results
        results="<%= PersonLocalServiceUtil.search(CRITERIO, searchContainer.getStart(), searchContainer.getEnd(), searchContainer.getOrderByComparator(), orderByCol,orderByType); %>"
        total="<%= PersonLocalServiceUtil.searchCount(CRITERIO); %>"
    />

 

Nota: Normalmente la paginación de objetos se muestra tras un formulario, si hay campos, por los cuáles filtrarás, los has de añadir en los parámetros de la funciones search y searchCount (que deberás crear a parte) y sustituir CRITERIO por los parámetros que necesites.
 

liferay-ui:search-container-row

En esta etiqueta tendremos la estructura de cada columna, es decir que campos se mostraran del objeto del cuál hacemos la paginación.

 

<liferay-ui:search-container-row
        className="net.sonicon.model.Person"
        keyProperty="personId"
        modelVar="person"
    >
        <liferay-ui:search-container-column-text
            name="name"
            value="<%= person.getName() %>"
        />
        <liferay-ui:search-container-column-text
            name="first-name"
            property="firstName"

            orderable="<%= true %>"

            orderableProperty="firstName"
        />
        <liferay-ui:search-container-column-text
            name="department"
            value="<%= DepartmentLocalServiceUtil

.getDepartmentByPersonID(personId).getName(); %>"

        />
    </liferay-ui:search-container-row>

Los parámetros son:

  • className: Especificas la clase del objeto que se le va ha hacer la paginación. En este caso es uno propio que hemos hecho con el 'service builder'.
  • keyProperty: Es el identificador del objecto de cada row.
  • modelVar: Es el objeto de la row.
 

Además dentro de esta etiqueta contiene N etiquetas de liferay-ui:search-container-column-text. Estas representan cada columna que se crea de la paginación.

 

En el ejemplo tenéis 3 columnas:

  • La primera: Una columna ‘Nombre’ que coje el parámetro name del objeto person.

  • La segunda: Una columna ‘Primer Nombre’ que coje el valor del parámetro especificado. Además especifica que se puede ordenar por esa columna (orderable = true) y por qué campo se ordena (orderableProperty = firstName).

  • La tercera: Una columna ‘Departamento’ que aprovecha el id del objeto person para obtener el nombre del departamento.

 

liferay-ui:search-iterator

Esta etiqueta marca donde se construirá la tabla de paginación especificada en las otras etiquetas.

 

Y con esto deberíamos tener una tabla con 3 columnas y con ordenación sobre los objetos de la clase Person.


¡Espero que os haya servido de ayuda!

 

Inputs con multi-idioma

Fecha de publicación 18/12/15 2:15

En un blog anterior explicamos cómo “Sacar partido de la configuración de un portlet”, utilizaremos ese ejemplo para, a continuación, explicar cómo gestionar la información de los inputs en varios idiomas.

Si bien podemos utilizar keys dentro del portlet para determinar su valor en los distintos idiomas, una opción menos rígida es introducir los valores en los distintos idiomas a través de la configuración del portlet.

En nuestro ejemplo teníamos portlet con un título, una imagen y un listado. Dicho título era fijo, aunque cambiaras el idioma el título mostrado sería el mismo. Ahora lo que queremos es poder decidir el texto que utilizaremos de título en función del idioma en que se visualice la página.

 

Diferencias respecto a lo que teníamos:

configuration.jsp: cambia el tipo de tag usado para los input, en esta ocasión utilizaremos <liferay-ui:input-localized> envuelta por un <aui:field-wrapper>. También cambia la forma de manejar sus valores, ahora utilizaremos la clase LocalizationUtil para tratarlos.

 

configuration.jsp
String localizedTitle = LocalizationUtil.getLocalizationXmlFromPreferences(portletPreferences, renderRequest, "title");
 
<liferay-ui:panel collapsible="<%= true %>" extended="<%= true %>" id="customText" persistState="<%= true %>" title="Text">
    <aui:fieldset cssClass="title-data" label="Title Text">
        <aui:field-wrapper cssClass="lfr-input-text-container" label="Title">
            <liferay-ui:input-localized name="title" xml="<%= localizedTitle %>" />
        </aui:field-wrapper>
    </aui:fieldset>
</liferay-ui:panel>

 

Es posible definir que solo muestre para elegir los idiomas del Site con “availableLocales”.

configuration.jsp
<liferay-ui:input-localized name="title" availableLocales="<%= LanguageUtil.getAvailableLocales(themeDisplay.getSiteGroupId()) %>" xml="<%= localizedTitle %>" />

 

También se puede definir que el idioma por defecto sea el que tiene el Site con “defaultLanguageId”:

configuration.jsp
Locale defaultLocale = LocaleUtil.getSiteDefault();
String defaultLanguageId = LocaleUtil.toLanguageId(defaultLocale);
<liferay-ui:input-localized name="title" defaultLanguageId="<%= defaultLanguageId %>" xml="<%= localizedTitle %>" />

 

ConfigurationActionImpl.java: Guardamos los datos localizados.

En este caso, es necesario tratar los datos para poder guardarlos.

 

ConfigurationActionImpl.java
PortletPreferences preferences = actionRequest.getPreferences();
  
// Treat preferences
LocalizationUtil.setLocalizedPreferencesValues(actionRequest, preferences, "title");
  
// Store preferences
preferences.store();
super.processAction(portletConfig, actionRequest, actionResponse);

 

view.jsp: Distinto método de obtener el valor en el idioma actual.

 

  • Obtenemos el valor:
view.jsp
String localizedTitle = LocalizationUtil.getPreferencesValue(preferences, "title", themeDisplay.getLanguageId());

 

  • Utilizamos el valor (ejemplo):
view.jsp
<h1> <%=localizedTitle%> </h1>
<% if (showsImage)  { %>
    <img src="<%=imageLink%>" alt="">
<% } %>
<div><%=contentList%></div>

 

Una vez aplicadas las modificaciones tendremos un nuevo campo en la configuración del portlet con la posibilidad de guardar una información distinta por cada idioma.

Configuration

Espero que os sea de utilidad, un saludo.

 

A fondo: liferay-portlet.xml

Fecha de publicación 2/12/15 12:57

¡Hola!

 

En esta entrada voy a explorar un poco lo que podemos llegar a hacer con el archivo 'liferay-portal.xml'. Este archivo describe mejoras del portlet JSR-286 en nuestro portal de Liferay.

La información mostrada va referida a la versión 6.2 por lo que no puedo asegurar que todo lo que especifique funcione para otras versiones de Liferay Portal.

 

Cuando generamos un nuevo portlet podemos autogenerar este archivo y ésta es su estructura base:

 

<liferay-portlet-app>

       <portlet>

              <portlet-name>my-greeting</portlet-name>

              <icon>/icon.png</icon>

              <instanciable>false</instanciable>

              <header-portlet-css>/css/main.css</header-portlet-css>

              <footer-portlet-javascript>/js/main.js</footer-portlet-javascript>

              <css-class-wrapper>my-greeting-portlet</css-class-wrapper>

       </portlet>

       <role-mapper>

              <role-name>administrator</role-name>

              <role-link>Administrator</role-link>

       </role-mapper>

       <role-mapper>

              <role-name>guest</role-name>

  <role-link>Guest</role-link>

       </role-mapper>

       <role-mapper>

              <role-name>power-user</role-name>

  <role-link>Power User</role-link>

       </role-mapper>

       <role-mapper>

              <role-name>user</role-name>

              <role-link>User</role-link>

       </role-mapper>

</liferay-portlet-app>

 

 

Para cada portlet tenemos las siguientes etiquetas base:

  • portlet-name: Tiene el nombre especificado para el portlet. Es necesario que tenga el mismo que el especificado en el archivo portlet.xml.
  • icon: Ruta hasta la imagen icono del portlet.
  • instanciable: Indica si un portlet se puede instanciar más de una vez en una misma página.
  • header-portlet-css: Ruta hasta un archivo css. Se añadirá dentro de la etiqueta <head> de la página (debido a la parte del nombre header).*
  • footer-portlet-javascript: Ruta hasta un archivo js. Se añadirá al final de la página, justo antes de la etiqueta </body> (debido a la parte del nombre footer).*
  • css-class-wrapper: Clase que se añade en el html al renderizar el portlet, se suele usar para aplicar estilos propios al portlet.

*Pueden existir varias etiquetas de este tipo en un mismo portlet.

 

Ahora que ya tenemos lo básico, voy a especificar unas cuantas cosas que podemos realizar:

 

Añadir el portlet en el Panel de Control

 

Para conseguir esto deberemos añadir dos etiquetas más: '<control-panel-entry-category>' y '<control-panel-entry-weight>'.

 

La primera etiqueta especifica en que sección de nuestro panel de control se colocará nuestro portlet, tenemos las siguientes:

 
  • Panel de control: ‘users’, ‘sites’, ‘apps’ y ‘configuration’.
  • Administración del sitio web: ‘site_administration.pages’, ‘site_administration.content’, ‘site_administration.users’ y ‘site_administration.configuration’.
  • Mi cuenta: ‘my’.
 

La segunda etiqueta determina el peso que tendra tu portlet dentro de cada sección. A más peso más abajo estará tu portlet en la lista.

 

Hacer un cron

 

Este apartado ya lo especificamos en otra entrada de blog hecha por Sergi.

 

Además

 

A parte de lo mencionado anteriormente, este archivo también sirve para enlazar a clases que hagan una funcionalidad especifica. Por ejemplo, implementar búqueda e indexación, crear notificaciones para los usuarios y especificar friendly url del portlet entre otras cosas.

 

Para finalizar os dejo el listado con todas las etiquetas que pueden ir dentro del archivo 'liferay-portal.xml'.

 

Espero que os haya ayudado, ¡un saludo!


 

Workflows en Liferay II

Fecha de publicación 30/10/15 16:39

Después del post del 09/10 que nos trajo David sobre Workflows en Liferay, en el que nos hacía una primera aproximación a las definiciones de nuevos flujos de trabajo en nuestro portal, en este post quiero explicaros qué pasos debéis seguir para utilizar Workflows sobre una entidad que hayáis creado en vuestro service.xml.

Deberéis tener instalado Kaleo, el motor de Workflows más común en Liferay. A continuación, seguid los siguientes pasos:

1.- Crear una clase que extienda la clase com.liferay.portal.kernel.workflow.BaseWorkflowHandler, en la que implementaremos los métodos necesarios (los no implementados) de la clase base: getClassNamegetType y updateStatus. El método más importante es este último, que es el que ejecutaremos desde nuestra definición de workflow en los puntos en los que se deba cambiar el estado del objeto sobre el que estamos aplicando el workflow. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Articulo updateStatus(
            int status, Map<String, Serializable> workflowContext)
    throws PortalException, SystemException {
     
    long resourcePrimKey = GetterUtil.getLong(
        (String)workflowContext.get(
            WorkflowConstants.CONTEXT_ENTRY_CLASS_PK));
    ServiceContext serviceContext = (ServiceContext)workflowContext.get(
            WorkflowConstants.CONTEXT_SERVICE_CONTEXT);
     
    long userId = serviceContext.getUserId();
     
    Articulo res = ArticuloLocalServiceUtil.updateStatus(
            userId, resourcePrimKey, status, serviceContext);
    return res;
}

 

2.- Crear la función updateStatus en el servicio local de nuestra entidad.

3.- Crear una instancia de workflow cada vez que creamos una nueva entrada de nuestra entidad. Recordar también eliminar la instancia de workflow cuando borremos el elemento.

1
2
3
WorkflowHandlerRegistryUtil.startWorkflowInstance(
            companyId, groupId, userId, Articulo.class.getName(), resourcePrimKey,
            articulo, serviceContext);
1
2
workflowInstanceLinkLocalService.deleteWorkflowInstanceLinks(
            companyId, groupId,Articulo.class.getName(), articulo.getResourcePrimKey());

 

4.- Incluir el elemento <workflow-handler> en el archivo liferay-portlet.xml de nuestro portlet indicando la clase que hemos creado en el punto 1.

 

 

5.- En el control panel hay que recordar asignar nuestra definición de workflow como workflow por defecto.

 

Y con esto ya tendríamos nuestro sistema de workflow configurado y listo para funcionar.

Espero que os sea de ayuda! (guiño)

Tareas Programadas en Liferay

Fecha de publicación 19/10/15 17:25

Para llevar a cabo un proyecto a menudo requerimos de procesos en Background. Una solución muy extendida es desarrollar estas tareas con código aislado, usando tecnologías que permiten su ejecución y programarlas mediante herramientas propias del sistema operativo. Pues bien, para proyectos Liferay no es necesario optar por este tipo de soluciones: podemos desarrollar y programar tareas en nuestros propios plugins mediante Liferay Quartz Scheduler.

 

Ventajas

Icon
  • Posibilidad de usar servicios de nuestro Back End.
  • Unificación de código (mejora en seguimiento y tener el repositorio en común entre otros).
  • Independencia de tecnologías y agentes externos.
  • Sencillez.

 

Consideraremos que ya se dispone de un portlet. El framework usado es indiferente (MVC Portlet, JSP Portlet, etc.)

 

Veamos cómo podemos programar una tarea en Liferay:

 

  1. 1. Creación de la clase:

    Dentro del portlet creamos una clase que implemente la interfaz 'com.liferay.portal.kernel.messaging.MessageListener'.
     
  2.  
  3. 2. Implementar el método 'receive()':
     

     

     
    package com.sonicon.testSchedule;
     
    import com.liferay.portal.kernel.messaging.Message;
    import com.liferay.portal.kernel.messaging.MessageListener;
    import com.liferay.portal.kernel.messaging.MessageListenerException;
    public class MessageListenerTest implements MessageListener {
        @Override
        public void receive(Message message) throws MessageListenerException {
            //Código a ejecutar aquí
        }
    }

     

    Nuestro método 'receive()' se ejecutará periódicamente en función de la configuración que definamos en el siguiente punto.

     

  4. 3. Registrar la clase en Liferay:


    Añadir el scheduler de la clase en 'liferay-portlet.xml' (tras la etiqueta '<icon>' del portlet):

     

     
    <portlet>
    ...
    <icon>/icon.png</icon>
    <scheduler-entry>
         <scheduler-event-listener-class>com.sonicon.testSchedule.MessageListenerTest</scheduler-event-listener-class>
         <trigger>
             <simple>
                 <simple-trigger-value>
                     30
                 </simple-trigger-value>
              <time-unit>second</time-unit>
           </simple>
         </trigger>
    </scheduler-entry>
    ...
    </portlet>

     

    Ya tenemos nuestra tarea programada. En este ejemplo sencillo, el scheduler se ha programado mediante un valor simple y se ejecutará cada 30 segundos. 

     

    Otra opción para el registro de la clase, es usar la etiqueta '<cron>' del siguiente modo (que sustituiría a la etiqueta '<simple>'):

     

     
    <scheduler-entry>
        <scheduler-event-listener-class>com.sonicon.testSchedule.MessageListenerTest</scheduler-event-listener-class>
        <trigger>
            <cron>
                <cron-trigger-value>0 15 10 ? * MON-FRI</cron-trigger-value>
            </cron>
        </trigger>
    </scheduler-entry>
     

Un saludo y hasta la próxima.

 

 

Más Info

Icon

 

 

 

Workflows en Liferay

Fecha de publicación 9/10/15 14:59

Un flujo de trabajo, o workflow, se puede describir como una automatización de una secuencia de acciones con sus estados para realizar un proceso.

 

Los flujos de trabajo para nuestro portal se pueden definir con el motor de workflow de Liferay, se llama Kaleo. El plugin kaleo-web esta disponible para las versiones EE y CE en el marketplace.

 

Estas definiciones se hacen mediante archivos ”.xml”. Puedes definir tantos flujos de trabajo como sean necesarios para tu portal. Un detalle importante es que puedes especificar roles en tus definiciones de workflow. Si estos roles no existieran en el portal, se crearían automáticamente durante el proceso de "deploy".

 

El flujo de trabajo lo podemos ver como un grafo donde los nodos son estados/tareas y las aristas son transiciones hechas (o no) por el usuario. Por ejemplo, podemos definir un workflow básico de resolución de incidencias, donde tenemos el camino verde que seria el camino esperado (CREADO - EN PROGRESO - EN TEST - RESUELTO) y, además, añadimos el camino rojo, para hacer una transición para volver del test que no ha ido bien y volver a hacer la incidencia:

 

Con Kaleo definiremos los Creado y Resuelto como estados inicial y final. Los otros nodos son tareas, que se diferencian básicamente con que están relacionadas con roles.

 

Untitled (1).png

 

Conceptos

Definición: El workflow se especifica en un archivo “.xml”. Todo la definición ha de ir dentro del tag "<workflow-definition>":

 

<workflow-definition

       xmlns="urn:liferay.com:liferay-workflow_6.2.0"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="urn:liferay.com:liferay-workflow_6.2.0 http://www.liferay.com/dtd/liferay-workflow-definition_6_2_0.xsd"

>

       <name>Test</name>

       <description>Un flujo de trabajo test.</description>

       <version>1</version>

       . . .

</workflow-definition>

 

Acción: Son scripts que se ejecutan dentro de tareas y estados. El código tiene que ir dentro de tags "<script>" y, además, se puede definir un lenguaje con el tag "<script-language>". Puedes usar Javascript, Python, Groovy y Ruby entre otros lenguajes para implementar tus acciones:

 

<actions>

       <action>

              <name>Reject</name>

              <script>

              <![CDATA[

         ...

   ]]>

               </script>

               <script-language>javascript</script-language>

               <execution-type>Reject</execution-type>

       </action>

       <notification>

        ...

       </notification>

</actions>

 

Notificaciones: Son avisos que se envían al usuario, se puede especificar cuando se envían (onAssignment, onEntry y onExit) y la forma (email, im, privatemessage). Como el código de la acción se puede especificar el lenguaje del mensaje, como freemarker en este caso. Las notificaciones se añaden dentro de las acciones:

 

<notification>

        <name>Creator Modification Notification</name>

        <template>Your submission was rejected by $(userName), please modify and resubmit.</template>

        <template-language>freemaker</template-language>

        <notification-type>email</notification-type>

        <notification-type>user-notification</notification-type>

        <execution-type>onAssignment</execution-type>

</notification>

 

Transiciones: Son los pasos de un estado/tarea a otro/a estado/tarea. En un mismo estado/tarea se pueden definir mas de una transición. El formato ha de ser el siguiente:

 

<transitions>

        <transition>

                <name>finalizar</name> <!-- Nombre transición -->

                <target>resuelto</target> <!-- Destino transición -->

        </transition>

        <transition>

                <name>repetir</name> <!-- Nombre transición -->

                <target>en-progreso</target> <!-- Destino transición -->

        </transition>

</transitions>

 

Estados: Se usa para definir como empieza y acaba el workflow. Puede contener acciones y transiciones. Se define asi:

 

<state>

       <name>creado</name>

       <metadata><![CDATA[{"xy":[36,51]}]]></metadata>

<!-- Desfinimos el estado inicial -->

       <initial>true</initial>

       <transitions>

        ...

       </transitions>

       <actions>

              <action>

               ...

              </action>

       </actions>

</state>

 

Asignaciones: Los roles se han de añadir en el tag "<assignments>":
 

<assignments>

       <roles>

              <role>

                     <role-type>organization</role-type>

                     <name>Organization Administrator</name>

              </role>

       </roles>

</assignments>

 

Tareas: Es el concepto más complejo del workflow. Son parecidas a los estados, sin embargo éstas estan relacionadas con un rol o unos roles, para especificar quién ha de completar la tarea. Como los estados, puede contener acciones y transiciones:

 

<task>

       <name>en-progreso</name>

       <metadata>

              <![CDATA[{"transitions":{"resubmit":{"bendpoints":[[303,140]]}},"xy":[328,199]}]]>

       </metadata>

       <actions>

              <action>

               ...

              </action>

              <notification>

               ...

              </notification>

       </actions>

       <assignments>

        ...

       </assignments>

       <transitions>

        ...

       </transitions>

</task>

 

Para más información sobre workflows esta la guía oficial de Liferay: https://dev.liferay.com/discover/portal/-/knowledge_base/6-2/creating-new-workflow-definitions


Espero que ayude a esclarecer el funcionamiento de esta potente herramienta que esta disponible para nuestro portal.

 

Sacar partido de la configuración de un portlet

Fecha de publicación 16/09/15 5:01

A menudo tenemos que definir ciertos comportamientos que tiene un portlet en diferentes situaciones. Un ejemplo claro seria un formulario de contacto; pensad en la situación que tenemos tres sitios web que necesitan de este portlet. Y cada uno necesita que el formulario se envíe a un correo electrónico diferente, con un asunto específico según el sitio de dónde se envíe. Para evitar tener que crear un portlet para cada web podemos aprovechar la configuración del portlet para determinar cómo queremos que sea su comportamiento.

Haremos un ejemplo simple en el que mediante el panel de configuración de un portlet, que muestra un título, una imagen y un listado, podamos decidir si queremos que se muestre la imagen.

 

Paso 1: Especificar cuál será el archivo ".jsp" de la configuración en el "portlet.xml".

Añadimos el siguiente código después del tag “<portlet-class>...</portlet-class>” con la localización de nuestro archivo ".jsp" como value.

< init-param >
      < name >config-template</ name >
      < value >/html/configuration.jsp</ value >
</ init-param >

 

Paso 2: Crear el archivo jsp “configuration.jsp”.

Creamos un archivo "configuration.jsp" en el directorio indicado en el portlet.xml, en nuestro caso, en “/html/configuration.jsp”. 

<%@include file="/html/init.jsp" %>
< liferay-portlet:actionURL   portletConfiguration = "true"   var = "configurationURL"   />
<%
boolean showsImage = GetterUtil.getBoolean(portletPreferences.getValue("showsImageCheck", StringPool.TRUE));
%>
< aui:form   action="<%= configurationURL %>" method="post" name="fm">
     < aui:input   name="<%= Constants.CMD %>" type="hidden" value="<%= Constants.UPDATE %>" />
     < liferay-ui:success   key = "success-setup"   message = "you-have-successfully-updated-the-setup"   />
     < liferay-ui:panel   collapsible="<%=true%>" extended="<%=true%>" id="config" persistState="<%= true %>" title="Configuration">
         < aui:fieldset   cssClass = "option-content-data"   label = "Options" >
             < aui:input   label = "Shows Image"   name = "preferences--showsImageCheck--"   type = "checkbox"   value="<%= showsImage %>" />
         </ aui:fieldset >
     </ liferay-ui:panel >
     < aui:button-row >
        < aui:button   type = "submit"   />
     </ aui:button-row >
</ aui:form >

Nota: Es posible estructurar las opciones de la configuración en distintos paneles siempre que el id del panel no se repita o separarlos por fields.

Creamos o añadimos (si es necesario) el archivo "init.jsp" con el siguiente contenido.

<%@ taglib uri=" prefix="c" %>
 
<%@ taglib uri="  prefix="aui" %>
<%@ taglib uri="  prefix="liferay-ui" %>
<%@ taglib uri="  prefix="liferay-portlet" %>
<%@ taglib uri="  prefix="liferay-theme" %>
<%@ taglib uri="  prefix="portlet" %>
 
<%@ page import="com.liferay.portal.kernel.util.Constants" %>
<%@ page import="com.liferay.portal.kernel.util.GetterUtil" %>
<%@ page import="com.liferay.portal.kernel.util.StringPool" %>
<%@ page import="com.liferay.portal.kernel.util.ParamUtil" %>
<%@ page import="com.liferay.portal.kernel.util.Validator" %>
<%@ page import="com.liferay.portlet.PortletPreferencesFactoryUtil" %>
<%@ page import="com.liferay.portal.util.PortalUtil" %>
 
<%@ page import="javax.portlet.ActionRequest" %>
<%@ page import="javax.portlet.PortletPreferences" %>
 
< portlet:defineObjects />
< liferay-theme:defineObjects />
 
<%
PortletPreferences preferences = null;
if (renderRequest != null) {
    preferences = renderRequest.getPreferences();
}
String portletResource = ParamUtil.getString(request, "portletResource");
if (Validator.isNotNull(portletResource)) {
    preferences = PortletPreferencesFactoryUtil.getPortletSetup(request, portletResource);
}
%>

 

 Y añadimos las dependencias necesarias en “liferay-plugin-package.properties”.

portal-dependency-jars=\
    jstl-api.jar,\
    jstl-impl.jar

 

Paso 3: Crear el archivo "ConfigurationActionImpl.java" y configurar el “liferay-portlet.xml”.

Creamos el archivo "ConfigurationActionImpl.java" dentro del package “org.portlet.name.action” y, además, tenemos que especificar “DefaultConfigurationAction” como superclase de ésta.

package org.portlet.name.action;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletConfig;
import javax.portlet.PortletPreferences;
 
import com.liferay.portal.kernel.portlet.DefaultConfigurationAction;
 
public class ConfigurationActionImpl extends DefaultConfigurationAction {
 
    @Override
    public void processAction(PortletConfig portletConfig, ActionRequest actionRequest,
        ActionResponse actionResponse) throws Exception {
 
        PortletPreferences prefs = actionRequest.getPreferences();
        // Treat preferences
 
        // Store preferences
        preferences.store();
        super.processAction(portletConfig, actionRequest, actionResponse);
    }
}

 

Tratamos las preferences si fuera necesario y las guardamos.

Añadimos el elemento al “liferay-portlet.xml” después del tag “<icon>...</icon>”.

 

< configuration-action-class >org.portlet.name.action.ConfigurationActionImpl</ configuration-action-class >

 

Paso 4: Modificamos nuestros archivo ".jsp" para utilizar los valores que extraemos de la configuración del portlet.

- Obtenemos el valor:

<%
boolean showsImage = GetterUtil.getBoolean(portletPreferences.getValue("showsImageCheck", StringPool.TRUE));
%>

 

- Utilizamos el valor (ejemplo):

< h1 > <%=titleText%> </ h1 >
<% if (showsImage)  { %>
    < img   src="<%=imageLink%>" alt="">
<% } %>
< div ><%=contentList%></ div >

Una vez realizado todos los pasos, al acceder a la configuración del portlet tendremos una nueva pestaña con las opciones que hayamos incluido.

Y este sería el resultado:

Un Saludo.

 

Las fases Action y Render de la ejecución de un portlet

Fecha de publicación 26/08/15 7:40

Muy buenas, en esta entrada explicaré las dos fases de ejecución que puede tener un portlet dentro del portal de Liferay.

 

Para aclarar el porqué Liferay está hecho así, primero debemos tener en cuenta que en una página de un portal de Liferay podemos tener incrustados diferentes portlets. Cada portlet lo podemos entender como una aplicación que hace cualquier funcionalidad y devuelve un código html que se acopla en dicha página.

 

En este sentido tendremos la fase de Render, esta fase la harán todos los portlets de la página. Incluye la funcionalidad para mostrar contenido al usuario según su estado (VIEW o EDIT entre otros).

 

Nota: Liferay permite especificar una prioridad de renderización en los portlets a través de render-weight en el archivo liferay-portlet.xml. Los portlets con un peso mayor serán renderizados antes.

 

Por otra parte, tenemos la fase Action. Esta fase incluye la acción que hace un usuario con un portlet, por lo que el portal solo procesa una acción a la vez. Una acción de un portlet puede ser cualquier petición a éste, como cambiar el estado o un cambio en la base de datos (Por ejemplo, si es un portlet publicador de noticias, añadir una noticia).

 

Podemos ver que las fases han de ser secuenciales: primero, si es necesario, se ejecuta la acción de un portlet y esta acción quizás modifica algún dato o comportamiento. Una vez hecho esto, empieza la fase de renderizar que hacen todos los portlets, incluyendo al que se le ha hecho la acción (Imágen 1).

 

Untitled.png

Imágen 1

 

Para ilustrar el funcionamiento, he añadido en una página prueba unos cuantos portlets: Camino de migas, Idiomas, Documentos y multimedia y un Visor de contenido web.

 

Cuando accedo a la página mediante la url ‘localhost:8080/web/prueba/welcome’ sólo se ejecuta la fase Render para todos los portlets que devuelve el código html para ver la página (Imágen 2).

 

image4.png

Imágen 2

 

Como usuario quiero añadir una carpeta usando el portlet de 'Documentos y multimedia', es decir, quiero realizar una acción sobre un portlet (Imágen 3).

image5.png

Imágen 3

 

Como vemos esta acción ha hecho que se cargue de nuevo la página. Me gustaría destacar dos cosas: primero, la url se ha modificado y ha añadido muchos parámetros (‘localhost:8080/web/prueba/welcome?p_p_id=....’). Así es como el servidor recoge la petición de la acción. La segunda es que si nos fijamos en los portlets siguen igual exceptuando 'Documentos y multimedia' (Imágen 4).

image6.png

Imágen 4

 

¿Qué ha pasado? Pues que la Action ha cambiado el estado del portlet para ver la vista de añadir carpeta. Así en la fase de Render los demás portlets muestran lo mismo y el de 'Documentos y multimedia' muestra un contenido diferente.

 

Sería otra acción añadir una carpeta nueva a 'Documentos y multimedia' y tendría el mismo procedimiento antes descrito.

 

Como hemos visto las url son importantes, para las fases de ejecución. A nivel de código podemos ver estas tres destacadas:

  • renderURL: sirve para mostrar un portlet usando solo la fase Render.

  • actionURL: sirve para indicar a un portlet que ha de ejecutar una acción antes de llegar a la fase Render.

  • resourceURL: sirve para obtener imágenes, XML o JSON entre otros. Es útil para hacer llamadas AJAX al servidor.

 

Fuentes:

https://dev.liferay.com/develop/tutorials/-/knowledge_base/6-1/understanding-the-two-phases-of-portlet-execution

http://www.liferay.com/es/community/forums/-/message_boards/message/43914750

 

Saludos,

— 20 Items per Page
Mostrando el intervalo 1 - 20 de 40 resultados.

Bloggers recientes Bloggers recientes

Oscar Rodríguez
Mensajes: 9
Estrellas: 2
Fecha: 28/09/16
David Berruezo
Mensajes: 14
Estrellas: 1
Fecha: 22/07/16
Javi Martín
Mensajes: 2
Estrellas: 1
Fecha: 20/05/16
Javier Torres
Mensajes: 5
Estrellas: 3
Fecha: 11/04/16
Sergi Mingueza
Mensajes: 4
Estrellas: 1
Fecha: 19/10/15
Matilde Gallardo
Mensajes: 1
Estrellas: 0
Fecha: 26/02/15
Adrià Vilà
Mensajes: 4
Estrellas: 4
Fecha: 31/08/14
Elena Ruiz
Mensajes: 1
Estrellas: 2
Fecha: 13/03/14