Skip to content

18. A client programmed for the web service / jSON

Now that the [dbproduitscategories] database is available on the web, we will write an application that uses it. We will then have the following client/server architecture:

The client application will have three layers:

  • a [Client HTTP] [3] layer to communicate with the web application / jSON that exposes the database;
  • a [DAO] [2] layer that will present the same interface as the [DAO] [4] layer;
  • a test layer JUnit [1] to verify that the client and server are functioning properly;

18.1. The Eclipse Project

The client's Eclipse project is as follows:

 
  1. the [spring.webjson.client.config] package contains the Spring configuration for the [DAO] layer;
  2. the [spring.webjson.client.dao] package contains the implementation of the [DAO] layer;
  3. the [spring.webjson.client.entities] package contains the objects exchanged with the web service / jSON. We are familiar with all of them;
  4. The [spring.webjson.client.infrastructure] package contains the exception classes used by the project. We are familiar with all of them;

18.2. Maven configuration of the project

The project is a Maven project configured by the following [pom.xml] file:


<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>
        <!-- jSON library used by 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>
        <!-- component used by 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>
        <!-- log library -->
        <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>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  1. lines 16–20: the parent Maven project [spring-boot-starter-parent], which allows us to define a number of dependencies without specifying their version, as this is defined in the parent project;
  2. lines 24–27: although we are not writing a web application, we need the [spring-web] dependency, which includes the [RestTemplate] class that allows for easy interfacing with a web application / jSON;
  3. lines 29–36: a library, jSON;
  4. lines 38–41: a dependency that will allow us to set a timeout for the client’s HTTP requests. A timeout is the maximum wait time for a server response. After this time, the client signals a timeout error by throwing an exception;
  5. lines 43–48: the Google Guava library;
  6. lines 50–53: the logging library;
  7. lines 54–64: the dependency for JUnit tests. In particular, it includes the JUnit 4 library required for testing. These dependencies have the [<scope>test</scope>] attribute, indicating that they are only required for the testing phase. They are not included in the final project archive;

18.3. Spring Configuration

  

The [AppConfig] class handles the Spring configuration for the HTTP client. Its code is as follows:


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 {
 
    // constants
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8081";
 
    // filters 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) {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // converter jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // exchange timeout
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // result
        return restTemplate;
    }
 
    @Bean
    public int timeout() {
        return TIMEOUT;
    }
 
    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • line 20: the class is a Spring configuration class;
  • line 21: other Spring components can be found in the [spring.webjson.client.dao] package;
  • line 25: a timeout of one second (1000 ms) is set;
  • lines 88–91: the bean that returns this value;
  • line 26: the URL of the web service / jSON;
  • lines 93-96: the bean that returns this value;
  • lines 72–86: the configuration of the [RestTemplate] class that handles communication with the web service / jSON. When it does not need to be configured, it can be referenced in the code simply as [new RestTemplate()]. Here, we want to set the timeout for communication with the web service / jSON. The bean [timeout] on line 89 is passed as a parameter to the method [restTemplate] on line 73;
  • line 75: the [HttpComponentsClientHttpRequestFactory] component is the component that allows us to set the exchange timeout (lines 82–83);
  • line 76: the [RestTemplate] class is constructed using this component. Since it relies on this component to communicate with the web service /jSON, the exchanges will indeed be subject to the timeout;
  • lines 78–80: we associate a jSON converter with the [RestTemplate] class. We already discussed this when examining the web service. The client and server exchange lines of text. A converter is responsible for serializing an object into text and, conversely, deserializing text into an object. There may be several converters associated with the [RestTemplate] class, and the one chosen at any given time depends on the HTTP headers sent by the server. Here, we have only one jSON converter since the text lines exchanged are of type jSON;
  • lines 82–83: the exchange timeouts are set;
  • lines 28–70: define jSON filters. These are the same as those on the server presented in section 17.3.2.1;
  • lines 29–32: the [jsonMapper] bean is the jSON mapper for the [MappingJackson2HttpMessageConverter] converter that we have associated with the [RestTemplate] class. We need this in the definition of the jSON filters;
  • lines 34–41: a bean defining the jSON filter [catégorie sans ses produits]. The [jsonMapperShortCategorie] method receives the [restTemplate] bean defined on line 73 as a parameter;
  • line 37: the [jsonMapper] method from line 30 is called to retrieve the jSON mapper;
  • lines 38–39: the filter is set to return a category without its products;
  • line 40: the mapper jSON is rendered as configured;
  • lines 42-51: the filter jSON to get a category with its products;
  • lines 53-60: the filter jSON to display a product without its category;
  • lines 62-70: the jSON filter to display a product with its category;

All these beans will be available to the [DAO] layer codes as well as to the JUnit tests.

18.4. Implementation of the HTTP client

Above, the [Client HTTP] layer communicates with the web service we just built. We will now examine it.

  

The [Client] class implements communication with the web service / jSON. It implements the following [IClient] interface:


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

The interface has only one method, [getResponse]:

  • line 6: the [getResponse] method is a generic method parameterized by two types:
    • [T1]: is the expected response type from the server in [Response<T1>], for example [List<Categorie>],
    • [T2]: is the type of the jSON parameter posted by the POST operations, for example [List<Produit>];
  • line 6: the method [getResponse] returns a result of type T1, for example [List<Categorie>];
  • line 6: the parameters of [getResponse] are as follows:
    • [String url]: the URL to be queried;
    • [HttpMethod method]: HTTP method of the query, GET or POST as appropriate;
    • [int errStatus]: error code to be used in class [DaoException], if an error occurs during communication with the server,
    • [T2 body]: the value to post if POST is present;

The [Client] class implements the [IClient] interface as follows:


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 {
 
    // injections
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    // generic request
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }
 
    // list of exception error messages
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  1. line 18: the [Client] class is a Spring component and can therefore be injected into other Spring components;
  2. lines 22–23: injection of the [RestTemplate] bean defined in [AppConfig] (see section 18.3), which handles communication with the server;
  3. lines 24–25: injection of the URL web service / jSON defined in [AppConfig] (see section 18.3);
  4. lines 37–39: the private method [getMessagesForException] is a utility method used to retrieve the list of error messages contained in an exception. We have encountered it several times;

Let’s continue:


    // generic request
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // the server response
        ResponseEntity<Response<T1>> response;
        try {
            // prepare the query
            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);
            }
            // execute the query
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // encapsulate the exception
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  1. line 18: the statement that sends the request to the server and receives its response. The [RestTemplate] component offers a large number of methods for communicating with the server, but only the [exchange] method accepts generic parameters. This is why it was chosen. The second parameter specifies the type of the expected response. The first parameter is the [RequestEntity] request (line 8). The result of the [exchange] method is of type [ResponseEntity<Response<T1>>] (line 5). The type [ResponseEntity] encapsulates the complete server response, including the HTTP headers and the document sent by the server. Similarly, the [RequestEntity] type encapsulates the entire client request, including the HTTP headers and any posted value;
  2. lines 8–16: we need to construct the request of type [RequestEntity]. It differs depending on whether we use a GET or a POST to make the request;
  3. line 10: the request for a GET. The [RequestEntity] class provides static methods to create the queries GET, POST, HEAD,... The [RequestEntity.get] method allows you to create a GET query by chaining the various methods that construct it:
    1. the [RequestEntity.get] method takes as a parameter the target URL in the form of a URI instance,
    2. the [accept] method allows you to define the elements of the HTTP header [Accept]. Here, we specify that we accept the [application/json] type that the server will send;
    3. the [build] method uses this information to construct the [RequestEntity] type of the request;
  4. Line 14: the request for a POST. The [RequestEntity.post] method allows us to create a POST request by chaining the various methods that construct it:
    1. the [RequestEntity.post] method takes as a parameter the target URL in the form of a URI instance,
    2. the [header] method defines a HTTP header. Here, we send the [Content-Type: application/json] header to the server to indicate that the posted value will arrive in the form of a jSON string;
    3. the method [accept] allows us to indicate that we accept the type [application/json] that the server will send;
    4. the method [body] sets the posted value. This is the fourth parameter of the generic method [getResponse] (line 1);
  5. lines 20–23: if a communication error occurs with the server, an exception of type [DaoException] is thrown with the error code set to the parameter [errStatus] passed as the third parameter of the generic method [getResponse] (line 3);

The [getResponse] method continues as follows:


// generic request
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // retrieve the body of the reply
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // server-side errors?
        if (status != 0) {
            // create an exception
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // it's good
            return entity.getBody();
        }
    }
  • line 4: we received the response from the server. It is of type [ResponseEntity<Response<T1>>] (line 5 of the previous code we examined), where the class [Response] is the class already used on the server side:

package spring.webjson.client.dao;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // the possible exception
    private String exception;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, String exception, T body) {
        this.status = status;
        this.exception = exception;
        this.body = body;
    }
 
    // getters and setters
...
}

Let's return to the [getResponse] method:

  1. line 6: we retrieve the [Response<T1>] document encapsulated in the response. This type has the fields [int status, String exception, T1 body];
  2. line 7: we retrieve the [status] from the response, which is an error code;
  3. lines 9–12: if there is an error, we throw an exception containing the two [status, exception] pieces of information from the server’s response;
  4. line 14: otherwise, we return the [T1] type contained in the [Response<T1>] response;

The [Client] class is generic. It can be used for any web client / jSON.

18.5. Implementation of the [Dao] layer

  

18.5.1. The [AbstractDao] class

The client-side [DAO] layer has the same interface as the server-side [DAO] layer (see section 4.7):


package spring.webjson.client.dao;
 
import java.util.List;
 
import spring.webjson.client.entities.AbstractCoreEntity;
 
public interface IDao<T extends AbstractCoreEntity> {
 
    // list of all T entities
    public List<T> getAllShortEntities();
 
    public List<T> getAllLongEntities();
 
    // particular entities - version short
    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);
 
    // particular entities - version long
    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);
 
    // update of several entities
    public List<T> saveEntities(Iterable<T> entities);
 
    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities();
 
    // deletion of multiple entities
    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);
}

The [AbstractDao] class implements the [IDao] interface. It is a class analogous to the server-side class of the same name (see section 4.8). It serves as the parent class for the classes [DaoCategorie] and [DaoProduit]. It is not identical for two reasons:

  1. on the server side, the [AbstractDao] class handles one piece of information:

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

which we don't need here.

  • On the server side, the [AbstractDao] class uses [@Transactional] annotations to encapsulate each method in a transaction. On the client side, there is no database to manage. This annotation therefore disappears;

The [AbstractDao] class simply verifies the validity of the call parameters for the methods of the [IDao] interface before delegating the call to the child classes:


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) {
        // argument validity
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // result
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
 
    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // argument validity
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // result
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }
 
    // méthodes privées ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // elements null ?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // empty elements?
        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>();
            }
        }
        // default result
        return null;
    }
 
    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // elements null ?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // empty elements?
        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>();
            }
        }
        // default result
        return null;
    }
 
    // méthodes protégées ----------------------------------------------
    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. The [DaoCategorie] class

  

The [DaoCategorie] class is as follows:


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;
 
...
}
  1. line 19: the [DaoClient] class is a Spring component into which other Spring components can be injected;
  2. line 20: the [DaoClient] class extends the [AbstractDao<Categorie>] class we just saw and therefore implements the [IDao<Categorie>] interface;
  3. lines 22–23: we inject the Spring context to access its beans;
  4. lines 24–25: we inject the HTTP client that we just built;

The implementations of the various methods of the [DaoCategorie] interface all follow the same pattern. We will present three methods, one based on a [GET] operation, and the other two on a [POST] operation.

18.5.2.1. The [getAllLongEntities] method

The [getAllLongEntities] method generates the longest possible version for all categories in the database:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // the List<Categorie> category list
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // redo the product --> category link
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • line 2: the method returns the list of categories in their full versions;
  • line 5: the jSON mapper, which will serialize the posted value (there is none) and deserialize the response returned by the [Client] class (categories in their long versions);
  • line 7: the [getResponse] method of the [Client] class is called. This method handles communication with the web service / jSON. Its parameters are as follows:
    • the URL of the queried service [/getAllLongCategories];
    • the [GET] method to be used;
    • the error code to use if an error occurs (232);
    • the posted value. There is none here;
  • line 7: in the expression [client.<List<Categorie>, Void>], the actual parameters of the generic types [T1, T2] of the method [getResponse] are specified. Note that [T1] is the type of the expected response and [T2] is the type of the posted value. Here, a result of type [List<Categorie>] is expected, and there is no posted value of type [Void];
  • Line 7: The result returned by the [getResponse] method is placed in an object of type [Object]. This is a bit odd, since we expect a type of [List<Categorie>]. This is because the [getResponse] method, which works with generic types [T1, T2], always returns a type [java.util.LinkedHashMap], which must then be used to return the correct type;
  • Line 9: We return the list of categories. To do this, we serialize the [map] [mapper.writeValueAsString(map)] object into a jSON string, which we then deserialize back into a [List<Categorie>] type;
  • Line 13: We have received a list of categories, some of which may have products. We receive the short version for these products. Therefore, when they are deserialized, the created [Produit] objects have their [categorie==null] field. The [linkCategorieWithProduits] method re-establishes the link between a [Produit] and its [Categorie];
  • lines 14–15: we catch the [DaoException] exception that the [getResponse] method might have thrown and immediately rethrow it. This strange behavior is due to the fact that if we do not do this, the exception of type [DaoException] will be caught by lines 16–18, and we do not want that;
  • Lines 16–18: We catch all other exceptions to encapsulate them in a [DaoException] type. Note that the [DAO] layer must only throw this type of exception;

The [linkCategorieWithProduits] method that recreates the links between [Produit] entities and [Categorie] entities is as follows:


    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. Filter Management jSON

Let’s revisit the handling of filters jSON in the previous method [getAllLongEntities]:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // the List<Categorie> category list
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
 
  • Line 5: We retrieve a jSON mapper from the Spring context that can handle the long versions of the categories. Let’s revisit the definition of this mapper in the Spring configuration [AppConfig]:

// filters 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) {
    ...
    }
 
  • The [jsonMapperLongCategorie] bean requested by the [getAlllongEntities] method is the bean in lines 7–15;
  • line 10: the mapper is provided by the [jsonMapper] method in lines 2–5. We can see that this jSON mapper belongs to the [RestTemplate] object, which manages the HTTP exchanges between the client and the server. This mapper is used by default to:
    • serialize the value posted to the server;
    • deserialize the response sent back by the server;

Let’s return to the code for [getAllLongEntities]:


            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // the List<Categorie> category list
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // redo the product --> category link
return linkCategorieWithProduits(categories);
  1. line 2: we retrieve the [jsonMapperLongCategorie] mapper from the Spring context;
  2. line 4: the [getResponse] method is executed. This involves:
    1. automatic serialization of the posted value (there is none here);
    2. automatic deserialization of the received response, in this case a List<Category> type. This is because the [Categorie] entity has a jSON filter, which needed to be handled. This is the reason for line 2;
  3. line 6: the result undergoes a second serialization/deserialization with this same mapper to retrieve the List<Category> type. Line 4: the type returned by [getResponse] is a [Object] type;

In the following methods, note that the jSON mapper requested from the Spring context is used for both the posted value (serialization) and the received value (deserialization). If one or both values have a jSON filter, they must be configured. The mapper can therefore have up to two configured filters. In the following, this never occurs. Either the posted value has no filter (List<Long>, List<String>), or the received value has none (List<CoreCategorie>, List<CoreProduit>). The entities with a jSON filter are only [Categorie] and [Produit].

18.5.2.3. The [getShortEntitiesById] method

The [getShortEntitiesById] method returns the short versions of the categories whose primary keys it receives as parameters:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // get a category without its products
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // the category
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(223, e2, simpleClassName);
        }
}
  • Line 5: the jSON mapper, which will serialize the posted value (a list of primary keys) and deserialize the response returned by the [Client] class (categories in their short versions). The selected filter will have no effect on the posted value since there is no filter for the elements in the posted list;
  • Line 7: The [getResponse] method of the parent class is called. This method handles communication with the web service /jSON. Its parameters are as follows:
    • the URL of the queried service [/getShortCategoriesById];
    • the [POST] method to be used;
    • the error code to use if an error occurs (204);
    • the posted value. Here, it is a list of primary keys;
  • line 7: in the expression [client.<List<Categorie>, List<Long>>], the actual parameters of the generic types [T1, T2] of the method [getResponse] are specified. Recall that [T1] is the type of the expected response and [T2] is the type of the posted value. Here, a result of type [List<Categorie>] is expected, and the posted value is a list of primary keys of type [List<Long>];
  • line 7: the result returned by the [getResponse] method is placed in an object of type [Object];
  • line 9: the list of categories is returned. To do this, the [map] [mapper.writeValueAsString(map)] object is serialized into a jSON string, which is then deserialized back to a [List<Categorie>] type;

18.5.2.4. The [saveEntities] method

The [saveEntities] method persists categories in the database. Its code is as follows:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // add categories
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // list of added core categories
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // categories are updated with the information received
            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);
        }
    }
  • line 2: the [saveEntities] method is used to persist the categories passed as parameters in the database. It returns these same categories enriched with their primary keys. If the categories are passed along with products, those are also persisted;
  • line 5: the jSON mapper, which serializes the posted value (a list of categories in their long versions) and deserializes the response returned by the [Client] class ([CoreCategorie] objects). The selected filter will have no effect on the result since the elements in the list received in response have no filter;
  • line 7: we call the parent’s [getResponse] method to handle communication with the web service / jSON;
    • the first parameter is URL [/saveCategories];
    • the second parameter is the HTTP method to be used, in this case [POST];
    • the third parameter is the error code to use if an error occurs (200);
    • the last parameter is the posted value, here the list of categories to persist;
  • line 7: the generic parameters [T1, T2] for the [getResponse] method are here [List<CoreCategorie>, List<Categorie>]. The first type is that of the expected response, the second is the type of the posted value;
  • line 7: the response obtained is placed in a [Object] type;
  • line 9: the response of type [List<CoreCategorie>] is reconstructed. The response to be returned is of type [List<Categorie>] (line 2) and not [List<CoreCategorie>]. The received response is the list of primary keys for the persisted categories and products;
  • lines 14–28: the received primary keys are assigned to categories and products (lines 17, 23, 24). Additionally, the links [Produit] → [Categorie] are reconstructed (lines 24–25);

All other methods follow the same pattern.

18.6. The JUnit test

Let’s return to the client/server architecture currently under construction:

We have built a [DAO] [2] layer with the same interface as the [DAO] [4] layer. To test the [DAO] [2] layer, we can therefore use the JUnit tests that were used to test the [DAO] [4] layer:

  

These three tests are run using the following test configurations:

 

The results of the three tests are as follows:

  1. in [1], the [JUnitTestCheckArguments] test;
  2. in [2], test [JUnitTestDao];
  3. in [3], the client-side test [JUnitTestPushTheLimits] (project [spring-webjson-client-generic]);
  4. in [3], the [JUnitTestPushTheLimits] test executed on the server side (project [spring-jdbc-generic-04]). We observe that the network layer causes very little slowdown compared to that caused by access to SGBD;

18.7. Web service implementation / jSON / JPA / Hibernate

We are now looking at the following architecture:

The modification is in [1]. The server’s [DAO] layer is based on a JPA implementation. We will first use a JPA / Hibernate implementation.

18.7.1. The Eclipse Project

For now, the projects loaded into Eclipse are as follows:

  

The [spring-webjson-server-jdbc-generic] project was based on the [spring-jdbc-generic-04] project, which configures the DAO / JDBC for accessing SGBD and MySQL. We will create a new project, [spring-webjson-server-jpa-generic], which will be based on the [spring-jpa-generic] project that configures the DAO / JPA / JDBC layer for accessing SGBD and MySQL. We know that in both cases, the [DAO] layer implements the same [IDao] interface. The code for the [web] layer therefore remains unchanged.

We can create the [spring-webjson-server-jpa-generic] project by copying and pasting from the [spring-webjson-server-jdbc-generic] project:

  • in [1], specify a folder created specifically for the new project;
  

There are three types of changes to make. The first are in the project’s Maven configuration 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-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>
        <!-- web layer -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- layer [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>
  • line 5: change the name of the Maven artifact;
  • lines 24–28: the dependency is now on the [spring-jpa-generic] project and no longer on [spring-jdbc-generic-04];

In the end, the dependencies are as follows:

  

Once this is done, we resolve all import issues that appeared in the various classes. For example, [Produit, Categorie] entities are no longer to be found in the [spring-jdbc-generic-04] project but in the [spring-jpa-generic] project. Simply adding [Ctrl-Maj-O] to the code of a class is enough to regenerate the imports.

The final change must be made in the configuration file [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 {
 
}
  • Line 9: We now import the configuration from the [spring-jpa-generic] project instead of the [spring-jdbc-generic-04] project;

With that done, we’re ready. We start the web service with the [spring-webjson-server-jpa-generic-hibernate-eclipselink] configuration:

Then we run the three tests for the generic client [spring-webjson-client-generic]:

  1. in [1], test [JUnitTestCheckArguments] (execution configuration [spring-webjson-client-generic-JUnitTestCheckArguments]);
  2. in [2], test [JUnitTestDao] (run configuration [spring-webjson-client-generic-JUnitTestDao]);
  3. in [3], the [JUnitTestPushTheLimits] test executed on the client side (run configuration [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  4. in [4], the [JUnitTestPushTheLimits] test run on the server side ([spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink] execution configuration);

18.7.2. Why does it work?

It works, and yet when you look closely at the code, it’s surprising that it works. While the [DAO] layers implemented by the [spring-jdbc-generic-04] and [spring-jpa-generic] projects do indeed present the same interface, they do not manipulate the same [Categorie] and [Produit] entities: in the [spring-jpa-generic] project, these entities have an additional field [EntityType entityType] that has two possible values:

  • EntityType.POJO: the entity is a normal object whose fields can be freely used;
  • EntityType.PROXY: the entity is a PROXY object rendered by the [JPA] layer. In this case, certain fields (actually the getters for these fields) do not behave as usual, and the following rules have been established:
    • if [Categorie.entityType==EntityType.PROXY], then the [getProduits] method must not be used;
    • if [Produit.entityType==EntityType.PROXY], then do not use the [getCategorie] method;

However, we have just ported the [spring-webjson-server-jdbc-generic] project to [spring-webjson-server-jpa-generic] without modifying the code. How is this possible?

Let’s examine the code for the [saveCategories] method:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            // we deserialize it
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // we persist categories
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • line 8: a List<Category> object is created from a string jSON:
    • in the posted value, the products do not have a [categorie] field. It is indeed unnecessary to post this field. If it were posted, deserialization would construct a [Produit] object with a [categorie] field pointing to a newly created [Categorie] object. For n products, this would result in n [Categorie] objects being created, whereas only one is needed. Furthermore, the [categorie] field of the products would not point to the correct [Categorie] object, which is the one to which they belong. So here the products have a [categorie==null] field;
    • in the classes [Categorie] and [Produit], the field [EntityType entityType] is defined as follows:

    protected EntityType entityType = EntityType.POJO;

Therefore, the entities [Categorie] and [Produit] created by serialization all have type POJO.

  • Line 11: the categories are persisted. This should not work. Indeed, if in the JDBC implementation, the [Produit.categorie] field is not needed for persistence (the [idCategorie] field is used), for the JPA implementation, it is absolutely necessary. This field must point to an entity [Categorie], but here it is null.

Let’s examine the code for the [DaoCategorie.saveEntities] method in the [DAO / JPA] layer:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // note the products to be inserted
        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);
                    }
                    // we take this opportunity to re-establish (if necessary) the product --> category relationship
                    produit.setCategorie(categorie);
                }
            }
        }
        // we persist categories / products
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // update the [idCategorie] field of inserted products
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // result
        return categories;
    }
  1. lines 13-14: we see that the link [Produit] --> [Categorie] is reestablished for the entities POJO (line 8), which is the case here. This explains why category persistence worked. This scenario is useful in other circumstances: we can never be sure that the user has correctly linked the products to the categories. So we do it for them;

Now let’s examine the [ProduitController.saveProduits] method that persists the products:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            // we deserialize it
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // we persist products
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // we return the answer
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  1. Line 8: A List<Product> object is reconstructed from the posted value. For the reasons explained earlier, each [Produit] object will have a field:
    1. [EntityType entityType] equal to [EntityType.POJO];
    2. [Categorie categorie] equal to null;
  2. line 11: product persistence should fail. Indeed, with JPA, a product can only be persisted if its [categorie] field points to a [Categorie] entity;

Let's look at the code for the [DaoProduit.saveEntities] method in the [DAO / JPA] layer:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // re-establish (if necessary) the link between a product and its category
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // we persist products
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  1. lines 3-8: for each [Produit] of type POJO, a link to a [Categorie] object with the correct primary key and a non-null version is created. This is sufficient for the JPA layer to correctly persist the product;

Let’s look at one final point. The objects [Categorie] and [Produit] have an additional field [EntityType entityType] that will be serialized to jSON when these objects are sent to the client. We can verify this with [Advanced Rest Client]:

On the client side, the entities [Categorie] and [Produit] have been defined without the field [EntityType entityType]. This is normal since the objects [Categorie] and [Produit] are serialized without their PROXY, [Categorie.produits], and [Produit.categorie] components. On the client side, therefore, there is no concept of a PROXY entity. There are only normal objects.

On the client side, the string jSON [1] is received by the following method:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // the List<Categorie> category list
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  1. line 5: configure the jSON mapper of the [RestTemplate] object to handle the jSON and [jsonFilterCategorie] filters of the[Categorie] object and the [jsonFilterProduit] filter of the [Produit] object;
  2. Line 7: The posted value (there is none here) and the received value (List<Category>) are serialized/deserialized using this mapper. Note that the presence of the [entityType] field in the received jSON string, even though this field does not exist in the [Categorie] and [Produit] entities on the client side, does not cause an error. It is ignored. If it had caused an error, we would have modified the client-side filters to ignore it.

To implement the web service / jSON / JPA / EclipseLink, simply change the implementation of JPA:

  

Note: Press Alt-F5, then regenerate all Maven projects.

We will launch the web service using the [spring-webjson-server-jpa-generic-hibernate-eclipselink] runtime configuration already used for Hibernate. Once this is done, run the three tests for the generic client [spring-webjson-client-generic]:

  • in [1], the [JUnitTestCheckArguments] test;
  • in [2], the [JUnitTestDao] test;
  • in [3], the client-side test [JUnitTestPushTheLimits] (project [spring-webjson-client-generic]);
  • in [4], the [JUnitTestPushTheLimits] test executed on the server side ([spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink] execution configuration);

18.9. Web service implementation / jSON / JPA / OpenJpa

To implement the web service / jSON / JPA / OpenJpa, simply change the implementation to JPA:

  

Note: Press Alt-F5, then regenerate all Maven projects.

We will launch the web service using the [spring-webjson-server-jpa-generic-openpa] runtime configuration:

Once this is done, run the three tests for the generic client [spring-webjson-client-generic]:

  1. in [1], the [JUnitTestCheckArguments] test (run configuration [spring-webjson-client-generic-JUnitTestCheckArguments]);
  2. in [2], test [JUnitTestDao] (run configuration [spring-webjson-client-generic-JUnitTestDao]);
  3. in [3], the client-side test [JUnitTestPushTheLimits] (run configuration [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  4. in [4], the [JUnitTestPushTheLimits] test executed on the server side (execution configuration [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

To make the tests work, changes had to be made to the DAO / JPA layer. In fact, for some inexplicable reason, the [DaoCategorie.saveEntities] and [DaoProduit.saveEntities] methods failed when populating the database, indicating that detached elements could not be persisted. A detached element is an element that has either:

  • a non-null primary key;
  • a non-null version;

Neither of these cases was checked. Not knowing where to look, I duplicated the entities to be persisted into a brand-new list, and then the tests worked. This change could have been made either:

  1. in the [DAO / JPA] layer;
  2. in the [web] layer, which creates the entities to be persisted;

I chose to do it in the [DAO / JPA] layer. There is, of course, a performance loss, but it is completely negligible compared to the response times of SGBD. The changes are as follows:

In the [DaoCategorie] class of the [spring-jpa-generic] project:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // clone category list -- sometimes necessary for OpenJpa -- bug not understood
        // ***************************************************************************************
        List<Categorie> categories2 = new ArrayList<Categorie>();
        for (Categorie categorie : categories) {
            // category
            Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
            EntityType categorieType = categorie.getEntityType();
            categorie2.setEntityType(categorieType);
            categories2.add(categorie2);
            // products
            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);
            }
        }
        // note the products to be inserted
        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);
                    }
                    // we take this opportunity to re-establish (if necessary) the product --> category relationship
                    produit.setCategorie(categorie);
                }
            }
        }
        // we persist categories / products
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // update the [idCategorie] field of inserted products
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // result
        return categories2;
    }
  1. lines 3–25: the list [categories] received as a parameter (line 2) is duplicated in the list [categories2] (line 6). It is this list that is persisted and returned to the caller (line 52). This has an important consequence: a different list is returned than the one passed as a parameter, so where we could previously write:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// operation of [categories]

We must now write:


List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// operation of [categories]

In the [DaoProduit] class of the [spring-jpa-generic] project, the [saveEntities] method is modified similarly:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // clone product list -- sometimes necessary for OpenJpa -- bug not understood
        // ***************************************************************************************
        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);
        }
 
        // re-establish (if necessary) the link between a product and its category
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // we persist products
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

To implement the web service / jSON / JPA / EclipseLink / PostgresQL, you must install:

  1. the [postgresql-config-jdbc] project for configuring the JDBC layer of PostgreSQL;
  2. the [postresql-config-jpa-eclipselink] configuration project for the JPA layer of PostgreSQL;
  3. Press Alt-F5 and regenerate all Maven projects;
  

We launch SGBD and PostgreSQL, and start the web service using the [spring-webjson-server-jpa-generic-hibernate-eclipselink] runtime configuration previously used. Once this is done, run the three tests for the generic client [spring-webjson-client-generic]:

  • in [1], the [JUnitTestCheckArguments] test ([spring-webjson-client-generic-JUnitTestCheckArguments] execution configuration);
  • in [2], test [JUnitTestDao] (run configuration [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], the client-side test [JUnitTestPushTheLimits] (run configuration [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], the [JUnitTestPushTheLimits] test executed on the server side (execution configuration [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);