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.PostUpdateTopicRequestListener; 019 import de.deepamehta.core.service.event.PreSendAssociationListener; 020 import de.deepamehta.core.service.event.PreSendTopicListener; 021 import de.deepamehta.core.service.event.ServiceResponseFilterListener; 022 023 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 024 import com.sun.jersey.spi.container.ContainerResponse; 025 026 import javax.ws.rs.GET; 027 import javax.ws.rs.Path; 028 import javax.ws.rs.PathParam; 029 import javax.ws.rs.Produces; 030 import javax.ws.rs.Consumes; 031 import javax.ws.rs.core.MultivaluedMap; 032 033 import java.text.DateFormat; 034 import java.text.SimpleDateFormat; 035 import java.util.Collection; 036 import java.util.LinkedHashSet; 037 import java.util.List; 038 import java.util.Locale; 039 import java.util.Set; 040 import java.util.TimeZone; 041 import java.util.logging.Logger; 042 043 044 045 @Path("/time") 046 @Consumes("application/json") 047 @Produces("application/json") 048 public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener, 049 PostCreateAssociationListener, 050 PostUpdateTopicListener, 051 PostUpdateTopicRequestListener, 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 postUpdateTopicRequest(Topic topic) { 182 storeParentsTimestamp(topic); 183 } 184 185 // --- 186 187 @Override 188 public void preSendTopic(Topic topic, ClientState clientState) { 189 enrichWithTimestamp(topic); 190 } 191 192 @Override 193 public void preSendAssociation(Association assoc, ClientState clientState) { 194 enrichWithTimestamp(assoc); 195 } 196 197 // --- 198 199 @Override 200 public void serviceResponseFilter(ContainerResponse response) { 201 DeepaMehtaObject object = responseObject(response); 202 if (object != null) { 203 long modified = enrichWithTimestamp(object); 204 setLastModifiedHeader(response, modified); 205 } 206 } 207 208 209 210 // ------------------------------------------------------------------------------------------------- Private Methods 211 212 private void storeTimestamps(DeepaMehtaObject object) { 213 long time = System.currentTimeMillis(); 214 storeCreationTime(object, time); 215 storeModificationTime(object, time); 216 } 217 218 private void storeTimestamp(DeepaMehtaObject object) { 219 long time = System.currentTimeMillis(); 220 storeModificationTime(object, time); 221 } 222 223 // --- 224 225 private void storeParentsTimestamp(Topic topic) { 226 for (DeepaMehtaObject object : getParents(topic)) { 227 storeTimestamp(object); 228 } 229 } 230 231 // --- 232 233 private void storeCreationTime(DeepaMehtaObject object, long time) { 234 storeTime(object, PROP_URI_CREATED, time); 235 } 236 237 private void storeModificationTime(DeepaMehtaObject object, long time) { 238 storeTime(object, PROP_URI_MODIFIED, time); 239 } 240 241 // --- 242 243 private void storeTime(DeepaMehtaObject object, String propUri, long time) { 244 object.setProperty(propUri, time, true); // addToIndex=true 245 } 246 247 // === 248 249 // ### FIXME: copy in CachingPlugin 250 private DeepaMehtaObject responseObject(ContainerResponse response) { 251 Object entity = response.getEntity(); 252 return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null; 253 } 254 255 private long enrichWithTimestamp(DeepaMehtaObject object) { 256 long created = getCreationTime(object); 257 long modified = getModificationTime(object); 258 CompositeValueModel comp = object.getCompositeValue().getModel(); 259 comp.put(PROP_URI_CREATED, created); 260 comp.put(PROP_URI_MODIFIED, modified); 261 return modified; 262 } 263 264 // --- 265 266 private void setLastModifiedHeader(ContainerResponse response, long time) { 267 setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time)); 268 } 269 270 // ### FIXME: copy in CachingPlugin 271 private void setHeader(ContainerResponse response, String header, String value) { 272 MultivaluedMap headers = response.getHttpHeaders(); 273 // 274 if (headers.containsKey(header)) { 275 throw new RuntimeException("Response already has a \"" + header + "\" header"); 276 } 277 // 278 headers.putSingle(header, value); 279 } 280 281 // --- 282 283 /** 284 * Returns all parent topics/associations of the given topic (recursively). 285 * Traversal is informed by the "parent" and "child" role types. 286 * Traversal stops when no parent exists or when an association is met. 287 */ 288 private Set<DeepaMehtaObject> getParents(Topic topic) { 289 Set<DeepaMehtaObject> parents = new LinkedHashSet(); 290 // 291 List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child", 292 "dm4.core.parent", null, false, false, 0).getItems(); 293 List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child", 294 "dm4.core.parent", null, false, false); 295 parents.addAll(parentTopics); 296 parents.addAll(parentAssocs); 297 // 298 for (Topic parentTopic : parentTopics) { 299 parents.addAll(getParents(parentTopic)); 300 } 301 // 302 return parents; 303 } 304 }