001package de.deepamehta.plugins.time; 002 003import de.deepamehta.core.Association; 004import de.deepamehta.core.DeepaMehtaObject; 005import de.deepamehta.core.Topic; 006import de.deepamehta.core.model.AssociationModel; 007import de.deepamehta.core.model.ChildTopicsModel; 008import de.deepamehta.core.model.TopicModel; 009import de.deepamehta.core.osgi.PluginActivator; 010import de.deepamehta.core.service.event.PostCreateAssociationListener; 011import de.deepamehta.core.service.event.PostCreateTopicListener; 012import de.deepamehta.core.service.event.PostUpdateAssociationListener; 013import de.deepamehta.core.service.event.PostUpdateTopicListener; 014import de.deepamehta.core.service.event.PostUpdateTopicRequestListener; 015import de.deepamehta.core.service.event.PreSendAssociationListener; 016import de.deepamehta.core.service.event.PreSendTopicListener; 017import de.deepamehta.core.service.event.ServiceResponseFilterListener; 018 019// ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 020import com.sun.jersey.spi.container.ContainerResponse; 021 022import javax.ws.rs.GET; 023import javax.ws.rs.Path; 024import javax.ws.rs.PathParam; 025import javax.ws.rs.Produces; 026import javax.ws.rs.Consumes; 027import javax.ws.rs.core.MultivaluedMap; 028 029import java.text.DateFormat; 030import java.text.SimpleDateFormat; 031import java.util.Collection; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Locale; 035import java.util.Set; 036import java.util.TimeZone; 037import java.util.logging.Logger; 038 039 040 041@Path("/time") 042@Consumes("application/json") 043@Produces("application/json") 044public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener, 045 PostCreateAssociationListener, 046 PostUpdateTopicListener, 047 PostUpdateTopicRequestListener, 048 PostUpdateAssociationListener, 049 PreSendTopicListener, 050 PreSendAssociationListener, 051 ServiceResponseFilterListener { 052 053 // ------------------------------------------------------------------------------------------------------- Constants 054 055 private static String PROP_CREATED = "dm4.time.created"; 056 private static String PROP_MODIFIED = "dm4.time.modified"; 057 058 private static String HEADER_LAST_MODIFIED = "Last-Modified"; 059 060 // ---------------------------------------------------------------------------------------------- Instance Variables 061 062 private DateFormat rfc2822; 063 064 private Logger logger = Logger.getLogger(getClass().getName()); 065 066 // -------------------------------------------------------------------------------------------------- Public Methods 067 068 069 070 // ********************************** 071 // *** TimeService Implementation *** 072 // ********************************** 073 074 075 076 // === Timestamps === 077 078 // Note: the timestamp getters must return 0 as default. Before we used -1 but Jersey's evaluatePreconditions() 079 // does not work as expected when called with a negative value which is not dividable by 1000. 080 081 @GET 082 @Path("/object/{id}/created") 083 @Override 084 public long getCreationTime(@PathParam("id") long objectId) { 085 return dms.hasProperty(objectId, PROP_CREATED) ? (Long) dms.getProperty(objectId, PROP_CREATED) : 0; 086 } 087 088 @GET 089 @Path("/object/{id}/modified") 090 @Override 091 public long getModificationTime(@PathParam("id") long objectId) { 092 return dms.hasProperty(objectId, PROP_MODIFIED) ? (Long) dms.getProperty(objectId, PROP_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_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_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_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_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) { 158 storeTimestamps(topic); 159 } 160 161 @Override 162 public void postCreateAssociation(Association assoc) { 163 storeTimestamps(assoc); 164 } 165 166 @Override 167 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 168 storeTimestamp(topic); 169 } 170 171 @Override 172 public void postUpdateAssociation(Association assoc, AssociationModel oldModel) { 173 storeTimestamp(assoc); 174 } 175 176 // --- 177 178 @Override 179 public void postUpdateTopicRequest(Topic topic) { 180 storeParentsTimestamp(topic); 181 } 182 183 // --- 184 185 @Override 186 public void preSendTopic(Topic topic) { 187 enrichWithTimestamp(topic); 188 } 189 190 @Override 191 public void preSendAssociation(Association assoc) { 192 enrichWithTimestamp(assoc); 193 } 194 195 // --- 196 197 @Override 198 public void serviceResponseFilter(ContainerResponse response) { 199 DeepaMehtaObject object = responseObject(response); 200 if (object != null) { 201 long modified = enrichWithTimestamp(object); 202 setLastModifiedHeader(response, modified); 203 } 204 } 205 206 207 208 // ------------------------------------------------------------------------------------------------- Private Methods 209 210 private void storeTimestamps(DeepaMehtaObject object) { 211 long time = System.currentTimeMillis(); 212 storeCreationTime(object, time); 213 storeModificationTime(object, time); 214 } 215 216 private void storeTimestamp(DeepaMehtaObject object) { 217 long time = System.currentTimeMillis(); 218 storeModificationTime(object, time); 219 } 220 221 // --- 222 223 private void storeParentsTimestamp(Topic topic) { 224 for (DeepaMehtaObject object : getParents(topic)) { 225 storeTimestamp(object); 226 } 227 } 228 229 // --- 230 231 private void storeCreationTime(DeepaMehtaObject object, long time) { 232 storeTime(object, PROP_CREATED, time); 233 } 234 235 private void storeModificationTime(DeepaMehtaObject object, long time) { 236 storeTime(object, PROP_MODIFIED, time); 237 } 238 239 // --- 240 241 private void storeTime(DeepaMehtaObject object, String propUri, long time) { 242 object.setProperty(propUri, time, true); // addToIndex=true 243 } 244 245 // === 246 247 // ### FIXME: copy in CachingPlugin 248 private DeepaMehtaObject responseObject(ContainerResponse response) { 249 Object entity = response.getEntity(); 250 return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null; 251 } 252 253 private long enrichWithTimestamp(DeepaMehtaObject object) { 254 long objectId = object.getId(); 255 long created = getCreationTime(objectId); 256 long modified = getModificationTime(objectId); 257 ChildTopicsModel childTopics = object.getChildTopics().getModel(); 258 childTopics.put(PROP_CREATED, created); 259 childTopics.put(PROP_MODIFIED, modified); 260 return modified; 261 } 262 263 // --- 264 265 private void setLastModifiedHeader(ContainerResponse response, long time) { 266 setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time)); 267 } 268 269 // ### FIXME: copy in CachingPlugin 270 private void setHeader(ContainerResponse response, String header, String value) { 271 MultivaluedMap headers = response.getHttpHeaders(); 272 // 273 if (headers.containsKey(header)) { 274 throw new RuntimeException("Response already has a \"" + header + "\" header"); 275 } 276 // 277 headers.putSingle(header, value); 278 } 279 280 // --- 281 282 /** 283 * Returns all parent topics/associations of the given topic (recursively). 284 * Traversal is informed by the "parent" and "child" role types. 285 * Traversal stops when no parent exists or when an association is met. 286 */ 287 private Set<DeepaMehtaObject> getParents(Topic topic) { 288 Set<DeepaMehtaObject> parents = new LinkedHashSet(); 289 // 290 List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child", 291 "dm4.core.parent", null, 0).getItems(); 292 List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child", 293 "dm4.core.parent", null).getItems(); 294 parents.addAll(parentTopics); 295 parents.addAll(parentAssocs); 296 // 297 for (Topic parentTopic : parentTopics) { 298 parents.addAll(getParents(parentTopic)); 299 } 300 // 301 return parents; 302 } 303}