001package de.deepamehta.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 dm4.hasProperty(objectId, PROP_CREATED) ? (Long) dm4.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 dm4.hasProperty(objectId, PROP_MODIFIED) ? (Long) dm4.getProperty(objectId, PROP_MODIFIED) : 0; 093 } 094 095 // --- 096 097 @Override 098 public void setModified(DeepaMehtaObject object) { 099 storeTimestamp(object); 100 } 101 102 103 104 // === Retrieval === 105 106 @GET 107 @Path("/from/{from}/to/{to}/topics/created") 108 @Override 109 public Collection<Topic> getTopicsByCreationTime(@PathParam("from") long from, 110 @PathParam("to") long to) { 111 return dm4.getTopicsByPropertyRange(PROP_CREATED, from, to); 112 } 113 114 @GET 115 @Path("/from/{from}/to/{to}/topics/modified") 116 @Override 117 public Collection<Topic> getTopicsByModificationTime(@PathParam("from") long from, 118 @PathParam("to") long to) { 119 return dm4.getTopicsByPropertyRange(PROP_MODIFIED, from, to); 120 } 121 122 @GET 123 @Path("/from/{from}/to/{to}/assocs/created") 124 @Override 125 public Collection<Association> getAssociationsByCreationTime(@PathParam("from") long from, 126 @PathParam("to") long to) { 127 return dm4.getAssociationsByPropertyRange(PROP_CREATED, from, to); 128 } 129 130 @GET 131 @Path("/from/{from}/to/{to}/assocs/modified") 132 @Override 133 public Collection<Association> getAssociationsByModificationTime(@PathParam("from") long from, 134 @PathParam("to") long to) { 135 return dm4.getAssociationsByPropertyRange(PROP_MODIFIED, from, to); 136 } 137 138 139 140 // **************************** 141 // *** Hook Implementations *** 142 // **************************** 143 144 145 146 @Override 147 public void init() { 148 // create the date format used in HTTP date/time headers, see: 149 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 150 rfc2822 = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.ENGLISH); 151 rfc2822.setTimeZone(TimeZone.getTimeZone("GMT+00:00")); 152 ((SimpleDateFormat) rfc2822).applyPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'"); 153 } 154 155 156 157 // ******************************** 158 // *** Listener Implementations *** 159 // ******************************** 160 161 162 163 @Override 164 public void postCreateTopic(Topic topic) { 165 storeTimestamps(topic); 166 } 167 168 @Override 169 public void postCreateAssociation(Association assoc) { 170 storeTimestamps(assoc); 171 } 172 173 @Override 174 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 175 storeTimestamp(topic); 176 } 177 178 @Override 179 public void postUpdateAssociation(Association assoc, AssociationModel newModel, AssociationModel oldModel) { 180 storeTimestamp(assoc); 181 } 182 183 // --- 184 185 @Override 186 public void postUpdateTopicRequest(Topic topic) { 187 storeParentsTimestamp(topic); 188 } 189 190 // --- 191 192 @Override 193 public void preSendTopic(Topic topic) { 194 enrichWithTimestamp(topic); 195 } 196 197 @Override 198 public void preSendAssociation(Association assoc) { 199 enrichWithTimestamp(assoc); 200 } 201 202 // --- 203 204 @Override 205 public void serviceResponseFilter(ContainerResponse response) { 206 DeepaMehtaObject object = responseObject(response); 207 if (object != null) { 208 setLastModifiedHeader(response, getModificationTime(object.getId())); 209 } 210 } 211 212 213 214 // ------------------------------------------------------------------------------------------------- Private Methods 215 216 private void storeTimestamps(DeepaMehtaObject object) { 217 long time = System.currentTimeMillis(); 218 storeCreationTime(object, time); 219 storeModificationTime(object, time); 220 } 221 222 private void storeTimestamp(DeepaMehtaObject object) { 223 long time = System.currentTimeMillis(); 224 storeModificationTime(object, time); 225 } 226 227 // --- 228 229 private void storeParentsTimestamp(Topic topic) { 230 for (DeepaMehtaObject object : getParents(topic)) { 231 storeTimestamp(object); 232 } 233 } 234 235 // --- 236 237 private void storeCreationTime(DeepaMehtaObject object, long time) { 238 storeTime(object, PROP_CREATED, time); 239 } 240 241 private void storeModificationTime(DeepaMehtaObject object, long time) { 242 storeTime(object, PROP_MODIFIED, time); 243 } 244 245 // --- 246 247 private void storeTime(DeepaMehtaObject object, String propUri, long time) { 248 object.setProperty(propUri, time, true); // addToIndex=true 249 } 250 251 // === 252 253 // ### FIXME: copy in CachingPlugin 254 private DeepaMehtaObject responseObject(ContainerResponse response) { 255 Object entity = response.getEntity(); 256 return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null; 257 } 258 259 private void enrichWithTimestamp(DeepaMehtaObject object) { 260 long objectId = object.getId(); 261 ChildTopicsModel childTopics = object.getChildTopics().getModel() 262 .put(PROP_CREATED, getCreationTime(objectId)) 263 .put(PROP_MODIFIED, getModificationTime(objectId)); 264 } 265 266 // --- 267 268 private void setLastModifiedHeader(ContainerResponse response, long time) { 269 setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time)); 270 } 271 272 // ### FIXME: copy in CachingPlugin 273 private void setHeader(ContainerResponse response, String header, String value) { 274 MultivaluedMap headers = response.getHttpHeaders(); 275 // 276 if (headers.containsKey(header)) { 277 throw new RuntimeException("Response already has a \"" + header + "\" header"); 278 } 279 // 280 headers.putSingle(header, value); 281 } 282 283 // --- 284 285 /** 286 * Returns all parent topics/associations of the given topic (recursively). 287 * Traversal is informed by the "parent" and "child" role types. 288 * Traversal stops when no parent exists or when an association is met. 289 */ 290 private Set<DeepaMehtaObject> getParents(Topic topic) { 291 Set<DeepaMehtaObject> parents = new LinkedHashSet(); 292 // 293 List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child", 294 "dm4.core.parent", null); 295 List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child", 296 "dm4.core.parent", null); 297 parents.addAll(parentTopics); 298 parents.addAll(parentAssocs); 299 // 300 for (Topic parentTopic : parentTopics) { 301 parents.addAll(getParents(parentTopic)); 302 } 303 // 304 return parents; 305 } 306}