Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
279 views
in Technique[技术] by (71.8m points)

java - Inserting links into RESTEasy XML results via JAXB

I want to insert links into XML via RESTeasy / JAXB. I tried to use the documentation for my code, but that did not work, so I just coded the given examples in the documentation: it still does not work and I have no idea why.

Background:

To implement the HATEOAS principles into my JBoss RESTEasy API I have to insert links into my JAXB XML results, so clients can navigate through the API.

I am now trying to understand how to do that, but I am not sure if the documentation is full of errors or I am just not able to understand the examples and explanations:

Unclear stuff:

As I understand it you have to use @AddLinks to declare that a result should have links inserted. Then I have to do that redundantly again (!?) with @LinkResource and "sometimes" specify which class the URI building process should come from (e.g. @LinkResource(value = car.class)). Then I have to add a RESTServiceDiscovery into the entity class, annotate it with @XmlElementRef... but in the examples RESTServiceDiscovery gets not used at all after declaring (!?).

Code:

I am really confused how to use all that, but of course I tried a lot of code myself, to get it to work.
The following code is just like the docu example:

BookController.java

import java.util.ArrayList;
import java.util.Collection;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import org.jboss.resteasy.links.AddLinks;
import org.jboss.resteasy.links.LinkResource;

import com.gasx.extsys.datamodel.vo.kplan.Book;

@Path("/")
@Consumes({ "application/xml", "application/json" })
@Produces({ "application/xml", "application/json" })
public class BookController {
    @AddLinks
    @LinkResource(value = Book.class)
    @GET
    @Path("books")
    public Collection<Book> getBooks() {
        ArrayList<Book> res = new ArrayList<Book>();
        res.add(new Book("Robert", "WhySOIsGreat"));
        res.add(new Book("Robert", "JavaUltimateGuide"));
        res.add(new Book("Not Robert", "ThisIsSparta!"));
        return res;
    };

    @AddLinks
    @LinkResource
    @GET
    @Path("book/{id}")
    public Book getBook(@PathParam("id") String id) {
        return new Book("Robert", "WhyIloveJAVA");
    };
} 

Book.java:

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlRootElement;

import org.jboss.resteasy.links.RESTServiceDiscovery;

@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class Book {
    @XmlAttribute
    private String author = "startAuthor";

    @XmlID
    @XmlAttribute
    private String title = "startTitle";

    @XmlElementRef
    private RESTServiceDiscovery rest;

    public Book() {

    }

    public Book(String author, String title) {
        this.author = author;
        this.title = title;
    }
}

Now calling a GET on books or book/1 throws this error:

2014-09-25 11:30:36,188 WARN  [http-/0.0.0.0:8080-1] (org.jboss.resteasy.core.SynchronousDispatcher:135) # Failed executing GET /book/1: org.jboss.resteasy.plugins.providers.jaxb.JAXBMarshalException: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
XmlElementRef points to a non-existent class.

I am not sure how this could work, so I tried adding the URI manually with following code in the Book.java:

import java.net.URI;
    public Book(String author, String title) {
        this.author = author;
        this.title = title;
        URI uri = URI.create("books/" + title);
        rest = new RESTServiceDiscovery();
        rest.addLink(uri, "self");
    }

But this still throws the same error.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I'm not too familiar with link injection, but one easy way to add links is to embed javax.ws.rs.core.Links into your JAXB entity classes. It comes with a built in XmlAdapter, Link.JaxbAdapter that will allow the Link type to be marshalled and unmarshalled by JAXB. Take for example you have a BookStore class that holds a collection of Books. It will also have Links from which you can control the navigational cases.

@XmlRootElement(name = "bookstore")
public class BookStore {

    private List<Link> links;
    private Collection<Book> books;

    @XmlElementRef
    public Collection<Book> getBooks() {
        return books;
    }

    public void setBooks(Collection<Book> books) {
        this.books = books;
    }

    @XmlElement(name = "link")
    @XmlJavaTypeAdapter(Link.JaxbAdapter.class) 
    public List<Link> getLinks() {
        return links;
    }

    public void setLinks(List<Link> links) {
        this.links = links;
    }

    @XmlTransient
    public URI getNext() {
        if (links == null) {
            return null;
        }
        for (Link link : links) {
            if ("next".equals(link.getRel())) {
                return link.getUri();
            }
        }
        return null;
    }

    @XmlTransient
    public URI getPrevious() {
        if (links == null) {
            return null;
        }
        for (Link link : links) {
            if ("previous".equals(link.getRel())) {
                return link.getUri();
            }
        }
        return null;
    }
}

The Book class is just a regular root element JAXB class

@XmlRootElement
public class Book {

    @XmlAttribute
    private String author;

    @XmlAttribute
    private String title;

    public Book() {}

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }
}

In the BookResource class, we can basically add links on demand based on your required logic of what links you want represented. In the example below, there is an in-memory db (this class is used as a stateful singleton class just for example) of books for which I add five books, with incrementing ids. When the request comes in, one or two links will be added to the returning BookStore. Depending on what id is being requested, we will add a "next" and/or a previous" link. The links will have rels that we refer to from our BookStore class.

@Path("/books")
public class BookResource {

    private final Map<Integer, Book> booksDB 
            = Collections.synchronizedMap(new LinkedHashMap<Integer, Book>());
    private final AtomicInteger idCounter = new AtomicInteger();

    public BookResource() {
        Book book = new Book("Book One", "Author One");
        booksDB.put(idCounter.incrementAndGet(), book);

        book = new Book("Book Two", "Author Two");
        booksDB.put(idCounter.incrementAndGet(), book);

        book = new Book("Book Three", "Author Three");
        booksDB.put(idCounter.incrementAndGet(), book);

        book = new Book("Book Four", "Author Four");
        booksDB.put(idCounter.incrementAndGet(), book);

        book = new Book("Book Five", "Author Five");
        booksDB.put(idCounter.incrementAndGet(), book);
    }

    @GET
    @Formatted
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_XML)
    public BookStore getBook(@Context UriInfo uriInfo, @PathParam("id") int id) {
        List<Link> links = new ArrayList<>();
        Collection<Book> books = new ArrayList<>();

        UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
        uriBuilder.path("books");
        uriBuilder.path("{id}");

        Book book = booksDB.get(id);
        if (book == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        synchronized(booksDB) {
            if (id + 1 <= booksDB.size()) {
                int next = id + 1;
                URI nextUri = uriBuilder.clone().build(next);
                Link link = Link.fromUri(nextUri).rel("next").type(MediaType.APPLICATION_XML).build();
                links.add(link);
            }
            if (id - 1 > 0) {
                int previous = id - 1;
                URI nextUri = uriBuilder.clone().build(previous);
                Link link = Link.fromUri(nextUri).rel("previous").type(MediaType.APPLICATION_XML).build();
                links.add(link);
            }
        }

        books.add(book);
        BookStore bookStore = new BookStore();
        bookStore.setLinks(links);
        bookStore.setBooks(books);
        return bookStore;
    }
}

And in the test case, we request the third book, and we can see there are links for the "next" and "previous" books in our in-memory db. We also call getNext() on our BookStore to retrieve the next book in the db, and the result will come with two different links.

public class BookResourceTest {

    private static Client client;

    @BeforeClass
    public static void setUpClass() {
        client = ClientBuilder.newClient();
    }

    @AfterClass
    public static void tearDownClass() {
        client.close();
    }

    @Test
    public void testBookResourceLinks() throws Exception {
        String BASE_URL = "http://localhost:8080/jaxrs-stackoverflow-book/rest/books/3";
        WebTarget target = client.target(BASE_URL);
        String xmlResult = target.request().accept(MediaType.APPLICATION_XML).get(String.class);
        System.out.println(xmlResult);

        Unmarshaller unmarshaller = JAXBContext.newInstance(BookStore.class).createUnmarshaller();
        BookStore bookStore = (BookStore)unmarshaller.unmarshal(new StringReader(xmlResult));
        URI next = bookStore.getNext();

        WebTarget nextTarget = client.target(next);
        String xmlNextResult = nextTarget.request().accept(MediaType.APPLICATION_XML).get(String.class);
        System.out.println(xmlNextResult);
    }
}

The result:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<bookstore>
    <book author="Author Three" title="Book Three"/>
    <link href="http://localhost:8080/jaxrs-stackoverflow-book/rest/books/4" rel="next" type="application/xml"/>
    <link href="http://localhost:8080/jaxrs-stackoverflow-book/rest/books/2" rel="previous" type="application/xml"/>
</bookstore>

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<bookstore>
    <book author="Author Four" title="Book Four"/>
    <link href="http://localhost:8080/jaxrs-stackoverflow-book/rest/books/5" rel="next" type="application/xml"/>
    <link href="http://localhost:8080/jaxrs-stackoverflow-book/rest/books/3" rel="previous" type="application/xml"/>
</bookstore>

FYI, I'm using Resteasy 3.0.8 with Wildfly 8.1


UPDATE: Using auto-discovery

So I tried out the reference guide example and I can't reproduce your problem. Not sure your complete environment, but here's what I'm using

  • Wildfly 8.1
  • Resteasy 3.0.8
  • Maven

Here's the code

Application class

@ApplicationPath("/rest")
public class BookApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
       Set<Class<?>> classes = new HashSet<>();
       classes.add(Bookstore.class);
       return classes;
   }
}

Resource class

@Path("/books")
@Produces({"application/xml", "application/json"})
public class Bookstore {

    @AddLinks
    @LinkResource(value = Book.class)
    @GET
    @Formatted
    public Collection<Book> getBooks() {
        List<Book> books = new ArrayList<>();
        books.add(new Book("Book", "Author"));
        return books;
    }
}

Book class

@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class Book {
    @XmlAttribute
    private String author;
    @XmlID @XmlAttribute
    private String title; 
    @XmlElementRef
    private RESTServiceDiscovery rest;

    public Book() {}

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }
}

pom.xml (Maybe you are missing some dependencies - Note below resteasy-client and resteasy-servlet-initializer was just for testing)

<?xml version="1.0" encoding="UTF-8"?>
<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>com.underdogdevs.web</groupId>
    <artifactId>jaxrs-stackoverflow-user</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>jaxrs-stackoverflow-user</name>

    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jackson2-provider</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jaxb-provider</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jaxrs</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>jaxrs-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-links</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-servlet-initializer</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency&gt

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...