Jersey: Default Cache Control to no-cache - java

While writing a RESTful web service, I am encountering issues if I enable any sort of caching on my client (currently a .NET thick client). By default Jersey is not sending any sort of cache control header, so the client is caching most pages automatically (which seems to be valid behaviour).
I would like to have Jersey by default send a cache control of "no-cache", and then in particular responses override the cache control.
Is there any way to do this with Jersey?
I've found that RESTeasy has the ability to use the #NoCache annotation to specify the setting for the whole class, but I've not found anything similar with Jersey.

This is easy with Jersey by using a ResourceFilterFactory - you can create any custom annotation you attach to your methods to set cache control settings. ResourceFilterFactories get called for each discovered resource method when the application initializes - in your ResourceFilterFactory you can check if the method has your #CacheControlHeader annotation (or whatever you want to call it) - if not, simply return response filter that adds "no-cache" directive to the response, otherwise it should use the settings from the annotation. Here is an example of how to do that:
public class CacheFilterFactory implements ResourceFilterFactory {
private static final List<ResourceFilter> NO_CACHE_FILTER = Collections.<ResourceFilter>singletonList(new CacheResponseFilter("no-cache"));
#Override
public List<ResourceFilter> create(AbstractMethod am) {
CacheControlHeader cch = am.getAnnotation(CacheControlHeader.class);
if (cch == null) {
return NO_CACHE_FILTER;
} else {
return Collections.<ResourceFilter>singletonList(new CacheResponseFilter(cch.value()));
}
}
private static class CacheResponseFilter implements ResourceFilter, ContainerResponseFilter {
private final String headerValue;
CacheResponseFilter(String headerValue) {
this.headerValue = headerValue;
}
#Override
public ContainerRequestFilter getRequestFilter() {
return null;
}
#Override
public ContainerResponseFilter getResponseFilter() {
return this;
}
#Override
public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
// attache Cache Control header to each response based on the annotation value
response.getHttpHeaders().putSingle(HttpHeaders.CACHE_CONTROL, headerValue);
return response;
}
}
}
The annotation can look like this:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface CacheControlHeader {
String value();
}
The ResourceFilterFactory can be registered in your application by adding the following init param to the definition of Jersey servlet in web.xml:
<init-param>
<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
<param-value>package.name.CacheFilterFactory</param-value>
</init-param>

Based on the solution by #martin-matula I created two Cache annotations. One #NoCache for no caching at all and one #CacheMaxAge for specific caching. The CacheMaxAge takes two arguments so you don't have to calculate the seconds yourself:
#GET
#CacheMaxAge(time = 10, unit = TimeUnit.MINUTES)
#Path("/awesome")
public String returnSomethingAwesome() {
...
}
The ResourceFilter now has this create method that by default doesn't interfere (so other caching mechanisms keep working):
#Override
public List<ResourceFilter> create(AbstractMethod am) {
if (am.isAnnotationPresent(CacheMaxAge.class)) {
CacheMaxAge maxAge = am.getAnnotation(CacheMaxAge.class);
return newCacheFilter("max-age: " + maxAge.unit().toSeconds(maxAge.time()));
} else if (am.isAnnotationPresent(NoCache.class)) {
return newCacheFilter("no-cache");
} else {
return Collections.emptyList();
}
}
private List<ResourceFilter> newCacheFilter(String content) {
return Collections
.<ResourceFilter> singletonList(new CacheResponseFilter(content));
}
You can see the full solution in my blogpost.
Thanks for the solution Martin!

#martin-matula's solution does not work with JAX-RS 2.0 / Jersey 2.x as ResourceFilterFactory and ResourceFilter have been removed. The solution can be adapted to JAX-RS 2.0 as follows.
Annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface CacheControlHeader {
String value();
}
DynamicFeature:
#Provider
public class CacheFilterFactory implements DynamicFeature {
private static final CacheResponseFilter NO_CACHE_FILTER =
new CacheResponseFilter("no-cache");
#Override
public void configure(ResourceInfo resourceInfo,
FeatureContext featureContext) {
CacheControlHeader cch = resourceInfo.getResourceMethod()
.getAnnotation(CacheControlHeader.class);
if (cch == null) {
featureContext.register(NO_CACHE_FILTER);
} else {
featureContext.register(new CacheResponseFilter(cch.value()));
}
}
private static class CacheResponseFilter implements ContainerResponseFilter {
private final String headerValue;
CacheResponseFilter(String headerValue) {
this.headerValue = headerValue;
}
#Override
public void filter(ContainerRequestContext containerRequestContext,
ContainerResponseContext containerResponseContext) {
// attache Cache Control header to each response
// based on the annotation value
containerResponseContext
.getHeaders()
.putSingle(HttpHeaders.CACHE_CONTROL, headerValue);
}
}
}
CacheFilterFactory needs to be registered with Jersey. I'm doing it via Dropwizard - using environment.jersey().register() - but on standalone systems I understand this can be done for example by letting Jersey scan your classes for #Provider annotations by defining the following in your web.xml:
<servlet>
<servlet-name>my.package.MyApplication</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<!-- Register resources and providers under my.package. -->
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>my.package</param-value>
</init-param>
</servlet>
See this post for more information about registering components.

I think you can use the
isNoCache(true)
which will stop caching in the browser.
See:
http://jersey.java.net/nonav/apidocs/1.12/jersey/javax/ws/rs/core/CacheControl.html#isNoCache%28%29
Hope this helps.

I found one annotation which can disable caching. You can use following annotation for your API:
#CacheControl(noCache = true)
Ref: Jersey Annotation for cache control

Related

Set roles in Jersey web service

I am creating a web service using Jersey + Jetty + Dropwizard + Hibernate.
Let's say I got a web resource like this:
#Path("/")
#PermitAll
public class Resource {
#RolesAllowed("user")
#GET
public String get() { return "GET"; }
#RolesAllowed("admin")
#POST
public String post(String content) { return content; }
#Path("sub")
public SubResource getSubResource() {
return new SubResource();
}
}
I know you can check a user's role with HttpServletRequest.isUserInRole.
The question is, how do I assign the roles to begin with? How does Jersey knows what to return for the isUserInRole method or knows how to filter people from not getting to specific resources based on their roles?
I do not have a web.xml or a webdefault.xml so the definitions should be done somewhere else.
You have to provide a provider class which supports the #RolesAllowed("admin") annotation and then you have to register the provider class in your application .Normally it is done in web.xml but as you are not using it,you have to provide it on your own.The related example could be find here
Jersey uses its own mechanism to check roles (see SecurityContext interface), however if it is not explicitly set, it falls back to container authentication, which is usually implemented using JaaS. If you had web.xml you would be able to configure basics of it there.
Then again frequently authentication is done differently. If you can log user in manually, for instance have a service that can check name/password, then you could implement a ContainerRequestFilter that would fill security context which would make #RolesAllowed annotation to just work.
For instance:
Updated based on comments
Resouce method logs in user:
#Path("some/login/path")
public void login() {
String name = getUsernameFromRequest();
String password = // like above
User user = loginService.login(name, password);
request.getSession().addAttribute("user", user);
}
Initialize security context in filter for all requests.
#Priority(Priorities.AUTHENTICATION)
public class JerseySecurityContextFilter implements ContainerRequestFilter {
public void filter(final ContainerRequestContext context) throws IOException {
User user = getUserFromSession();
if (user != null) {
// this sets security context for the rest of processing
// it should make #RolesAllowed work
context.setSecurityContext(new JerseySecurityContext(user);
}
}
final class JerseySecurityContext implements SecurityContext {
private final User user;
public JerseySecurityContext(final User user) {
this.user = user;
}
public boolean isUserInRole(final String roleName) {
return user.hasRole(roleName);
}
public Principal getUserPrincipal() {
return new Principal() {
public String getName() {
return user.getName();
}
};
}
public String getAuthenticationScheme() {
return null;
}
}
If you would rather use JaaS then you would have to configure security in your application configuration. These are some examples for embedded Jetty.

Jax-RS - Custom attribute to get header value

EDIT: I just realized, is it even possible to perform a custom action with a custom attribute in Java? Or is it just informational?
I want to include an authentication token in my Jax-RS service header, but I don't want to add a parameter to every request to get the header and check it like so:
public Response getUser(#Context HttpHeaders headers) {
if(authorize(headers.getRequestHeader("token").get(0)) {
// Do something
}
}
I would much rather add an attribute to each request (or even the class if that is possible:
#Authorize
public Response getUser() {
// Do something
}
This way I can also add the attribute to only the requests I want to.
And if the request isn't authorized, I can override it and return a 401.
A custom attribute is easy to write, but how can I get the header information in the attribute without passing it in every time?
NOTE: I would rather not use a web.xml. I don't have one right now and I don't like using them. I want to keep my code clean without xml and I think if I used a filter/web.xml it would apply to all calls. If that is the only way, I will, but I much prefer the approach with custom attributes.
"I think if I used a filter/web.xml it would apply to all calls"
Actually there are #NameBinding annotations we can use. For example
#NameBinding
#Rentention(RetentionPoilicy.RUNTIME)
#Target({ElementType.METHOD, ElementType.TYPE})
public #interface Authorize {}
Then just annotate the filter and the methods/classes you want filtered.
#Authorize
public Response getUser() {
// Do something
}
#Provider
#Authorize
#Priority(Priorities.AUTHORIZATION)
public class AuthorizationRequestFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext)
throws IOException {
MultivauledMap<String, String> headers - requestContext.getHeaders();
...
if (!authorized) {
throw new NotAuthorizedException();
}
}
}
Notice the use of #Priority. This is important. Say you want the authenticate also, so you create a filter for authentication. If you don't set the priority, either filter may occur first. It's unpredictable. If we provide the authentication filter with #Priority(Priorities.AUTHENTICATION), then that filter will always occur before the #Priority(Priorities.AUTHORIZATION) filter.
You will also need to register this filter with the Application subclass (See some other Deployment Options (Jersey, but the Application subclass is portable with implementations))
#ApplicationPath("/api")
public class YourApplication extends Application {
private Set<Class<?>> classes = new HashSet<>();
private Set<Object> singletons = new HashSet<>();
public YourApplication() {
classes.add(AuthorizationRequestFilter.class);
}
#Override
public Set<Class<?>> getClasses() {
return classes;
}
#Override
public Set<Object> singletons() {
return singletons;
}
}
See more on Filters and Interceptors
See the WebAppplicationException Hierarchy for more exceptions like NotAuthorizedException
See the Priorities class and Priories guide
The best way to solve your use case would be to use name binding and filters. In this way, you can use a filter to do your authorization logic as well as return a 401 in the case of unauthorized requests.
You can find more information here.
Name binding via annotations is only supported as part of the Server API. In name binding, a name-binding annotation is first defined using the #NameBinding meta-annotation:
#Target({ ElementType.TYPE, ElementType.METHOD })
#Retention(value = RetentionPolicy.RUNTIME)
#NameBinding
public #interface Logged { }
The defined name-binding annotation is then used to decorate a filter or interceptor class (more than one filter or interceptor may be decorated with the same name-binding annotation):
#Logged
public class LoggingFilter
implements ContainerRequestFilter, ContainerResponseFilter {
...
}
At last, the name-binding annotation is applied to the resource method(s) to which the name-bound JAX-RS provider(s) should be bound to:
#Path("/")
public class MyResourceClass {
#GET
#Produces("text/plain")
#Path("{name}")
#Logged
public String hello(#PathParam("name") String name) {
return "Hello " + name;
}
}
A name-binding annotation may also be attached to a custom JAX-RS Application subclass. In such case a name-bound JAX-RS provider bound by the annotation will be applied to all resource and sub-resource methods in the JAX-RS application:
#Logged
#ApplicationPath("myApp")
public class MyApplication extends javax.ws.rs.core.Application {
...
}
Based on peeskillet's answer, which the concept is right but the code is slightly wrong, this is the final code for the answer.
Using #NameBinding, this works:
Authorize.java
#NameBinding
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD, ElementType.TYPE})
public #interface Authorize {
}
AuthorizeFilter.java
note: still needs to do the actual token authorization. This is just checking if the token exists right now.
#Provider
#Authorize
#Priority(Priorities.AUTHORIZATION)
public class AuthorizeFilter implements ContainerRequestFilter
{
#Override
public void filter(ContainerRequestContext requestContext) throws IOException
{
MultivaluedMap<String, String> headers = requestContext.getHeaders();
String token = headers.getFirst("token");
if (token == null || token.isEmpty()) {
Response.ResponseBuilder responseBuilder = Response
.status(Response.Status.UNAUTHORIZED)
.type(MediaType.APPLICATION_JSON)
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Credentials", "true")
.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD")
.header("Access-Control-Max-Age", "1209600");
requestContext.abortWith(responseBuilder.build());
}
}
}
ApplicationConfig.java
note: add the filter here so it doesn't have to be included in the web.xml
#ApplicationScoped
#ApplicationPath("/api")
public class ApplicationConfig extends Application
{
#Override
public Set<Class<?>> getClasses()
{
return getRestResourceClasses();
}
private Set<Class<?>> getRestResourceClasses()
{
Set<Class<?>> resources = new java.util.HashSet<Class<?>>();
resources.add(com.example.AuthorizeFilter.class);
resources.add(com.example.UserService.class);
return resources;
}
}

jersey 2: How to create custom HTTP param binding

I am trying to create a custom http param binding for my restful service. Please see the example below.
#POST
#Path("/user/{userId}/orders")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public MyResult foo(#PathParam("userId") String someString, #UserAuthHeaderParam String authString){
}
You can see that there is a UserAuthHeaderParam annotation in the function signature. What I want to do is have a custom http param binding other than the standard javax.ws.rs.*Param .
I have try to implement org.glassfish.hk2.api.InjectionResolver which basically extract the value from http header:
public class ProtoInjectionResolver implements InjectionResolver<UserAuthHeaderParam>{
...
#Override
public Object resolve(Injectee injectee, ServiceHandle< ? > root)
{
return "Hello World";
}
...
}
When I call the restful service, the server get below exceptions. It indicates that the framework fails to resolve the param in the function signature:
org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at Injectee(requiredType=String,parent=MyResource,qualifiers={}),position=0,optional=false,self=false,unqualified=null,2136594195),
java.lang.IllegalArgumentException: While attempting to resolve the dependencies of rs.server.MyResource errors were found
Please help. Any advise is appreciated. I do make a lot of search on google but fails to make it work. Jersey 2.*. How to replace InjectableProvider and AbstractHttpContextInjectable of Jersey 1.* might be the similar question.
-- UPDATES:
I use AbstractBinder to bind my resolver to UserAuthHeaderParam:
public class MyApplication extends ResourceConfig
{
public MyApplication()
{
register(new AbstractBinder()
{
#Override
protected void configure()
{
// bindFactory(UrlStringFactory.class).to(String.class);
bind(UrlStringInjectResolver.class).to(new TypeLiteral<InjectionResolver<UrlInject>>()
{
}).in(Singleton.class);
}
});
packages("rs");
}
}
Thank you!
If all you want is to pass value directly from the header to the method you don't need to create custom annotations. Let's say you have a header Authorization, then you can easily access it by declaring your method like this:
#GET
public String authFromHeader(#HeaderParam("Authorization") String authorization) {
return "Header Value: " + authorization + "\n";
}
You can test it by calling curl, e.g.
$ curl --header "Authorization: 1234" http://localhost:8080/rest/resource
Header Value: 1234
Given that the answer to your question, how to create custom binding is as follows.
First you have to declare your annotation like this:
#java.lang.annotation.Target(PARAMETER)
#java.lang.annotation.Retention(RUNTIME)
#java.lang.annotation.Documented
public #interface UserAuthHeaderParam {
}
Having your annotation declared you have to define how it will be resolved. Declare the Value Factory Provider (this is where you'll have access to the header parameters - see my comment):
#Singleton
public class UserAuthHeaderParamValueFactoryProvider extends AbstractValueFactoryProvider {
#Inject
protected UserAuthHeaderParamValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, ServiceLocator locator) {
super(mpep, locator, Parameter.Source.UNKNOWN);
}
#Override
protected Factory<?> createValueFactory(Parameter parameter) {
Class<?> classType = parameter.getRawType();
if (classType == null || (!classType.equals(String.class))) {
return null;
}
return new AbstractHttpContextValueFactory<String>() {
#Override
protected String get(HttpContext httpContext) {
// you can get the header value here
return "testString";
}
};
}
}
Now declare an injection resolver
public class UserAuthHeaderParamResolver extends ParamInjectionResolver<UserAuthHeaderParam> {
public UserAuthHeaderParamResolver() {
super(UserAuthHeaderParamValueFactoryProvider.class);
}
}
and a Binder for your configuration
public class HeaderParamResolverBinder extends AbstractBinder {
#Override
protected void configure() {
bind(UserAuthHeaderParamValueFactoryProvider.class)
.to(ValueFactoryProvider.class)
.in(Singleton.class);
bind(UserAuthHeaderParamResolver.class)
.to(new TypeLiteral<InjectionResolver<UserAuthHeaderParam>>() {})
.in(Singleton.class);
}
}
now the last thing, in your ResourceConfig add register(new HeaderParamResolverBinder()), like this
#ApplicationPath("rest")
public class MyApplication extends ResourceConfig {
public MyApplication() {
register(new HeaderParamResolverBinder());
packages("your.packages");
}
}
Given that, you should be now able to use the value as you wanted:
#GET
public String getResult(#UserAuthHeaderParam String param) {
return "RESULT: " + param;
}
I hope this helps.
I don't know how to resolve your exception. However, may I propose you a different way to do the same thing. I hope it helps.
I've faced exactly the same problem: I need extra parameters in the http header (btw, also related to authentication). Besides, I need to send them in every call, since I want to do a "typical" rest implementation, without maintaining a session.
I'm using Jersey 2.7 - but I'd say it should work in 2.0. I've followed their documentation
https://jersey.java.net/documentation/2.0/filters-and-interceptors.html
It's quite clear there, but anyway I copy-paste my implementation below.
It works fine. True there are some other ways to secure a rest service, for example this is a good one:
http://www.objecthunter.net/tinybo/blog/articles/89
But they depend on the application server implementation and the database you use. The filter, in my opinion, is more flexible and easier to implement.
The copy-paste: I've defined a filter for authentication, which applies to every call and it is executed before the service (thanks to #PreMatching).
#PreMatching
public class AuthenticationRequestFilter implements ContainerRequestFilter {
#Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
final MultivaluedMap<String, String> headers = requestContext.getHeaders();
if (headers == null) {
throw new...
}
// here I get parameters from the header, via headers.get("parameter_name")
// In particular, I get the profile, which I plan to use as a Jersey role
// then I authenticate
// finally, I inform the Principal and the role in the SecurityContext object, so that I can use #RolesAllowed later
requestContext.setSecurityContext(new SecurityContext() {
#Override
public boolean isUserInRole(final String arg0) {
//...
}
#Override
public boolean isSecure() {
//...
}
#Override
public Principal getUserPrincipal() {
//...
}
#Override
public String getAuthenticationScheme() {
//...
}
});
}
}
You have to include this filter class in your implementation of ResourceConfig,
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig() {
// my init
// my packages
register(AuthenticationRequestFilter.class); // filtro de autenticación
// other register
}
}
Hope it helps!
If your need is to retrieve all the http headers binding into one object, a solution could be to use the #Context annotation to get javax.ws.rs.core.HttpHeaders; which contains the list of all request headers.
#POST
#Path("/user/{userId}/orders")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public MyResult foo(#PathParam("userId") String someString, #Context HttpHeaders headers){
// You can list all available HTTP request headers via following code :
for(String header : headers.getRequestHeaders().keySet()){
System.out.println(header);
}
}
here is my actual implementatipn of UserAuthHeaderParamValueFactoryProvider class
import javax.inject.Inject;
import javax.inject.Singleton;
import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory;
import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider;
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
import org.glassfish.jersey.server.model.Parameter;
#Singleton
public class UserAuthHeaderParamValueFactoryProvider extends AbstractValueFactoryProvider {
#Inject
protected UserAuthHeaderParamValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, ServiceLocator locator) {
super(mpep, locator, Parameter.Source.UNKNOWN);
}
#Override
protected Factory<?> createValueFactory(Parameter parameter) {
Class<?> classType = parameter.getRawType();
if (classType == null || (!classType.equals(String.class))) {
return null;
}
return new AbstractContainerRequestValueFactory<String>() {
#Override
public String provide() {
//you can use get any header value.
return getContainerRequest().getHeaderString("Authorization");
}
};
}

Use ContainerRequestFilter in Jersey without web.xml

I am trying to intercept requests in Jersey running inside Glassfish.
I created an implementation of ContainerRequestFilter
package mycustom.api.rest.security;
#Provider
public class SecurityProvider implements ContainerRequestFilter {
#Override
public ContainerRequest filter(ContainerRequest request) {
return request;
}
}
My app is started using a subclass of PackagesResourceConfig.
When Glassfish starts, Jerseys find my provider:
INFO: Provider classes found:
class mycustom.rest.security.SecurityProvider
But it never hits that filter method. What am I missing??
Everything else seems to be working fine. I added a couple of ContextResolver providers to do JSON mapping and they work fine. Requests hit my resources fine, it just never goes through the filter.
I don't think container filters are loaded in as providers. I think you have to set the response filters property. Strangely PackagesResourceConfig doesn't have a setProperty() but you could overload getProperty() and getProperties():
public Object getProperty(String propertyName) {
if(propertyName.equals(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS)) {
return new String[] {"mycustom.rest.security.SecurityProvider"};
} else {
return super.getProperty(propertyName);
}
}
public Map<String,Object> getProperties() {
propName = ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS;
Map<String,Object> result = super.getProperties();
result.put(propName,getProperty(propName));
return result;
}
Actually, reading the javadocs more closely, it appears the preferred method is:
myConfig.getProperties().put(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS,
new String [] {"mycustom.rest.security.SecurityProvider"});

Jersey: Error when a class has both JAX-RS and JAX-WS annotations

Using Jersey 1.7, JAX-WS 2.2.3, Tomcat 6.0.30 and the following method declaration prevents Jersey servlet to start:
#POST
#Produces("text/plain")
public void postIt(#WebParam(name = "paramOne") final String paramOne,
final String paramTwo) {
// ...
}
The generated exception is:
SEVERE: Missing dependency for method public
java.lang.String com.sun.jersey.issue.MyResource.postIt(
java.lang.String,java.lang.String) at parameter at index 0
SEVERE: Method, public void
com.sun.jersey.issue.MyResource.postIt(
java.lang.String,java.lang.String),
annotated with POST of resource,
class com.sun.jersey.issue.MyResource,
is not recognized as valid resource method.
If the #WebParam annotation is removed, it all works fine.
Now, please have in mind that I am not trying to work with mere strings, rather, I am migrating complicated Objects that got marshalled/unmarshalled using SOAP to RESTful services, but I must provide both interfaces for a while, without breaking the previous WASDs. The method is just a minimalistic scenario.
Has any of you any idea of the status of this? Has it been fixed? Suggestions?
The specification is clear on this. Section 3.3.2.1 tells us that:
Resource methods MUST NOT have more
than one parameter that is not
annotated with one of the above listed
annotations.
The above listed annotations are the JAX-RS parameter annotations: #QueryParam, #MatrixParam, etc.
There is, however, a Jersey specific way to solve this problem. Using InjectableProvider. So, a method that defines two non-JAX-RS parameters:
#POST
public void postIt(#CustomInjectable final Customer customer,
final Transaction transaction) {
// ...
}
Of course, we have to code the annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface CustomInjectable {
}
An implementation of InjectableProvider that knows how to provide Customers:
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import com.sun.jersey.api.model.Parameter;
#Provider
public class CustomerInjectableProvider implements
InjectableProvider<CustomInjectable, Parameter> {
// you can use #Context variables, as in any Provider/Resource
#Context
private Request request;
public ComponentScope getScope() {
// ComponentScope.Singleton, Request or Undefined
}
public Injectable getInjectable(ComponentContext i,
CustomInjectable annotation,
Parameter param) {
Injectable injectable = null;
if (Customer.getClass().isAssignableFrom(param.getParameterClass()) {
injectable = getInjectable();
}
return injectable;
}
private Injectable getInjectable() {
return new Injectable<Customer>() {
public Customer getValue() {
// parse the customer from request... or session... or whatever...
}
};
}
}
But, Jersey considers only the last annotation (see JERSEY-ISSUE-731), so be careful.
And, a more portable way (if you do care about that, anyway):
// simple bean
public class CustomerWithTransaction {
private Customer customer;
private Transaction transaction;
// getters and setters
}
Then change the method to:
#POST
public void postIt(CustomerWithTransaction customerWithTransaction) {
// ...
}
Then create your own MessageBodyReader for CustomerWithTransaction, where you can also access any context variables (request, headers, etc.).

Categories

Resources