All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.net.causal.shoelaces.jersey.common.JacksonJsonStringWriterProvider Maven / Gradle / Ivy

The newest version!
package au.net.causal.shoelaces.jersey.common;

import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.Providers;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.base.ProviderBase;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

/**
 * A provider that makes Jersey use Jackson to write String responses with application/json media type in proper JSON string form with quotes as opposed to the incorrect
 * plaintext form that it does by default.
 * 

* * Jersey/Jackson by default writes String responses as plaintext even when the response is annotated with the application/json media type. * This was an intentional design decision by them, but can be worked around by registering this provider which will fix this problem. *

* * Jersey chooses its provider by firstly looking at the response Java type finding the closest matching handled Java type by registered writers, and after finding that set of * writers looks at the supported media types of those writers, finding the closest match (see MessageBodyFactory.WorkerComparator class). Jersey's StringMessageProvider * handles String.class and * / * media type, whose first match is closer than the JacksonJsonProvider which matches Object.class and application/json. This means that Jackson's * provider will never be chosen for the String return type by default. Another issue with Jackson's provider is that it for some reason blacklists String as one of its handled * types, so even with a better match it will still refuse to handle it by default. This class's solution is to create a wrapper provider that delegates to the Jackson one * for Strings, registering itself to handle String which will match as close as the plaintext one, and for application/json media type which will match closer than the * plain text one. Before it delegates to the Jackson provider after matching, it removes String from the Jackson type blacklist. The Jackson provider itself is looked up * from the provider context, which means the Jackson provider must be registered with Jersey as well as this one for this one to work. */ @Produces(MediaType.APPLICATION_JSON) public class JacksonJsonStringWriterProvider implements MessageBodyWriter { @Context protected Providers workers; /** * Finds an already-registered delegate for handling the specified media type for arbitrary objects. * This should typically find the Jackson provider, however if another has been registered then this will * be picked up. * * @param annotations an array of the annotations attached to the message entity instance. * @param mediaType the media type of the HTTP entity. This will usually be {@link MediaType#APPLICATION_JSON_TYPE} since this is what this class is annotated * to handle, but subclasses may change this. * * @return a delegate writer for the given media type. * * @throws RuntimeException if no writer could be found for the given media type. This provider should only be used when there is a known provider for handled * media type already registered, such as Jackson for application/json. */ protected MessageBodyWriter delegate(Annotation[] annotations, MediaType mediaType) { MessageBodyWriter delegate = workers.getMessageBodyWriter(Object.class, Object.class, annotations, mediaType); if (delegate == null) throw new RuntimeException("Expected to find a MessageBodyWriter implementation to handle " + mediaType + " - Object types but none were found."); //For Jackson writers, to ensure they actually handle strings remove String as an 'untouchable' type //Otherwise they'll just ignore Strings since by default they are blacklisted if (delegate instanceof ProviderBase) ((ProviderBase)delegate).removeUntouchable(String.class); return delegate; } @Override public long getSize(String s, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return delegate(annotations, mediaType).getSize(s, type, genericType, annotations, mediaType); } @Override public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return delegate(annotations, mediaType).isWriteable(type, genericType, annotations, mediaType); } @Override public void writeTo(String s, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { delegate(annotations, mediaType).writeTo(s, type, genericType, annotations, mediaType, httpHeaders, entityStream); } }