Insert links into RESTEasy XML results via JAXB

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

History:

To embed HATEOAS principles in my JBoss RESTEasy API, I need to insert links into my JAXB XML results so that clients can navigate the API.

Now I'm trying to figure out how to do this, but I'm not sure if the documentation is filled with errors or I just can’t understand the examples and explanations:

Fuzzy things:

As I understand it, you should use @AddLinks to declare that links should be inserted as a result. Then I have to do it again (!?) Using @LinkResource and "sometimes" specify which class the URI building process should start from (for example, @LinkResource(value = car.class) ). Then I have to add RESTServiceDiscovery to the entity class, annotate it with @XmlElementRef ... but in the examples RESTServiceDiscovery not used at all after the declaration (!?).

code:

I am really confused how to use all this, but of course I tried a lot of code myself to make it work.
The following code is similar to a sample document:

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 GET on books or book/1 causes 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'm not sure how this can work, so I tried adding the URI manually using the following code in 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 it still causes the same error.

+6
source share
2 answers

I'm not too familiar with link injection, but one easy way to add links is to insert javax.ws.rs.core.Link s in your JAXB entity classes. It comes with a built-in XmlAdapter, Link.JaxbAdapter , which allows the Link type to be marshaled and unmarshaled by JAXB. For example, you have a BookStore class that contains a Books collection. It will also have a Link from which you can manage navigation 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 simply the regular root element of the 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 BookResource we can basically add links on demand based on your logic of which links you want to submit. In the example below, there is an in-memory bit (this class is used, for example, as one of the Singleton classes) of books for which I add five books with increasing identifiers. When the request arrives, one or two links will be added to the BookStore return. Depending on which identifier is requested, we will add a β€œnext” link and / or a previous link. Links will have rel , which we call 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 example, we request the third book, and we see that there are links to the "next" and "previous" books in our in-memory db. We also call getNext() on our BookStore to get the next book in db, and the result will have 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); } } 

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 am using Resteasy 3.0.8 with Wildfly 8.1


UPDATE: using automatic detection

So, I tried the example reference manual and I cannot reproduce your problem. Not sure about your complete environment, but here is what I use

  • Wildfly 8.1
  • Resteasy 3.0.8
  • Maven

Here is 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 were only 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 great in browser

enter image description here

Works fine with api client

 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 fuzzy stuff

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

This means that the method will use link injection.

Annotate JAX-RS methods for which you need Atom links with @LinkResource , so RESTEasy knows which links are created for which resources.

This allows you to customize which links are entered and which objects. 8.2.4. Determining which JAX-RS methods are tied to which resources are more in-depth.

Add the RESTServiceDiscovery fields to the resource classes in which you want to enter Atom feeds.

"injected" means that the framework will create it for you, so you never have to explicitly do it yourself (as you tried to do). Perhaps some studies on injection dependency and inversion control (IoC)

Luck. Hope all this helps.

+11
source

I just want to add that if you want to add your ATOM links to JSON, you need to disable / exclude all Jackson providers. In WildFly 8.x, create META-INF / jboss-deployment-structure.xml with the following:

 <jboss-deployment-structure> <deployment> <exclusions> <module name="org.jboss.resteasy.resteasy-jackson-provider" /> <module name="org.jboss.resteasy.resteasy-jackson2-provider" /> </exclusions> <dependencies> <module name="org.jboss.resteasy.resteasy-jettison-provider" /> </dependencies> </deployment> </jboss-deployment-structure> 

This way, the drop provider will correctly create a JSON view with ATOM links enabled

Hope this helps;)

0
source

Source: https://habr.com/ru/post/975762/


All Articles