Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JAX-RS HATEOAS Using Jersey, Unwanted Link properties in JSON

Tags:

Since Jersey 2.9, it's been possible to create link relations for hypermedia-driven REST APIs through declarative linking.

This code, for example:

@InjectLink(
    resource = ItemResource.class,
    style = Style.ABSOLUTE,
    bindings = @Binding(name = "id", value = "${instance.id}"),
    rel = "self"
)
@XmlJavaTypeAdapter(Link.JaxbAdapter.class)
@XmlElement(name="link")
Link self;

...in theory is expected to produce JSON like this:

"link" : {
    "rel" : "self",
    "href" : "http://localhost/api/resource/1"
}

However, Jersey produces different JSON with a lot of properties that I don't need:

"link" : {
   "rel" : "self",
   "uri" : "http://localhost/api/resource/1",
   "type": null,
   "uriBuilder" : null
}

Notice also that instead of href, it uses uri. I looked at Jersey's implementation of the Link object and found JerseyLink.

I want to use Jersey's declarative linking instead of rolling out my own implementation. I ended up using Jackson annotations just to ignore other JerseyLink properties.

@JsonIgnoreProperties({ "uriBuilder", "params", "type", "rels" })

Has anyone used declarative linking with Jersey and had the expected JSON output (e.g., href instead of uri, without extra Jersey properties) without having to use JsonIgnoreProperties or other hacks?

Thanks.

EDIT

I resolved this using an approach which I think is a hack but works well for me and doesn't require the use of a complicated adapter.

I realized that I can actually expose a different object instead of the Link injected by Jersey.

I created a wrapper object named ResourceLink:

public class ResourceLink {
  private String rel;
  private URI href;

  //getters and setters
}

Then in my representation object I have a getter method:

public ResourceLink getLink() {
   ResourceLink link = new ResourceLink();
   link.setRel(self.getRel());
   link.setHref(self.getUri());

   return link;
}

So I used Jersey to inject the link but returned a different object in a getter method in my representation object. This would be the property that would be serialized to JSON and not the injected link object because I didn't create a getter method for it.

like image 213
arjaynacion Avatar asked Jul 26 '14 06:07

arjaynacion


People also ask

Is JAX-RS and Jersey same?

JAX-RS is an specification (just a definition) and Jersey is a JAX-RS implementation. Jersey framework is more than the JAX-RS Reference Implementation. Jersey provides its own API that extend the JAX-RS toolkit with additional features and utilities to further simplify RESTful service and client development.

What is the purpose of using JAX-RS?

JAX-RS is a Java programming language API designed to make it easy to develop applications that use the REST architecture. The JAX-RS API uses Java programming language annotations to simplify the development of RESTful web services.

What can a JAX-RS method return?

If the URI path template variable cannot be cast to the specified type, the JAX-RS runtime returns an HTTP 400 ("Bad Request") error to the client. If the @PathParam annotation cannot be cast to the specified type, the JAX-RS runtime returns an HTTP 404 ("Not Found") error to the client.


2 Answers

Invesitigation

Environment: Jersey 2.13 ( all provider versions are also 2.13 ).

Whether you use declarative or programmatic linking, the serialization shouldn't differ. I chose programmatic, just because I can :-)

Test classes:

@XmlRootElement
public class TestClass {
    private javax.ws.rs.core.Link link;

    public void setLink(Link link) { this.link = link; }

    @XmlElement(name = "link")
    @XmlJavaTypeAdapter(Link.JaxbAdapter.class)
    public Link getLink() { return link; }
}

@Path("/links")
public class LinkResource {  
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getResponse() {
        URI uri = URI.create("https://stackoverflow.com/questions/24968448");
        Link link = Link.fromUri(uri).rel("stackoverflow").build();
        TestClass test = new TestClass();
        test.setLink(link);
        return Response.ok(test).build();
    }
}

@Test
public void testGetIt() {
    WebTarget baseTarget = target.path("links");
    String json = baseTarget.request().accept(
            MediaType.APPLICATION_JSON).get(String.class);
    System.out.println(json); 
}

Results with different Providers (with no extra configurations)

jersey-media-moxy

Dependency

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-moxy</artifactId>
</dependency>

Result (weird)

{
    "link": "javax.ws.rs.core.Link$JaxbLink@cce17d1b"
}

jersey-media-json-jackson

Dependency

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
</dependency>

Result (close, but what's with the params?)

{
    "link": {
        "params": {
            "rel": "stackoverflow"
        },
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

jackson-jaxrs-json-provider

Dependency

<dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-json-provider</artifactId>
    <version>2.4.0</version>
</dependency>

Result (Two different results, with two different JSON providers)

resourceConfig.register(JacksonJsonProvider.class);

{
    "link": {
        "uri": "https://stackoverflow.com/questions/24968448",
        "params": {
            "rel": "stackoverflow"
        },
        "type": null,
        "uriBuilder": {
            "absolute": true
        },
        "rels": ["stackoverflow"],
        "title": null,
        "rel": "stackoverflow"
    }
}

resourceConfig.register(JacksonJaxbJsonProvider.class);

{
    "link": {
        "params": {
            "rel": "stackoverflow"
        },
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

My Conclusions

We are annotating the field with @XmlJavaTypeAdapter(Link.JaxbAdapter.class). Let look at a snippet of this adapter

public static class JaxbAdapter extends XmlAdapter<JaxbLink, Link> {...}

So from Link, we are being marshalled to JaxbLink

public static class JaxbLink {

    private URI uri;
    private Map<QName, Object> params;
    ...
}

jersey-media-moxy

Seems to be a bug... See below in solutions.

The others

The other two are dependent on jackson-module-jaxb-annotations to handle marshalling using JAXB annotations. jersey-media-json-jackson will automatically register the required JaxbAnnotationModule. For jackson-jaxrs-json-provider, using JacksonJsonProvider will not support JAXB annotations (without confgiruation), and using JacksonJsonJaxbProvider will give us the JAXB annotation support.

So if we have JAXB annotation support, we will get marshalled to JaxbLink, which will give this result

{
    "link": {
        "params": {
            "rel": "stackoverflow"
        },
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

The ways we can get the result with all the unwanted properties, is to 1), use the jackson-jaxrs-json-provider's JacksonJsonProvider or 2), create a ContextResolver for ObjectMapper where we don't register the JaxbAnnotationModule. You seem to be doing one of those.


Solutions

The above still doesn't get us where we want to get to (i.e. no params).

For jersey-media-json-jackson and jackson-jaxrs-json-provider...

...which use Jackson, the only thing I can think of at this point is to create a custom serializer

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import javax.ws.rs.core.Link;

public class LinkSerializer extends JsonSerializer<Link>{

    @Override
    public void serialize(Link link, JsonGenerator jg, SerializerProvider sp) 
            throws IOException, JsonProcessingException {
        jg.writeStartObject();
        jg.writeStringField("rel", link.getRel());
        jg.writeStringField("href", link.getUri().toString());
        jg.writeEndObject();
    }
}

Then create a ContextResolver for the ObjectMapper, where we register the serializer

@Provider
public class ObjectMapperContextResolver 
                          implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperContextResolver() {
        mapper = new ObjectMapper();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Link.class, new LinkSerializer());
        mapper.registerModule(simpleModule);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }
}

This is the result

{
    "link": {
        "rel": "stackoverflow",
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

With jersey-media-moxy, it appears there's a Bug with missing setters in the JaxbLink class, so the marshalling reverts to calling toString, which is what's shown above. A work around, as proposed here by Garard Davidson, is just to create another adapter

import java.net.URI;
import java.util.HashMap;  
import java.util.Map;  

import javax.ws.rs.core.Link;  
import javax.xml.bind.annotation.XmlAnyAttribute;
import javax.xml.bind.annotation.XmlAttribute;

import javax.xml.bind.annotation.adapters.XmlAdapter;  
import javax.xml.namespace.QName;  

public class LinkAdapter  
    extends XmlAdapter<LinkJaxb, Link> {  

    public LinkAdapter() {  
    }  

    public Link unmarshal(LinkJaxb p1) {  

        Link.Builder builder = Link.fromUri(p1.getUri());  
        for (Map.Entry<QName, Object> entry : p1.getParams().entrySet()) {  
            builder.param(entry.getKey().getLocalPart(), entry.getValue().toString());  
        }  
        return builder.build();  
    }  

    public LinkJaxb marshal(Link p1) {  

        Map<QName, Object> params = new HashMap<>();  
        for (Map.Entry<String,String> entry : p1.getParams().entrySet()) {  
            params.put(new QName("", entry.getKey()), entry.getValue());  
        }  
        return new LinkJaxb(p1.getUri(), params);  
    }  
}  

class LinkJaxb  {  

    private URI uri;  
    private Map<QName, Object> params;  


    public LinkJaxb() {  
        this (null, null);  
    }  

    public LinkJaxb(URI uri) {  
        this(uri, null);  
    }  

    public LinkJaxb(URI uri, Map<QName, Object> map) {  

        this.uri = uri;  
        this.params = map!=null ? map : new HashMap<QName, Object>();  

    }  

    @XmlAttribute(name = "href")  
    public URI getUri() {   
        return uri;  
    }  

    @XmlAnyAttribute  
    public Map<QName, Object> getParams() {   
        return params;  
    }  

    public void setUri(URI uri) {  
        this.uri = uri;  
    }  

    public void setParams(Map<QName, Object> params) {  
        this.params = params;  
    }    
}

Using this adapter instead

@XmlElement(name = "link")
@XmlJavaTypeAdapter(LinkAdapter.class)
private Link link;

will give us the desired output

{
    "link": {
        "href": "https://stackoverflow.com/questions/24968448",
        "rel": "stackoverflow"
    }
}

UPDATE

Now that I think about it, the LinkAdapter would work with the Jackson provider also. No need to create a Jackson Serializer/Deserializer. The Jackson module should already support the JAXB annotations out the box, given the JacksonFeature is enabled. The examples above show using the JAXB/JSON providers separately, but given just the JacksonFeature is enabled, the JAXB version of the provider should be used. This may actually be the more preferred solution. No need to create an ContextResolvers for the ObjectMapper :-D

It's also possible to declare the annotation at the package level, as seen here

like image 122
Paul Samsotha Avatar answered Oct 05 '22 02:10

Paul Samsotha


I'd like to share with my solution for serialising/deserialising Link objects using with Jackson and the mix-in annotations.

LinkMixin:

@JsonAutoDetect(
        fieldVisibility = JsonAutoDetect.Visibility.NONE,
        getterVisibility = JsonAutoDetect.Visibility.NONE,
        isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonDeserialize(using = LinkMixin.LinkDeserializer.class)
public abstract class LinkMixin extends Link {

    private static final String HREF = "href";

    @JsonProperty(HREF)
    @Override
    public abstract URI getUri();

    @JsonAnyGetter
    public abstract Map<String, String> getParams();

    public static class LinkDeserializer extends JsonDeserializer<Link> {

        @Override
        public Link deserialize(
                final JsonParser p,
                final DeserializationContext ctxt) throws IOException {
            final Map<String, String> params = p.readValueAs(
                    new TypeReference<Map<String, String>>() {});
            if (params == null) {
                return null;
            }
            final String uri = params.remove(HREF);
            if (uri == null) {
                return null;
            }
            final Builder builder = Link.fromUri(uri);
            params.forEach(builder::param);
            return builder.build();
        }
    }
}

Example:

final ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Link.class, LinkMixin.class);
final Link link = Link.fromUri("http://example.com")
        .rel("self")
        .title("xxx")
        .param("custom", "my")
        .build();
final String json = mapper
        .writerWithDefaultPrettyPrinter()
        .writeValueAsString(Collections.singleton(link));
System.out.println(json);
final List<Link> o = mapper.readValue(json, new TypeReference<List<Link>>() {});
System.out.println(o);

Output:

[ {
  "href" : "http://example.com",
  "rel" : "self",
  "title" : "xxx",
  "custom" : "my"
} ]
[<http://example.com>; rel="self"; title="xxx"; custom="my"]
like image 24
Alexey Gavrilov Avatar answered Oct 05 '22 04:10

Alexey Gavrilov