001 package de.deepamehta.plugins.time; 002 003 import de.deepamehta.plugins.time.service.TimeService; 004 005 import de.deepamehta.core.Association; 006 import de.deepamehta.core.DeepaMehtaObject; 007 import de.deepamehta.core.Topic; 008 import de.deepamehta.core.model.AssociationModel; 009 import de.deepamehta.core.model.CompositeValueModel; 010 import de.deepamehta.core.model.TopicModel; 011 import de.deepamehta.core.osgi.PluginActivator; 012 import de.deepamehta.core.service.ClientState; 013 import de.deepamehta.core.service.Directives; 014 import de.deepamehta.core.service.event.PostCreateAssociationListener; 015 import de.deepamehta.core.service.event.PostCreateTopicListener; 016 import de.deepamehta.core.service.event.PostUpdateAssociationListener; 017 import de.deepamehta.core.service.event.PostUpdateTopicListener; 018 import de.deepamehta.core.service.event.PreSendAssociationListener; 019 import de.deepamehta.core.service.event.PreSendTopicListener; 020 import de.deepamehta.core.service.event.ServiceResponseFilterListener; 021 022 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 023 import com.sun.jersey.spi.container.ContainerResponse; 024 025 import javax.ws.rs.GET; 026 import javax.ws.rs.PUT; 027 import javax.ws.rs.POST; 028 import javax.ws.rs.DELETE; 029 import javax.ws.rs.HeaderParam; 030 import javax.ws.rs.Path; 031 import javax.ws.rs.PathParam; 032 import javax.ws.rs.Produces; 033 import javax.ws.rs.Consumes; 034 import javax.ws.rs.core.MultivaluedMap; 035 036 import java.text.DateFormat; 037 import java.text.SimpleDateFormat; 038 import java.util.Collection; 039 import java.util.Date; 040 import java.util.Locale; 041 import java.util.TimeZone; 042 import java.util.logging.Logger; 043 044 045 046 @Path("/time") 047 @Consumes("application/json") 048 @Produces("application/json") 049 public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener, 050 PostCreateAssociationListener, 051 PostUpdateTopicListener, 052 PostUpdateAssociationListener, 053 PreSendTopicListener, 054 PreSendAssociationListener, 055 ServiceResponseFilterListener { 056 057 // ------------------------------------------------------------------------------------------------------- Constants 058 059 private static String PROP_URI_CREATED = "dm4.time.created"; 060 private static String PROP_URI_MODIFIED = "dm4.time.modified"; 061 062 private static String HEADER_LAST_MODIFIED = "Last-Modified"; 063 064 // ---------------------------------------------------------------------------------------------- Instance Variables 065 066 private DateFormat rfc2822; 067 068 private Logger logger = Logger.getLogger(getClass().getName()); 069 070 // -------------------------------------------------------------------------------------------------- Public Methods 071 072 073 074 // ********************************** 075 // *** TimeService Implementation *** 076 // ********************************** 077 078 079 080 // === Timestamps === 081 082 // Note: the timestamp getters must return 0 as default. Before we used -1 but Jersey's evaluatePreconditions() 083 // does not work as expected when called with a negative value which is not dividable by 1000. 084 085 @Override 086 public long getCreationTime(DeepaMehtaObject object) { 087 return object.hasProperty(PROP_URI_CREATED) ? (Long) object.getProperty(PROP_URI_CREATED) : 0; 088 } 089 090 @Override 091 public long getModificationTime(DeepaMehtaObject object) { 092 return object.hasProperty(PROP_URI_MODIFIED) ? (Long) object.getProperty(PROP_URI_MODIFIED) : 0; 093 } 094 095 096 097 // === Retrieval === 098 099 @GET 100 @Path("/from/{from}/to/{to}/topics/created") 101 @Override 102 public Collection<Topic> getTopicsByCreationTime(@PathParam("from") long from, 103 @PathParam("to") long to) { 104 return dms.getTopicsByPropertyRange(PROP_URI_CREATED, from, to); 105 } 106 107 @GET 108 @Path("/from/{from}/to/{to}/topics/modified") 109 @Override 110 public Collection<Topic> getTopicsByModificationTime(@PathParam("from") long from, 111 @PathParam("to") long to) { 112 return dms.getTopicsByPropertyRange(PROP_URI_MODIFIED, from, to); 113 } 114 115 @GET 116 @Path("/from/{from}/to/{to}/assocs/created") 117 @Override 118 public Collection<Association> getAssociationsByCreationTime(@PathParam("from") long from, 119 @PathParam("to") long to) { 120 return dms.getAssociationsByPropertyRange(PROP_URI_CREATED, from, to); 121 } 122 123 @GET 124 @Path("/from/{from}/to/{to}/assocs/modified") 125 @Override 126 public Collection<Association> getAssociationsByModificationTime(@PathParam("from") long from, 127 @PathParam("to") long to) { 128 return dms.getAssociationsByPropertyRange(PROP_URI_MODIFIED, from, to); 129 } 130 131 132 133 // **************************** 134 // *** Hook Implementations *** 135 // **************************** 136 137 138 139 @Override 140 public void init() { 141 // create the date format used in HTTP date/time headers, see: 142 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 143 rfc2822 = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.ENGLISH); 144 rfc2822.setTimeZone(TimeZone.getTimeZone("GMT+00:00")); 145 ((SimpleDateFormat) rfc2822).applyPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'"); 146 } 147 148 149 150 // ******************************** 151 // *** Listener Implementations *** 152 // ******************************** 153 154 155 156 @Override 157 public void postCreateTopic(Topic topic, ClientState clientState, Directives directives) { 158 storeTimestamps(topic); 159 } 160 161 @Override 162 public void postCreateAssociation(Association assoc, ClientState clientState, Directives directives) { 163 storeTimestamps(assoc); 164 } 165 166 @Override 167 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel, ClientState clientState, 168 Directives directives) { 169 storeTimestamp(topic); 170 } 171 172 @Override 173 public void postUpdateAssociation(Association assoc, AssociationModel oldModel, ClientState clientState, 174 Directives directives) { 175 storeTimestamp(assoc); 176 } 177 178 // --- 179 180 @Override 181 public void preSendTopic(Topic topic, ClientState clientState) { 182 enrichWithTimestamp(topic); 183 } 184 185 @Override 186 public void preSendAssociation(Association assoc, ClientState clientState) { 187 enrichWithTimestamp(assoc); 188 } 189 190 // --- 191 192 @Override 193 public void serviceResponseFilter(ContainerResponse response) { 194 DeepaMehtaObject object = responseObject(response); 195 if (object != null) { 196 long modified = enrichWithTimestamp(object); 197 setLastModifiedHeader(response, modified); 198 } 199 } 200 201 202 203 // ------------------------------------------------------------------------------------------------- Private Methods 204 205 private void storeTimestamps(DeepaMehtaObject object) { 206 long time = System.currentTimeMillis(); 207 storeCreationTime(object, time); 208 storeModificationTime(object, time); 209 } 210 211 private void storeTimestamp(DeepaMehtaObject object) { 212 long time = System.currentTimeMillis(); 213 storeModificationTime(object, time); 214 } 215 216 // --- 217 218 private void storeCreationTime(DeepaMehtaObject object, long time) { 219 storeTime(object, PROP_URI_CREATED, time); 220 } 221 222 private void storeModificationTime(DeepaMehtaObject object, long time) { 223 storeTime(object, PROP_URI_MODIFIED, time); 224 } 225 226 // --- 227 228 private void storeTime(DeepaMehtaObject object, String propUri, long time) { 229 object.setProperty(propUri, time, true); // addToIndex=true 230 } 231 232 // === 233 234 // ### FIXME: copy in CachingPlugin 235 private DeepaMehtaObject responseObject(ContainerResponse response) { 236 Object entity = response.getEntity(); 237 return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null; 238 } 239 240 private long enrichWithTimestamp(DeepaMehtaObject object) { 241 long created = getCreationTime(object); 242 long modified = getModificationTime(object); 243 CompositeValueModel comp = object.getCompositeValue().getModel(); 244 comp.put(PROP_URI_CREATED, created); 245 comp.put(PROP_URI_MODIFIED, modified); 246 return modified; 247 } 248 249 // --- 250 251 private void setLastModifiedHeader(ContainerResponse response, long time) { 252 setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time)); 253 } 254 255 // ### FIXME: copy in CachingPlugin 256 private void setHeader(ContainerResponse response, String header, String value) { 257 MultivaluedMap headers = response.getHttpHeaders(); 258 // 259 if (headers.containsKey(header)) { 260 throw new RuntimeException("Response already has a \"" + header + "\" header"); 261 } 262 // 263 headers.putSingle(header, value); 264 } 265 }