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 }