Skip to content

14. [TD]: Exposición en la web de la capa [metier]

Palabras clave: arquitectura multicapa, Spring, inyección de dependencias, servicio web / jSON, cliente / servidor.

Volvamos a la arquitectura actual de la aplicación TD:

Vamos a evolucionar esta arquitectura hacia la siguiente:

con el fin de exponer en la web la interfaz [IMetier] de la capa de negocio. Para ello, seguiremos la metodología descrita en el apartado 13.5.

14.1. Support

  

Los proyectos de este capítulo se encuentran en la carpeta [support / chap-14].

14.2. El proyecto Eclipse de la capa [métier]

  

14.2.1. Configuración de Maven

El proyecto de la capa [métier] es un proyecto Maven configurado mediante el siguiente archivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-metier-dao-spring-data</artifactId>
    <version>0.1.0</version>

    <!-- dependencias -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
    <dependencies>
        <!-- capa [DAO] -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-dao-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Prueba de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <!-- utiliza UTF-8 para todo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  • líneas 18-22: la dependencia de la capa [DAO] creada en el apartado 12;
  • líneas 23-34: las dependencias necesarias para las pruebas;

14.2.2. Configuración de Spring

  

El proyecto de la capa [métier] es un proyecto Spring configurado mediante el siguiente archivo [MetierConfig]:


package elections.metier.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import elections.dao.config.DaoConfig;

@Import({ DaoConfig.class })
@ComponentScan({ "elections.metier.service" })
public class MetierConfig {
}
  • Aquí no utilizamos la notación [@Configuration], que convierte la clase en una clase de configuración de Spring. La presencia de las anotaciones [@Import, @ComponentScan] la convierte automáticamente en una clase de configuración;
  • línea 8: se importa el archivo de configuración de la capa [DAO]. De este modo, se dispone de todos los beans definidos en dicho archivo;
  • línea 9: hay que buscar otros beans de Spring en la carpeta [elections.metier.service];

14.2.3. Implementación de la capa [métier]

  

La implementación de la capa [métier] es la que se definió en el apartado 8.5.

14.2.4. Prueba de la capa [métier]

  

La clase de prueba es la descrita en el apartado 8.6.


Tarea a realizar: implementa el proyecto de la capa [métier] y supera su prueba unitaria. Genera el archivo de la capa en el repositorio local de Maven (run as/ Maven / install).


14.3. El proyecto Eclipse de la capa [web]

La capa web es una capa Spring MVC:

El proyecto Eclipse tiene la siguiente estructura:

  • [Boot.java] es la clase que inicia el servicio web;
  • [WebConfig.java] es la clase de configuración del servicio web;
  • [Response.java] es la respuesta generada por las distintas instancias de URL del servicio web;
  • [ElectionsController] es la clase de implementación del servicio web;

14.4. Configuración de Maven

El proyecto es un proyecto Maven configurado mediante el siguiente archivo [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-webjson-metier-dao-spring-data</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>elections-webjson-metier-dao-spring-data</name>
    <description>couche métier exposée comme un service web / jSON</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- capa de negocio -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-metier-dao-spring-data</artifactId>
            <version>0.1.0</version>
        </dependency>
        <!-- capa MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • líneas 19-23: la dependencia del archivo de la capa [métier]. Es la que hemos creado en el apartado 14;
  • líneas 25-28: la dependencia para disponer de una aplicación Spring MVC;

14.5. Configuración de Spring

 

La clase [WebConfig] configura el servicio web:


package elections.webjson.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import com.fasterxml.jackson.databind.ObjectMapper;

import elections.metier.config.MetierConfig;

@EnableWebMvc
@Import({ MetierConfig.class })
@ComponentScan({ "elections.webjson.service" })
public class WebConfig {
    // -------------------------------- configuración de la capa [web]
    @Autowired
    private ApplicationContext context;

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
    // mapeador jSON
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

}
  • El significado de esta configuración se ha explicado en el apartado 13.5.3.1. Solo explicaremos las novedades:
  • línea 22: se importa el archivo de configuración de la capa [métier] para poder utilizar todos sus beans;
  • línea 23: se indica que se encontrarán otros beans en la carpeta [elections.webjson.server.service];

14.6. La clase de inicio del servicio web

 

La clase [Boot] inicia el servicio web de la siguiente manera:


package elections.webjson.boot;

import org.springframework.boot.SpringApplication;

import elections.webjson.config.WebConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(WebConfig.class, args);
    }
}
  • línea 10: el método estático [SpringApplication.run] utilizará el archivo de configuración [WebConfig]. Debido a la anotación [@EnableAutoConfiguration], Spring Boot iniciará el servidor Tomcat y desplegará el servicio web en él;

14.7. La respuesta del servicio web URL

 

Todas las solicitudes URL del servicio web / jSON envían el mismo tipo de respuesta:


package elections.webjson.service;

import java.util.List;

public class Response<T> {

    // ----------------- propiedades
    // estado de la operación
    private int status;
    // posibles mensajes de error
    private List<String> messages;
    // el cuerpo de la respuesta
    private T body;

    // constructores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters y setters
...
}

Esta clase se ha presentado y analizado en el apartado 13.5.5.3.

14.8. La implementación del servicio web / jSON

 

El servicio web / jSON se implementa mediante la siguiente clase [ElectionsController]:


package elections.webjson.service;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.metier.service.IElectionsMetier;

@Controller
public class ElectionsController {

    // dependencias de Spring
    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private IElectionsMetier metier;

    @RequestMapping(value = "/getElectionsConfig", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getElectionsConfig() throws JsonProcessingException {
        // respuesta
        Response<ElectionsConfig> response;
        try {
            response = new Response<>(0, null,
                    new ElectionsConfig(metier.getNbSiegesAPourvoir(), metier.getSeuilElectoral()));
        } catch (ElectionsException e1) {
            response = new Response<>(e1.getCode(), e1.getErreurs(), null);
        } catch (RuntimeException e2) {
            response = new Response<>(1000, getErreursForException(e2), null);
        }
        // respuesta
        return jsonMapper.writeValueAsString(response);
    }

    @RequestMapping(value = "/getListesElectorales", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getListesElectorales() throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    @RequestMapping(value = "/setListesElectorales", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String setListesElectorales(HttpServletRequest request) throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    @RequestMapping(value = "/calculerSieges", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String calculerSieges(HttpServletRequest request) throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    // métodos privados -----------------------------
    // lista de mensajes de error de una RuntimeException
    private List<String> getErreursForException(Exception e) {
        // se recupera la lista de mensajes de error de la excepción
        Throwable cause = e;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // se recupera el mensaje solo si !=null y no está vacío
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa siguiente
            cause = cause.getCause();
        }
        return erreurs;
    }

}

Tarea: siguiendo lo realizado en el apartado 13.5.5, completa el código de la clase [ElectionsController].


Notas:

  • aquí no hay filtros jSON, ya que las tablas [CONF] y [LISTES] no están vinculadas entre sí mediante una relación de clave externa, lo que aligera considerablemente el código del servicio web;
  • no hay que olvidar las diferentes anotaciones de Spring necesarias;
  • A los URL se les asignará el nombre de los métodos asociados;
  • el método [setListeElectorales] se invoca con una operación [POST]. El valor enviado es la matriz de listas en competición (de tipo ListeElectorale[]) con sus atributos [sieges, voix, elimine], que deben registrarse en la base de datos. Este método devuelve un tipo [Response<Void>] con un campo [status=0] si no se ha producido ningún error; en caso contrario, devuelve otro valor;
  • el método [calculerSieges] se invoca con una operación [POST]. El valor enviado es la matriz de las listas que se presentan a las elecciones (de tipo ListeElectorale[]) con sus atributos [nom, voix]. Este método devuelve un tipo [Response<ListeElectorale[]>] cuyo cuerpo contiene las listas electorales con sus campos [sieges, elimine] inicializados;

14.9. Tests

Una vez que haya iniciado el servicio web, deberá realizar las siguientes pruebas para asegurarse de que el servicio web funciona correctamente con la utilidad [Advanced Rest Client]:

 

La respuesta jSON a la solicitud anterior es la siguiente: [1]:

1
2

En [2], copia la respuesta al portapapeles y, a continuación, pégala en cualquier editor de texto [3]:

Aísla el valor del campo [body] y cambia, por ejemplo, los votos de las listas. A continuación, en [4], se establecen en 100 los votos de todas las listas:

Comprueba que tu cadena jSON comience por [ et se termine par ]. Estos caracteres sirven para delimitar una tabla jSON. En [5], pega la cadena jSON anterior. Este será el valor que se publicará para el siguiente URL. Para ello, hay que seleccionar el método HTTP [POST] [7].

  • en [6], solicite el URL [setListesElectorales]. Este URL se solicita con un POST. El valor introducido es la tabla jSON de las listas en competición cuyos resultados hay que registrar en la base de datos;

Se obtiene el siguiente resultado:

 

El campo [status=0] indica que no se ha producido ningún error. Para comprobarlo, vuelve a solicitar las listas de la competición y verifica que se hayan tenido en cuenta las modificaciones que habías realizado en las listas:

Volvemos a ejecutar un [POST] para calcular los escaños obtenidos por las listas:

  • en [1]: el URL del cálculo de escaños;
  • en [2]: se genera un [POST];
  • en [3]: las listas que se presentan a las elecciones. Se asignan al campo [voix] los valores del TD, todos los [sieges] se ponen a 0 y todos los campos [elimine] se ponen a «false»;

El resultado obtenido es el siguiente: