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    }