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 }