Skip to content

18. Um cliente programado para o serviço web / jSON

Agora que a base [dbproduitscategories] está disponível na web, vamos escrever um aplicativo que a utilize. Teremos, então, a seguinte arquitetura cliente/servidor:

A aplicação cliente terá três camadas:

  • uma camada [Client HTTP] [3] para se comunicar com o aplicativo web / jSON que expõe o banco de dados;
  • uma camada [DAO] [2] que apresentará a mesma interface que a camada [DAO] [4];
  • uma camada de testes JUnit [1] para verificar se o cliente e o servidor estão funcionando corretamente;

18.1. O projeto Eclipse

O projeto Eclipse do cliente é o seguinte:

 
  • o pacote [spring.webjson.client.config] contém a configuração Spring da camada [DAO];
  • o pacote [spring.webjson.client.dao] contém a implementação da camada [DAO];
  • o pacote [spring.webjson.client.entities] contém os objetos trocados com o serviço web / jSON. Nós os conhecemos; todos;
  • o pacote [spring.webjson.client.infrastructure] contém as classes de exceção utilizadas pelo projeto. Conhecemos todas elas;

18.2. Configuração do Maven para o projeto

O projeto é um projeto Maven configurado pelo seguinte arquivo [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>dvp.spring.database</groupId>
    <artifactId>spring-webjson-client-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <description>Client console du serveur web / jSON</description>
    <name>spring-webjson-client-generic</name>

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

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

    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <!-- biblioteca jSON utilizada pelo Spring -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- componente utilizado pelo Spring RestTemplate -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- biblioteca de logs -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Teste do Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- plug-ins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • linhas 16-20: o projeto pai do Maven [spring-boot-starter-parent], que nos permite definir várias dependências sem especificar suas versões, já que estas são definidas no projeto pai;
  • linhas 24-27: embora não estejamos desenvolvendo um aplicativo web, precisamos da dependência [spring-web], que traz consigo a classe [RestTemplate], a qual permite a integração fácil com um aplicativo web / jSON;
  • linhas 29-36: uma biblioteca jSON;
  • linhas 38-41: uma dependência que nos permitirá associar um timeout às solicitações HTTP do cliente. Um timeout é o tempo máximo de espera pela resposta do servidor. Após esse tempo, o cliente sinaliza um erro de timeout lançando uma exceção;
  • linhas 43-48: a biblioteca Google Guava;
  • linhas 50-53: a biblioteca de logs;
  • linhas 54-64: a dependência para os testes JUnit. Ela inclui, notadamente, a biblioteca JUnit 4, necessária para os testes. Essas dependências possuem o atributo [<scope>test</scope>], indicando que são necessárias apenas para a fase de testes. Elas não são incluídas no arquivo final do projeto;

18.3. Configuração do Spring

  

A classe [AppConfig] realiza a configuração do Spring para o cliente HTTP. Seu código é o seguinte:


package spring.webjson.client.config;

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

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@ComponentScan({ "spring.webjson.client.dao" })
public class AppConfig {

    // constantes
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8081";

    // filtros jSON
    @Bean
    public ObjectMapper jsonMapper(RestTemplate restTemplate) {
        return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortProduit(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongProduit(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // criação do componente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // conversor jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // tempo limite das trocas
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // resultado
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • linha 20: a classe é uma classe de configuração do Spring;
  • linha 21: outros componentes Spring devem ser buscados no pacote [spring.webjson.client.dao];
  • linha 25: define-se um timeout de um segundo (1000 ms);
  • linhas 88-91: o bean que retorna esse valor;
  • linha 26: o URL do serviço web / jSON;
  • linhas 93-96: o bean que retorna esse valor;
  • linhas 72-86: a configuração da classe [RestTemplate] que garante as trocas com o serviço web / jSON. Quando não é necessário configurá-la, é possível utilizá-la no código simplesmente como [new RestTemplate()]. Aqui, queremos definir o timeout para as trocas com o serviço web / jSON. O bean [timeout] da linha 89 é passado como parâmetro para o método [restTemplate] da linha 73;
  • linha 75: o componente [HttpComponentsClientHttpRequestFactory] é o componente que nos permite definir o timeout das trocas (linhas 82-83);
  • linha 76: a classe [RestTemplate] é construída com esse componente. Como ela se baseia nele para se comunicar com o serviço web / jSON, as trocas serão efetivamente submetidas ao timeout;
  • linhas 78-80: associamos à classe [RestTemplate] um conversor jSON. Já abordamos esse assunto ao analisar o serviço web. O cliente e o servidor trocam linhas de texto. Um conversor é responsável por serializar um objeto em texto e, inversamente, por deserializar um texto em objeto. Pode haver vários conversores associados à classe [RestTemplate], e o conversor escolhido em um determinado momento depende dos cabeçalhos HTTP enviados pelo servidor. Aqui, temos apenas um conversor jSON, uma vez que as linhas de texto trocadas são do tipo jSON;
  • linhas 82-83: definem-se os timeout das trocas;
  • linhas 28-70: definem os filtros jSON. São os mesmos que os do servidor apresentados no parágrafo 17.3.2.1;
  • linhas 29-32: o bean [jsonMapper] é o mapeador jSON do conversor [MappingJackson2HttpMessageConverter] que associamos à classe [RestTemplate]. Precisamos dele na definição dos filtros jSON;
  • linhas 34-41: um bean que define o filtro jSON [catégorie sans ses produits]. O método [jsonMapperShortCategorie] recebe como parâmetro o bean [restTemplate] definido na linha 73;
  • linha 37: chama-se o método [jsonMapper] da linha 30 para recuperar o mapeador jSON;
  • linhas 38-39: define-se o filtro para obter uma categoria sem seus produtos;
  • linha 40: o mapeador jSON é configurado dessa forma;
  • linhas 42-51: o filtro jSON para obter uma categoria com seus produtos;
  • linhas 53-60: o filtro jSON para obter um produto sem sua categoria;
  • linhas 62-70: o filtro jSON para obter um produto com sua categoria;

Todos esses beans estarão disponíveis nos códigos da camada [DAO], bem como nos testes JUnit.

18.4. Implementação do cliente HTTP

Acima, a camada [Client HTTP] se comunica com o serviço web que acabamos de criar. Vamos analisá-la agora.

  

A classe [Client] implementa as trocas de dados com o serviço web / jSON. Ela implementa a seguinte interface [IClient]:


package spring.webjson.client.dao;

import org.springframework.http.HttpMethod;

public interface IClient {
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body);
}

A interface possui apenas um método, [getResponse]:

  • linha 6: o método [getResponse] é um método genérico parametrizado por dois tipos:
    • [T1]: é o tipo de resposta esperada do servidor em [Response<T1>], por exemplo, [List<Categorie>],
    • [T2]: é o tipo do parâmetro jSON enviado pelas operações POST, por exemplo, [List<Produit>];
  • linha 6: o método [getResponse] retorna um resultado do tipo T1, por exemplo, [List<Categorie>];
  • linha 6: os parâmetros de [getResponse] são os seguintes:
    • [String url]: o URL a ser consultado;
    • [HttpMethod method]: método HTTP da consulta, GET ou POST, conforme o caso;
    • [int errStatus]: código de erro a ser utilizado na classe [DaoException], caso ocorra um erro durante a comunicação com o servidor,
    • [T2 body]: o valor a ser enviado caso exista POST;

A classe [Client] implementa a interface [IClient] da seguinte maneira:


package spring.webjson.client.dao;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import spring.webjson.client.infrastructure.DaoException;

@Component
public class Client implements IClient {

    // injeções
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // local
    private String simpleClassName = getClass().getSimpleName();

    // solicitação genérica
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }

    // lista de mensagens de erro de uma exceção
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  • linha 18: a classe [Client] é um componente Spring e, portanto, pode ser injetada em outros componentes Spring;
  • linhas 22-23: injeção do bean [RestTemplate] definido em [AppConfig] (ver parágrafo 18.3), que garante a comunicação com o servidor;
  • linhas 24-25: injeção do bean URL do serviço web / jSON, definido em [AppConfig] (ver parágrafo 18.3);
  • linhas 37-39: o método privado [getMessagesForException] é um método utilitário que permite obter a lista de mensagens de erro contidas em uma exceção. Já o encontramos várias vezes;

Vamos continuar:


    // solicitação genérica
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // a resposta do servidor
        ResponseEntity<Response<T1>> response;
        try {
            // preparando a solicitação
            RequestEntity<?> request = null;
            if (method == HttpMethod.GET) {
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            if (method == HttpMethod.POST) {
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(body);
            }
            // executando a consulta
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // encapsulando a exceção
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  • linha 18: a instrução que envia a solicitação ao servidor e recebe sua resposta. O componente [RestTemplate] oferece um grande número de métodos de comunicação com o servidor, mas apenas o método [exchange] aceita parâmetros genéricos. Por esse motivo, ele foi escolhido. O segundo parâmetro define o tipo da resposta esperada. O primeiro parâmetro é a solicitação do tipo [RequestEntity] (linha 8). O resultado do método [exchange] é do tipo [ResponseEntity<Response<T1>>] (linha 5). O tipo [ResponseEntity] encapsula a resposta completa do servidor, incluindo os cabeçalhos HTTP e o documento enviado por ele. Da mesma forma, o tipo [RequestEntity] encapsula toda a solicitação do cliente, incluindo os cabeçalhos HTTP e o eventual valor enviado por POST;
  • linhas 8-16: precisamos construir a solicitação do tipo [RequestEntity]. Ela varia dependendo se utilizamos um GET ou um POST para fazer a solicitação;
  • linha 10: a consulta para um GET. A classe [RequestEntity] oferece métodos estáticos para criar as consultas GET, POST, HEAD,... O método [RequestEntity.get] permite criar uma consulta GET encadeando os diferentes métodos que a constroem:
    • o método [RequestEntity.get] aceita como parâmetro o URL de destino na forma de uma instância URI,
    • o método [accept] permite definir os elementos do cabeçalho HTTP [Accept]. Aqui, indicamos que aceitamos o tipo [application/json] que o servidor enviará;
    • o método [build] utiliza essas diferentes informações para construir o tipo [RequestEntity] da solicitação;
  • linha 14: a solicitação para um POST. O método [RequestEntity.post] permite criar uma solicitação POST encadeando os diferentes métodos que a constroem:
    • o método [RequestEntity.post] aceita como parâmetro o URL de destino na forma de uma instância URI,
    • o método [header] define um cabeçalho HTTP. Aqui, enviamos ao servidor o cabeçalho [Content-Type: application/json] para indicar que o valor enviado chegará na forma de uma string jSON;
    • o método [accept] permite indicar que aceitamos o tipo [application/json] que o servidor enviará;
    • o método [body] define o valor enviado. Esse valor é o quarto parâmetro do método genérico [getResponse] (linha 1);
  • linhas 20-23: se ocorrer um erro de comunicação com o servidor, é lançada uma exceção do tipo [DaoException] com o código de erro correspondente ao parâmetro [errStatus] passado como terceiro parâmetro do método genérico [getResponse] (linha 3);

O método [getResponse] prossegue da seguinte forma:


// solicitação genérica
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // recuperando o corpo da resposta
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // erros no servidor?
        if (status != 0) {
            // cria-se uma exceção
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // Tudo certo
            return entity.getBody();
        }
    }
  • linha 4: recebemos a resposta do servidor. Ela é do tipo [ResponseEntity<Response<T1>>] (linha 5 do código analisado anteriormente), em que a classe [Response] é a classe já utilizada no lado do servidor:

package spring.webjson.client.dao;

public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // a eventual exceção
    private String exception;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, String exception, T body) {
        this.status = status;
        this.exception = exception;
        this.body = body;
    }

    // getters e setters
...
}

Voltemos ao método [getResponse]:

  • linha 6: recuperamos o documento do tipo [Response<T1>] encapsulado na resposta. Esse tipo possui os campos [int status, String exception, T1 body];
  • linha 7: recuperamos o [status] da resposta, que é um código de erro;
  • linhas 9-12: se houver erro, lançamos uma exceção contendo as duas informações [status, exception] da resposta do servidor;
  • linha 14: caso contrário, retornamos o tipo [T1] contido na resposta do tipo [Response<T1>];

A classe [Client] é geral. Ela pode ser utilizada para qualquer cliente web / jSON.

18.5. Implementação da camada [Dao]

  

18.5.1. A classe [AbstractDao]

A camada [DAO] do lado do cliente apresenta a mesma interface que a camada [DAO] do lado do servidor (ver parágrafo 4.7):


package spring.webjson.client.dao;

import java.util.List;

import spring.webjson.client.entities.AbstractCoreEntity;

public interface IDao<T extends AbstractCoreEntity> {

    // lista de todas as entidades T
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // de entidades específicas — versão resumida
    public List<T> getShortEntitiesById(Iterable<Long> ids);

    public List<T> getShortEntitiesById(Long... ids);

    public List<T> getShortEntitiesByName(Iterable<String> names);

    public List<T> getShortEntitiesByName(String... names);

    // de entidades específicas — versão longa
    public List<T> getLongEntitiesById(Iterable<Long> ids);

    public List<T> getLongEntitiesById(Long... ids);

    public List<T> getLongEntitiesByName(Iterable<String> names);

    public List<T> getLongEntitiesByName(String... names);

    // atualização de várias entidades
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // exclusão de todas as entidades
    public void deleteAllEntities();

    // remoção de várias entidades
    public void deleteEntitiesById(Iterable<Long> ids);

    public void deleteEntitiesById(Long... ids);

    public void deleteEntitiesByName(Iterable<String> names);

    public void deleteEntitiesByName(String... names);

    public void deleteEntitiesByEntity(Iterable<T> entities);

    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}

A classe [AbstractDao] implementa a interface [IDao]. Trata-se de uma classe análoga à classe de mesmo nome no lado do servidor (ver parágrafo 4.8). Ela serve como classe pai para as classes [DaoCategorie] e [DaoProduit]. Ela não é idêntica por dois motivos:

  • no lado do servidor, a classe [AbstractDao] gerencia uma informação:

    // injeções
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

das quais não precisamos aqui.

  • No lado do servidor, a classe [AbstractDao] utiliza anotações [@Transactional] para encapsular cada método em uma transação. No lado do cliente, não há banco de dados para gerenciar. Portanto, essa anotação desaparece;

A classe [AbstractDao] limita-se a verificar a validade dos parâmetros de chamada dos métodos da interface [IDao] antes de delegar a chamada às classes filhas:


package spring.webjson.client.dao;

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

import spring.webjson.client.entities.AbstractCoreEntity;
import spring.webjson.client.infrastructure.MyIllegalArgumentException;

import com.google.common.collect.Lists;

public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {

    // local
    protected String simpleClassName = getClass().getSimpleName();

    @Override
    public List<T1> getShortEntitiesById(Iterable<Long> ids) {
        // validade do argumento
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // resultado
        return getShortEntitiesById(Lists.newArrayList(ids));
    }

    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // validade do argumento
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // resultado
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }

    // métodos privados ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // elementos nulos?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // elementos vazios?
        if (!elements.iterator().hasNext()) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),simpleClassName);
            } else {
                return new ArrayList<T1>();
            }
        }
        // resultado padrão
        return null;
    }

    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // elementos nulos?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // elementos vazios?
        if (elements.length == 0) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("L'argument ne peut être une liste vide"),
                        simpleClassName);
            } else {
                return new ArrayList<T1>();
            }
        }
        // resultado padrão
        return null;
    }

    // métodos protegidos ----------------------------------------------
    abstract protected List<T1> getShortEntitiesById(List<Long> ids);

    abstract protected List<T1> getShortEntitiesByName(List<String> names);

    abstract protected List<T1> getLongEntitiesById(List<Long> ids);

    abstract protected List<T1> getLongEntitiesByName(List<String> names);

    abstract protected List<T1> saveEntities(List<T1> entities);

    abstract protected void deleteEntitiesById(List<Long> ids);

    abstract protected void deleteEntitiesByName(List<String> names);
}

18.5.2. A classe [DaoCategorie]

  

A classe [DaoCategorie] é a seguinte:


package spring.webjson.client.dao;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import spring.webjson.client.entities.Categorie;
import spring.webjson.client.entities.CoreCategorie;
import spring.webjson.client.entities.CoreProduit;
import spring.webjson.client.entities.Produit;
import spring.webjson.client.infrastructure.DaoException;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    @Autowired
    private ApplicationContext context;
    @Autowired
    private IClient client;

...
}
  • linha 19: a classe [DaoClient] é um componente Spring no qual, portanto, é possível injetar outros componentes Spring;
  • linha 20: a classe [DaoClient] estende a classe [AbstractDao<Categorie>] que acabamos de ver e, portanto, implementa a interface [IDao<Categorie>];
  • linhas 22-23: injetamos o contexto Spring para ter acesso aos seus beans;
  • linhas 24-25: injetamos o cliente HTTP que acabamos de construir;

As implementações dos diferentes métodos da interface [DaoCategorie] seguem todas o mesmo esquema. Apresentaremos três métodos: um baseado em uma operação [GET] e os outros dois em uma operação [POST].

18.5.2.1. O método [getAllLongEntities]

O método [getAllLongEntities] gera a versão completa de todas as categorias no banco de dados:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // obter todas as categorias
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // a lista de categorias List<Categoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // reestabelecer a ligação produto --> categoria
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • linha 2: o método retorna a lista de categorias em suas versões completas;
  • linha 5: o mapeador jSON, que permitirá serializar o valor enviado (não há nenhum) e deserializar a resposta retornada pela classe [Client] (categorias em suas versões completas);
  • linha 7: é chamado o método [getResponse] da classe [Client]. É esse método que garante as trocas de dados com o serviço web / jSON. Seus parâmetros são os seguintes:
    • o URL do serviço consultado [/getAllLongCategories];
    • o método [GET] a ser utilizado;
    • o código de erro a ser utilizado em caso de erro (232);
    • o valor enviado. Neste caso, não há nenhum;
  • linha 7: na expressão [client.<List<Categorie>, Void>], são designados os parâmetros efetivos dos tipos genéricos [T1, T2] do método [getResponse]. Vale lembrar que [T1] é o tipo da resposta esperada e [T2], o tipo do valor enviado. Aqui, espera-se um resultado do tipo [List<Categorie>] e não há nenhum valor enviado do tipo [Void];
  • linha 7: o resultado retornado pelo método [getResponse] é armazenado em um objeto do tipo [Object]. Isso é um pouco estranho, já que se espera um tipo [List<Categorie>]. Isso se deve ao fato de que o método [getResponse], ao trabalhar com tipos genéricos [T1, T2], retorna sistematicamente um tipo [java.util.LinkedHashMap], que precisa ser utilizado para gerar o tipo correto;
  • linha 9: retorna-se a lista de categorias. Para isso, serializa-se o objeto [map] [mapper.writeValueAsString(map)] em uma string jSON, que é resseralizada para um tipo [List<Categorie>];
  • linha 13: recebemos uma lista de categorias, algumas das quais podem conter produtos. Recebemos a versão resumida desses produtos. Portanto, quando são deserializados, os objetos [Produit] criados possuem o campo [categorie==null]. O método [linkCategorieWithProduits] recria a ligação entre um [Produit] e seu [Categorie];
  • linhas 14-15: interrompe-se a exceção do tipo [DaoException] que poderia ter sido lançada pelo método [getResponse] para relançá-la imediatamente. Esse comportamento estranho se deve ao fato de que, se isso não for feito, a exceção do tipo [DaoException] será interceptada pelas linhas 16-18, e isso não é o que queremos;
  • linhas 16-18: todas as outras exceções são interceptadas para serem encapsuladas no tipo [DaoException]. Vale lembrar que a camada [DAO] deve lançar apenas esse tipo de exceção;

O método [linkCategorieWithProduits], que recria as ligações entre as entidades [Produit] e as entidades [Categorie], é o seguinte:


    private List<Categorie> linkCategorieWithProduits(List<Categorie> categories) {
        for (Categorie categorie : categories) {
            List<Produit> produits = categorie.getProduits();
            if (produits != null) {
                for (Produit produit : produits) {
                    produit.setCategorie(categorie);
                }
            }
        }
        return categories;
}

18.5.2.2. Gerenciamento de filtros jSON

Voltemos à gestão dos filtros jSON no método [getAllLongEntities] anterior:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // obter todas as categorias
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // a lista de categorias List<Categoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });

  • linha 5: recuperamos do contexto Spring um mapeador jSON capaz de gerenciar as versões completas das categorias. Voltemos à definição desse mapeador na configuração do Spring [AppConfig]:

// filtros jSON
    @Bean
    public ObjectMapper jsonMapper(RestTemplate restTemplate) {
        return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
}
    @Bean
    public RestTemplate restTemplate(int timeout) {
    ...
    }

  • o bean [jsonMapperLongCategorie] solicitado pelo método [getAlllongEntities] é o bean das linhas 7 a 15;
  • linha 10: o mapeador é fornecido pelo método [jsonMapper] das linhas 2 a 5. Vemos que esse mapeador jSON é o do objeto [RestTemplate], que gerencia as trocas HTTP entre o cliente e o servidor. É esse mapeador que é usado por padrão para:
    • serializar o valor enviado ao servidor;
    • desserializar a resposta enviada pelo servidor;

Voltemos ao código de [getAllLongEntities]:


            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // obter todas as categorias
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // lista de categorias List<Categoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // reestabelecemos a ligação produto --> categoria
return linkCategorieWithProduits(categories);
  • linha 2: obtém-se o mapeador [jsonMapperLongCategorie] do contexto Spring;
  • linha 4: o método [getResponse] é executado. Nesse momento, ocorre:
    • serialização automática do valor enviado (não há nenhum aqui);
    • deserialização automática da resposta recebida, neste caso um tipo List<Categoria>. É porque a entidade [Categorie] possui um filtro jSON [jsonFilterCategorie] que foi necessário lidar com ele. Essa é a razão da linha 2;
  • linha 6: o resultado passa por uma segunda serialização/desserialização com esse mesmo mapeador para recuperar o tipo List<Categoria>. Na linha 4, o tipo retornado por [getResponse] é um tipo [Object];

Nos métodos a seguir, vale lembrar que o mapeador jSON solicitado ao contexto Spring é utilizado tanto para o valor enviado (serialização) quanto para o valor recebido (desserialização). Se um ou ambos os valores tiverem um filtro jSON, é necessário configurá-los. O mapeador pode, portanto, ter até dois filtros configurados. No exemplo a seguir, isso nunca ocorre. Ou o valor enviado não possui filtro (List<Long>, List<String>), ou é o valor recebido que não possui (List<CoreCategorie>, List<CoreProduit>). As entidades com o filtro jSON são apenas [Categorie] e [Produit].

18.5.2.3. O método [getShortEntitiesById]

O método [getShortEntitiesById] retorna as versões abreviadas das categorias cujas chaves primárias são passadas como parâmetros:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // obter uma categoria sem seus produtos
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // a categoria
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(223, e2, simpleClassName);
        }
}
  • linha 5: o mapeador jSON, que permitirá serializar o valor enviado (uma lista de chaves primárias) e deserializar a resposta retornada pela classe [Client] (categorias em suas versões curtas). O filtro escolhido não terá nenhum efeito sobre o valor enviado, pois, para os elementos da lista enviada, não há filtro;
  • linha 7: chama-se o método [getResponse] da classe pai. É esse método que garante as trocas com o serviço web / jSON. Seus parâmetros são os seguintes:
    • o URL do serviço consultado [/getShortCategoriesById];
    • o método [POST] a ser utilizado;
    • o código de erro a ser utilizado em caso de erro (204);
    • o valor enviado. Neste caso, trata-se de uma lista de chaves primárias;
  • linha 7: na expressão [client.<List<Categorie>, List<Long>>], são designados os parâmetros efetivos dos tipos genéricos [T1, T2] do método [getResponse]. Vale lembrar que [T1] é o tipo da resposta esperada e [T2], o tipo do valor enviado. Aqui, espera-se um resultado do tipo [List<Categorie>], e o valor enviado é uma lista de chaves primárias do tipo [List<Long>];
  • linha 7: o resultado retornado pelo método [getResponse] é armazenado em um objeto do tipo [Object];
  • linha 9: retorna-se a lista de categorias. Para isso, serializa-se o objeto [map] [mapper.writeValueAsString(map)] em uma string jSON, que é resseralizada para um tipo [List<Categorie>];

18.5.2.4. O método [saveEntities]

O método [saveEntities] mantém as categorias no banco de dados. Seu código é o seguinte:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // adicionar categorias
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // a lista das categorias principais adicionadas
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // atualizamos as categorias com as informações recebidas
            for (int i = 0; i < entities.size(); i++) {
                Categorie categorie = entities.get(i);
                CoreCategorie coreCategorie = coreCategories.get(i);
                categorie.setId(coreCategorie.getId());
                List<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    List<CoreProduit> coreProduits = coreCategorie.getCoreProduits();
                    for (int j = 0; j < produits.size(); j++) {
                        Produit produit = produits.get(j);
                        produit.setId(coreProduits.get(j).getId());
                        produit.setIdCategorie(categorie.getId());
                        produit.setCategorie(categorie);
                    }
                }
            }
            return entities;
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(220, e2, simpleClassName);
        }
    }
  • linha 2: o método [saveEntities] serve para persistir no banco de dados as categorias passadas como parâmetro. Ele torna essas mesmas categorias enriquecidas com suas chaves primárias. Se as categorias forem passadas com produtos, estes também são persistidos;
  • linha 5: o mapeador jSON, que permitirá serializar o valor enviado (uma lista de categorias em suas versões completas) e deserializar a resposta retornada pela classe [Client] (objetos [CoreCategorie]). O filtro escolhido não terá nenhum efeito sobre o resultado, uma vez que os elementos da lista recebida como resposta não possuem filtro;
  • linha 7: chama-se o método [getResponse] da classe pai para realizar as trocas de dados com o serviço web / jSON;
    • o primeiro parâmetro é o URL [/saveCategories];
    • o segundo parâmetro é o método HTTP a ser utilizado, neste caso um [POST];
    • o terceiro parâmetro é o código de erro a ser usado em caso de erro (200);
    • o último parâmetro é o valor enviado, neste caso a lista de categorias a serem mantidas;
  • linha 7: os parâmetros genéricos [T1, T2] do método [getResponse] são, neste caso, [List<CoreCategorie>, List<Categorie>]. O primeiro tipo é o da resposta esperada, o segundo, o tipo do valor enviado;
  • linha 7: a resposta obtida é colocada em um tipo [Object];
  • linha 9: reconstitui-se a resposta do tipo [List<CoreCategorie>]. A resposta a ser devolvida é do tipo [List<Categorie>] (linha 2) e não [List<CoreCategorie>]. A resposta recebida é a lista das chaves primárias das categorias e produtos persistentes;
  • linhas 14-28: as chaves primárias recebidas são atribuídas às categorias e aos produtos (linhas 17, 23, 24). Além disso, reconstrói-se as ligações [Produit] --> [Categorie] (linhas 24-25);

Todos os outros métodos seguem o mesmo esquema.

18.6. O teste JUnit

Voltemos à arquitetura cliente/servidor que está sendo construída:

Construímos uma camada [DAO] [2] com a mesma interface que a camada [DAO] [4]. Para testar a camada [DAO] [2], é possível, portanto, utilizar os testes JUnit que foram usados para testar a camada [DAO] [4]:

  

Esses três testes são executados a partir das seguintes configurações de execução:

 

Os resultados dos três testes são os seguintes:

  • em [1], o teste [JUnitTestCheckArguments];
  • em [2], o teste [JUnitTestDao];
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do cliente (projeto [spring-webjson-client-generic]);
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do servidor (projeto [spring-jdbc-generic-04]). Observa-se que a camada de rede causa muito pouca lentidão em comparação com a causada pelos acessos ao SGBD;

18.7. Implementação do serviço web / jSON / JPA / Hibernate

Passamos agora a analisar a seguinte arquitetura:

A modificação está no [1]. A camada [DAO] do servidor se baseia em uma implementação JPA. Primeiramente, vamos utilizar uma implementação JPA / Hibernate.

18.7.1. O projeto no Eclipse

Por enquanto, os projetos carregados no Eclipse são os seguintes:

  

O projeto [spring-webjson-server-jdbc-generic] baseava-se no projeto [spring-jdbc-generic-04], que configura a camada DAO / JDBC para acesso ao SGBD e ao MySQL. Vamos criar um novo projeto [spring-webjson-server-jpa-generic], que, por sua vez, se baseará no projeto [spring-jpa-generic], que configura a camada DAO / JPA / JDBC de acesso ao SGBD MySQL. Sabemos que, em ambos os casos, a camada [DAO] implementa a mesma interface [IDao]. Portanto, o código da camada [web] não sofre alterações.

Podemos criar o projeto [spring-webjson-server-jpa-generic] copiando e colando o projeto [spring-webjson-server-jdbc-generic]:

  • em [1], indicando uma pasta criada especificamente para o novo projeto;
  

Há três tipos de alterações a serem feitas. As primeiras estão no arquivo [pom.xml] de configuração do Maven do projeto:


<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>dvp.spring.database</groupId>
    <artifactId>spring-webjson-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-webjson-server-jpa-generic</name>
    <description>démo spring mvc</description>

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

    <dependencies>
        <!-- camada web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- camada [DAO] -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plug-ins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • linha 5: altera-se o nome do artefato Maven;
  • linhas 24-28: a dependência agora é do projeto [spring-jpa-generic] e não mais do [spring-jdbc-generic-04];

No final, as dependências são as seguintes:

  

Feito isso, resolvemos todos os problemas de importação que surgiram nas diferentes classes. Por exemplo, as entidades [Produit, Categorie] não precisam mais ser buscadas no projeto [spring-jdbc-generic-04], mas sim no projeto [spring-jpa-generic]. Basta inserir [Ctrl-Maj-O] no código de uma classe para regenerar as importações.

A última modificação deve ser feita no arquivo de configuração [AppConfig]:


package spring.webjson.server.config;

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

@Configuration
@ComponentScan(basePackages = { "spring.webjson.server.service" })
@Import({ spring.data.config.AppConfig.class, WebConfig.class })
public class AppConfig {

}
  • linha 9: agora importamos a configuração do projeto [spring-jpa-generic] e não mais a do projeto [spring-jdbc-generic-04];

Feito isso, estamos prontos. Iniciamos o serviço web com a configuração [spring-webjson-server-jpa-generic-hibernate-eclipselink]:

Em seguida, executamos os três testes do cliente genérico [spring-webjson-client-generic]:

  • no [1], o teste [JUnitTestCheckArguments] (configuração de execução [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • em [2], o teste [JUnitTestDao] (configuração de execução [spring-webjson-client-generic-JUnitTestDao]);
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do cliente (configuração de execução [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • em [4], o teste [JUnitTestPushTheLimits] executado no lado do servidor (configuração de execução [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.7.2. Por que isso funciona?

Funciona e, no entanto, quando se analisa o código com atenção, é surpreendente que funcione. Embora as camadas [DAO] implementadas pelos projetos [spring-jdbc-generic-04] e [spring-jpa-generic] apresentem, de fato, a mesma interface, elas não manipulam as mesmas entidades [Categorie] e [Produit]: no projeto [spring-jpa-generic], essas entidades possuem um campo adicional [EntityType entityType] que tem dois valores possíveis:

  • EntityType.POJO: a entidade é um objeto normal, cujos campos podem ser utilizados livremente;
  • EntityType.PROXY: a entidade é um objeto PROXY gerado pela camada [JPA]. Nesse caso, alguns campos (na verdade, os getters desses campos) não apresentam o comportamento habitual, e foram estabelecidas as seguintes regras:
    • se [Categorie.entityType==EntityType.PROXY], então não se deve usar o método [getProduits];
    • se for [Produit.entityType==EntityType.PROXY], não se deve usar o método [getCategorie];

No entanto, acabamos de migrar o projeto [spring-webjson-server-jdbc-generic] para [spring-webjson-server-jpa-generic] sem nenhuma alteração no código. Como isso é possível?

Vamos examinar o código do método [saveCategories]:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // recuperamos o valor enviado
            String body = CharStreams.toString(request.getReader());
            // deserializa-se
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // persistimos as categorias
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • linha 8: um objeto List<Categoria> é criado a partir de uma string jSON:
    • no valor enviado, os produtos não possuem o campo [categorie]. De fato, é desnecessário enviar esse campo. Se ele fosse enviado, a desserialização criaria um objeto [Produit] com um campo [categorie] apontando para um objeto [Categorie] recém-criado. Para n produtos, teríamos, assim, n objetos [Categorie] criados, quando na verdade é necessário apenas um. Além disso, o campo [categorie] dos produtos não apontaria para o objeto correto [Categorie], que é aquele ao qual eles pertencem. Portanto, aqui os produtos possuem um campo [categorie==null];
    • nas classes [Categorie] e [Produit], o campo [EntityType entityType] está definido da seguinte forma:

    protected EntityType entityType = EntityType.POJO;

Portanto, as entidades [Categorie] e [Produit] criadas pela serialização têm, todas, o tipo POJO.

  • linha 11: as categorias são persistidas. Aqui, isso não deveria funcionar. De fato, se na implementação JDBC, o campo [Produit.categorie] não é necessário para a persistência (é o campo [idCategorie] que é utilizado), para a implementação JPA, ele é absolutamente necessário. Esse campo deve apontar para uma entidade [Categorie], mas aqui seu valor é null.

Vamos examinar o código do método [DaoCategorie.saveEntities] da camada [DAO / JPA]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // registramos os produtos que serão inseridos
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // aproveitamos para restabelecer (se necessário) a relação produto --> categoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // persistimos as categorias/produtos
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // atualiza-se o campo [idCategorie] dos produtos inseridos
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // resultado
        return categories;
    }
  • linhas 13-14: vemos que a ligação [Produit] --> [Categorie] é restabelecida para as entidades POJO (linha 8), o que é o caso aqui. Isso explica por que a persistência das categorias funcionou. Esse cenário é útil em outras circunstâncias: nunca se pode ter certeza de que o usuário vinculou corretamente os produtos às categorias. Portanto, fazemos isso por ele;

Agora, vamos examinar o método [ProduitController.saveProduits], que mantém os produtos:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // recupera-se o valor gravado
            String body = CharStreams.toString(request.getReader());
            // deserializa-se
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // os produtos são mantidos
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // retornamos a resposta
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  • linha 8: um objeto List<Produto> é reconstruído a partir do valor enviado. Pelas razões explicadas anteriormente, cada objeto [Produit] terá um campo:
    • [EntityType entityType] igual a [EntityType.POJO];
    • [Categorie categorie] igual a null;
  • linha 11: a persistência dos produtos deve falhar. De fato, com JPA, a persistência de um produto só é possível se seu campo [categorie] apontar para uma entidade [Categorie];

Vejamos o código do método [DaoProduit.saveEntities] da camada [DAO / JPA]:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // restabelece-se (se necessário) a ligação entre um produto e sua categoria
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // os produtos são mantidos
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  • linhas 3-8: para cada [Produit] do tipo POJO, é criado um link para um objeto [Categorie] com a chave primária correta e uma versão diferente de null. Isso é suficiente para que a camada JPA persista o produto corretamente;

Vamos examinar um último ponto. Os objetos [Categorie] e [Produit] possuem um campo adicional [EntityType entityType] que será serializado como jSON quando esses objetos forem enviados ao cliente. É possível verificar isso com [Advanced Rest Client]:

Do lado do cliente, as entidades [Categorie] e [Produit] foram definidas sem o campo [EntityType entityType]. Isso é normal, uma vez que os objetos [Categorie] e [Produit] são serializados sem suas partes PROXY, [Categorie.produits] e [Produit.categorie]. Do lado do cliente, portanto, não existe o conceito de entidade PROXY. Existem apenas objetos normais.

Do lado do cliente, a sequência jSON [1] é recebida pelo seguinte método [DaoCategorie.getAllShortEntities]:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // obter todas as categorias
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // a lista de categorias List<Categoria>
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  • linha 5: configura-se o mapeador jSON do objeto [RestTemplate] de modo a gerenciar os filtros jSON e [jsonFilterCategorie] doobjeto [Categorie] e o filtro [jsonFilterProduit] do objeto [Produit];
  • linha 7: o valor enviado (não há nenhum aqui) e o valor recebido (List<Categoria>) são serializados/desserializados com esse mapeador. Observa-se que a presença do campo [entityType] na cadeia jSON recebida, embora esse campo não exista nas entidades [Categorie] e [Produit] no lado do cliente, não causa nenhum erro. Ele é ignorado. Se tivesse causado um erro, teríamos modificado os filtros no lado do cliente para que ele fosse ignorado.

Para implementar o serviço web / jSON / JPA / EclipseLink, basta alterar a implementação JPA:

  

Observação: pressione Alt+F5 e, em seguida, gere novamente todos os projetos Maven.

O serviço web será iniciado com a configuração de execução [spring-webjson-server-jpa-generic-hibernate-eclipselink], já utilizada para o Hibernate. Feito isso, execute os três testes do cliente genérico [spring-webjson-client-generic]:

  • no [1], o teste [JUnitTestCheckArguments];
  • no [2], o teste [JUnitTestDao];
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do cliente (projeto [spring-webjson-client-generic]);
  • em [4], o teste [JUnitTestPushTheLimits] executado no lado do servidor (configuração de execução [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.9. Implementação do serviço web / jSON / JPA / OpenJpa

Para implementar o serviço web / jSON / JPA / OpenJpa, basta alterar a implementação JPA:

  

Observação: pressione Alt+F5 e, em seguida, gere novamente todos os projetos Maven.

O serviço web será iniciado com a configuração de execução [spring-webjson-server-jpa-generic-openpa]:

Feito isso, execute os três testes do cliente genérico [spring-webjson-client-generic]:

  • no [1], o teste [JUnitTestCheckArguments] (configuração de execução [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • em [2], o teste [JUnitTestDao] (configuração de execução [spring-webjson-client-generic-JUnitTestDao]);
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do cliente (configuração de execução [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • em [4], o teste [JUnitTestPushTheLimits] executado no lado do servidor (configuração de execução [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

Para que os testes funcionassem, foi necessário fazer alterações na camada DAO / JPA. De fato, de forma incompreensível, os métodos [DaoCategorie.saveEntities] e [DaoProduit.saveEntities] apresentaram falhas ao preencher o banco de dados, indicando que elementos destacados não podiam ser persistidos. Um elemento destacado é aquele que possui:

  • uma chave primária que não seja null;
  • uma versão que não seja null;

Nenhum dos dois casos foi verificado. Como não sabia onde procurar, dupliquei as entidades a serem persistidas em uma lista totalmente nova e, então, os testes funcionaram. Essa modificação poderia ter sido feita:

  • na camada [DAO / JPA];
  • na camada [web], que cria as entidades a serem persistidas;

Optei por fazê-la na camada [DAO / JPA]. É claro que há uma perda de desempenho, mas ela é perfeitamente insignificante em comparação com os tempos de resposta da camada SGBD. As modificações são as seguintes:

Na classe [DaoCategorie] do projeto [spring-jpa-generic]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // clona-se a lista de categorias — necessário às vezes para OpenJpa — bug não compreendido
        // ***************************************************************************************
        List<Categorie> categories2 = new ArrayList<Categorie>();
        for (Categorie categorie : categories) {
            // categoria
            Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
            EntityType categorieType = categorie.getEntityType();
            categorie2.setEntityType(categorieType);
            categories2.add(categorie2);
            // produtos
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                List<Produit> produits2 = new ArrayList<Produit>();
                for (Produit produit : produits) {
                    Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(),
                            produit.getIdCategorie(), produit.getPrix(), produit.getDescription(), produit.getCategorie());
                    produit2.setEntityType(produit.getEntityType());
                    produits2.add(produit2);
                }
                categorie2.setProduits(produits2);
            }
        }
        // registramos os produtos que serão inseridos
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories2) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // aproveita-se para restabelecer (se necessário) a relação produto --> categoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // salvamos as categorias/produtos
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // atualiza-se o campo [idCategorie] dos produtos inseridos
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // resultado
        return categories2;
    }
  • linhas 3-25: a lista [categories] recebida como parâmetro (linha 2) é duplicada na lista [categories2] (linha 6). É essa lista que é armazenada e retornada ao chamador (linha 52). Isso tem uma consequência importante: retorna-se uma lista diferente daquela passada como parâmetro e, portanto, onde antes se poderia escrever:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// análise de [categories]

Agora é preciso escrever:


List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// processamento de [categories]

Na classe [DaoProduit] do projeto [spring-jpa-generic], o método [saveEntities] é alterado de forma semelhante:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // clona-se a lista de produtos — necessário, às vezes, para OpenJpa — bug não incluído
        // ***************************************************************************************
        List<Produit> produits2 = new ArrayList<Produit>();
        for (Produit produit : entities) {
            Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(), produit.getIdCategorie(),
                    produit.getPrix(), produit.getDescription(), produit.getCategorie());
            produit2.setEntityType(produit.getEntityType());
            produits2.add(produit2);
        }

        // restabelece-se (se necessário) a ligação entre um produto e sua categoria
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // os produtos são salvos
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

Para implementar o serviço web / jSON / JPA / EclipseLink / PostgresQL, é necessário instalar:

  • o projeto [postgresql-config-jdbc] de configuração da camada JDBC de PostgreSQL;
  • o projeto [postresql-config-jpa-eclipselink] de configuração da camada JPA do PostgreSQL;
  • pressione Alt-F5 e gere todos os projetos Maven;
  

Executamos o SGBD e o PostgreSQL e iniciamos o serviço web com a configuração de execução [spring-webjson-server-jpa-generic-hibernate-eclipselink] já utilizada anteriormente. Feito isso, execute os três testes do cliente genérico [spring-webjson-client-generic]:

  • no [1], o teste [JUnitTestCheckArguments] (configuração de execução [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • em [2], o teste [JUnitTestDao] (configuração de execução [spring-webjson-client-generic-JUnitTestDao]);
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do cliente (configuração de execução [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • em [4], o teste [JUnitTestPushTheLimits] executado no lado do servidor (configuração de execução [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);