users@jersey.java.net

[Jersey] Re: Implementing a Query by Example

From: Noah White <emailnbw_at_gmail.com>
Date: Tue, 22 Oct 2013 11:04:38 -0400

Hi Antonio,

Great question.

I'm a little surprised that Book is not being automatically unmarshalled for you. I wonder if its due to it being a @QueryParam or the lack of @Consumes on the method.

I've had success automatically unmarshalling from JSON (I don't think I've tried XML) using JAXB (legacy app or I would have opted for Jackson w/POJOs), JAXBContextResolver and Natural Notation when dealing with an entity body, for example w/a @POST. It's looked something like this:

@POST
@Path("/foos")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createFoo(Foo myFoo) {

}

In that case when a JSON representation of Foo (a JAXB entity bean just like your Book example) is in the POSTs entity body it will be automatically unmarshalled into myFoo - w/missing properties set to null/default values. I would think since it's using JAXB as described above if your entity is in the JAXB context it would automatically unmarshal it for you. Perhaps it has to due w/the fact its a @QueryParam.

One option which adds an extra step but gives you automatic unmarshalling for free would be to use a @POST with the QBE entity as the entity body. This has an advantage over using a query param in that you don't have to deal with the length restriction imposed on the URL if you are dealing with a lot of XML grammar etc. On the server you would create a search query entity using the request entity body and the Response would include a link to run the query and return the results when the client performs a GET on it.

Or you could be less 'RESTy' and skip the search query entity creation/link response and instead run the search and return the results in the POST response.

BTW - I haven't used QBE, how does JPA/Eclipselink handle QBE when some of the properties are NULL? Does it leave them out of the query or AND them in with propName=NULL? I'm curious because if you want to perform a projection style QBE you'd only be specifying certain properties and JAXB will initialize the missing ones the appropriate defaults which may be NULL.

FWIW I've implemented projection style queries w/Jersey but I used bean validation to intercept and parse the query param containing property names (as simple name=val text), then used reflection against the resource entity to validate the parsed param name and values and finally in the controller I used the Criteria API to dynamically construct the projection query. This works pretty well but can get more complex if the query is trying to include relations. This was all before JPA-RS which I haven't played with yet. I'm curious to know if/how it supports queries of this type.

-Noah

On Oct 22, 2013, at 9:28 AM, Antonio Goncalves <antonio.mailing_at_gmail.com> wrote:

> Hi all,
>
> I'm using a very manual approach to solve this problem and I would like to share it with you in case you have a better idea. Basically my resource doesn't do much, it just uses a @QueryParam to get the XML and JPA to do a QBE :
>
> @GET
> @Path("/query")
> @Produces("application/xml")
> public Response findByQBE(@QueryParam("example") Book example) {
>
> CriteriaBuilder builder = em.getCriteriaBuilder();
> CriteriaQuery<Book> criteria = builder.createQuery(Book.class);
> // ...
> // query by example
> return Response.ok(books).build();
> }
>
>
> The manual work that I do is in the Book entity. Basically I give it a constructor with a String (needed by JAX-RS) to be able to unmarshall the XML into the bean itself, and I create a toXML() method that marshalls the bean into XML :
>
> @Entity
> @XmlRootElement
> public class Book implements Serializable {
>
> @Id @GeneratedValue
> private String isbn;
> private String title;
> private String description;
> // ... other attributes
>
> public Book() {
> }
>
> public Book(String xml) throws JAXBException {
> // Uses JAXB to unmarshall the XML string into the bean itself
> JAXBContext ctx = JAXBContext.newInstance(Book.class);
> Unmarshaller m = ctx.createUnmarshaller();
> Book book = (Book) m.unmarshal(new StringReader(xml));
> this.isbn = book.getIsbn();
> this.title = book.getTitle();
> this.description = book.getDescription();
> this.nbOfPages = book.getNbOfPages();
> this.publisher = book.getPublisher();
> }
>
> public String toXML() throws JAXBException {
> // Uses JAXB to marshall the bean into an XML string
> StringWriter writer = new StringWriter();
> JAXBContext ctx = JAXBContext.newInstance(Book.class);
> Marshaller m = ctx.createMarshaller();
> m.marshal(this, writer);
> return writer.toString();
> }
>
> // ... getters & setters
> }
>
> Then, to invoke my resource with the Client API, I just call the toXML() method as follow :
>
> Book example = new Book();
> example.setTitle("Java");
> Response response = client.target(URL + "/books").path("/query")
> .queryParam("example", example.toXML())
> .request(MediaType.APPLICATION_XML)
> .get();
>
> This way I invoke the resource with a request that looks like what I want :
>
> /rest/books/query/example=<book><title>Java</title></book>
>
>
> So it does work except there is many manual code that, in my mind, could be automatised. Am I wrong ? Do you have a better idea ? In this case I suppose a provider would make sense but I already use JAXB and XML so it feels weird to do the job a second time.
>
> Thanks for you thoughts
>
> Antonio
>
>
> 2013/10/22 Antonio Goncalves <antonio.mailing_at_gmail.com>
> Hi all,
>
> I need to implement a query by example service and be able to consume it with the new client API.
>
> The idea is that I have a Book entity with a JAXB annotation :
>
> @Entity
> @XmlRootElement
> public class Book implements Serializable {
>
> @Id @GeneratedValue
> private Long id = null;
>
> private String isbn;
> private String title;
> private String description;
> }
>
>
> I now need a service that would be able to receive an 'example' of the Book in XML format. So if the service receives :
>
> <book><isbn>1234</isbn></book>
>
> It will return all the books with an ISBN like '1234. If the service receives :
>
> <book><isbn>1234</isbn><title>Java</title><description>Best book</description></book>
>
> Il will return all the books with an ISBN like '1234' AND a title like 'Java' AND a description like 'Best Book'. As you can see, the query string depends on which attributes of the entity are set. So I was thinking of having a URI that would look like :
>
> /rest/books/query=<book><isbn>1234</isbn></book>
> /rest/books/query=<book><isbn>1234</isbn><title>Java</title><description>Best book</description></book>
>
> So I assume that the resource would then look like :
>
> @GET
> public Response findByQBE(@QueryParam("query") Book example) {
> ...
> }
>
> And the client API
>
> Book example = new Book();
> example.setTitle("Java");
> client.target(URL + "/books").path("/qbe").queryParam("query", example).request(MediaType.APPLICATION_XML).get();
>
> But that doesn't work (I get a 404).
>
> I feel I don't need to write a provider to marshall the POJO into an XML String. So I think I'm doing something wrong here.
>
> Any idea
>
> Thanks
>
> --
> Antonio Goncalves
> Software architect and Java Champion
>
> Web site | Twitter | LinkedIn | Paris JUG | Devoxx France
>
>
>
> --
> Antonio Goncalves
> Software architect and Java Champion
>
> Web site | Twitter | LinkedIn | Paris JUG | Devoxx France