02.02.2013

Implementierung von MessageBodyReader und MessageBodyWriter - JAX-RS 2.0

In diesem Teil des Tutorials werden komplexere Objekte vom Type Item über die Client-API abgefragt. Dazu wird die REST-Schnittstelle erweitert und es werden MessageBodyReader und MessageBodyWriter erstellt, um die Objekte für die Formate text/plain, application/json und application/xml zu übertragen.

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. Auf Seite des Client wird der Item-Stream von einem ItemMessageBodyReader geparsed, so dass der Client mit Objekten vom Typ Item arbeiten kann.

Wie schon angesprochen, sollen komplexere Objekte über die REST-Schnittstelle übertragen werden. Deshalb legen wir eine Klasse Item an, die eine id, einen name und eine description jeweils vom Typ String haben.

Die folgende Klasse wird in dem Projekt JAXRSLibrary unter dem Namen Item abgespeichert. Die Klasse wird in dem Library-Projekt abgelegt, weil sowohl Client als auch Server auf die Klasse zugreifen.

package org.hameister.itemservice;

import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Item implements Serializable {
    private String id;
    private String name;
    private String description;

    public Item() {      
    }
    
    public Item(String id, String name, String description) {
        this.id = id;
        this.name = name;
        this.description = description;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

Bei der Klasse Item handelt es sich um ein einfaches Pojo oder eine Bean, die mit der Annotation @XmlRootElement versehen ist, damit sie als XML-Element übetragen werden kann.

Nachdem die Klasse Item angelegt wurde, erweitern wir die REST-Schnittstelle, um Zugriff auf Objekte von Typ Item zu haben.

Dazu wird einfach eine neue Methode item eingefügt, die als Rückgabewert den Typ Item hat. Mit der Annotation @Producer werden die Rückgabeformate festgelegt. In dem Beispiel werden Objekte zurückgeliefert, die sich nur durch den Zeitstempel in der Description unterscheiden.

package org.hameister.itemservice;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;

@Path("/itemservice")
public class ItemService {

...

    @GET
    @Path("item")
    @Produces({"application/xml, text/plain, application/json"})
    public Item item() {
        return new Item("id1", "Item 1", "Item Description " + System.currentTimeMillis());
    }
}

Würden wir die Methode nun mit der Client-API mit folgendem Kommando aufrufen,

		String server = "http://localhost:8080/JAXRSServer/webresources";
        Item returnValueItem = client.target(server+"/itemservice/item").request("application/json").get(Item.class);

dann würde auf Client-Seite die Meldung Exception in thread "main" javax.ws.rs.InternalServerErrorException und im Server-Log folgende Fehlermeldung erscheinen:

org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: 
         MessageBodyWriter not found for media type=application/json, 
                                         type=class org.hameister.itemservice.Item, 
                                         genericType=class org.hameister.itemservice.Item

Der Grund dafür ist, dass der MessageBodyWriter für die Klasse Item und das Format JSON fehlt. Für einen Aufruf mit dem Format text/plain würde das gleiche passieren. Nur für application/xml wird ein Ergebnis zurückgeliefert. Grund dafür ist die Annotation @XmlRootElement in der Klasse Item.

Die Lösung für das Problem mit text/plain und application/json ist das Erstellen eines MessageBodyWriter für Items.

Dazu wird eine Klasse ItemMessageBodyWriter erstellt, die das Interface MessageBodyWriter implementiert und in dem Projekt JAXRSLibrary abgelegt wird.

@Provider
@Produces({"text/plain", "application/json"})
public class ItemMessageBodyWriter implements MessageBodyWriter<Item> {

    @Override
    public boolean isWriteable(Class<?> type, Type type1, Annotation[] antns, MediaType mt) {
        return true;
    }

    @Override
    public long getSize(Item t, Class<?> type, Type type1, Annotation[] antns, MediaType mt) {
        return -1;
    }

    @Override
    public void writeTo(Item t, Class<?> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, Object> mm, OutputStream out) throws IOException, WebApplicationException {
        if (mt.getType().equals("application") && mt.getSubtype().equals("json")) {
            StringBuffer buffer = new StringBuffer();

            buffer = buffer.append("{");
            buffer = buffer.append("\"id\":\"").append(t.getId()).append("\",");
            buffer = buffer.append("\"name\":\"").append(t.getName()).append("\",");
            buffer = buffer.append("\"description\":\"").append(t.getDescription()).append("\"");
            buffer = buffer.append("}");
            try (PrintStream printStream = new PrintStream(out)) {
                printStream.print(buffer.toString());
            }
            return;
        } else if(mt.getType().equals("text") && mt.getSubtype().equals("plain")) {
            try (PrintStream printStream = new PrintStream(out)) {
                printStream.print((t.getId()+"/"+t.getName()+"/"+t.getDescription()));
            }
            return;
        }
        throw new UnsupportedOperationException("Not supported MediaType: " + mt);
    }
}

Wichtig ist die Annotation @Provider oberhalb der Klasse und die Annotation @Produces, die die Formate angibt, die der ItemMessageBodyWriter zurückliefern kann.

In der Methode writeTo werden die Werte des Parameters Item t ausgelesen und in den OutputStream geschrieben. Im Fall von application/json wird ein einfacher JSON-String zusammengebaut und für das Format text/plain ein selbst definiertes Text-Format.

ANMERKUNG

In dem Beispiel wird der JSON-String "per Hand" zusammengesetzt. Diese Variante wurde hier nur gewählt, um zu zeigen, dass es funktioniert. Weiter unten wird das Framework Jackson benutzt, um JSON zu verarbeiten.
Für das Format text/plain wurde ein eigenes Format definiert. Das soll als Beispiel dafür dienen, dass in den OutputSteam prinzipiell alles geschrieben werden kann. Wichtig dabei ist, dass es beim Empänger wieder gelesen werden kann. Aber dazu kommen wir gleich.

Nachdem die Klasse auf dem Server deployed ist und die Abfrage erneut gestartet wird, tritt ein anderes Problem auf. Im Client-Log erscheint folgende Fehlermeldung:

Caused by: org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: 
                MessageBodyReader not found for media type=application/json, 
                type=class org.hameister.itemservice.Item, 
                genericType=class org.hameister.itemservice.Item.

Diesmal ist es so, dass der Server den Request richtig verarbeitet und die Response verschickt, aber die Client-API die Antwort nicht verarbeiten kann, weil kein MessageBodyReader für die Klasse Item vorhanden ist. Deshalb erstellen wir einen ItemMessageBodyReader im Projekt JAXRSLibrary der folgendermaßen aussieht:

package org.hameister.itemservice;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Scanner;
import javax.ws.rs.Consumes;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import org.codehaus.jackson.map.ObjectMapper;

@Provider
@Consumes({"text/plain", "application/json"})
public class ItemMessageBodyReader implements MessageBodyReader<Item> {

    @Override
    public boolean isReadable(Class<?> type, Type type1, Annotation[] antns, MediaType mt) {
        return true;
    }

    @Override
    public Item readFrom(Class<Item> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm, InputStream in) throws IOException, WebApplicationException {
        if (mt.getType().equals("application") && mt.getSubtype().equals("json")) {
            ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(in, Item.class);
        } else if (mt.getType().equals("text") && mt.getSubtype().equals("plain")) {
            Scanner scanner = new Scanner(in).useDelimiter("/");
            String id = scanner.next();
            String name = scanner.next();
            String description = scanner.next();
            Item item = new Item(id,name,description);
            return item;
        }
        throw new UnsupportedOperationException("Not supported MediaType: " + mt);
    }
}

Ähnlich wie beim ItemMessageBodyWriter, wird beim ItemMessageBodyReader das Interface MessageBodyReader mit zwei Methoden implementiert. Bei der readFrom-Methode wird wieder zwischen application/json und text/plain unterschieden. Zum Verarbeiten von JSON wird dieses mal der ObjectMapper des Jackson-Frameworks verwendet. Einen Parser "per Hand" oder mit ANTLR zu erstellen, wäre natürlich auch möglich. Beim Interpretieren der Text-Antwort wird ein Scanner aus dem Java-Util-Package verwendet.

Zum Testen der Methode erweitern wir die Testklasse JAXRESTClientTest im Projekt JAXRSClient und legen drei weitere JUnit-Tests an, die die Methode mit ihren drei Rückgabeformaten aufrufen.

Wichtig ist, dass die Klasse ItemMessageBodyReader.class beim Client mit register angemeldet wird, so dass die Response vom Server verarbeitet werden kann.

public class JAXRESTClientTest {

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

    public JAXRESTClientTest() {
    }

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

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

...

    @Test
    public void itemJSON() {
        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(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(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"));

    }
}

Im nächsten Teil dieses Tutorials wird der REST-Service so erweitert, dass auch Listen (List) mit Items übertragen werden können.