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 uma aplicação 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 comunicar com a aplicação web / jSON que expõe a base 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 a funcionar 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. Conhecemo-los todos;
  • o pacote [spring.webjson.client.infrastructure] contém as classes de exceção utilizadas pelo projeto. Conhecemo-las todas;

18.2. Configuração Maven do projeto

O projeto é um projeto Maven configurado pelo seguinte ficheiro [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 registos -->
        <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>
    <!-- plugins -->
    <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 Maven pai [spring-boot-starter-parent], que nos permite definir várias dependências sem indicar a respetiva versão, uma vez que esta é definida no projeto pai;
  • linhas 24-27: embora não estejamos a escrever uma aplicação web, precisamos da dependência [spring-web], que traz consigo a classe [RestTemplate], que permite interagir facilmente com uma aplicação 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. Passado 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 registos;
  • linhas 54-64: a dependência para os testes JUnit. Esta inclui, nomeadamente, a biblioteca JUnit 4, necessária para os testes. Estas dependências têm o atributo [<scope>test</scope>], indicando que só são necessárias para a fase de testes. Não são incluídas no arquivo final do projeto;

18.3. Configuração do Spring

  

A classe [AppConfig] realiza a configuração Spring do cliente HTTP. O 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 procurados no pacote [spring.webjson.client.dao];
  • linha 25: define-se um timeout com a duração de um segundo (1000 ms);
  • linhas 88-91: o bean que devolve este valor;
  • linha 26: o URL do serviço web / jSON;
  • linhas 93-96: o bean que devolve este valor;
  • linhas 72-86: a configuração da classe [RestTemplate] que assegura as trocas com o serviço web / jSON. Quando não é necessário configurá-la, pode-se utilizá-la no código através de um simples [new RestTemplate()]. Neste caso, pretendemos definir o timeout para as trocas de dados com o serviço web / jSON. O bean [timeout] da linha 89 é passado como parâmetro do 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 este componente. Como se baseia neste para comunicar com o serviço web / jSON, as trocas serão efetivamente submetidas ao timeout;
  • linhas 78-80: associa-se à classe [RestTemplate] um conversor jSON. Já abordámos este tema durante a análise do serviço web. O cliente e o servidor trocam linhas de texto. Um conversor encarrega-se de serializar um objeto em texto e, inversamente, de deserializar um texto em objeto. Podem existir vários conversores associados à classe [RestTemplate] e o conversor escolhido num determinado momento depende dos cabeçalhos HTTP enviados pelo servidor. Neste caso, 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: é chamado o método [jsonMapper] da linha 30 para recuperar o mapeador jSON;
  • linhas 38-39: define-se o filtro para obter uma categoria sem os seus produtos;
  • linha 40: o mapeador jSON é configurado desta forma;
  • linhas 42-51: o filtro jSON para obter uma categoria com os seus produtos;
  • linhas 53-60: o filtro jSON para obter um produto sem a sua categoria;
  • linhas 62-70: o filtro jSON para obter um produto com a sua categoria;

Todos estes 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] que comunica com o serviço web que acabámos 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] devolve 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 consultar;
    • [HttpMethod method]: método HTTP da consulta, GET ou POST, conforme o caso,
    • [int errStatus]: código de erro a utilizar na classe [DaoException], caso ocorra um erro durante a comunicação com o servidor,
    • [T2 body]: o valor a enviar caso exista o POST;

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


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();

    // pedido genérico
    @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, podendo, portanto, ser injetada noutros componentes Spring;
  • linhas 22-23: injeção do bean [RestTemplate] definido em [AppConfig] (ver parágrafo 18.3), que assegura 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 numa exceção. Já o encontrámos várias vezes;

Continuemos:


    // pedido genérico
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // a resposta do servidor
        ResponseEntity<Response<T1>> response;
        try {
            // prepara-se 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);
            }
            // execução da 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 o pedido ao servidor e recebe a sua resposta. O componente [RestTemplate] oferece um número significativo de métodos de interação com o servidor, mas apenas o método [exchange] aceita parâmetros genéricos. Foi por esta razão que 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 este. Da mesma forma, o tipo [RequestEntity] encapsula toda a solicitação do cliente, incluindo os cabeçalhos HTTP e o eventual valor enviado;
  • linhas 8-16: temos de construir a solicitação do tipo [RequestEntity]. Esta varia consoante se utilize um GET ou um POST para efetuar a solicitação;
  • linha 10: a consulta para um GET. A classe [RequestEntity] disponibiliza 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 irá enviar;
    • o método [build] utiliza estas 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 lhe indicar que o valor enviado chegará na forma de uma cadeia jSON;
    • o método [accept] permite indicar que aceitamos o tipo [application/json] que o servidor irá enviar;
    • o método [body] define o valor enviado. Este é 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, como código de erro, o parâmetro [errStatus] passado como terceiro parâmetro do método genérico [getResponse] (linha 3);

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


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

package spring.webjson.client.dao;

public class Response<T> {

    // ----------------- propriedades
    // estado 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. Este 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 um erro, lançamos uma exceção que inclui as duas informações [status, exception] da resposta do servidor;
  • linha 14: caso contrário, devolvemos o tipo [T1] contido na resposta do tipo [Response<T1>];

A classe [Client] é geral. 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 curta
    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);

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

    // eliminaçã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 com o mesmo nome do lado do servidor (ver parágrafo 4.8). Serve de classe pai para as classes [DaoCategorie] e [DaoProduit]. Não é idêntica por duas razões:

  • no lado do servidor, a classe [AbstractDao] gere 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 numa transação. No lado do cliente, não há nenhuma base de dados para gerir. Por isso, esta 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 por predefiniçã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 por predefiniçã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 é possível injetar outros componentes Spring;
  • linha 20: a classe [DaoClient] estende a classe [AbstractDao<Categorie>] que acabámos de ver e, por isso, 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 acabámos de construir;

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

18.5.2.1. O método [getAllLongEntities]

O método [getAllLongEntities] gera a versão completa de todas as categorias na base 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 devolve a lista de categorias nas suas versões completas;
  • linha 5: o mapeador jSON, que permitirá serializar o valor enviado (não há nenhum) e deserializar a resposta devolvida pela classe [Client] (categorias nas suas versões completas);
  • linha 7: é chamado o método [getResponse] da classe [Client]. É este método que assegura as comunicações com o serviço web / jSON. Os seus parâmetros são os seguintes:
    • o URL do serviço consultado [/getAllLongCategories];
    • o método [GET] a utilizar;
    • o código de erro a utilizar em caso de erro (232);
    • o valor enviado. Neste caso, não há nenhum;
  • linha 7: na expressão [client.<List<Categorie>, Void>], indicam-se os parâmetros efetivos dos tipos genéricos [T1, T2] do método [getResponse]. Recorde-se 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 devolvido pelo método [getResponse] é armazenado num objeto do tipo [Object]. Isto é um pouco estranho, uma vez que se espera um tipo [List<Categorie>]. Isto deve-se ao facto de o método [getResponse], ao trabalhar com tipos genéricos [T1, T2], devolver sistematicamente um tipo [java.util.LinkedHashMap], que tem de ser então utilizado para obter o tipo correto;
  • linha 9: devolve-se a lista de categorias. Para tal, serializa-se o objeto [map] [mapper.writeValueAsString(map)] numa cadeia jSON, que é resseralizada para um tipo [List<Categorie>];
  • linha 13: recebemos uma lista de categorias, algumas das quais podem ter produtos. Recebemos a versão resumida desses produtos. Assim, quando são deserializados, os objetos [Produit] criados têm o seu campo [categorie==null]. O método [linkCategorieWithProduits] recria a ligação entre um [Produit] e o seu [Categorie];
  • linhas 14-15: interrompe-se a exceção do tipo [DaoException] que o método [getResponse] poderia ter lançado, para a relançar imediatamente. Este comportamento estranho deve-se ao facto de que, se não o fizermos, a exceção do tipo [DaoException] será interrompida pelas linhas 16-18, e isso não é o que pretendemos;
  • linhas 16-18: interceptamos todas as outras exceções para as encapsular num tipo [DaoException]. Recorde-se que a camada [DAO] só deve lançar este 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. Gestão dos 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 gerir as versões longas das categorias. Voltemos à definição deste mapeador na configuração 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. Vê-se que este mapeador jSON é o do objeto [RestTemplate], que gere as trocas HTTP entre o cliente e o servidor. É este mapeador que é utilizado por predefinição para:
    • serializar o valor enviado ao servidor;
    • deserializar a resposta devolvida 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>>() {
                    });
            // reestabelecer 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. Nesta altura, 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<Categorie>. É porque a entidade [Categorie] tem um filtro jSON [jsonFilterCategorie] que foi necessário gerir este. É por isso que existe a linha 2;
  • linha 6: o resultado é submetido a uma segunda serialização/deserialização com este mesmo mapeador para recuperar o tipo List<Categoria>. Na linha 4, o tipo devolvido por [getResponse] é um tipo [Object];

Nos métodos que se seguem, é importante ter em conta que o mapeador jSON solicitado ao contexto Spring é utilizado tanto para o valor enviado (serialização) como para o valor recebido (deserializaçã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 que se segue, isso nunca acontece. Ou o valor enviado não tem filtro (List<Long>, List<String>), ou é o valor recebido que não tem (List<CoreCategorie>, List<CoreProduit>). As entidades com um filtro jSON são apenas [Categorie] e [Produit].

18.5.2.3. O método [getShortEntitiesById]

O método [getShortEntitiesById] devolve as versões curtas das categorias cujas chaves primárias recebe 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 os 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 devolvida pela classe [Client] (categorias nas suas versões curtas). O filtro escolhido não terá qualquer efeito sobre o valor enviado, uma vez que, para os elementos da lista enviada, não existe qualquer filtro;
  • linha 7: é chamado o método [getResponse] da classe pai. É este método que assegura as comunicações com o serviço web / jSON. Os seus parâmetros são os seguintes:
    • o URL do serviço consultado [/getShortCategoriesById];
    • o método [POST] a utilizar;
    • o código de erro a utilizar 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>>], indicam-se os parâmetros efetivos dos tipos genéricos [T1, T2] do método [getResponse]. Recorde-se 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 devolvido pelo método [getResponse] é colocado num objeto do tipo [Object];
  • linha 9: é devolvida a lista de categorias. Para tal, serializa-se o objeto [map] [mapper.writeValueAsString(map)] numa cadeia jSON, que é resseralizada para um tipo [List<Categorie>];

18.5.2.4. O método [saveEntities]

O método [saveEntities] persiste as categorias na base de dados. O 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 na base de dados as categorias passadas como parâmetros. Este método enriquece essas mesmas categorias com as suas chaves primárias. Se as categorias forem passadas juntamente com produtos, estes também são persistidos;
  • linha 5: o mapeador jSON, que permitirá serializar o valor enviado (uma lista de categorias nas suas versões completas) e deserializar a resposta devolvida pela classe [Client] (objetos [CoreCategorie]). O filtro escolhido não terá qualquer efeito no resultado, uma vez que os elementos da lista recebida como resposta não têm filtro;
  • linha 7: chama-se o método [getResponse] da classe pai para efetuar 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 utilizar, neste caso um [POST];
    • o terceiro parâmetro é o código de erro a utilizar em caso de erro (200);
    • o último parâmetro é o valor enviado, neste caso a lista de categorias a conservar;
  • 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: coloca-se a resposta obtida num tipo [Object];
  • linha 9: reconstitui-se a resposta do tipo [List<CoreCategorie>]. A resposta a devolver é 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, reconstituem-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á a ser 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 serviram para testar a camada [DAO] [4]:

  

Estes 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 do lado do cliente (projeto [spring-webjson-client-generic]);
  • em [3], o teste [JUnitTestPushTheLimits] executado no lado do servidor (projeto [spring-jdbc-generic-04]). Verifica-se que a camada de rede provoca muito pouca lentidão em comparação com a provocada pelos acessos ao SGBD;

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

Passamos agora a analisar a seguinte arquitetura:

A alteração encontra-se em [1]. A camada [DAO] do servidor baseia-se numa implementação JPA. Vamos, em primeiro lugar, utilizar uma implementação JPA / Hibernate.

18.7.1. O projeto 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 aceder 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]. O código da camada [web] não sofre, portanto, 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;
  

Existem três tipos de alterações a efetuar. As primeiras encontram-se no ficheiro [pom.xml] de configuração 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>
    <!-- plugins -->
    <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 artefacto Maven;
  • linhas 24-28: a dependência passa a ser do projeto [spring-jpa-generic] e não mais do [spring-jdbc-generic-04];

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

  

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

A última alteração deve ser efetuada no ficheiro 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 importa-se a configuração do projeto [spring-jpa-generic] e não mais a do projeto [spring-jdbc-generic-04];

Feito isto, 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 é que isto funciona?

Funciona e, no entanto, quando se analisa atentamente o código, é surpreendente que funcione. Embora as camadas [DAO] implementadas pelos projetos [spring-jdbc-generic-04] e [spring-jpa-generic] apresentem, de facto, a mesma interface, não manipulam as mesmas entidades [Categorie] e [Produit]: no projeto [spring-jpa-generic], estas entidades têm um campo adicional [EntityType entityType] que pode assumir dois valores:

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

No entanto, acabámos de migrar o projeto [spring-webjson-server-jdbc-generic] para [spring-webjson-server-jpa-generic] sem alterar o código. Como é que isso é possível?

Vamos analisar 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) {
...
            // recupera-se 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: é criado um objeto List<Categoria> a partir de uma cadeia de caracteres jSON:
    • no valor enviado, os produtos não têm o campo [categorie]. De facto, é desnecessário enviar este campo. Se fosse enviado, a deserialização criaria um objeto [Produit] com um campo [categorie] a apontar para um objeto [Categorie] recém-criado. Para n produtos, teríamos assim n objetos [Categorie] criados, quando na verdade basta um único. Além disso, o campo [categorie] dos produtos não apontaria para o objeto correto [Categorie], que é aquele a que pertencem. Assim, neste caso, os produtos têm um campo [categorie==null];
    • nas classes [Categorie] e [Produit], o campo [EntityType entityType] está definido da seguinte forma:

    protected EntityType entityType = EntityType.POJO;

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

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

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


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // regista-se os produtos que vão ser 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);
                    }
                    // aproveita-se para restabelecer (se necessário) a relação produto --> categoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // persistem 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: verifica-se que a ligação [Produit] --> [Categorie] é restabelecida para as entidades POJO (linha 8), o que é o caso aqui. Isso explica por que razão a persistência das categorias funcionou. Este cenário é útil noutras circunstâncias: nunca se pode ter a certeza de que o utilizador associou corretamente os produtos às categorias. Por isso, fazemo-lo por ele;

Agora, analisemos o método [ProduitController.saveProduits], que persiste 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 deverá falhar. Com efeito, com JPA, a persistência de um produto só é possível se o 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 a sua categoria
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // os produtos são guardados
        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, é criada uma ligação a um objeto [Categorie] com a chave primária correta e uma versão que não seja null. Isto é suficiente para que a camada JPA persista corretamente o produto;

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

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

Do lado do cliente, a cadeia 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 forma a gerir os filtros jSON e [jsonFilterCategorie] doobjeto [Categorie] e o filtro [jsonFilterProduit] do objeto [Produit];
  • linha 7: o valor enviado (não existe nenhum aqui) e o valor recebido (List<Categorie>) são serializados/deserializados com este mapeador. Verifica-se que a presença do campo [entityType] na cadeia jSON recebida, apesar de esse campo não existir nas entidades [Categorie] e [Produit] do lado do cliente, não provoca qualquer erro. É ignorado. Se tivesse provocado um erro, teríamos alterado os filtros do lado do cliente para que fosse ignorado.

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

  

Nota: prima Alt+F5 e, em seguida, regenera 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 isto, 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 do 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:

  

Nota: prima Alt+F5 e, em seguida, regenera todos os projetos Maven.

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

Feito isto, 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 introduzir alterações na camada DAO / JPA. Com efeito, de forma incompreensível, os métodos [DaoCategorie.saveEntities] e [DaoProduit.saveEntities] apresentaram erros durante o preenchimento da base de dados, indicando que os elementos destacados não podiam ser guardados. Um elemento destacado é um elemento que tem:

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

Nenhum dos dois casos estava a ser verificado. Como não sabia onde procurar, dupliquei as entidades a persistir numa lista totalmente nova e, nessa altura, os testes funcionaram. Esta alteração poderia ter sido feita:

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

Optei por fazê-lo na camada [DAO / JPA]. É claro que há uma perda de desempenho, mas esta é perfeitamente insignificante quando comparada com os tempos de resposta da camada SGBD. As alteraçõ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 -- por vezes necessário 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);
            }
        }
        // regista-se os produtos que vão ser 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);
                }
            }
        }
        // guardamos 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). É esta lista que é guardada e devolvida ao chamador (linha 52). Isto tem uma consequência importante: é devolvida uma lista diferente da lista passada como parâmetro e, por isso, onde antes se podia escrever:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// análise de [categories]

Agora é necessário 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 — por vezes necessário 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 a sua categoria
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // os produtos são guardados
        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;
  • prima Alt-F5 e regenera 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 isto, executa-se os três testes do cliente genérico [spring-webjson-client-generic]:

  • em [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]);