Operaciones CRUD en Liferay 7 con MVCPortlet y JSP

4
12131

Aprende a realizar operaciones CRUD en Liferay 7 accediendo desde un MVCPortlet a la capa de servicio generada por Service Builder y creando la vista con JSP.

Índice de contenidos

1. Introducción

En el tutorial Persistencia en Liferay 7 con Service Builder, vimos cómo definir nuestro modelo, crear en base de datos las tablas correspondientes a él y generar automáticamente la capa de modelo, persistencia y servicio. Ahora vamos a hacer uso de la capa de servicio desde un portlet para poder realizar las operaciones CRUD básicas sobre nuestro modelo compuesto por libros y escritores: crearlos, listarlos, modificarlos y eliminarlos.

Este tutorial es una continuación del antes mencionado, siendo su lectura obligatoria. No obstante, no es necesario realizar los pasos llevados a cabo en él, pues daremos las indicaciones para crear un proyecto de cero.

Puedes encontrar el código de este tutorial en este repositorio. He ido separando en diferentes commits los pasos que se dan en el tutorial para así facilitar el seguimiento del mismo.

2. Entorno

Este tutorial se ha desarrollado en el siguiente entorno:

  • Portátil MacBook Pro (Retina, 15′, mediados 2015), macOS Sierra 10.12.5
  • Liferay Community Edition Portal 7.0.2 GA3 (Wilberforce / Build 7002 / August 5, 2016)
  • Java 1.8.0_131
  • PostgreSQL 9.6.2
  • IntelliJ IDEA Ultimate 2017.1.3

3. Preparar el proyecto

Si seguiste el tutorial de Service Builder, entonces ya tendrás un proyecto preparado con entidades Libro y Escritor relacionadas de forma M-N: un libro puede ser escrito por varios escritores y un escritor puede escribir diferentes libros. Si no lo tienes, sigue una serie de pasos para generar un proyecto similar al del tutorial de Service Builder, con la salvedad de que no tendremos clases de actualización de base de datos (UpgradeProcess y UpgradeStepRegistrator), ya que generaremos el modelo final de primeras y no haremos cambios sobre él, es decir, nuestro service.xml no cambiará.

Para preparar el proyecto, empieza abriendo la terminal y sigue los siguientes pasos:

  1. Creamos el proyecto Liferay:
    ~/workspaces/pruebas
    $ blade init tutorial-liferay7-crud
    
  2. Añadimos el paquete Liferay Portal + Tomcat:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ ./gradlew initBundle
    
  3. Arrancamos el servidor local:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ blade server start
    
  4. Una vez se haya levantado, accedemos a http://localhost:8080/ y vemos el asistente de configuración de Liferay Portal. Como vamos a persistir en base de datos, no usaremos Hypersonic, sino que emplearemos otra base de datos (puedes seguir el tutorial Configurar Liferay 7 con PostgreSQL para ello).
  5. Tras haber configurado Liferay Portal y accedido a él (nos habrá pedido reiniciarlo al elegir otra base de datos como PostgreSQL), utilizamos la plantilla service-builder de Blade CLI para generar los módulos libro-api y libro-service:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ blade create -t service-builder -p tutoriales.liferay.crud.libro libro
    
  6. Creamos, desde el directorio libro, el módulo libro-web, donde tendremos nuestro portlet MVC:
    ~/workspaces/pruebas/tutorial-liferay7-crud/modules/libro
    $ blade create -t mvc-portlet -p tutoriales.liferay.crud.libro -c MyMvcPortlet libro-web
    
  7. Abrimos el archivo bnd.bnd del módulo libro-web y cambiamos la línea:
    Bundle-SymbolicName: tutoriales.liferay.crud.libro
    

    por:

    Bundle-SymbolicName: tutoriales.liferay.crud.libro.web
    
  8. Modificamos el archivo service.xml:
    <?xml version="1.0"?>
    <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">
    
    <service-builder package-path="tutoriales.liferay.crud.libro">
        <namespace>LIBRO</namespace>
    
        <entity name="Libro" uuid="true" local-service="true" remote-service="false">
            <!-- PK fields -->
            <column name="libroId" primary="true" type="long"/>
    
            <!-- Group instance -->
            <column name="groupId" type="long"/>
    
            <!-- Audit fields -->
            <column name="companyId" type="long"/>
            <column name="userId" type="long"/>
            <column name="userName" type="String"/>
            <column name="createDate" type="Date"/>
            <column name="modifiedDate" type="Date"/>
    
            <!-- Other fields -->
            <column name="titulo" type="String"/>
            <column name="publicacion" type="Date"/>
            <column name="genero" type="String"/>
            <column name="escritores" type="Collection" entity="Escritor" mapping-table="Libros_Escritores"/>
    
            <!-- Order -->
            <order by="asc">
                <order-column name="titulo"/>
            </order>
    
            <!-- Finder methods -->
            <finder name="Titulo" return-type="Collection">
                <finder-column name="titulo"/>
            </finder>
        </entity>
    
        <entity name="Escritor" uuid="true" local-service="true" remote-service="false">
            <!-- PK fields -->
            <column name="escritorId" primary="true" type="long"/>
    
            <!-- Group instance -->
            <column name="groupId" type="long"/>
    
            <!-- Audit fields -->
            <column name="companyId" type="long"/>
            <column name="userId" type="long"/>
            <column name="userName" type="String"/>
            <column name="createDate" type="Date"/>
            <column name="modifiedDate" type="Date"/>
            <column name="libros" type="Collection" entity="Libro" mapping-table="Libros_Escritores"/>
    
            <!-- Other fields -->
            <column name="nombre" type="String"/>
    
            <!-- Order -->
            <order by="asc">
                <order-column name="nombre"/>
            </order>
    
            <!-- Finder methods -->
            <finder name="Nombre" return-type="Collection">
                <finder-column name="nombre"/>
            </finder>
        </entity>
    </service-builder>
    
  9. Generamos el código con Service Builder:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ ./gradlew buildService
    
  10. Añadimos el módulo libro-api como dependencia de libro-web, para lo cual incluimos la línea compileOnly project(«:modules:libro:libro-api») en el archivo build.gradle del módulo libro-web.
  11. Desplegamos los módulos:
    ~/workspaces/pruebas/tutorial-liferay7-crud
    $ blade deploy
    

    Si nos dio algún error del tipo «Unresolved requirement: Import-Package: tutoriales.liferay.crud.libro.exception», volvemos a ejecutar blade deploy. Este error es debido a que blade deploy despliega, en orden, libro-service, libro-web y libro-api y, como libro-service depende de libro-api, no puede desplegarlo. La segunda vez que ejecutamos el comando, como ya está desplegado libro-api, no tenemos problema. Si esto no funcionase, podemos ejecutar gradle clean && gradle build && gradle deploy en cada módulo y volver a probar.

Si accedemos a Liferay Portal, podemos buscar el portlet por el nombre «libro-web Portlet» y añadirlo a nuestro portal (puedes ver cómo hacerlo en el tutorial sobre Blade CLI). En este momento, el portlet simplemente mostrará los textos «libro-web Portlet» y «Hello from libro-web JSP!».

4. Analizar el módulo libro-web

Tras usar Blade CLI para crear este módulo, en el paquete java se generó únicamente la clase MyMvcPortlet. En resources, el archivo Language.properties y los JSP init.jsp y view.jsp. Muy pocos archivos en comparación con todos los que generó Service Builder en los módulos libro-api y libro-service.

Si seguimos el patrón de diseño MVC (Modelo-Vista-Controlador), nuestro modelo fue el que generamos con Service Builder; la vista, los JSP; y el controlador, el portlet MyMvcPortlet. Nuestro portlet recibirá peticiones de la vista, operará con el modelo y responderá a la vista.

4.1. MVCPortlet

MyMvcPortlet extiende MVCPortlet, que es la clase que Liferay nos insta a emplear, y está anotada con @Component para indicar que es un servicio declarativo de OSGi. Sus elementos son:

  • immediate = true. Indica que el componente se active inmediatamente después de ser instalado.
  • service = Portlet.class. Especifica el tipo bajo el cual registrar este componente como servicio. En nuestro caso será javax.portlet.Portlet.
  • property = {…}. Conjunto de propiedades del componente. Explicaremos algunas más adelante.

En principio, la lógica del controlador será sencilla, así que tendremos todo nuestro código en la clase MyMvcPortlet. Posteriormente lo separaremos en comandos MVC de Liferay.

4.2. JSP

Los archivos JSP formarán nuestra vista. Liferay nos dice que es una buena práctica que el archivo init.jsp contenga todos nuestros import de Java, declaraciones taglib e inicialización de variables.

init.jsp es incluido por view.jsp, que es el JSP que representa la vista del portlet. Pero ¿dónde se establece que esto sea así? En la clase MyMvcPortlet. Si nos fijamos en la propiedad javax.portlet.init-param.view-template, veremos que su valor es /view.jsp.

4.3. Language.properties

El archivo Language.properties está compuesto de pares nombre=valor y sirve para definir los textos que aparecen en nuestro portlet. Gracias a él podemos internacionalizar nuestro portlet, pues podemos crear archivos Language_en.properties, Language_es.properties, Language_fr.properties, etc. en los que incluir los textos en diferentes idiomas.

5. Realizar operaciones CRUD

Vamos a dar funcionalidad a nuestro portlet para que sea capaz de realizar operaciones CRUD sobre el modelo de libros y escritores definido. De esta manera, aprenderemos cómo desarrollar con portlets MVC y vista en JSP y cómo acceder desde el portlet al código generado por Service Builder.

5.1. [C] Crear

Queremos que nuestro portlet permita guardar escritores en base de datos. Tendrá un campo para escribir el nombre y un botón para guardar. La clase MyMvcPortlet recibirá el nombre del escritor y empleará la capa de servicio generada por Service Builder para añadir un nuevo escritor a base de datos.

En el tutorial Persistencia en Liferay 7 con Service Builder, vimos que las clases LibroLocalServiceUtil y EscritorLocalServiceUtil son nuestro punto de entrada a la capa de servicio. Para añadir un escritor desde MyMvcPortlet, tendríamos que usar EscritorLocalServiceUtil, pero vemos que su método addEscritor pide por parámetro un objeto Escritor. Escritor es una interfaz implementada por EscritorImpl, pero esta implementación es del módulo libro-service, del cual libro-web no depende —y queremos mantener esto así—, así que no podemos crear un Escritor desde MyMvcPortlet. ¿Qué hacemos entonces? Pues, básicamente, añadir un método a EscritorLocalServiceUtil que nos permita añadir escritores a partir de su nombre —y de un par de campos de multitenencia y auditoría—.

Vale, tenemos que añadir el nuevo método a EscritorLocalServiceUtil, pero esta clase es de libro-api, así que no debemos escribir nuestros propios métodos ahí. Lo que hay que hacer es desarrollar nuestro método en EscritorLocalServiceImpl (una de las poquitas clases que podemos editar) y, con Service Builder, regenerar EscritorLocalServiceUtil para que lo añada a ella y así podamos utilizarlo desde nuestro portlet. Vamos a ello.

Empezamos añadiendo el código a nuestra clase EscritorLocalServiceImpl:

package tutoriales.liferay.crud.libro.service.impl;

import aQute.bnd.annotation.ProviderType;
import tutoriales.liferay.crud.libro.model.Escritor;
import tutoriales.liferay.crud.libro.model.impl.EscritorImpl;
import tutoriales.liferay.crud.libro.service.base.EscritorLocalServiceBaseImpl;

@ProviderType
public class EscritorLocalServiceImpl extends EscritorLocalServiceBaseImpl {

    public void addEscritor(long groupId, long companyId, long userId, String userName, String nombre) {
        final Escritor escritor = new EscritorImpl();
        escritor.setEscritorId(counterLocalService.increment());
        escritor.setGroupId(groupId);
        escritor.setCompanyId(companyId);
        escritor.setUserId(userId);
        escritor.setUserName(userName);
        escritor.setNombre(nombre);

        addEscritor(escritor);
    }

}

Detalles a tener en cuenta:

  • Parámetros del método. Recordemos que nuestro Escritor tenía una serie de campos además del nombre, por lo que tendremos que especificarlos, aunque no todos: no hace falta preocuparse por los atributos createDate y modifiedDate (cuándo se creo y cuándo se modificó el escritor), pues estos se asignan automáticamente; sin embargo, no nos libramos de establecer cuál es el groupId y el companyId (identificadores del sitio y de la instancia del portal, respectivamente) y el userId y userName (identificador y nombre del usuario que crea el escritor).
  • Creación de Escritor. Aquí ya podemos importar EscritorImpl para crear una instancia de Escritor.
  • Adición de Escritor. Una vez construimos el escritor, lo añadimos a base de datos con el método addEscritor que ya tenía la clase.

Ahora ejecutamos Service Builder para generar el método en EscritorLocalServiceUtil:

~/workspaces/pruebas/tutorial-liferay7-crud
$ ./gradlew buildService

Ya podemos usar el método desde nuestro portlet. Veamos su código primero:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.portlet.bridges.mvc.MVCPortlet;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.WebKeys;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.Portlet;
import javax.portlet.ProcessAction;

@Component(
        immediate = true,
        property = {
                "com.liferay.portlet.display-category=category.sample",
                "com.liferay.portlet.instanceable=true",
                "javax.portlet.display-name=Libros y escritores",
                "javax.portlet.init-param.template-path=/",
                "javax.portlet.init-param.view-template=/view.jsp",
                "javax.portlet.resource-bundle=content.Language",
                "javax.portlet.security-role-ref=power-user,user"
        },
        service = Portlet.class
)
public class MyMvcPortlet extends MVCPortlet {

    @ProcessAction(name = "addEscritor")
    public void addEscritor(ActionRequest request, ActionResponse response) {
        final ThemeDisplay td = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
        final String nombre = ParamUtil.getString(request, "nombreEscritor");

        EscritorLocalServiceUtil.addEscritor(td.getSiteGroupId(), td.getCompanyId(), td.getUser().getUserId(), td.getUser().getFullName(), nombre);
    }

}

Hemos creado un método addEscritor anotado con @ProcessAction y un atributo name con valor «addEscritor». Esta anotación sirve para indicar que el método addEscritor será el que se ejecute cuando desde la vista (desde JSP) se realice una petición al servidor para procesar la acción «addEscritor». Por cierto, el nombre de la acción y el nombre del método no tienen por qué ser iguales.

Cuando el portlet reciba la petición de ejecutar esta acción, creará un escritor. Para ello necesita su nombre, y este viene dado por la vista en la petición, en el parámetro cuyo nombre es «nombreEscritor». Además del nombre, el método EscritorLocalServiceUtil.addEscritor necesita otra serie de campos. Afortunadamente, estos los podemos obtener de la petición creando un objeto ThemeDisplay.

Ahora nos queda definir la vista en el archivo view.jsp. Vamos a tener un formulario con un campo de texto en el que introducir el nombre del escritor y un botón para crear dicho escritor.

<%@ page language="java" contentType="text/html; charset=UTF-8" %>

<%@ include file="./init.jsp" %>

<portlet:actionURL name="addEscritor" var="addEscritorUrl"/>

<aui:form action="${addEscritorUrl}">
    <aui:input name="nombreEscritor" type="textarea" label="Escribe aquí el nombre del escritor:"/>
    <aui:button name="addEscritorButton" type="submit" value="Crear escritor"/>
</aui:form>

Analicemos el código. Por una parte, creamos una variable actionURL, llamada addEscritorUrl, que apunta a «addEscritor», el nombre de la acción para indicar al portlet que cree un escritor (recordemos la anotación @ProcessAction(name = «addEscritor»)). Después, creamos un formulario al que le pasamos la variable addEscritorUrl para que sepa dónde mandar los datos. Dentro del formulario tenemos un botón y un campo de texto en el que escribir el nombre del autor. El nombre de este campo («nombreEscritor») es el que usamos en el portlet, el que vimos que utilizábamos para recoger su valor de la petición (ParamUtil.getString(request, «nombreEscritor»);).

Con el código listo, desplegamos con blade deploy y accedemos a Liferay Portal para ver nuestro portlet con los nuevos cambios. Seguramente te pase como a mí y veas el portlet, aparentemente, correcto:

Sin embargo, si intentas añadir un escritor, obtendrás una excepción java.lang.NoSuchMethodError y el portlet fallará:

Para solucionarlo, ejecuta gradle clean && gradle build && gradle deploy en cada módulo (al menos en libro-api), recarga la página y vuelve a probar:

5.2. [R] Leer

El siguiente paso tras crear escritores es poder listarlos: realizar una operación de lectura de base de datos y pintar la colección de escritores recibida en la vista.

Para la creación de escritores, el flujo consistía en que la vista mandaba al portlet el nombre de un escritor y éste hacía uso de la capa de servicio para crear uno nuevo y guardarlo en base de datos. Ahora el sentido será el contrario: cuando se vaya a pintar el portlet, éste hará una petición a base de datos para recuperar todos los escritores y se los mandará a la vista para que los pinte.

Empezamos añadiendo a nuestra clase MyMvcPortlet el siguiente método:

@Override
public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
    final List<Escritor> escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE);

    renderRequest.setAttribute("escritores", escritores);

    super.render(renderRequest, renderResponse);
}

¿Para qué sirve el método render que sobreescribimos? Antes de nada, recordemos que las propiedades «javax.portlet.init-param.template-path=/» y «javax.portlet.init-param.view-template=/view.jsp» de nuestro portlet sirven para saber con qué vista se pinta el portlet: indican, respectivamente, que los JSP se encuentran en la raíz del directorio resources y que aquel que hará de vista será view.jsp. Pues bien, en nuestro caso no nos basta con que se pinte directamente el portlet a partir del archivo view.jsp; antes debemos pasarle la lista de escritores. Ahí es cuando entra en juego el método render.

En el método render, usamos EscritorLocalServiceUtil.getEscritors para recuperar todos los escritores y los añadimos al objeto RenderRequest como atributo de nombre «escritores».

Ahora modificamos el archivo view.jsp para añadir lo siguiente:

<jsp:useBean id="escritores" type="java.util.List<tutoriales.liferay.crud.libro.model.Escritor>" scope="request"/>

<liferay-ui:search-container emptyResultsMessage="No has creado todavía ningún escritor.">
    <liferay-ui:search-container-results results="${escritores}"/>

    <liferay-ui:search-container-row className="tutoriales.liferay.crud.libro.model.Escritor" modelVar="escritor">
        <liferay-ui:search-container-column-text name="Nombre" property="nombre"/>
    </liferay-ui:search-container-row>

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

En la primera línea, recogemos la lista de escritores que mandamos desde el portlet. A continuación, empleamos el Liferay’s Search Container (search-container) para crear una tabla en la que listar los escritores, aunque de momento solo va a tener una columna para mostrar sus nombres. Analicemos su código:

  • El atributo emptyResultsMessage nos permite definir el texto que se pintará si la lista de escritores es vacía.
  • En search-container-results indicamos la lista que vamos a pintar: results=»${escritores}».
  • Con search-container-row aclaramos cuál es la clase de los elementos de nuestra lista: tutoriales.liferay.crud.libro.model.Escritor.
  • Definimos la columna con search-container-column-text. El atributo name es el nombre de la columna y el atributo property es el campo de la clase Escritor que va en ella.

Por cierto, el componente search-container proporciona funcionalidad extra, como paginación, que queda fuera de este tutorial y no usaremos, pero está bien saberlo.

Desplegamos y vemos nuestra lista de escritores:

5.3. [U] Actualizar

Para poder actualizar el nombre de un escritor, vamos a añadir a la tabla una nueva columna que tenga un botón de edición. Al pulsarlo, éste repintará el portlet —cambiará el JSP— para mostrar un campo de texto en el que poner el nuevo nombre y un botón para confirmar los cambios. Cuando se confirme, el portlet pintará su vista inicial, la del listado de escritores —volverá al JSP inicial—. De esta manera, aprenderemos además lo que hay que hacer para que el portlet cambie entre diferentes JSP.

5.3.1. Redirigir al formulario de edición

Empezamos añadiendo una nueva columna a nuestro search-container del archivo view.jsp:

<liferay-ui:search-container-row className="tutoriales.liferay.crud.libro.model.Escritor" modelVar="escritor">
    <liferay-ui:search-container-column-text name="Nombre" property="nombre"/>
    <liferay-ui:search-container-column-jsp name="Editar" path="/escritorActionButtons.jsp"/>
</liferay-ui:search-container-row>

Esta columna es de tipo search-container-column-jsp, lo que quiere decir que incluirá el JSP que indiquemos en su atributo path. El nuevo archivo escritorActionButtons.jsp es el siguiente:

<%@ include file="./init.jsp" %>

<%@ page import="com.liferay.portal.kernel.util.WebKeys" %>
<%@ page import="com.liferay.taglib.search.ResultRow" %>
<%@ page import="tutoriales.liferay.crud.libro.model.Escritor" %>

<%
    final ResultRow row = (ResultRow) request.getAttribute(WebKeys.SEARCH_CONTAINER_RESULT_ROW);
    final Escritor escritor = (Escritor) row.getObject();
%>

<portlet:actionURL name="displayEscritorEdition" var="displayEscritorEditionUrl">
    <portlet:param name="idEscritor" value="<%=String.valueOf(escritor.getEscritorId())%>"/>
</portlet:actionURL>

<liferay-ui:icon-menu>
    <liferay-ui:icon image="edit" message="Editar" url="<%=displayEscritorEditionUrl%>"/>
</liferay-ui:icon-menu>

Este JSP pinta un icono con imagen «edit» que, al ser pulsado, realiza la acción «displayEscritorEdition» guardada en la variable «displayEscritorEditionUrl». Esta acción, que ahora añadiremos a MyMvcPortlet para que la procese, consistirá en que el portlet cambie su vista view.jsp por una nueva vista escritorEdit.jsp en la que haya un campo para poder editar el nombre del escritor. Como tenemos que saber qué escritor queremos editar, habrá que mandar el identificador del mismo. Esto lo hacemos con la línea <portlet:param name=»idEscritor» value=»<%=String.valueOf(escritor.getEscritorId())%>»/>. El escritor lo sacamos de la petición, de su atributo WebKeys.SEARCH_CONTAINER_RESULT_ROW.

Añadimos la acción «displayEscritorEdition» a MyMvcPortlet:

@ProcessAction(name = "displayEscritorEdition")
public void displayEscritorEdition(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
    final String id = request.getParameter("idEscritor");
    final Escritor escritor = EscritorLocalServiceUtil.getEscritor(Long.valueOf(id));

    request.setAttribute("escritor", escritor);
    response.setRenderParameter("mvcPath", "/escritorEdit.jsp");
}

El método recupera el escritor a partir de su identificador, lo añade a la request y redirige a escritorEdit.jsp. Eso último se hace añadiendo a la response el parámetro «mvcPath» —convención de Liferay— con valor la ruta del JSP.

5.3.2. Crear formulario de edición

Creamos la vista escritorEdit.jsp:

<%@ page contentType="text/html; charset=UTF-8" %>

<%@ include file="./init.jsp" %>

<jsp:useBean id="escritor" type="tutoriales.liferay.crud.libro.model.Escritor" scope="request"/>

<portlet:actionURL name="editEscritor" var="editEscritorUrl"/>

<aui:form action="${editEscritorUrl}">
    <aui:input name="nombreEscritor" label="Modifica aquí el nombre del escritor:" value="<%=escritor.getNombre() %>"/>
    <aui:input name="idEscritor" type="hidden" value="<%=String.valueOf(escritor.getEscritorId()) %>"/>
    <aui:button name="editEscritorButton" type="submit" value="Editar escritor"/>
</aui:form>

Este JSP recibe el escritor (useBean), crea una variable en la que guarda una acción para indicar al portlet que debe editar un escritor (actionURL) y pinta un formulario (form) con un campo de texto en el que escribir el nombre («nombreEscritor»), un botón para realizar la acción y un campo de texto oculto («idEscritor») en el que guarda el identificador del escritor para así mandárselo al portlet y que éste sepa qué escritor modificar.

Ahora nos queda añadir a MyMvcPortlet el método para procesar la acción de edición. Este método va a llamar a la capa de servicio para actualizar base de datos, y aquí nos pasa algo parecido a lo que nos ocurría con la creación: el método updateEscritor de EscritorLocalServiceUtil recibe por parámetro un objeto Escritor que no podemos construir desde el portlet. Procedemos, por tanto, de la misma manera que antes: creamos en EscritorLocalServiceImpl un método de actualización a partir del id y nombre del escritor, generamos el código con ./gradlew buildService y usamos el método generado desde nuestro portlet.

El método de actualización en EscritorLocalServiceImpl es:

public void updateEscritor(long id, String nombre) throws PortalException {
    final Escritor escritor = getEscritor(id);
    escritor.setNombre(nombre);

    updateEscritor(escritor);
}

Tras generar el código con Service Builder, creamos en nuestra clase MyMvcPortlet un método que responderá a la petición de ejecución de la acción «editEscritor»:

@ProcessAction(name = "editEscritor")
public void editEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
    final String id = request.getParameter("idEscritor");
    final String nombre = request.getParameter("nombreEscritor");

    EscritorLocalServiceUtil.updateEscritor(Long.valueOf(id), nombre);

    response.setRenderParameter("mvcPath", "/view.jsp");
}

La acción recupera el identificador y el nuevo nombre del escritor, se los envía al método que acabamos de crear para que actualice y, por último, redirige a la vista inicial (view.jsp) utilizando el, ya visto, parámetro «mvcPath».

Desplegamos (recuerda hacer gradle clean && gradle build && gradle deploy en, al menos, libro-api si te falla) y así quedaría nuestro portlet:

5.4. [D] Eliminar

La última operación es la de borrado y, con todo lo que llevamos ya montado, implementarla será fácil.

Vamos a aprovechar la columna en la que pusimos el botón de edición para poner también el de borrado, por lo que podemos empezar cambiándole el nombre, pues la llamamos «Editar». Ahora modificamos nuestro archivo escritorActionButtons.jsp para añadirle la URL a la acción de borrado («deleteEscritor») y el nuevo botón:

<portlet:actionURL name="deleteEscritor" var="deleteEscritorUrl">
    <portlet:param name="idEscritor" value="<%=String.valueOf(escritor.getEscritorId())%>"/>
</portlet:actionURL>

<liferay-ui:icon-menu>
    <liferay-ui:icon image="edit" message="Editar" url="<%=displayEscritorEditionUrl%>"/>
    <liferay-ui:icon image="delete" message="Eliminar" url="<%=deleteEscritorUrl%>"/>
</liferay-ui:icon-menu>

Escribimos el método de borrado en nuestra clase MyMvcPortlet:

@ProcessAction(name = "deleteEscritor")
public void deleteEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
    final String id = request.getParameter("idEscritor");

    EscritorLocalServiceUtil.deleteEscritor(Long.valueOf(id));
}

Desplegamos y ya podemos borrar escritores:

6. Simplificar el controlador con comandos MVC

Veamos cómo ha quedado nuestro controlador:

public class MyMvcPortlet extends MVCPortlet {

    @Override
    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
        final List<Escritor> escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE);

        renderRequest.setAttribute("escritores", escritores);

        super.render(renderRequest, renderResponse);
    }

    @ProcessAction(name = "addEscritor")
    public void addEscritor(ActionRequest request, ActionResponse response) {
        final ThemeDisplay td = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
        final String nombre = ParamUtil.getString(request, "nombreEscritor");

        EscritorLocalServiceUtil.addEscritor(td.getSiteGroupId(), td.getCompanyId(), td.getUser().getUserId(), td.getUser().getFullName(), nombre);
    }

    @ProcessAction(name = "displayEscritorEdition")
    public void displayEscritorEdition(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
        final String id = request.getParameter("idEscritor");
        final Escritor escritor = EscritorLocalServiceUtil.getEscritor(Long.valueOf(id));

        request.setAttribute("escritor", escritor);
        response.setRenderParameter("mvcPath", "/escritorEdit.jsp");
    }

    @ProcessAction(name = "editEscritor")
    public void editEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
        final String id = request.getParameter("idEscritor");
        final String nombre = request.getParameter("nombreEscritor");

        EscritorLocalServiceUtil.updateEscritor(Long.valueOf(id), nombre);

        response.setRenderParameter("mvcPath", "/view.jsp");
    }

    @ProcessAction(name = "deleteEscritor")
    public void deleteEscritor(ActionRequest request, ActionResponse response) throws IOException, PortletException, PortalException {
        final String id = request.getParameter("idEscritor");

        EscritorLocalServiceUtil.deleteEscritor(Long.valueOf(id));
    }

}

Es una clase que realiza pocas operaciones y no es muy larga. Pero ¿qué ocurriría si nuestro portlet soportase más funcionalidad? Seguramente necesitaríamos más métodos anotados con @ProcessAction(name = «nueva_acción») y estos serían más complejos y tendrían más líneas de código. La clase iría creciendo, empeorando así la legibilidad. Para solucionar esto, Liferay ha creado comandos MVC que nos permiten aliviar el código de MVCPortlet: MVC Action Command, MVC Render Command y MVC Resource Command.

6.1. MVC Action Command

Los métodos de nuestra clase MyMvcPortlet podemos transformarlos en MVC Action Commands. Liferay proporciona la interfaz MVCActionCommand y una clase BaseMVCActionCommand que la implementa, por lo que nosotros deberemos extender BaseMVCActionCommand en lugar de implementar MVCActionCommand.

Por supuesto, puedes nombrar como desess a las clases que creemos, pero es buena práctica llamarlas «XXXMvcActionCommand», donde «XXX» es el nombre de la acción. Por ejemplo, si transformamos el método encargado de añadir escritores, como lo nombramos «addEscritor», tiene sentido que creemos una clase llamada «AddEscritorMvcActionCommand»:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.portlet.bridges.mvc.BaseMVCActionCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCActionCommand;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.WebKeys;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;

@Component(
        immediate = true,
        property = {
                "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet",
                "mvc.command.name=addEscritor"
        },
        service = MVCActionCommand.class
)
public class AddEscritorMvcActionCommand extends BaseMVCActionCommand {

    @Override
    protected void doProcessAction(ActionRequest request, ActionResponse response) throws Exception {
        final ThemeDisplay td = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
        final String nombre = ParamUtil.getString(request, "nombreEscritor");

        EscritorLocalServiceUtil.addEscritor(td.getSiteGroupId(), td.getCompanyId(), td.getUser().getUserId(), td.getUser().getFullName(), nombre);
    }

}

El método addEscritor de MyMvcPortlet lo eliminamos y ponemos su contenido en nuestra nueva clase, en el método doProcessAction. Esta clase está anotada como componente de OSGi y sus propiedades son esenciales:

  • javax.portlet.name. Aquí tendremos que poner el nombre de nuestro portlet, que será aquel definido en MyMvcPortlet, en la propiedad homónima. Es decir, la propiedad «javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet» deberá estar tanto en MyMvcPortlet como en AddEscritorMvcActionCommand: la primera identifica al portlet, la segunda lo referencia.
  • mvc.command.name. Esta propiedad define el nombre de la acción a procesar. Debe ser el que teníamos en la ya borrada anotación @ProcessAction.

Haremos lo mismo con las acciones «editEscritor» y «deleteEscritor».

6.2. MVC Render Command

En nuestra clase MyMvcPortlet ya solamente quedan dos métodos: render y displayEscritorEdition. Recordemos que este último, anotado con @ProcessAction, recibía la petición «displayEscritorEdition» que venía del actionURL de escritorActionButtons.jsp y su propósito era redirigir a escritorEdit.jsp. Vamos a editar el archivo JSP para transformar la actionURL en una renderURL:

<portlet:renderURL var="displayEscritorEditionUrl">
    <portlet:param name="mvcRenderCommandName" value="displayEscritorEdition"/>
    <portlet:param name="idEscritor" value="<%= String.valueOf(escritor.getEscritorId()) %>"/>
</portlet:renderURL>

Como vemos, es muy parecida a la antigua actionURL, solo que contiene un parámetro nombrado «mvcRenderCommandName» que indica cuál va a ser el componente MVCRenderCommand que procese la petición. MVCRenderCommand es una interfaz que nosotros tenemos que implementar:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCRenderCommand;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.model.Escritor;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.PortletException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

@Component(
        immediate = true,
        property = {
                "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet",
                "mvc.command.name=displayEscritorEdition"
        },
        service = MVCRenderCommand.class
)
public class EditEscritorMvcRenderCommand implements MVCRenderCommand {

    @Override
    public String render(RenderRequest request, RenderResponse response) throws PortletException {
        final String id = request.getParameter("idEscritor");

        try {
            final Escritor escritor = EscritorLocalServiceUtil.getEscritor(Long.valueOf(id));
            request.setAttribute("escritor", escritor);

        } catch (PortalException e) {
            throw new RuntimeException(e);
        }

        return "/escritorEdit.jsp";
    }

}

Como vemos, de nuevo indicamos el nombre del portlet a través de la propiedad javax.portlet.name y el nombre del comando con mvc.command.name, nombre que indicamos en renderURL.

Por último, borramos el método displayEscritorEdition de MyMvcPortlet, clase que ya quedará así de simple:

public class MyMvcPortlet extends MVCPortlet {

    @Override
    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
        final List<Escritor> escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE);

        renderRequest.setAttribute("escritores", escritores);

        super.render(renderRequest, renderResponse);
    }

}

6.3. MVC Resource Command

El último de los comandos, cuya interfaz es MVCResourceCommand y está implementado por la clase abstracta BaseMVCResourceCommand, es utilizado para procesar peticiones a recursos.

Vamos a dar la opción de descargar los escritores existentes. El usuario pulsará un botón y el navegador le ofrecerá la posibilidad de abrir o descargar un archivo de texto plano con los datos de los escritores que estén guardados en base de datos. Para ello, empezamos creando nuestro comando:

package tutoriales.liferay.crud.libro.portlet;

import com.liferay.portal.kernel.portlet.PortletResponseUtil;
import com.liferay.portal.kernel.portlet.bridges.mvc.BaseMVCResourceCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCResourceCommand;
import com.liferay.portal.kernel.util.ContentTypes;
import org.osgi.service.component.annotations.Component;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;

import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

@Component(
        property = {
                "javax.portlet.name=tutoriales_liferay_crud_libro_portlet_MyMvcPortlet",
                "mvc.command.name=downloadEscritores"
        },
        service = MVCResourceCommand.class
)
public class DownloadEscritoresMvcResourceCommand extends BaseMVCResourceCommand {

    @Override
    protected void doServeResource(ResourceRequest request, ResourceResponse response) throws Exception {
        final String escritores = EscritorLocalServiceUtil.getEscritors(0, Integer.MAX_VALUE).toString();

        final InputStream stream = new ByteArrayInputStream(escritores.getBytes(StandardCharsets.UTF_8));

        PortletResponseUtil.sendFile(request, response, "escritores.txt", stream, ContentTypes.APPLICATION_TEXT);
    }

}

El método recoge los escritores de base de datos, hace un simple toString de ellos (nada más sofisticado para no meter ruido en lo que de verdad nos interesa en este momento) y crea un InputStream a partir de él. Con el método de utilidad PortletResponseUtil.sendFile somos capaces de hacer que el navegador nos permita descargar un archivo de texto con esta información. Por cierto, vemos que las propiedades de este comando —en la anotación @Component— son las mismas que debíamos definir en los otros dos comandos.

Al archivo view.jsp le añadimos el siguiente código:

<portlet:resourceURL id="downloadEscritores" var="downloadEscritoresUrl"/>

<aui:button name="downloadEscritoresButton" type="submit" value="Descargar lista de escritores" onClick="<%=downloadEscritoresUrl%>"/>

Lo que hemos hecho ha sido definir una resourceURL cuyo identificador es el nombre del comando que hemos creado y un botón que nos permita ejecutar la petición.

Si desplegamos, podemos ver en nuestro portlet el nuevo botón de descarga y comprobar que todas las operaciones CRUD que realizábamos antes de crear los comandos siguen funcionando.

7. Trabajar con relaciones M-N

Ya hemos visto cómo hacer para comunicar la vista y el controlador, es decir, cómo se hablan los JSP con el MVCPortlet y sus comandos. También hemos visto cómo emplear la capa de servicio generada por Service Builder y añadir métodos personalizados. Sin embargo, todo esto lo hemos hecho únicamente con la entidad Escritor, ignorando completamente a Libro. Esto ha sido así porque estábamos centrados en aprender los conceptos mencionados, y meter más código hubiese sido añadir complejidad que no tenía que ver con la lección. Ahora, ya aprendido el tema, vamos a ver cómo guardar libros y escritores y que estos estén relacionados.

La funcionalidad a implementar que tendrá nuestro portlet será:

  • [C] Crear libros teniendo que elegir los escritores de cada uno.
  • [R] Listar libros junto a sus escritores.
  • [U] Editar los escritores de un libro.
  • [D] Eliminar libros y la relación con sus escritores y eliminar escritores y la relación con sus libros.

Los escritores con los que estábamos operando se persisten en la tabla LIBRO_Escritor. Además de esta tabla, recordemos que teníamos otras dos: LIBRO_Libro y LIBRO_Libros_Escritores. En la primera se guardan los libros y en la segunda los identificadores de los libros y de los escritores para saber quiénes son los autores de cada libro y qué libros ha escrito cada uno.

Para manejar la persistencia de escritores, implementábamos nuestros métodos personalizados en EscritorLocalServiceImpl y los usábamos, junto a los ya existentes, a través de la clase EscritorLocalServiceUtil desde MyMvcPortlet y sus comandos. Haremos lo propio para los libros con las clases LibroLocalServiceImpl y LibroLocalServiceUtil. ¿Y qué pasa con la relación? No existe un LibrosEscritoresLocalServiceUtil, sino que, al definir la relación M-N en el service.xml y generar el código con Service Builder, se generaron métodos en EscritorLocalServiceUtil y en LibroLocalServiceUtil que nos permiten operar con los identificadores de los libros y de los escritores. En concreto, emplearemos el método public static void setLibroEscritors(long libroId, long[] escritorIds) de la clase EscritorLocalServiceUtil:

package tutoriales.liferay.crud.libro.service.impl;

import aQute.bnd.annotation.ProviderType;
import com.liferay.portal.kernel.exception.PortalException;
import tutoriales.liferay.crud.libro.model.Escritor;
import tutoriales.liferay.crud.libro.model.Libro;
import tutoriales.liferay.crud.libro.model.impl.LibroImpl;
import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil;
import tutoriales.liferay.crud.libro.service.base.LibroLocalServiceBaseImpl;

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Date;

@ProviderType
public class LibroLocalServiceImpl extends LibroLocalServiceBaseImpl {

    public void addLibro(long groupId, long companyId, long userId, String userName, String titulo, LocalDate publicacion, String genero, Collection<Escritor> escritores) {
        final Libro libro = new LibroImpl();

        libro.setLibroId(counterLocalService.increment());
        libro.setGroupId(groupId);
        libro.setCompanyId(companyId);
        libro.setUserId(userId);
        libro.setUserName(userName);

        libro.setTitulo(titulo);
        libro.setPublicacion(localDateToDate(publicacion));
        libro.setGenero(genero);

        addLibro(libro);

        EscritorLocalServiceUtil.setLibroEscritors(libro.getLibroId(), getEscritorIds(escritores));
    }

    private Date localDateToDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }

    public void updateLibro(long id, Collection<Escritor> escritores) throws PortalException {
        EscritorLocalServiceUtil.setLibroEscritors(id, getEscritorIds(escritores));
    }

    private long[] getEscritorIds(Collection<Escritor> escritores) {
        return escritores.stream().mapToLong(Escritor::getEscritorId).toArray();
    }

}

En el método addLibro, primero creamos y añadimos el nuevo libro. Después empleamos el método de EscritorLocalServiceUtil antes mencionado para pasarle el identificador del libro y de los escritores que lo han compuesto; con esto, se añadirán nuevas filas a nuestra tabla LIBRO_Libros_Escritores.

Si queremos modificar quiénes son los escritores de un libro, volveremos a utilizar el mismo método, que se encargará de dejar en LIBRO_Libros_Escritores, para el libro dado, únicamente los identificadores de los escritores que pasamos al método.

Para listar los libros y los escritores de cada uno, utilizamos el siguiente código (que usaríamos en el método render de nuestra clase MyMvcPortlet):

  • Para recoger todos los libros:
    final List<Libro> libros = LibroLocalServiceUtil.getLibros(0, Integer.MAX_VALUE);
    
  • Para obtener los escritores de un libro dado:
    final List<Escritor> escritores = EscritorLocalServiceUtil.getLibroEscritors(libro.getLibroId());
    

Ahora nos queda eliminar filas de LIBRO_Libros_Escritores, y esto no puede ser más fácil: no hay que hacer nada. Basta con que borremos un escritor o un libro para que se borren las filas de la relación que hacen referencia a él, es decir, emplearíamos el método EscritorLocalServiceUtil.deleteEscritor(long escritorId) o LibroLocalServiceUtil.deleteLibro(long libroId) y fin.

El código JSP y los comandos del controlador los omitimos para no hacer el tutorial más largo innecesariamente, pues no aportan nada nuevo a lo ya visto.

8. Conclusiones

En este tutorial hemos visto cómo la capa de servicio generada por Service Builder nos facilita la vida: da acceso a nuestro portlet MVCPortlet para realizar operaciones de persistencia como las CRUD y nos permite generar nuestros propios métodos, todo ello de manera simple cuando sabemos qué clases emplear.

Por otra parte, nos damos cuenta del interés de Liferay por modularizar su framework, llevándolo al desarrollo de portlets a través del cumplimiento del principio de responsabilidad única con los comandos MVC, que nos permiten aligerar nuestro MVCPortlet y definir en cada uno de ellos una acción del usuario.

Por último, hemos sido capaces de emplear la ya veterana tecnología JSP para crear nuestra vista y realizar la comunicación entre ella y los comandos MVC.

9. Referencias

4 COMENTARIOS

  1. Gracias por el tutorial. Estaría bien mencionar que es necesario incluir esta línea en el build.gradle:

    compileOnly project(«:modules:libro:libro-api»)

    De lo contrario, en MyMVCPortlet.java no funciona el import tutoriales.liferay.crud.libro.service.EscritorLocalServiceUtil; y en consecuencia, no se encuentra el método addEscritor.

    Saludos.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad