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