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                if (timeService == null) {
080                    throw new RuntimeException("Time service is not available");
081                }
082                //
083                long time = timeService.getModificationTime(object);
084                Response.ResponseBuilder response = request.evaluatePreconditions(new Date(time));
085                if (response != null) {
086                    logger.info("### Precondition of " + request.getMethod() + " request failed (object " +
087                        object.getId() + ")");
088                    throw new WebApplicationException(response.build());
089                }
090            }
091        }
092    
093        @Override
094        public void serviceResponseFilter(ContainerResponse response) {
095            DeepaMehtaObject object = responseObject(response);
096            if (object != null) {
097                setCacheControlHeader(response, "max-age=0");
098            }
099        }
100    
101    
102    
103        // ------------------------------------------------------------------------------------------------- Private Methods
104    
105        private DeepaMehtaObject requestObject(ContainerRequest request) {
106            // Example URL: "http://localhost:8080/core/topic/2695?fetch_composite=false"
107            //   request.getBaseUri()="http://localhost:8080/"
108            //   request.getPath()="core/topic/2695"
109            //   request.getAbsolutePath()="http://localhost:8080/core/topic/2695"
110            //   request.getRequestUri()="http://localhost:8080/core/topic/2695?fetch_composite=false"
111            Matcher m = cachablePath.matcher(request.getPath());
112            if (m.matches()) {
113                String objectType = m.group(1);     // group 1 is "topic" or "association"
114                long objectId = Long.parseLong(m.group(2));
115                if (objectType.equals("topic")) {
116                    return dms.getTopic(objectId, false);
117                } else if (objectType.equals("association")) {
118                    return dms.getAssociation(objectId, false);
119                } else {
120                    throw new RuntimeException("Unexpected object type: \"" + objectType + "\"");
121                }
122            } else {
123                return null;
124            }
125        }
126    
127        // ---
128    
129        // ### FIXME: copy in TimePlugin
130        private DeepaMehtaObject responseObject(ContainerResponse response) {
131            Object entity = response.getEntity();
132            return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null;
133        }
134    
135        private void setCacheControlHeader(ContainerResponse response, String value) {
136            setHeader(response, HEADER_CACHE_CONTROL, value);
137        }
138    
139        // ### FIXME: copy in TimePlugin
140        private void setHeader(ContainerResponse response, String header, String value) {
141            MultivaluedMap headers = response.getHttpHeaders();
142            //
143            if (headers.containsKey(header)) {
144                throw new RuntimeException("Response already has a \"" + header + "\" header");
145            }
146            //
147            headers.putSingle(header, value);
148        }
149    }