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