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