001package systems.dmx.caching;
002
003import systems.dmx.time.TimeService;
004
005import systems.dmx.core.DMXObject;
006import systems.dmx.core.osgi.PluginActivator;
007import systems.dmx.core.service.Inject;
008import systems.dmx.core.service.event.ServiceRequestFilterListener;
009import systems.dmx.core.service.event.ServiceResponseFilterListener;
010import systems.dmx.core.util.JavaUtils;
011
012// ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
013import com.sun.jersey.spi.container.ContainerRequest;
014import com.sun.jersey.spi.container.ContainerResponse;
015
016import javax.servlet.http.HttpServletRequest;
017
018import javax.ws.rs.Path;
019import javax.ws.rs.WebApplicationException;
020import javax.ws.rs.core.Context;
021import javax.ws.rs.core.MultivaluedMap;
022import javax.ws.rs.core.Response;
023
024import java.util.Date;
025import java.util.logging.Logger;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029
030
031@Path("/cache")
032public class CachingPlugin extends PluginActivator implements ServiceRequestFilterListener,
033                                                              ServiceResponseFilterListener {
034
035    // ------------------------------------------------------------------------------------------------------- Constants
036
037    private static String CACHABLE_PATH = "core/(topic|association)/(\\d+)";
038
039    private static String HEADER_CACHE_CONTROL = "Cache-Control";
040
041    // ---------------------------------------------------------------------------------------------- Instance Variables
042
043    @Inject
044    private TimeService timeService;
045
046    @Context
047    HttpServletRequest req;
048
049    private Pattern cachablePath = Pattern.compile(CACHABLE_PATH);
050
051    private Logger logger = Logger.getLogger(getClass().getName());
052
053    // -------------------------------------------------------------------------------------------------- Public Methods
054
055
056
057    // ********************************
058    // *** Listener Implementations ***
059    // ********************************
060
061
062
063    @Override
064    public void serviceRequestFilter(ContainerRequest request) {
065        long objectId = requestObjectId(request);
066        if (objectId != -1) {
067            if (timeService == null) {
068                throw new RuntimeException("Time service is not available");
069            }
070            //
071            long time = timeService.getModificationTime(objectId);
072            Response.ResponseBuilder builder = request.evaluatePreconditions(new Date(time));
073            if (builder != null) {
074                Response response = builder.build();
075                Response.Status status = Response.Status.fromStatusCode(response.getStatus());
076                logger.fine("### Preconditions of request \"" + JavaUtils.requestInfo(req) +
077                    "\" are not met -- Responding with " + JavaUtils.responseInfo(status));
078                throw new WebApplicationException(response);
079            }
080        }
081    }
082
083    @Override
084    public void serviceResponseFilter(ContainerResponse response) {
085        DMXObject object = responseObject(response);
086        if (object != null) {
087            setCacheControlHeader(response, "max-age=0");
088        }
089    }
090
091
092
093    // ------------------------------------------------------------------------------------------------- Private Methods
094
095    private long requestObjectId(ContainerRequest request) {
096        // Example URL: "http://localhost:8080/core/topic/2695?include_childs=true"
097        //   request.getBaseUri()="http://localhost:8080/"
098        //   request.getPath()="core/topic/2695"
099        //   request.getAbsolutePath()="http://localhost:8080/core/topic/2695"
100        //   request.getRequestUri()="http://localhost:8080/core/topic/2695?include_childs=true"
101        Matcher m = cachablePath.matcher(request.getPath());
102        if (m.matches()) {
103            long objectId = Long.parseLong(m.group(2));
104            //
105            String objectType = m.group(1);     // group 1 is "topic" or "association"
106            if (!objectType.equals("topic") && !objectType.equals("association")) {
107                throw new RuntimeException("Unexpected object type: \"" + objectType + "\"");
108            }
109            //
110            return objectId;
111        } else {
112            return -1;
113        }
114    }
115
116    // ---
117
118    // ### FIXME: copy in TimePlugin
119    private DMXObject responseObject(ContainerResponse response) {
120        Object entity = response.getEntity();
121        return entity instanceof DMXObject ? (DMXObject) entity : null;
122    }
123
124    private void setCacheControlHeader(ContainerResponse response, String value) {
125        setHeader(response, HEADER_CACHE_CONTROL, value);
126    }
127
128    // ### FIXME: copy in TimePlugin
129    private void setHeader(ContainerResponse response, String header, String value) {
130        MultivaluedMap headers = response.getHttpHeaders();
131        //
132        if (headers.containsKey(header)) {
133            throw new RuntimeException("Response already has a \"" + header + "\" header");
134        }
135        //
136        headers.putSingle(header, value);
137    }
138}