001    package de.deepamehta.plugins.caching;
002    
003    import de.deepamehta.plugins.time.service.TimeService;
004    
005    import de.deepamehta.core.DeepaMehtaObject;
006    import de.deepamehta.core.osgi.PluginActivator;
007    import de.deepamehta.core.service.Inject;
008    import de.deepamehta.core.service.event.ServiceRequestFilterListener;
009    import de.deepamehta.core.service.event.ServiceResponseFilterListener;
010    
011    // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
012    import com.sun.jersey.spi.container.ContainerRequest;
013    import com.sun.jersey.spi.container.ContainerResponse;
014    
015    import javax.ws.rs.WebApplicationException;
016    import javax.ws.rs.core.MultivaluedMap;
017    import javax.ws.rs.core.Response;
018    
019    import java.util.Date;
020    import java.util.logging.Logger;
021    import java.util.regex.Matcher;
022    import java.util.regex.Pattern;
023    
024    
025    
026    public class CachingPlugin extends PluginActivator implements ServiceRequestFilterListener,
027                                                                  ServiceResponseFilterListener {
028    
029        // ------------------------------------------------------------------------------------------------------- Constants
030    
031        private static String CACHABLE_PATH = "core/(topic|association)/(\\d+)";
032    
033        private static String HEADER_CACHE_CONTROL = "Cache-Control";
034    
035        // ---------------------------------------------------------------------------------------------- Instance Variables
036    
037        @Inject
038        private TimeService timeService;
039    
040        private Pattern cachablePath = Pattern.compile(CACHABLE_PATH);
041    
042        private Logger logger = Logger.getLogger(getClass().getName());
043    
044        // -------------------------------------------------------------------------------------------------- Public Methods
045    
046    
047    
048        // ********************************
049        // *** Listener Implementations ***
050        // ********************************
051    
052    
053    
054        @Override
055        public void serviceRequestFilter(ContainerRequest request) {
056            // ### TODO: optimization. Retrieving and instantiating an entire DeepaMehtaObject just to query its timestamp
057            // might be inefficient. Knowing the sole object ID should be sufficient. However, this would require extending
058            // the Time API and in turn the Core Service API by ID-based property getter methods.
059            DeepaMehtaObject object = requestObject(request);
060            if (object != null) {
061                if (timeService == null) {
062                    throw new RuntimeException("Time service is not available");
063                }
064                //
065                long time = timeService.getModificationTime(object);
066                Response.ResponseBuilder response = request.evaluatePreconditions(new Date(time));
067                if (response != null) {
068                    logger.info("### Precondition of " + request.getMethod() + " request failed (object " +
069                        object.getId() + ")");
070                    throw new WebApplicationException(response.build());
071                }
072            }
073        }
074    
075        @Override
076        public void serviceResponseFilter(ContainerResponse response) {
077            DeepaMehtaObject object = responseObject(response);
078            if (object != null) {
079                setCacheControlHeader(response, "max-age=0");
080            }
081        }
082    
083    
084    
085        // ------------------------------------------------------------------------------------------------- Private Methods
086    
087        private DeepaMehtaObject requestObject(ContainerRequest request) {
088            // Example URL: "http://localhost:8080/core/topic/2695?include_childs=true"
089            //   request.getBaseUri()="http://localhost:8080/"
090            //   request.getPath()="core/topic/2695"
091            //   request.getAbsolutePath()="http://localhost:8080/core/topic/2695"
092            //   request.getRequestUri()="http://localhost:8080/core/topic/2695?include_childs=true"
093            Matcher m = cachablePath.matcher(request.getPath());
094            if (m.matches()) {
095                String objectType = m.group(1);     // group 1 is "topic" or "association"
096                long objectId = Long.parseLong(m.group(2));
097                if (objectType.equals("topic")) {
098                    return dms.getTopic(objectId);
099                } else if (objectType.equals("association")) {
100                    return dms.getAssociation(objectId);
101                } else {
102                    throw new RuntimeException("Unexpected object type: \"" + objectType + "\"");
103                }
104            } else {
105                return null;
106            }
107        }
108    
109        // ---
110    
111        // ### FIXME: copy in TimePlugin
112        private DeepaMehtaObject responseObject(ContainerResponse response) {
113            Object entity = response.getEntity();
114            return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null;
115        }
116    
117        private void setCacheControlHeader(ContainerResponse response, String value) {
118            setHeader(response, HEADER_CACHE_CONTROL, value);
119        }
120    
121        // ### FIXME: copy in TimePlugin
122        private void setHeader(ContainerResponse response, String header, String value) {
123            MultivaluedMap headers = response.getHttpHeaders();
124            //
125            if (headers.containsKey(header)) {
126                throw new RuntimeException("Response already has a \"" + header + "\" header");
127            }
128            //
129            headers.putSingle(header, value);
130        }
131    }