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.ChildTopicsModel; 010 import de.deepamehta.core.model.TopicModel; 011 import de.deepamehta.core.osgi.PluginActivator; 012 import de.deepamehta.core.service.event.PostCreateAssociationListener; 013 import de.deepamehta.core.service.event.PostCreateTopicListener; 014 import de.deepamehta.core.service.event.PostUpdateAssociationListener; 015 import de.deepamehta.core.service.event.PostUpdateTopicListener; 016 import de.deepamehta.core.service.event.PostUpdateTopicRequestListener; 017 import de.deepamehta.core.service.event.PreSendAssociationListener; 018 import de.deepamehta.core.service.event.PreSendTopicListener; 019 import de.deepamehta.core.service.event.ServiceResponseFilterListener; 020 021 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 022 import com.sun.jersey.spi.container.ContainerResponse; 023 024 import javax.ws.rs.GET; 025 import javax.ws.rs.Path; 026 import javax.ws.rs.PathParam; 027 import javax.ws.rs.Produces; 028 import javax.ws.rs.Consumes; 029 import javax.ws.rs.core.MultivaluedMap; 030 031 import java.text.DateFormat; 032 import java.text.SimpleDateFormat; 033 import java.util.Collection; 034 import java.util.LinkedHashSet; 035 import java.util.List; 036 import java.util.Locale; 037 import java.util.Set; 038 import java.util.TimeZone; 039 import java.util.logging.Logger; 040 041 042 043 @Path("/time") 044 @Consumes("application/json") 045 @Produces("application/json") 046 public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener, 047 PostCreateAssociationListener, 048 PostUpdateTopicListener, 049 PostUpdateTopicRequestListener, 050 PostUpdateAssociationListener, 051 PreSendTopicListener, 052 PreSendAssociationListener, 053 ServiceResponseFilterListener { 054 055 // ------------------------------------------------------------------------------------------------------- Constants 056 057 private static String PROP_CREATED = "dm4.time.created"; 058 private static String PROP_MODIFIED = "dm4.time.modified"; 059 060 private static String HEADER_LAST_MODIFIED = "Last-Modified"; 061 062 // ---------------------------------------------------------------------------------------------- Instance Variables 063 064 private DateFormat rfc2822; 065 066 private Logger logger = Logger.getLogger(getClass().getName()); 067 068 // -------------------------------------------------------------------------------------------------- Public Methods 069 070 071 072 // ********************************** 073 // *** TimeService Implementation *** 074 // ********************************** 075 076 077 078 // === Timestamps === 079 080 // Note: the timestamp getters must return 0 as default. Before we used -1 but Jersey's evaluatePreconditions() 081 // does not work as expected when called with a negative value which is not dividable by 1000. 082 083 @GET 084 @Path("/object/{id}/created") 085 @Override 086 public long getCreationTime(@PathParam("id") long objectId) { 087 return dms.hasProperty(objectId, PROP_CREATED) ? (Long) dms.getProperty(objectId, PROP_CREATED) : 0; 088 } 089 090 @GET 091 @Path("/object/{id}/modified") 092 @Override 093 public long getModificationTime(@PathParam("id") long objectId) { 094 return dms.hasProperty(objectId, PROP_MODIFIED) ? (Long) dms.getProperty(objectId, PROP_MODIFIED) : 0; 095 } 096 097 098 099 // === Retrieval === 100 101 @GET 102 @Path("/from/{from}/to/{to}/topics/created") 103 @Override 104 public Collection<Topic> getTopicsByCreationTime(@PathParam("from") long from, 105 @PathParam("to") long to) { 106 return dms.getTopicsByPropertyRange(PROP_CREATED, from, to); 107 } 108 109 @GET 110 @Path("/from/{from}/to/{to}/topics/modified") 111 @Override 112 public Collection<Topic> getTopicsByModificationTime(@PathParam("from") long from, 113 @PathParam("to") long to) { 114 return dms.getTopicsByPropertyRange(PROP_MODIFIED, from, to); 115 } 116 117 @GET 118 @Path("/from/{from}/to/{to}/assocs/created") 119 @Override 120 public Collection<Association> getAssociationsByCreationTime(@PathParam("from") long from, 121 @PathParam("to") long to) { 122 return dms.getAssociationsByPropertyRange(PROP_CREATED, from, to); 123 } 124 125 @GET 126 @Path("/from/{from}/to/{to}/assocs/modified") 127 @Override 128 public Collection<Association> getAssociationsByModificationTime(@PathParam("from") long from, 129 @PathParam("to") long to) { 130 return dms.getAssociationsByPropertyRange(PROP_MODIFIED, from, to); 131 } 132 133 134 135 // **************************** 136 // *** Hook Implementations *** 137 // **************************** 138 139 140 141 @Override 142 public void init() { 143 // create the date format used in HTTP date/time headers, see: 144 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 145 rfc2822 = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.ENGLISH); 146 rfc2822.setTimeZone(TimeZone.getTimeZone("GMT+00:00")); 147 ((SimpleDateFormat) rfc2822).applyPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'"); 148 } 149 150 151 152 // ******************************** 153 // *** Listener Implementations *** 154 // ******************************** 155 156 157 158 @Override 159 public void postCreateTopic(Topic topic) { 160 storeTimestamps(topic); 161 } 162 163 @Override 164 public void postCreateAssociation(Association assoc) { 165 storeTimestamps(assoc); 166 } 167 168 @Override 169 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 170 storeTimestamp(topic); 171 } 172 173 @Override 174 public void postUpdateAssociation(Association assoc, AssociationModel oldModel) { 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) { 189 enrichWithTimestamp(topic); 190 } 191 192 @Override 193 public void preSendAssociation(Association assoc) { 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_CREATED, time); 235 } 236 237 private void storeModificationTime(DeepaMehtaObject object, long time) { 238 storeTime(object, PROP_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 objectId = object.getId(); 257 long created = getCreationTime(objectId); 258 long modified = getModificationTime(objectId); 259 ChildTopicsModel childTopics = object.getChildTopics().getModel(); 260 childTopics.put(PROP_CREATED, created); 261 childTopics.put(PROP_MODIFIED, modified); 262 return modified; 263 } 264 265 // --- 266 267 private void setLastModifiedHeader(ContainerResponse response, long time) { 268 setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time)); 269 } 270 271 // ### FIXME: copy in CachingPlugin 272 private void setHeader(ContainerResponse response, String header, String value) { 273 MultivaluedMap headers = response.getHttpHeaders(); 274 // 275 if (headers.containsKey(header)) { 276 throw new RuntimeException("Response already has a \"" + header + "\" header"); 277 } 278 // 279 headers.putSingle(header, value); 280 } 281 282 // --- 283 284 /** 285 * Returns all parent topics/associations of the given topic (recursively). 286 * Traversal is informed by the "parent" and "child" role types. 287 * Traversal stops when no parent exists or when an association is met. 288 */ 289 private Set<DeepaMehtaObject> getParents(Topic topic) { 290 Set<DeepaMehtaObject> parents = new LinkedHashSet(); 291 // 292 List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child", 293 "dm4.core.parent", null, 0).getItems(); 294 List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child", 295 "dm4.core.parent", null).getItems(); 296 parents.addAll(parentTopics); 297 parents.addAll(parentAssocs); 298 // 299 for (Topic parentTopic : parentTopics) { 300 parents.addAll(getParents(parentTopic)); 301 } 302 // 303 return parents; 304 } 305 }