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 }