Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 479
Robert Avatar asked Sep 24 '14 10:09

Robert


1 Answers

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>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>jboss-javaee-7.0-with-resteasy</artifactId>
                <version>8.1.0.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>jboss-javaee-7.0-with-tools</artifactId>
                <version>8.1.0.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                    <compilerArguments>
                        <endorseddirs>${endorsed.dir}</endorseddirs>
                    </compilerArguments>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.6</version>
                <executions>
                    <execution>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${endorsed.dir}</outputDirectory>
                            <silent>true</silent>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>javax</groupId>
                                    <artifactId>javaee-endorsed-api</artifactId>
                                    <version>7.0</version>
                                    <type>jar</type>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Works fine in the browser

enter image description here

Works fine with client api

public class BookTest {
    private static Client client;
    @BeforeClass
    public static void setUpClass() {
        client = ClientBuilder.newClient();
    }
    @AfterClass
    public static void tearDownClass() {
        client.close();
    }

    @Test
    public void testBookLink() {
        String BASE_URL
                = "http://localhost:8080/jaxrs-stackoverflow-user/rest/books";
        WebTarget target = client.target(BASE_URL);
        String result = target.request()
                .accept(MediaType.APPLICATION_XML).get(String.class);
        System.out.println(result);
    }
}

Result

Running jaxrs.book.test.BookTest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<collection xmlns:atom="http://www.w3.org/2005/Atom">
    <book author="Author" title="Book">
        <atom:link rel="list" href="http://localhost:8080/jaxrs-stackoverflow-user/rest/books"/>
    </book>
</collection>

As for your Unclear Stuff

Annotate the JAX-RS method with @AddLinks to indicate that you want Atom links injected in your response entity.

This is to indicate that the method will make use of link injection.

Annotate the JAX-RS methods you want Atom links for with @LinkResource, so that RESTEasy knows which links to create for which resources.

This allows you to customize what links are injected and for what entities. 8.2.4. Specifying which JAX-RS methods are tied to which resources goes more in depth.

Add RESTServiceDiscovery fields to the resource classes where you want Atom links injected.

"injected" means the framework will instantiate it for you, so you never have to explicitly do it yourself (as you attempted to do). Maybe do some research on dependency injection and Inversion of Control (IoC)

Good Luck. Hope all this helps.

like image 97
Paul Samsotha Avatar answered Nov 17 '22 23:11

Paul Samsotha