02.02.2013

MessageBodyReader und MessageBodyWriter für List- JAX-RS 2.0

In diesem Teil des Tutorials wird gezeigt, wie Listen (java.util.List) mit Objekten vom Typ Item über eine REST-Schnittstelle übertragen werden. Um das zu erreichen wird ein MessageBodyReader und ein MessageBodyWriter erstellt.

Auf der Abbildung sieht man, wie der Client einen Request an den Service /itemService/items des Servers absetzt. Bevor die Response mit einer Liste von Items an den Client gesendet wird, muss der ItemListMessageBodyWriter durchlaufen werden, damit die Liste mit Items als Stream übertragen wird. Auf Seite des Client wird der Item-Stream von einem ItemListMessageBodyReader geparsed, so dass der Client mit einer Liste von Objekten vom Typ Item arbeiten kann.

Als erstes wird deshalb der REST-Service um die Methode items erweitert, die einfach eine Liste mit Objekten vom Type Item zurückliefert.

package org.hameister.itemservice;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
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("items")
    @Produces({"application/xml, text/plain, application/json"})
    public List<Item> items() {
        List<Item> list = new ArrayList<Item>();
        for (int i = 1; i < 11; i++) {
            list.add(new Item("id" + i, "Item " + i, "Item Description " + System.currentTimeMillis()));
        }
        return list;
    }
}

In der Methode des REST-Service werden 10 Objekte vom Typ Item erstellt und in einer Liste gespeichert, die am Ende zurückgegeben wird.

Damit die ‹bertragung von Listen mit Objekten funktioniert, wird anschließend ein ItemListMessageBodyWriter im Projekt JAXRSLibrary erstellt, der das Interface MessageBodyWriter implementiert (siehe ItemMessageBodyWriter).

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

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

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

    @Override
    public void writeTo(List<Item> list, 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")) {
            ObjectMapper mapper = new ObjectMapper().setVisibility(JsonMethod.FIELD, Visibility.ANY);
            try (PrintStream printStream = new PrintStream(out)) {
                printStream.print(mapper.writeValueAsString(list));
            }
            return;
        } else if(mt.getType().equals("text") && mt.getSubtype().equals("plain")) {
            try (PrintStream printStream = new PrintStream(out)) {
                for(Item t : list) {
                    printStream.print("|"+(t.getId()+"/"+t.getName()+"/"+t.getDescription()));
                }
            }
            return;
        }
        throw new UnsupportedOperationException("Not supported yet.");
    }
}

Zum Verarbeiten von JSON wird, wie schon beim ItemMessageBodyWriter, das Framework Jackson verwendet. Für text/plain kommt ein selbst definiertes Textformat zum Einsatz.

Außerdem wird eine Klasse ItemListMessageBodyReader im Projekt JAXRSLibrary, die das Interface MessageBodyReader implementiert, angelegt, damit das Lesen auf Client-Seite auch funktioniert. (siehe ItemMessageBodyReader)

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

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

    @Override
    public List<Item> readFrom(Class<List<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, new TypeReference<List<Item>>() {
            });
        } else if (mt.getType().equals("application") && mt.getSubtype().equals("xml")) {
            try {
                List<Item> items = new ArrayList<>();

                DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
                Document doc = dBuilder.parse(in);
                NodeList itemList = doc.getElementsByTagName("item");

                for (int i = 0; i < itemList.getLength(); i++) {
                    Element itemNode = (Element) itemList.item(i);
                    if (itemNode.getNodeType() == Node.ELEMENT_NODE) {
                        Item item = new Item();
                        item.setId(itemNode.getElementsByTagName("id").item(0).getTextContent());
                        item.setName(itemNode.getElementsByTagName("name").item(0).getTextContent());
                        item.setDescription(itemNode.getElementsByTagName("description").item(0).getTextContent());
                        items.add(item);
                    }
                }

                return items;
            } catch (SAXException ex) {
                Logger.getLogger(ItemListMessageBodyReader.class.getName()).log(Level.SEVERE, null, ex);
            } catch (ParserConfigurationException ex) {
                Logger.getLogger(ItemListMessageBodyReader.class.getName()).log(Level.SEVERE, null, ex);
            }
        } else if (mt.getType().equals("text") && mt.getSubtype().equals("plain")) {
            //TODO Simplify and optimize performance!
            int c;
            StringBuffer buffer = new StringBuffer();
            while((c = in.read()) != -1) {
                buffer = buffer.append((char)c);
            }
            
            List<Item> items = new ArrayList<>();
            StringTokenizer itemTokenizer = new StringTokenizer(buffer.toString(), "|");
            while (itemTokenizer.hasMoreTokens()) {
                String itemString = itemTokenizer.nextToken();
                StringTokenizer st = new StringTokenizer(itemString, "/");
                String id = st.nextToken();
                String name = st.nextToken();
                String description = st.nextToken();
                Item item = new Item(id, name, description);
                items.add(item);
            }
            return items;
        }
        throw new UnsupportedOperationException("Not supported MediaType: " + mt);
    }
}

Zum Lesen von JSON wird, wie schon beim MessageBodyWriter, wieder das Framework Jackson verwendet. Im Gegensatz zum ItemMessageBodyReader wird für die Liste von Items im XML-Format auch ein MessageBodyReader benötigt. Zum Parsen wird der InputStream in einen DOM geladen, um Zugriff auf die Item-Elemente zu bekommen. Zum Verarbeiten des Text-Formats wird ein StringTokenizer verwendet.

Zum Testen der neuen Methode im REST-Service werden drei JUnit-Tests angelegt und in der Klasse JAXRESTClientTest ergänzt.

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

...

    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(ItemListMessageBodyReader.class);

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

    @Test
    public void itemListXML() {
        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(ItemListMessageBodyReader.class);

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

Zu beachten ist auch hier, dass der ItemListMessageBodyReader beim Client registriert werden muss, damit die Antwort vom Server verarbeitet werden kann.

Im nächsten Teil dieses Tutorials wird geht es um ContainerRequestFilter und ContainerResponseFilter.