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.
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:
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 (!?).
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.
I'm not too familiar with link injection, but one easy way to add links is to embed javax.ws.rs.core.Link
s 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 Link
s 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 rel
s 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
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
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>
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>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With