02.02.2013

ReaderInterceptor und WriterInterceptor für JAX-RS 2.0

In diesem Teil des Tutorials werden Interceptoren vorgestellt. Seit JAX-RS 2.0 existieren diese auch für REST-Services und ermöglichen das Abfangen von Requests und Responses. Genau genommen, hat man die Möglichkeit sich an die Stellen "einzuhängen" bevor MessageBodyReader bzw. MessageBodyWriter aufgerufen werden. Um das zu erreichen, müssen die Interfaces ReaderInterceptor und/oder WriterInterceptor implementiert werden.

Auf der Abbildung sieht man, wie der Client einen Request an den Service /itemService/item des Servers absetzt. Bevor die Response mit dem Item an den Client gesendet wird, muss der ItemMessageBodyWriter durchlaufen werden, damit das Item als Stream übertragen wird. In der Abbildung oben wird der ItemMessageBodyWriter von einem WriterInterceptor umschlossen, der vor dem MessageBodyWriter und dem Versenden ausgeführt wird. Auf Seite des Client wird der Item-Stream von einem ItemMessageBodyReader geparsed, so dass der Client mit Objekten vom Typ Item arbeiten kann.

Anmerkung: In der Abbildung ist nur der ItemMessageBodyWriter abgebildet. Wenn Objekte von Typ String oder Listen mit Items übertragen werden, dann kommt ein Standard-MessageBodyWriter bzw. der ItemListMessageBodyWriter zum Einsatz.

In dem folgenden Beispiel wird ein Interceptor bei jedem Aufruf des REST-Services angesprochen. D.h. bevor der MessageBodyWriter aufgerufen wird, um das Resultat (Response) an den Client zu liefern.

Zu Demonstrationszwecken wird noch der Content-Type aus dem Header ausgelesen und ein neuer Wert in den Header geschrieben. Hauptaufgabe des Beispiel-Interceptor ist es aber, die Ergebnismenge zu verändern. Es werden folgende Dinge durchgeführt:

  • Bei einen String als Rückgabewert, wird der String verändert.
  • Bei einem Item als Rückgabewert, wird der Name des Item verändert.
  • Bei einer List<Item> als Rückgabewert, werden alles Names verändert und ein weiteres Item in der Liste ergänzt.

Der Quellcode, der im Projekt JAXRSServer abgelegt werden sollte, sieht folgendermaßen aus:

package org.hameister.itemservice;

import java.io.IOException;
import java.util.List;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;

@Provider
public class TestInterceptor implements ReaderInterceptor, WriterInterceptor {

    @Override
    public Object aroundReadFrom(ReaderInterceptorContext ric) throws IOException, WebApplicationException {
        System.out.println("aroundReadFrom");
        return ric.proceed();
    }

    @Override
    public void aroundWriteTo(WriterInterceptorContext wic) throws IOException, WebApplicationException {
        System.out.println("Called before the MessageBodyWriters are executed.");
        
        // Access the headers
        MultivaluedMap<String, Object> headers = wic.getHeaders();
        if(headers.containsKey("Content-Type")) {
            System.out.println("Content-Type: "+headers.get("Content-Type"));
        }
        
        // Add header informations
        wic.getHeaders().add("Manipulated", "true");
        
        // Change the entities
        Object entity = wic.getEntity();
        if(entity instanceof String) {
            wic.setEntity("Changed_string_"+entity); // Replace the entity
        } else if(entity instanceof Item) {
            Item item = (Item)entity;
            item.setName("Changed_item_"+item.getName()); // Change an entity
        } else if(entity instanceof List) {
            List<Item> list = (List)entity;
            for(Item item: list) {
                item.setName("Changed_"+item.getName()); // Change an object
            }
            list.add(new Item("ID_JH", "Item Added", "This item was added by the Interceptor!")); //Add an object
        }
        wic.proceed();
    }
}

Zu beachten ist die Annotation @Provider, die dafür sorgt, dass der Interceptor registriert wird. Außerdem muss vor dem Verlassen der Methode immer proceed() auf dem jeweiligen Context aufgerufen werden.

In dem Beispiel-Interceptor wird die Methode aroundReadFrom vom Interface ReaderInterceptor, muss dazu verwendet, den Aufruf mit System.out zu protokollieren. In der Methode aroundWriteTo vom Interface WriterInterceptor werden die oben angesprochenen Veränderungen an der Ergebnismenge durchgeführt.

Wenn nun alle bisher erstellten JUnit-Tests ausgeführt werden, dann schlagen diese fehl. Der Grund dafür ist, dass durch den Interceptor die Rückgabewerte verändert wurden. Damit die Tests wieder fehlerfrei durchlaufen, müssen folgende Anpassungen durchgeführt werden:

package org.hameister.itemservice.test;

import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientFactory;
import javax.ws.rs.client.InvocationCallback;
import junit.framework.Assert;
import org.hameister.itemservice.Item;
import org.hameister.itemservice.ItemListMessageBodyReader;
import org.hameister.itemservice.ItemListMessageBodyWriter;
import org.hameister.itemservice.ItemMessageBodyReader;
import org.hameister.itemservice.ItemMessageBodyWriter;
import org.junit.After;
import org.junit.AfterClass;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class JAXRESTClientTest {

    Client client;
    private static final String SERVER = "http://localhost:8080/JAXRSServer/webresources";

    public JAXRESTClientTest() {
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
        client = ClientFactory.newClient();
    }

    @After
    public void tearDown() {
        client.close();
    }

    @Test
    public void itemstringTextPlain() {
        String returnValue = client.target(SERVER + "/itemservice/itemstring").request("text/plain").get(String.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value ''itemstring'':{0}", returnValue);
        Assert.assertEquals("Changed_string_Item 1", returnValue);
    }

    @Test
    public void itemstringJSON() {
        String returnValue = client.target(SERVER + "/itemservice/itemstring").request("application/json").get(String.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value ''itemstring'':{0}", returnValue);
        Assert.assertEquals("Changed_string_Item 1", returnValue);
    }

    @Test
    public void itemstringXML() {
        String returnValue = client.target(SERVER + "/itemservice/itemstring").request("application/xml").get(String.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value 'itemstring':{0}", returnValue);
        Assert.assertEquals("Changed_string_Item 1", returnValue);
    }

    @Test
    public void itemJSON() {
        client.register(ItemMessageBodyWriter.class);
        client.register(ItemMessageBodyReader.class);
        Item returnValueItem = client.target(SERVER + "/itemservice/item").request("application/json").get(Item.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value 'itemstring':{0} {1}", new Object[]{returnValueItem.getName(), returnValueItem.getDescription()});
        Assert.assertEquals("Changed_item_Item 1", returnValueItem.getName());
        Assert.assertTrue("Wrong description text in Item: " + returnValueItem.getDescription(), returnValueItem.getDescription().startsWith("Item Description"));
    }

    @Test
    public void itemXML() {
        client.register(ItemMessageBodyWriter.class);
        client.register(ItemMessageBodyReader.class);

        Item returnValueItem = client.target(SERVER + "/itemservice/item").request("application/xml").get(Item.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value 'itemstring':{0} {1}", new Object[]{returnValueItem.getName(), returnValueItem.getDescription()});
        Assert.assertEquals("Changed_item_Item 1", returnValueItem.getName());
        Assert.assertTrue("Wrong description text in Item: " + returnValueItem.getDescription(), returnValueItem.getDescription().startsWith("Item Description"));

    }

    @Test
    public void itemTextPlain() {
        client.register(ItemMessageBodyWriter.class);
        client.register(ItemMessageBodyReader.class);

        Item returnValueItem = client.target(SERVER + "/itemservice/item").request("text/plain").get(Item.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value 'itemstring':{0} {1}", new Object[]{returnValueItem.getName(), returnValueItem.getDescription()});
        Assert.assertEquals("Changed_item_Item 1", returnValueItem.getName());
        Assert.assertTrue("Wrong description text in Item: " + returnValueItem.getDescription(), returnValueItem.getDescription().startsWith("Item Description"));

    }

    private void checkItemList(List<Item> returnValueItems) {
        Assert.assertEquals("11 Items expected.", 11, returnValueItems.size());

        System.out.println("Return value 'number of items':" + returnValueItems.size());
        for (int i = 0; i < 10; i++) {
            Item item = returnValueItems.get(i);
            Assert.assertTrue("Item name is not correct: " + item.getName(), item.getName().startsWith("Changed_Item "));
            Assert.assertTrue("Wrong description text in Item: " + item.getDescription(), item.getDescription().startsWith("Item Description"));

            Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value ''item'':{0} {1}", new Object[]{item.getName(), item.getDescription()});
        }

        //Check the additional Item
        Item item = returnValueItems.get(10);
        Assert.assertTrue("Item name is not correct: " + item.getName(), item.getName().startsWith("Item Added"));
        Assert.assertTrue("Wrong description text in Item: " + item.getDescription(), item.getDescription().startsWith("This item was added by the Interceptor"));

    }

    @Test
    public void itemListJSON() {
        client.register(ItemListMessageBodyWriter.class);
        client.register(ItemListMessageBodyReader.class);

        List<Item> returnValueItems = client.target(SERVER + "/itemservice/items").request("application/json").get(List.class);
        checkItemList(returnValueItems);
    }

    @Test
    public void itemListXML() {
        client.register(ItemListMessageBodyWriter.class);
        client.register(ItemListMessageBodyReader.class);

        List<Item> returnValueItems = client.target(SERVER + "/itemservice/items").request("application/xml").get(List.class);
        checkItemList(returnValueItems);
    }

    @Test
    public void itemListTextPlain() {
        client.register(ItemListMessageBodyWriter.class);
        client.register(ItemListMessageBodyReader.class);

        List<Item> returnValueItems = client.target(SERVER + "/itemservice/items").request("text/plain").get(List.class);
        checkItemList(returnValueItems);
    }

    @Test
    public void itemFilter() {
        String returnValue = client.target(SERVER + "/itemservice/itemstring").request("text/plain").cookie("Filter", "f1").get(String.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value ''itemstring'':{0}", returnValue);
        Assert.assertEquals("Item was not filtered.", "", returnValue);
    }

    @Test
    public void itemNoFilter() {
        String returnValue = client.target(SERVER + "/itemservice/itemstring").request("text/plain").cookie("Filter", "f2").get(String.class);
        Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "Return value ''itemstring'':{0}", returnValue);
        Assert.assertEquals("Item was not filtered.", "Changed_string_Item 1", returnValue);
    }
}

Im nächsten Teil des Tutorials geht es um den asynchronen Aufruf von Methoden bei JAX-RS 2.0.