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 }