Skip to content

18. Un client programmato per il servizio web / jSON

Ora che la base [dbproduitscategories] è disponibile sul web, scriveremo un’applicazione che la sfrutti. Avremo quindi la seguente architettura client/server:

L’applicazione client avrà tre livelli:

  • un livello [Client HTTP] [3] per comunicare con l’applicazione web / jSON che espone il database;
  • un livello [DAO] [2] che presenterà la stessa interfaccia del livello [DAO] [4];
  • un livello di test JUnit [1] per verificare che il client e il server funzionino correttamente;

18.1. Il progetto Eclipse

Il progetto Eclipse del client è il seguente:

 
  • il pacchetto [spring.webjson.client.config] contiene la configurazione Spring del livello [DAO];
  • il pacchetto [spring.webjson.client.dao] contiene l'implementazione del livello [DAO];
  • il pacchetto [spring.webjson.client.entities] contiene gli oggetti scambiati con il servizio web / jSON. Li conosciamo tutti;
  • il pacchetto [spring.webjson.client.infrastructure] contiene le classi di eccezione utilizzate dal progetto. Le conosciamo tutte;

18.2. Configurazione Maven del progetto

Il progetto è un progetto Maven configurato dal seguente file [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>
        <!-- libreria jSON utilizzata da 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 utilizzato da 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>
        <!-- libreria di log -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot Test -->
        <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>
    <!-- plugin -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • righe 16-20: il progetto Maven padre [spring-boot-starter-parent] che ci permette di definire un certo numero di dipendenze senza specificarne la versione, poiché questa è definita nel progetto padre;
  • righe 24-27: sebbene non stiamo sviluppando un'applicazione web, abbiamo bisogno della dipendenza [spring-web] che include la classe [RestTemplate], la quale consente di interfacciarsi facilmente con un'applicazione web / jSON;
  • righe 29-36: una libreria jSON;
  • righe 38-41: una dipendenza che ci consentirà di associare un timeout alle richieste HTTP del client. Un timeout è il tempo massimo di attesa per la risposta del server. Superato tale tempo, il client segnala un errore timeout generando un'eccezione;
  • righe 43-48: la libreria Google Guava;
  • righe 50-53: la libreria di log;
  • righe 54-64: la dipendenza per i test JUnit. In particolare, include la libreria JUnit 4 necessaria per i test. Queste dipendenze hanno l'attributo [<scope>test</scope>] che indica che sono necessarie solo per la fase di test. Non sono incluse nell'archivio finale del progetto;

18.3. Configurazione Spring

  

La classe [AppConfig] gestisce la configurazione Spring del client HTTP. Il suo codice è il seguente:


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 {

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

    // filtri 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) {
        // creazione del componente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // convertitore jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // timeout delle comunicazioni
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // risultato
        return restTemplate;
    }

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

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • riga 20: la classe è una classe di configurazione Spring;
  • riga 21: altri componenti Spring sono da ricercare nel pacchetto [spring.webjson.client.dao];
  • riga 25: si imposta un timeout di un secondo (1000 ms);
  • righe 88-91: il bean che restituisce questo valore;
  • riga 26: l'URL del servizio web / jSON;
  • righe 93-96: il bean che restituisce questo valore;
  • righe 72-86: la configurazione della classe [RestTemplate] che gestisce le comunicazioni con il servizio web / jSON. Quando non è necessario configurarla, è possibile utilizzarla nel codice semplicemente inserendo [new RestTemplate()]. In questo caso, vogliamo impostare il timeout per gli scambi con il servizio web / jSON. Il bean [timeout] della riga 89 viene passato come parametro al metodo [restTemplate] della riga 73;
  • riga 75: il componente [HttpComponentsClientHttpRequestFactory] è quello che ci permette di impostare il timeout degli scambi (righe 82-83);
  • riga 76: la classe [RestTemplate] è costruita con questo componente. Poiché si basa su di esso per comunicare con il servizio web / jSON, gli scambi saranno effettivamente sottoposti al timeout;
  • righe 78-80: alla classe [RestTemplate] viene associato un convertitore jSON. Ne abbiamo già parlato durante l’analisi del servizio web. Il client e il server si scambiano righe di testo. Un convertitore si occupa di serializzare un oggetto in testo e, viceversa, di deserializzare un testo in oggetto. Possono esserci diversi convertitori associati alla classe [RestTemplate] e quello scelto in un dato momento dipende dalle intestazioni HTTP inviate dal server. In questo caso, abbiamo un solo convertitore jSON poiché le righe di testo scambiate sono di tipo jSON;
  • righe 82-83: si impostano i timeout degli scambi;
  • righe 28-70: definiscono i filtri jSON. Sono gli stessi di quelli del server presentati al paragrafo 17.3.2.1;
  • righe 29-32: il bean [jsonMapper] è il mappatore jSON del convertitore [MappingJackson2HttpMessageConverter] che abbiamo associato alla classe [RestTemplate]. Ne abbiamo bisogno nella definizione dei filtri jSON;
  • righe 34-41: un bean che definisce il filtro jSON [catégorie sans ses produits]. Il metodo [jsonMapperShortCategorie] riceve come parametro il bean [restTemplate] definito alla riga 73;
  • riga 37: si richiama il metodo [jsonMapper] della riga 30 per recuperare il mappatore jSON;
  • righe 38-39: si imposta il filtro per ottenere una categoria senza i relativi prodotti;
  • riga 40: si restituisce il mappatore jSON così configurato;
  • righe 42-51: il filtro jSON per ottenere una categoria con i relativi prodotti;
  • righe 53-60: il filtro jSON per ottenere un prodotto senza la relativa categoria;
  • righe 62-70: il filtro jSON per ottenere un prodotto con la relativa categoria;

Tutti questi bean saranno disponibili nei codici del livello [DAO] e nei test JUnit.

18.4. Implementazione del client HTTP

Quello sopra è il livello [Client HTTP] che comunica con il servizio web che abbiamo appena realizzato. Lo analizziamo ora.

  

La classe [Client] implementa gli scambi con il servizio web / jSON. Essa implementa la seguente interfaccia [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);
}

L'interfaccia ha un solo metodo, [getResponse]:

  • riga 6: il metodo [getResponse] è un metodo generico parametrizzato da due tipi:
    • [T1]: è il tipo di risposta atteso dal server in [Response<T1>], ad esempio [List<Categorie>],
    • [T2]: è il tipo del parametro jSON inviato dalle operazioni POST, ad esempio [List<Produit>];
  • riga 6: il metodo [getResponse] restituisce un risultato di tipo T1, ad esempio [List<Categorie>];
  • riga 6: i parametri di [getResponse] sono i seguenti:
    • [String url]: l’URL da interrogare;
    • [HttpMethod method]: metodo HTTP della richiesta, GET o POST a seconda dei casi,
    • [int errStatus]: codice di errore da utilizzare nella classe [DaoException], in caso di errore durante la comunicazione con il server,
    • [T2 body]: il valore da inviare se è presente POST;

La classe [Client] implementa l'interfaccia [IClient] nel modo seguente:


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 {

    // iniezioni
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

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

    // richiesta generica
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }

    // elenco dei messaggi di errore di un'eccezione
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  • riga 18: la classe [Client] è un componente Spring che può quindi essere iniettato in altri componenti Spring;
  • righe 22-23: iniezione del bean [RestTemplate] definito in [AppConfig] (cfr. paragrafo 18.3) che garantisce la comunicazione con il server;
  • righe 24-25: iniezione del bean URL del servizio web / jSON definito in [AppConfig] (cfr. paragrafo 18.3);
  • righe 37-39: il metodo privato [getMessagesForException] è un metodo di utilità che consente di ottenere l’elenco dei messaggi di errore contenuti in un’eccezione. Lo abbiamo incontrato più volte;

Continuiamo:


    // richiesta generica
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // risposta del server
        ResponseEntity<Response<T1>> response;
        try {
            // si prepara la richiesta
            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);
            }
            // si esegue la richiesta
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // si incapsula l'eccezione
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  • riga 18: l'istruzione che invia la richiesta al server e ne riceve la risposta. Il componente [RestTemplate] offre un numero significativo di metodi di interazione con il server, ma solo il metodo [exchange] ammette parametri generici. È per questo motivo che è stato scelto. Il secondo parametro definisce il tipo di risposta attesa. Il primo parametro è la richiesta di tipo [RequestEntity] (riga 8). Il risultato del metodo [exchange] è di tipo [ResponseEntity<Response<T1>>] (riga 5). Il tipo [ResponseEntity] incapsula la risposta completa del server, le intestazioni HTTP e il documento inviato da quest’ultimo. Allo stesso modo, il tipo [RequestEntity] incapsula l’intera richiesta del client, comprese le intestazioni HTTP e l’eventuale valore inviato tramite POST;
  • righe 8-16: dobbiamo costruire la richiesta di tipo [RequestEntity]. Essa varia a seconda che si utilizzi un GET o un POST per effettuare la richiesta;
  • riga 10: la query per un GET. La classe [RequestEntity] offre metodi statici per creare le richieste GET, POST, HEAD,... Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i diversi metodi che la compongono:
    • il metodo [RequestEntity.get] accetta come parametro l'URL di destinazione sotto forma di un'istanza URI,
    • il metodo [accept] consente di definire gli elementi dell’intestazione HTTP [Accept]. Qui indichiamo che accettiamo il tipo [application/json] che il server invierà;
    • il metodo [build] utilizza queste diverse informazioni per costruire il tipo [RequestEntity] della richiesta;
  • riga 14: la richiesta per un POST. Il metodo [RequestEntity.post] consente di creare una richiesta POST concatenando i diversi metodi che la compongono:
    • il metodo [RequestEntity.post] accetta come parametro l'URL di destinazione sotto forma di un'istanza URI,
    • il metodo [header] definisce un'intestazione HTTP. In questo caso si invia al server l'intestazione [Content-Type: application/json] per indicargli che il valore inviato gli perverrà sotto forma di una stringa jSON;
    • il metodo [accept] consente di indicare che accettiamo il tipo [application/json] che il server invierà;
    • il metodo [body] imposta il valore inviato via POST. Si tratta del quarto parametro del metodo generico [getResponse] (riga 1);
  • righe 20-23: se si verifica un errore di comunicazione con il server, viene generata un’eccezione di tipo [DaoException] con come codice di errore il parametro [errStatus] passato come terzo parametro del metodo generico [getResponse] (riga 3);

Il metodo [getResponse] prosegue come segue:


// richiesta generica
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // si recupera il corpo della risposta
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // errori lato server?
        if (status != 0) {
            // si crea un'eccezione
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // Tutto a posto
            return entity.getBody();
        }
    }
  • riga 4: abbiamo ricevuto la risposta dal server. È di tipo [ResponseEntity<Response<T1>>] (riga 5 del codice precedentemente esaminato), dove la classe [Response] è quella già utilizzata sul lato server:

package spring.webjson.client.dao;

public class Response<T> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // l'eventuale eccezione
    private String exception;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

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

    // getter e setter
...
}

Torniamo al metodo [getResponse]:

  • riga 6: recuperiamo il documento di tipo [Response<T1>] incapsulato nella risposta. Questo tipo presenta i campi [int status, String exception, T1 body];
  • riga 7: recuperiamo il [status] dalla risposta, che è un codice di errore;
  • righe 9-12: in caso di errore, viene generata un’eccezione che riporta le due informazioni [status, exception] presenti nella risposta del server;
  • riga 14: in caso contrario, restituiamo il tipo [T1] contenuto nella risposta di tipo [Response<T1>];

La classe [Client] è generica. Può essere utilizzata per qualsiasi client web / jSON.

18.5. Implementazione del livello [Dao]

  

18.5.1. La classe [AbstractDao]

Il livello lato client [DAO] presenta la stessa interfaccia del livello lato server [DAO] (cfr. paragrafo 4.7):


package spring.webjson.client.dao;

import java.util.List;

import spring.webjson.client.entities.AbstractCoreEntity;

public interface IDao<T extends AbstractCoreEntity> {

    // elenco di tutte le entità T
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // di entità specifiche - versione breve
    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);

    // di entità specifiche - versione estesa
    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);

    // aggiornamento di più entità
    public List<T> saveEntities(Iterable<T> entities);

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

    // eliminazione di tutte le entità
    public void deleteAllEntities();

    // eliminazione di più entità
    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);
}

La classe [AbstractDao] implementa l'interfaccia [IDao]. Si tratta di una classe analoga alla classe omonima sul lato server (cfr. paragrafo 4.8). Funge da classe padre per le classi [DaoCategorie] e [DaoProduit]. Non è identica per due motivi:

  • sul lato server, la classe [AbstractDao] gestisce un’informazione:

    // inserimenti
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

di cui qui non abbiamo bisogno.

  • Sul lato server, la classe [AbstractDao] utilizza le annotazioni [@Transactional] per incapsulare ogni metodo in una transazione. Sul lato client, non c'è alcun database da gestire. Questa annotazione viene quindi eliminata;

La classe [AbstractDao] si limita a verificare la validità dei parametri di chiamata dei metodi dell’interfaccia [IDao] prima di delegare la chiamata alle classi figlie:


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> {

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

    @Override
    public List<T1> getShortEntitiesById(Iterable<Long> ids) {
        // validità dell'argomento
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // risultato
        return getShortEntitiesById(Lists.newArrayList(ids));
    }

    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // validità dell'argomento
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // risultato
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }

    // metodi privati ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // elementi nulli?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // elementi vuoti?
        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>();
            }
        }
        // risultato predefinito
        return null;
    }

    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // elementi nulli?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // elementi vuoti?
        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>();
            }
        }
        // risultato predefinito
        return null;
    }

    // metodi protetti ----------------------------------------------
    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. La classe [DaoCategorie]

  

La classe [DaoCategorie] è la seguente:


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;

...
}
  • riga 19: la classe [DaoClient] è un componente Spring in cui è quindi possibile iniettare altri componenti Spring;
  • riga 20: la classe [DaoClient] estende la classe [AbstractDao<Categorie>] che abbiamo appena visto e implementa quindi l'interfaccia [IDao<Categorie>];
  • righe 22-23: si inietta il contesto Spring per poter accedere ai suoi bean;
  • righe 24-25: si inietta il client HTTP che abbiamo appena creato;

Le implementazioni dei diversi metodi dell’interfaccia [DaoCategorie] seguono tutte lo stesso schema. Presenteremo tre metodi: uno basato su un’operazione [GET], gli altri due su un’operazione [POST].

18.5.2.1. Il metodo [getAllLongEntities]

Il metodo [getAllLongEntities] genera la versione estesa di tutte le categorie presenti nel database:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtri jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // recupera tutte le categorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // l'elenco delle categorie List<Categoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // ricreare il collegamento prodotto --> categoria
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • riga 2: il metodo restituisce l'elenco delle categorie nelle loro versioni estese;
  • riga 5: il mappatore jSON che consentirà di serializzare il valore inviato (non ce n'è uno) e di deserializzare la risposta restituita dalla classe [Client] (le categorie nelle loro versioni estese);
  • riga 7: viene chiamato il metodo [getResponse] della classe [Client]. È questo metodo che gestisce le comunicazioni con il servizio web / jSON. I suoi parametri sono i seguenti:
    • l'ID URL del servizio interrogato [/getAllLongCategories];
    • il metodo [GET] da utilizzare;
    • il codice di errore da utilizzare in caso di errore (232);
    • il valore inviato. In questo caso non ce n'è nessuno;
  • riga 7: nell’espressione [client.<List<Categorie>, Void>] si specificano i parametri effettivi dei tipi generici [T1, T2] del metodo [getResponse]. Si ricorda che [T1] è il tipo della risposta attesa e [T2] il tipo del valore inviato. In questo caso ci si aspetta un risultato di tipo [List<Categorie>] e non è presente alcun valore inviato di tipo [Void];
  • riga 7: il risultato restituito dal metodo [getResponse] viene inserito in un oggetto di tipo [Object]. È un po’ strano, dato che ci si aspetta un tipo [List<Categorie>]. Ciò è dovuto al fatto che il metodo [getResponse], che opera con tipi generici [T1, T2], restituisce sistematicamente un tipo [java.util.LinkedHashMap], che deve quindi essere utilizzato per ottenere il tipo corretto;
  • riga 9: si restituisce l’elenco delle categorie. A tal fine, si serializza l’oggetto [map] [mapper.writeValueAsString(map)] in una stringa jSON che viene poi riserializzata in un tipo [List<Categorie>];
  • riga 13: si riceve un elenco di categorie, alcune delle quali possono contenere prodotti. Si riceve la versione abbreviata di tali prodotti. Pertanto, una volta deserializzati, gli oggetti [Produit] creati presentano il campo [categorie==null]. Il metodo [linkCategorieWithProduits] ricrea il collegamento tra un [Produit] e il suo [Categorie];
  • righe 14-15: si interrompe l’eccezione di tipo [DaoException] che il metodo [getResponse] avrebbe potuto generare, per poi rilanciarla immediatamente. Questo comportamento anomalo è dovuto al fatto che, se non lo si facesse, l’eccezione di tipo [DaoException] verrebbe interrotta dalle righe 16-18, e questo non è auspicabile;
  • righe 16-18: si intercettano tutte le altre eccezioni per incapsularle in un tipo [DaoException]. Si ricorda che il livello [DAO] deve generare solo questo tipo di eccezione;

Il metodo [linkCategorieWithProduits] che ricrea i collegamenti tra le entità [Produit] e le entità [Categorie] è il seguente:


    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. Gestione dei filtri jSON

Torniamo alla gestione dei filtri jSON nel metodo [getAllLongEntities] precedente:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtri jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // recupera tutte le categorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // l'elenco delle categorie List<Categoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });

  • riga 5: dal contesto Spring viene recuperato un mappatore jSON in grado di gestire le versioni estese delle categorie. Torniamo alla definizione di questo mappatore nella configurazione Spring [AppConfig]:

// filtri 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) {
    ...
    }

  • il bean [jsonMapperLongCategorie] richiesto dal metodo [getAlllongEntities] è il bean delle righe 7-15;
  • riga 10: il mappatore è fornito dal metodo [jsonMapper] delle righe 2-5. Si nota che questo mappatore jSON è quello dell'oggetto [RestTemplate] che gestisce gli scambi HTTP tra il client e il server. È questo mappatore che viene utilizzato per impostazione predefinita per:
    • serializzare il valore inviato al server;
    • deserializzare la risposta restituita dal server;

Torniamo al codice di [getAllLongEntities]:


            // filtri jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // recupera tutte le categorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // l'elenco delle categorie List<Categoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // ricreiamo il collegamento prodotto --> categoria
return linkCategorieWithProduits(categories);
  • riga 2: si ottiene il mappatore [jsonMapperLongCategorie] dal contesto Spring;
  • riga 4: viene eseguito il metodo [getResponse]. A questo punto avviene:
    • la serializzazione automatica del valore inviato (qui non ce n’è);
    • deserializzazione automatica della risposta ricevuta, in questo caso di tipo List<Categorie>. Poiché l’entità [Categorie] ha un filtro jSON [jsonFilterCategorie], era necessario gestirlo. Questo è il motivo della riga 2;
  • riga 6: il risultato viene sottoposto a una seconda serializzazione/deserializzazione con lo stesso mappatore per recuperare il tipo List<Categorie>. Riga 4: il tipo restituito da [getResponse] è un tipo [Object];

Nei metodi che seguono, si ricorderà che il mappatore jSON richiesto al contesto Spring viene utilizzato sia per il valore inviato (serializzazione) che per il valore ricevuto (deserializzazione). Se uno o entrambi i valori hanno un filtro jSON, è necessario configurarli. Il mappatore può quindi avere fino a due filtri configurati. Nel seguito, ciò non accade mai. O il valore inviato non ha alcun filtro (List<Long>, List<String>), oppure è il valore ricevuto a non averne (List<CoreCategorie>, List<CoreProduit>). Le entità con un filtro jSON sono esclusivamente [Categorie] e [Produit].

18.5.2.3. Il metodo [getShortEntitiesById]

Il metodo [getShortEntitiesById] restituisce le versioni abbreviate delle categorie di cui riceve le chiavi primarie come parametri:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // filtri jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // recupera una categoria senza i relativi prodotti
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // la 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);
        }
}
  • riga 5: il mappatore jSON che consentirà di serializzare il valore inviato (un elenco di chiavi primarie) e di deserializzare la risposta restituita dalla classe [Client] (le categorie nelle loro versioni abbreviate). Il filtro scelto non avrà alcun effetto sul valore inviato poiché, per gli elementi dell’elenco inviato, non è previsto alcun filtro;
  • riga 7: viene chiamato il metodo [getResponse] della classe padre. È questo metodo che gestisce le comunicazioni con il servizio web / jSON. I suoi parametri sono i seguenti:
    • l'ID URL del servizio interrogato [/getShortCategoriesById];
    • il metodo [POST] da utilizzare;
    • il codice di errore da utilizzare in caso di errore (204);
    • il valore inviato. In questo caso si tratta di un elenco di chiavi primarie;
  • riga 7: nell’espressione [client.<List<Categorie>, List<Long>>] si specificano i parametri effettivi dei tipi generici [T1, T2] del metodo [getResponse]. Si ricorda che [T1] è il tipo della risposta attesa e [T2] il tipo del valore inviato. In questo caso ci si aspetta un risultato di tipo [List<Categorie>] e il valore inviato è un elenco di chiavi primarie di tipo [List<Long>];
  • riga 7: il risultato restituito dal metodo [getResponse] viene inserito in un oggetto di tipo [Object];
  • riga 9: viene restituito l’elenco delle categorie. A tal fine, si serializza l’oggetto [map] [mapper.writeValueAsString(map)] in una stringa jSON che viene poi riserializzata in un tipo [List<Categorie>];

18.5.2.4. Il metodo [saveEntities]

Il metodo [saveEntities] salva le categorie nel database. Il suo codice è il seguente:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // filtri jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // aggiungere categorie
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // l'elenco delle categorie principali aggiunte
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // si aggiornano le categorie con le informazioni ricevute
            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);
        }
    }
  • riga 2: il metodo [saveEntities] serve a salvare nel database le categorie passate come parametro. Arricchisce queste stesse categorie con le loro chiavi primarie. Se le categorie vengono passate insieme ai prodotti, anche questi ultimi vengono salvati;
  • riga 5: il mappatore jSON che consentirà di serializzare il valore inviato (un elenco di categorie nelle loro versioni estese) e di deserializzare la risposta restituita dalla classe [Client] (oggetti [CoreCategorie]). Il filtro scelto non avrà alcun effetto sul risultato, poiché gli elementi dell’elenco ricevuto in risposta non sono filtrati;
  • riga 7: si chiama il metodo [getResponse] della classe padre per effettuare gli scambi con il servizio web / jSON;
    • il primo parametro è URL [/saveCategories];
    • il secondo parametro è il metodo HTTP da utilizzare, in questo caso un [POST];
    • il terzo parametro è il codice di errore da utilizzare in caso di errore (200);
    • l'ultimo parametro è il valore inviato, in questo caso l'elenco delle categorie da salvare;
  • riga 7: i parametri generici [T1, T2] del metodo [getResponse] sono qui [List<CoreCategorie>, List<Categorie>]. Il primo tipo è quello della risposta attesa, il secondo è il tipo del valore inviato;
  • riga 7: si inserisce la risposta ottenuta in un tipo [Object];
  • riga 9: si ricostituisce la risposta di tipo [List<CoreCategorie>]. La risposta da restituire è di tipo [List<Categorie>] (riga 2) e non [List<CoreCategorie>]. La risposta ricevuta è l’elenco delle chiavi primarie delle categorie e dei prodotti salvati;
  • righe 14-28: le chiavi primarie ricevute vengono assegnate alle categorie e ai prodotti (righe 17, 23, 24). Inoltre, si ricostruiscono i collegamenti [Produit] --> [Categorie] (righe 24-25);

Tutti gli altri metodi seguono lo stesso schema.

18.6. Il test JUnit

Torniamo all’architettura client/server in fase di realizzazione:

Abbiamo realizzato un livello [DAO] [2] con la stessa interfaccia del livello [DAO] [4]. Per testare il livello [DAO] [2], è quindi possibile utilizzare i test JUnit che sono stati utilizzati per testare il livello [DAO] [4]:

  

Questi tre test vengono eseguiti a partire dalle seguenti configurazioni di esecuzione:

 

I risultati dei tre test sono i seguenti:

  • in [1], il test [JUnitTestCheckArguments];
  • in [2], il test [JUnitTestDao];
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (progetto [spring-webjson-client-generic]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato server (progetto [spring-jdbc-generic-04]). Si osserva che il livello di rete comporta un rallentamento minimo rispetto a quello causato dagli accessi a SGBD;

18.7. Implementazione del servizio web / jSON / JPA / Hibernate

Ci concentriamo ora sulla seguente architettura:

La modifica è contenuta in [1]. Il livello [DAO] del server si basa su un'implementazione JPA. Per prima cosa utilizzeremo un’implementazione JPA / Hibernate.

18.7.1. Il progetto Eclipse

Al momento, i progetti caricati in Eclipse sono i seguenti:

  

Il progetto [spring-webjson-server-jdbc-generic] si basava sul progetto [spring-jdbc-generic-04], che configura il livello DAO / JDBC per l’accesso a SGBD e MySQL. Creeremo un nuovo progetto [spring-webjson-server-jpa-generic] che a sua volta si baserà sul progetto [spring-jpa-generic], il quale configura il livello DAO / JPA / JDBC per l’accesso a SGBD e MySQL. Sappiamo che in entrambi i casi il livello [DAO] implementa la stessa interfaccia [IDao]. Il codice del livello [web] rimane quindi invariato.

Possiamo creare il progetto [spring-webjson-server-jpa-generic] copiando e incollando il progetto [spring-webjson-server-jdbc-generic]:

  • in [1], indicando una cartella creata appositamente per il nuovo progetto;
  

Ci sono tre tipi di modifiche da apportare. Le prime si trovano nel file [pom.xml] di configurazione Maven del progetto:


<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>
        <!-- livello web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- livello [DAO] -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plugin -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • riga 5: si modifica il nome dell'artefatto Maven;
  • righe 24-28: la dipendenza è ora sul progetto [spring-jpa-generic] e non più su [spring-jdbc-generic-04];

Alla fine, le dipendenze sono le seguenti:

  

Una volta fatto ciò, si risolvono tutti i problemi di importazione che sono emersi nelle diverse classi. Ad esempio, le entità [Produit, Categorie] non vanno più cercate nel progetto [spring-jdbc-generic-04] ma nel progetto [spring-jpa-generic]. È sufficiente inserire [Ctrl-Maj-O] nel codice di una classe per rigenerare le importazioni.

L’ultima modifica va apportata nel file di configurazione [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 {

}
  • riga 9: ora si importa la configurazione del progetto [spring-jpa-generic] e non più quella del progetto [spring-jdbc-generic-04];

A questo punto siamo pronti. Avviamo il servizio web con la configurazione [spring-webjson-server-jpa-generic-hibernate-eclipselink]:

Quindi eseguiamo i tre test del client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], il test [JUnitTestDao] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.7.2. Perché funziona?

Funziona, eppure, se si osserva attentamente il codice, è sorprendente che funzioni. Sebbene i livelli [DAO] implementati dai progetti [spring-jdbc-generic-04] e [spring-jpa-generic] presentino effettivamente la stessa interfaccia, non gestiscono le stesse entità [Categorie] e [Produit]: nel progetto [spring-jpa-generic], queste entità hanno un campo aggiuntivo [EntityType entityType] che può assumere due valori:

  • EntityType.POJO: l’entità è un oggetto normale di cui è possibile utilizzare liberamente tutti i campi;
  • EntityType.PROXY: l'entità è un oggetto PROXY reso dal livello [JPA]. In questo caso, alcuni campi (in realtà i getter di questi campi) non si comportano come di consueto e sono state stabilite le seguenti regole:
    • se [Categorie.entityType==EntityType.PROXY], allora non si deve utilizzare il metodo [getProduits];
    • se [Produit.entityType==EntityType.PROXY], allora non si deve utilizzare il metodo [getCategorie];

Eppure abbiamo appena trasferito il progetto [spring-webjson-server-jdbc-generic] a [spring-webjson-server-jpa-generic] senza modificare il codice. Com’è possibile?

Esaminiamo il codice del metodo [saveCategories]:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // si recupera il valore inviato
            String body = CharStreams.toString(request.getReader());
            // lo deserializziamo
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // si salvano le categorie
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • riga 8: viene creato un oggetto List<Categorie> a partire da una stringa jSON:
    • nel valore inviato, i prodotti non hanno il campo [categorie]. È infatti superfluo inviare questo campo. Se venisse inviato, la deserializzazione creerebbe un oggetto [Produit] con un campo [categorie] che punta a un oggetto [Categorie] appena creato. Per n prodotti, si avrebbero così n oggetti [Categorie] creati, mentre ne occorre uno solo. Inoltre, il campo [categorie] dei prodotti non punterebbe all’oggetto corretto [Categorie], ovvero quello a cui appartengono. Pertanto, in questo caso i prodotti hanno un campo [categorie==null];
    • nelle classi [Categorie] e [Produit], il campo [EntityType entityType] è definito come segue:

    protected EntityType entityType = EntityType.POJO;

Pertanto, le entità [Categorie] e [Produit] create dalla serializzazione hanno tutte il tipo POJO.

  • riga 11: si salvano le categorie. In questo caso non dovrebbe funzionare. Infatti, se nell’implementazione JDBC, il campo [Produit.categorie] non è necessario per la persistenza (viene utilizzato il campo [idCategorie]), per l’implementazione JPA è assolutamente necessario. Questo campo deve puntare a un'entità [Categorie], mentre qui il suo valore è null.

Esaminiamo il codice del metodo [DaoCategorie.saveEntities] del livello [DAO / JPA]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // si annotano i prodotti che verranno inseriti
        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);
                    }
                    // si approfitta dell'occasione per ristabilire (se necessario) la relazione prodotto --> categoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // si salvano le categorie / i prodotti
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // si aggiorna il campo [idCategorie] dei prodotti inseriti
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // risultato
        return categories;
    }
  • righe 13-14: si nota che il collegamento [Produit] --> [Categorie] viene ripristinato per le entità POJO (riga 8), come avviene in questo caso. Questo spiega perché la persistenza delle categorie abbia funzionato. Questo scenario è utile anche in altre circostanze: non si può mai essere sicuri che l’utente abbia collegato correttamente i prodotti alle categorie. Quindi lo facciamo al posto suo;

Ora esaminiamo il metodo [ProduitController.saveProduits] che persiste i prodotti:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // si recupera il valore inviato
            String body = CharStreams.toString(request.getReader());
            // lo si deserializza
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // si mantengono i prodotti
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // si restituisce la risposta
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  • riga 8: un oggetto List<Prodotto> viene ricostituito a partire dal valore inviato. Per i motivi spiegati in precedenza, ogni oggetto [Produit] avrà un campo:
    • [EntityType entityType] uguale a [EntityType.POJO];
    • [Categorie categorie] uguale a null;
  • riga 11: la persistenza dei prodotti dovrebbe fallire. Infatti, con JPA, la persistenza di un prodotto è possibile solo se il suo campo [categorie] punta a un’entità [Categorie];

Esaminiamo il codice del metodo [DaoProduit.saveEntities] del livello [DAO / JPA]:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // si ristabilisce (se necessario) il collegamento tra un prodotto e la sua categoria
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // si salvano i prodotti
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  • righe 3-8: per ogni [Produit] di tipo POJO, viene creato un collegamento a un oggetto [Categorie] con la chiave primaria corretta e una versione diversa da null. Questo è sufficiente affinché il livello JPA conservi correttamente il prodotto;

Esaminiamo un ultimo punto. Gli oggetti [Categorie] e [Produit] hanno un campo aggiuntivo [EntityType entityType] che verrà serializzato in jSON quando questi oggetti verranno inviati al cliente. È possibile verificarlo con [Advanced Rest Client]:

Dal lato client, le entità [Categorie] e [Produit] sono state definite senza il campo [EntityType entityType]. Ciò è normale poiché gli oggetti [Categorie] e [Produit] vengono serializzati senza la parte PROXY, [Categorie.produits], [Produit.categorie]. Dal lato client, quindi, non esiste il concetto di entità PROXY. Ci sono solo oggetti normali.

Sul lato client, la stringa jSON [1] viene ricevuta dal seguente metodo [DaoCategorie.getAllShortEntities]:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // filtri jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // si ottengono tutte le categorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // l'elenco delle categorie List<Categoria>
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  • riga 5: si configura il mappatore jSON dell’oggetto [RestTemplate] in modo da gestire i filtri jSON e [jsonFilterCategorie] dell’oggetto [Categorie] e il filtro [jsonFilterProduit] dell'oggetto [Produit];
  • riga 7: il valore inviato (qui non ce n’è uno) e il valore ricevuto (List<Categorie>) vengono serializzati/deserializzati con questo mappatore. Si nota che la presenza del campo [entityType] nella stringa jSON ricevuta, nonostante tale campo non esista nelle entità [Categorie] e [Produit] sul lato client, non provoca alcun errore. Viene ignorato. Se avesse provocato un errore, avremmo modificato i filtri lato client affinché venisse ignorato.

Per implementare il servizio web / jSON / JPA / EclipseLink, è sufficiente modificare l’implementazione JPA:

  

Nota: premere Alt+F5, quindi rigenerare tutti i progetti Maven.

Il servizio web verrà avviato con la configurazione di esecuzione [spring-webjson-server-jpa-generic-hibernate-eclipselink] già utilizzata per Hibernate. Fatto ciò, eseguire i tre test del client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments];
  • in [2], il test [JUnitTestDao];
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (progetto [spring-webjson-client-generic]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.9. Implementazione del servizio web / jSON / JPA / OpenJpa

Per implementare il servizio web / jSON / JPA / OpenJpa, è sufficiente modificare l'implementazione JPA:

  

Nota: premere Alt+F5, quindi rigenerare tutti i progetti Maven.

Il servizio web verrà avviato con la configurazione di esecuzione [spring-webjson-server-jpa-generic-openpa]:

Fatto ciò, eseguire i tre test del client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], il test [JUnitTestDao] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

Per far funzionare i test, è stato necessario apportare modifiche al livello DAO / JPA. Infatti, per ragioni incomprensibili, i metodi [DaoCategorie.saveEntities] e [DaoProduit.saveEntities] hanno generato un errore durante il popolamento del database, segnalando che gli elementi distaccati non potevano essere salvati. Un elemento distaccato è un elemento che presenta:

  • una chiave primaria diversa da null;
  • una versione diversa da null;

Nessuno dei due casi veniva verificato. Non sapendo dove cercare, ho duplicato le entità da salvare in un elenco completamente nuovo e lì i test hanno funzionato. Questa modifica avrebbe potuto essere effettuata:

  • nel livello [DAO / JPA];
  • nel livello [web] che crea le entità da salvare;

Ho scelto di farlo nel livello [DAO / JPA]. Ovviamente c’è una perdita di prestazioni, ma è del tutto trascurabile rispetto ai tempi di risposta del SGBD. Le modifiche sono le seguenti:

Nella classe [DaoCategorie] del progetto [spring-jpa-generic]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // cloniamo l'elenco delle categorie -- talvolta necessario per OpenJpa -- bug non compreso
        // ***************************************************************************************
        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);
            // prodotti
            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);
            }
        }
        // si annotano i prodotti che verranno inseriti
        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);
                    }
                    // si approfitta dell’occasione per ripristinare (se necessario) la relazione prodotto --> categoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // si salvano le categorie e i prodotti
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // si aggiorna il campo [idCategorie] dei prodotti inseriti
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // risultato
        return categories2;
    }
  • righe 3-25: la lista [categories] ricevuta come parametro (riga 2) viene duplicata nella lista [categories2] (riga 6). È questa lista che viene salvata e restituita al chiamante (riga 52). Ciò comporta una conseguenza importante: viene restituita una lista diversa da quella passata come parametro e quindi, laddove prima si poteva scrivere:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// elaborazione di [categories]

ora bisogna scrivere:


List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// elaborazione di [categories]

Nella classe [DaoProduit] del progetto [spring-jpa-generic], il metodo [saveEntities] viene modificato in modo analogo:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // si clona l'elenco dei prodotti -- talvolta necessario per OpenJpa -- bug non incluso
        // ***************************************************************************************
        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);
        }

        // si ripristina (se necessario) il collegamento tra un prodotto e la sua categoria
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // si salvano i prodotti
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

Per implementare il servizio web / jSON / JPA / EclipseLink / PostgresQL, è necessario installare:

  • il progetto [postgresql-config-jdbc] di configurazione del livello JDBC di PostgreSQL;
  • il progetto [postresql-config-jpa-eclipselink] di configurazione del livello JPA di PostgreSQL;
  • premere Alt-F5 e rigenerare tutti i progetti Maven;
  

Si avvia SGBD PostgreSQL e si avvia il servizio web con la configurazione di esecuzione [spring-webjson-server-jpa-generic-hibernate-eclipselink] già utilizzata in precedenza. Fatto ciò, si eseguono i tre test del client generico [spring-webjson-client-generic]:

  • in [1], il test [JUnitTestCheckArguments] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], il test [JUnitTestDao] (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], il test [JUnitTestPushTheLimits] eseguito sul lato client (configurazione di esecuzione [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], il test [JUnitTestPushTheLimits] eseguito sul lato server (configurazione di esecuzione [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);