17

The CXF documentation mentions caching as Advanced HTTP:

CXF JAXRS provides support for a number of advanced HTTP features by handling If-Match, If-Modified-Since and ETags headers. JAXRS Request context object can be used to check the preconditions. Vary, CacheControl, Cookies and Set-Cookies are also supported.

I'm really interested in using (or at least exploring) these features. However, while "provides support" sounds really interesting, it isn't particularly helpful in implementing such features. Any help or pointers on how to use If-Modified-Since, CacheControl or ETags?

sfussenegger
  • 35,575
  • 15
  • 95
  • 119

3 Answers3

27

Actually, the answer isn't specific to CXF - it's pure JAX-RS:

// IPersonService.java
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;

@GET
@Path("/person/{id}")
Response getPerson(@PathParam("id") String id, @Context Request request);


// PersonServiceImpl.java
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;

public Response getPerson(String name, Request request) {
  Person person = _dao.getPerson(name);

  if (person == null) {
    return Response.noContent().build();
  }

  EntityTag eTag = new EntityTag(person.getUUID() + "-" + person.getVersion());

  CacheControl cc = new CacheControl();
  cc.setMaxAge(600);

  ResponseBuilder builder = request.evaluatePreconditions(person.getUpdated(), eTag);

  if (builder == null) {
    builder = Response.ok(person);
  }

  return builder.cacheControl(cc).lastModified(person.getUpdated()).build();
}
sfussenegger
  • 35,575
  • 15
  • 95
  • 119
  • 4
    Great answer. My only comment is that the EntityTag you generate probably doesn't need the UUID of the person. It is only important that an eTag change between revisions of the same resource. Assuming the ID is immutable, UUID is redundant with the path to the resource (although your Impl calls that parameter "name", so maybe it is not immutable. Also, you should ensure that this value is representation-specific. For instance, if two representations of a resource vary by the media type, use the media type value along with the version identifier to make a representation-specific ETag value. – benvolioT Aug 04 '11 at 20:01
  • I never use a Response object - just let CXF handle that part. How would you do it without it? – oligofren Jun 30 '12 at 12:12
  • @oligofren I had never used them myself before but that was the only solution I found. – sfussenegger Jul 02 '12 at 15:06
  • OK. I madea new question concerning this. Maybe someone else knows :) http://stackoverflow.com/questions/11317543/is-it-possible-to-set-etags-using-jax-rs-without-resorting-to-response-objects – oligofren Jul 03 '12 at 19:10
5

With the forthcoming JAX-RS 2.0 it will be possible to apply Cache-Control declaratively, as explained in http://jalg.net/2012/09/declarative-cache-control-with-jax-rs-2-0/

You can already test this at least with Jersey. Not sure about CXF and RESTEasy though.

Jan Algermissen
  • 4,930
  • 4
  • 26
  • 39
  • It's more like declaratively applying filters that can do such things as caching, but anyway a big improvement. Thanks for letting me (us) know. – sfussenegger Sep 26 '12 at 09:49
0

CXF didn't implements dynamic filtering as explained here : http://www.jalg.net/2012/09/declarative-cache-control-with-jax-rs-2-0

And if you use to return directly your own objects and not CXF Response, it's hard to add a cache control header.

I find an elegant way by using a custom annotation and creating a CXF Interceptor that read this annotation and add the header.

So first, create a CacheControl annotation

@Target(ElementType.METHOD )
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheControl {
    String value() default "no-cache";
}

Then, add this annotation to your CXF operation method (interface or implementation it works on both if you use an interface)

@CacheControl("max-age=600")
public Person getPerson(String name) {
    return personService.getPerson(name);
}

Then create a CacheControl interceptor that will handle the annotation and add the header to your response.

public class CacheInterceptor extends AbstractOutDatabindingInterceptor{
    public CacheInterceptor() {
        super(Phase.MARSHAL);
    }

    @Override
    public void handleMessage(Message outMessage) throws Fault {
        //search for a CacheControl annotation on the operation
        OperationResourceInfo resourceInfo = outMessage.getExchange().get(OperationResourceInfo.class);
        CacheControl cacheControl = null;
        for (Annotation annot : resourceInfo.getOutAnnotations()) {
            if(annot instanceof CacheControl) {
                cacheControl = (CacheControl) annot;
                break;
            }
        }

        //fast path for no cache control
        if(cacheControl == null) {
            return;
        }

        //search for existing headers or create new ones
        Map<String, List<String>> headers = (Map<String, List<String>>) outMessage.get(Message.PROTOCOL_HEADERS);
        if (headers == null) {
            headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            outMessage.put(Message.PROTOCOL_HEADERS, headers);
        }

        //add Cache-Control header
        headers.put("Cache-Control", Collections.singletonList(cacheControl.value()));
    }
}

Finally configure CXF to use your interceptor, you can find all the needed information here : http://cxf.apache.org/docs/interceptors.html

Hope it will help.

Loïc

loicmathieu
  • 5,181
  • 26
  • 31