Enterprise Java

Exposing HTTP Restful API with Inbound Adapters. Part 1 (XML)

1. Introduction

The purpose of this post is to implement an HTTP Restful API using Spring Integration HTTP inbound adapters. This tutorial is divided into two parts:
 
 
 
 
 
 
 

  • XML configuration example (this same post).
  • Java DSL example. This will be explained in the next part of this tutorial, showing how to configure the application using Spring Integration Java DSL, with examples with both Java 7 and Java 8.

Before looking at the code, let’s take a glance at the following diagram, which shows the different services exposed by the application:

diagram

GET operations are handled by an HTTP inbound gateway, while the rest (PUT, POST and DELETE) are handled by HTTP inbound channel adapters, since no response body is sent back to the client. Each operation will be explained in the following sections:

  1. Introduction
  2. Application configuration
  3. Get operation
  4. Put and post operations
  5. Delete operation
  6. Conclusion

The source code is available at Github.

2. Application configuration

The web.xml file contains the definition of the Dispatcher Servlet:

<servlet>
    <servlet-name>springServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:xpadro/spring/integration/configuration/http-inbound-config.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>springServlet</servlet-name>
    <url-pattern>/spring/*</url-pattern>
</servlet-mapping>

The http-inbound-config.xml file will be explained in the following sections.

The pom.xml file is detailed below. It is important to note the jackson libraries. Since we will be using JSON to represent our resources, these libraries must be present in the class path. Otherwise, the framework won’t register the required converter.

<properties>
    <spring-version>4.1.3.RELEASE</spring-version>
    <spring-integration-version>4.1.0.RELEASE</spring-integration-version>
    <slf4j-version>1.7.5</slf4j-version>
    <junit-version>4.9</junit-version>
    <jackson-version>2.3.0</jackson-version>
</properties>

<dependencies>
    <!-- Spring Framework - Core -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring-version}</version>
    </dependency>
    
    <!-- Spring Framework - Integration -->
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-core</artifactId>
        <version>${spring-integration-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-http</artifactId>
        <version>${spring-integration-version}</version>
    </dependency>
    
    <!-- JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    
    <!-- Testing -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit-version}</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j-version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j-version}</version>
    </dependency>
</dependencies>

3. Get operation

The configuration of the flow is shown below:

http-inbound-config.xml

The gateway receives requests to this path: /persons/{personId}. Once a request has arrived, a message is created and sent to httpGetChannel channel. The gateway will then wait for a service activator (personEndpoint) to return a response:


    
    


Now, some points need to be explained:

  • supported-methods: this attribute indicates which methods are supported by the gateway (only GET requests).
  • payload-expression: What we are doing here is getting the value from personId variable in the URI template and putting it in the message’s payload. For example, the request path ‘/persons/3’ will become a Message with a value ‘3’ as its payload.
  • request-mapping: We can include this element to specify several attributes and filter which requests will be mapped to the gateway. In the example, only requests that contain the value ‘application/json’ for Content-Type header (consumes attribute) and Accept header (produces attribute) will be handled by this gateway.

Once a request is mapped to this gateway, a message is built and sent to the service activator. In the example, we defined a simple bean that will get the required information from a service:

@Component
public class PersonEndpoint {
    private static final String STATUSCODE_HEADER = "http_statusCode";
    
    @Autowired
    private PersonService service;
    
    public Message<?> get(Message<String> msg) {
        long id = Long.valueOf(msg.getPayload());
        ServerPerson person = service.getPerson(id);
        
        if (person == null) {
            return MessageBuilder.fromMessage(msg)
                .copyHeadersIfAbsent(msg.getHeaders())
                .setHeader(STATUSCODE_HEADER, HttpStatus.NOT_FOUND)
                .build(); 
        }
        
        return MessageBuilder.withPayload(person)
            .copyHeadersIfAbsent(msg.getHeaders())
            .setHeader(STATUSCODE_HEADER, HttpStatus.OK)
            .build();
    }
    
    //Other operations
}

Depending on the response received from the service, we will return the requested person or a status code indicating that no person was found.

Now we will test that everything works as expected. First, we define a ClientPerson class to which the response will be converted:

@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientPerson implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @JsonProperty("id")
    private int myId;
    private String name;
    
    public ClientPerson() {}
    
    public ClientPerson(int id, String name) {
        this.myId = id;
        this.name = name;
    }
    
    //Getters and setters
}

Then we implement the test. The buildHeaders method is where we specify Accept and Content-Type headers. Remember that we restricted requests with ‘application/json’ values in those headers.

@RunWith(BlockJUnit4ClassRunner.class)
public class GetOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    private HttpHeaders buildHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON); 
        
        return headers;
    }
    
    @Test
    public void getResource_responseIsConvertedToPerson() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
        assertEquals("John" , response.getBody().getName());
        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
    
    @Test
    public void getResource_responseIsReceivedAsJson() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<String> response = restTemplate.exchange(URL, HttpMethod.GET, entity, String.class, 1);
        assertEquals("{\"id\":1,\"name\":\"John\",\"age\":25}", response.getBody());
        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
    
    @Test(expected=HttpClientErrorException.class)
    public void getResource_sendXml_415errorReturned() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_XML);
        HttpEntity<Integer> entity = new HttpEntity<>(headers);
        restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
    }
    
    @Test(expected=HttpClientErrorException.class)
    public void getResource_expectXml_receiveJson_406errorReturned() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_XML));
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<Integer> entity = new HttpEntity<>(headers);
        restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
    }
    
    @Test(expected=HttpClientErrorException.class)
    public void getResource_resourceNotFound_404errorReturned() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 8);
    }
}

Not specifying a correct value in the Content-Type header will result in a 415 Unsupported Media Type error, since the gateway does not support this media type.

On the other hand, specifying an incorrect value in the Accept header will result in a 406 Not Acceptable error, since the gateway is returning another type of content than the expected.

4. Put and post operations

For PUT and POST operations, we are using the same HTTP inbound channel adapter, taking advantage of the possibility to define several paths and methods to it. Once a request arrives, a router will be responsible to delivering the message to the correct endpoint.

http-inbound-config.xml

<int-http:inbound-channel-adapter channel="routeRequest" 
    status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"
    supported-methods="POST, PUT" 
    path="/persons, /persons/{personId}"
    request-payload-type="xpadro.spring.integration.server.model.ServerPerson">
    
    <int-http:request-mapping consumes="application/json"/>
</int-http:inbound-channel-adapter>

<int:router input-channel="routeRequest" expression="headers.http_requestMethod">
    <int:mapping value="PUT" channel="httpPutChannel"/>
    <int:mapping value="POST" channel="httpPostChannel"/>
</int:router>

<int:service-activator ref="personEndpoint" method="put" input-channel="httpPutChannel"/>
<int:service-activator ref="personEndpoint" method="post" input-channel="httpPostChannel"/>

This channel adapter includes two new attributes:

  • status-code-expression: By default, the channel adapter acknowledges that the request has been received and returns a 200 status code. If we want to override this behavior, we can specify a different status code in this attribute. Here, we specify that these operations will return a 204 No Content status code.
  • request-payload-type: This attribute specifies what class will the request body be converted to. If we do not define it, it will not be able to convert to the class that the service activator is expecting (ServerPerson).

When a request is received, the adapter sends it to the routeRequest channel, where a router is expecting it. This router will inspect the message headers and depending on the value of the ‘http_requestMethod’ header, it will deliver it to the appropriate endpoint.

Both PUT and POST operations are handled by the same bean:

@Component
public class PersonEndpoint {
    @Autowired
    private PersonService service;
    
    //Get operation
    
    public void put(Message<ServerPerson> msg) {
        service.updatePerson(msg.getPayload());
    }
    
    public void post(Message<ServerPerson> msg) {
        service.insertPerson(msg.getPayload());
    }
}

Return type is void because no response is expected; the inbound adapter will handle the return of the status code.

PutOperationsTest validates that the correct status code is returned and that the resource has been updated:

@RunWith(BlockJUnit4ClassRunner.class)
public class PutOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    //build headers method
    
    @Test
    public void updateResource_noContentStatusCodeReturned() {
        HttpEntity<Integer> getEntity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.GET, getEntity, ClientPerson.class, 4);
        ClientPerson person = response.getBody();
        person.setName("Sandra");
        HttpEntity<ClientPerson> putEntity = new HttpEntity<ClientPerson>(person, buildHeaders());
        
        response = restTemplate.exchange(URL, HttpMethod.PUT, putEntity, ClientPerson.class, 4);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        
        response = restTemplate.exchange(URL, HttpMethod.GET, getEntity, ClientPerson.class, 4);
        person = response.getBody();
        assertEquals("Sandra", person.getName());
    }
}

PostOperationsTest validates that the new resource has been added:

@RunWith(BlockJUnit4ClassRunner.class)
public class PostOperationsTest {
    private static final String POST_URL = "http://localhost:8081/int-http-xml/spring/persons";
    private static final String GET_URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    //build headers method
    
    @Test
    public void addResource_noContentStatusCodeReturned() {
        ClientPerson person = new ClientPerson(9, "Jana");
        HttpEntity<ClientPerson> entity = new HttpEntity<ClientPerson>(person, buildHeaders());
        
        ResponseEntity<ClientPerson> response = restTemplate.exchange(POST_URL, HttpMethod.POST, entity, ClientPerson.class);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        
        HttpEntity<Integer> getEntity = new HttpEntity<>(buildHeaders());
        response = restTemplate.exchange(GET_URL, HttpMethod.GET, getEntity, ClientPerson.class, 9);
        person = response.getBody();
        assertEquals("Jana", person.getName());
    }
}

5. Delete operation

The last operation of our restful API is the delete operation. This time we use a single channel adapter for this purpose:

<int-http:inbound-channel-adapter channel="httpDeleteChannel" 
    status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"
    supported-methods="DELETE" 
    path="/persons/{personId}" 
    payload-expression="#pathVariables.personId">
    
    <int-http:request-mapping consumes="application/json"/>
</int-http:inbound-channel-adapter>

<int:service-activator ref="personEndpoint" method="delete" input-channel="httpDeleteChannel"/>

The channel adapter lets us define the returning status code and we are using the payload-expression attribute to map the requested personId to the message body. The configuration is a little bit different from  those in previous operations but there’s nothing not already explained here.

The service activator, our person endpoint, will request the person service to delete this resource.

public void delete(Message<String> msg) {
    long id = Long.valueOf(msg.getPayload());
    service.deletePerson(id);
}

Finally, the required test:

@RunWith(BlockJUnit4ClassRunner.class)
public class DeleteOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    //build headers method
    
    @Test
    public void deleteResource_noContentStatusCodeReturned() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.DELETE, entity, ClientPerson.class, 3);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        
        try {
            response = restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 3);
            Assert.fail("404 error expected");
        } catch (HttpClientErrorException e) {
            assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
        }
    }
}

6. Conclusion

This post has been an introduction to our application in order to understand how it is structured from a known point of view (xml configuration). In the next part of this tutorial, we are going to implement this same application using Java DSL. The application will be configured to run with Java 8, but when lambdas are used, I will also show how it can be done with Java 7.

I’m publishing my new posts on Google plus and Twitter. Follow me if you want to be updated with new content.

Xavier Padro

Xavier is a software developer working in a consulting firm based in Barcelona. He is specialized in web application development with experience in both frontend and backend. He is interested in everything related to Java and the Spring framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
David
David
7 years ago

Hello, thanks for your tutorial. I have a question regarding routing based on the path:
Say for example I had the following paths:

path=”/persons/{applicationName}/{personId}”
where applicationName could have a different value such as appnameone, appnametwo, appnamethree etc

How can I route based on application name which is on the path to a channel? I noticed in your example you are doing it based on headers.http_requestMethod but I need to do it based on the path. Thanks in advance for your reply.

So if appnameone = hbo route channelone
appnametwo = netflix route to channeltwo
appnamethree = hulu route to channelthree

john
john
6 years ago

06

07

08

09

10

11

12
how the inbound channel adapter sends its requests to route request channel without specifying

Back to top button