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    }