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.CompositeValueModel;
010    import de.deepamehta.core.model.TopicModel;
011    import de.deepamehta.core.osgi.PluginActivator;
012    import de.deepamehta.core.service.ClientState;
013    import de.deepamehta.core.service.Directives;
014    import de.deepamehta.core.service.event.PostCreateAssociationListener;
015    import de.deepamehta.core.service.event.PostCreateTopicListener;
016    import de.deepamehta.core.service.event.PostUpdateAssociationListener;
017    import de.deepamehta.core.service.event.PostUpdateTopicListener;
018    import de.deepamehta.core.service.event.PostUpdateTopicRequestListener;
019    import de.deepamehta.core.service.event.PreSendAssociationListener;
020    import de.deepamehta.core.service.event.PreSendTopicListener;
021    import de.deepamehta.core.service.event.ServiceResponseFilterListener;
022    
023    // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
024    import com.sun.jersey.spi.container.ContainerResponse;
025    
026    import javax.ws.rs.GET;
027    import javax.ws.rs.Path;
028    import javax.ws.rs.PathParam;
029    import javax.ws.rs.Produces;
030    import javax.ws.rs.Consumes;
031    import javax.ws.rs.core.MultivaluedMap;
032    
033    import java.text.DateFormat;
034    import java.text.SimpleDateFormat;
035    import java.util.Collection;
036    import java.util.LinkedHashSet;
037    import java.util.List;
038    import java.util.Locale;
039    import java.util.Set;
040    import java.util.TimeZone;
041    import java.util.logging.Logger;
042    
043    
044    
045    @Path("/time")
046    @Consumes("application/json")
047    @Produces("application/json")
048    public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener,
049                                                                            PostCreateAssociationListener,
050                                                                            PostUpdateTopicListener,
051                                                                            PostUpdateTopicRequestListener,
052                                                                            PostUpdateAssociationListener,
053                                                                            PreSendTopicListener,
054                                                                            PreSendAssociationListener,
055                                                                            ServiceResponseFilterListener {
056    
057        // ------------------------------------------------------------------------------------------------------- Constants
058    
059        private static String PROP_URI_CREATED  = "dm4.time.created";
060        private static String PROP_URI_MODIFIED = "dm4.time.modified";
061    
062        private static String HEADER_LAST_MODIFIED = "Last-Modified";
063    
064        // ---------------------------------------------------------------------------------------------- Instance Variables
065    
066        private DateFormat rfc2822;
067    
068        private Logger logger = Logger.getLogger(getClass().getName());
069    
070        // -------------------------------------------------------------------------------------------------- Public Methods
071    
072    
073    
074        // **********************************
075        // *** TimeService Implementation ***
076        // **********************************
077    
078    
079    
080        // === Timestamps ===
081    
082        // Note: the timestamp getters must return 0 as default. Before we used -1 but Jersey's evaluatePreconditions()
083        // does not work as expected when called with a negative value which is not dividable by 1000.
084    
085        @Override
086        public long getCreationTime(DeepaMehtaObject object) {
087            return object.hasProperty(PROP_URI_CREATED) ? (Long) object.getProperty(PROP_URI_CREATED) : 0;
088        }
089    
090        @Override
091        public long getModificationTime(DeepaMehtaObject object) {
092            return object.hasProperty(PROP_URI_MODIFIED) ? (Long) object.getProperty(PROP_URI_MODIFIED) : 0;
093        }
094    
095    
096    
097        // === Retrieval ===
098    
099        @GET
100        @Path("/from/{from}/to/{to}/topics/created")
101        @Override
102        public Collection<Topic> getTopicsByCreationTime(@PathParam("from") long from,
103                                                         @PathParam("to") long to) {
104            return dms.getTopicsByPropertyRange(PROP_URI_CREATED, from, to);
105        }
106    
107        @GET
108        @Path("/from/{from}/to/{to}/topics/modified")
109        @Override
110        public Collection<Topic> getTopicsByModificationTime(@PathParam("from") long from,
111                                                             @PathParam("to") long to) {
112            return dms.getTopicsByPropertyRange(PROP_URI_MODIFIED, from, to);
113        }
114    
115        @GET
116        @Path("/from/{from}/to/{to}/assocs/created")
117        @Override
118        public Collection<Association> getAssociationsByCreationTime(@PathParam("from") long from,
119                                                                     @PathParam("to") long to) {
120            return dms.getAssociationsByPropertyRange(PROP_URI_CREATED, from, to);
121        }
122    
123        @GET
124        @Path("/from/{from}/to/{to}/assocs/modified")
125        @Override
126        public Collection<Association> getAssociationsByModificationTime(@PathParam("from") long from,
127                                                                         @PathParam("to") long to) {
128            return dms.getAssociationsByPropertyRange(PROP_URI_MODIFIED, from, to);
129        }
130    
131    
132    
133        // ****************************
134        // *** Hook Implementations ***
135        // ****************************
136    
137    
138    
139        @Override
140        public void init() {
141            // create the date format used in HTTP date/time headers, see:
142            // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3
143            rfc2822 = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.ENGLISH);
144            rfc2822.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));
145            ((SimpleDateFormat) rfc2822).applyPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'");
146        }
147    
148    
149    
150        // ********************************
151        // *** Listener Implementations ***
152        // ********************************
153    
154    
155    
156        @Override
157        public void postCreateTopic(Topic topic, ClientState clientState, Directives directives) {
158            storeTimestamps(topic);
159        }
160    
161        @Override
162        public void postCreateAssociation(Association assoc, ClientState clientState, Directives directives) {
163            storeTimestamps(assoc);
164        }
165    
166        @Override
167        public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel, ClientState clientState,
168                                                                                           Directives directives) {
169            storeTimestamp(topic);
170        }
171    
172        @Override
173        public void postUpdateAssociation(Association assoc, AssociationModel oldModel, ClientState clientState,
174                                                                                        Directives directives) {
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, ClientState clientState) {
189            enrichWithTimestamp(topic);
190        }
191    
192        @Override
193        public void preSendAssociation(Association assoc, ClientState clientState) {
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_URI_CREATED, time);
235        }
236    
237        private void storeModificationTime(DeepaMehtaObject object, long time) {
238            storeTime(object, PROP_URI_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 created = getCreationTime(object);
257            long modified = getModificationTime(object);
258            CompositeValueModel comp = object.getCompositeValue().getModel();
259            comp.put(PROP_URI_CREATED, created);
260            comp.put(PROP_URI_MODIFIED, modified);
261            return modified;
262        }
263    
264        // ---
265    
266        private void setLastModifiedHeader(ContainerResponse response, long time) {
267            setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time));
268        }
269    
270        // ### FIXME: copy in CachingPlugin
271        private void setHeader(ContainerResponse response, String header, String value) {
272            MultivaluedMap headers = response.getHttpHeaders();
273            //
274            if (headers.containsKey(header)) {
275                throw new RuntimeException("Response already has a \"" + header + "\" header");
276            }
277            //
278            headers.putSingle(header, value);
279        }
280    
281        // ---
282    
283        /**
284         * Returns all parent topics/associations of the given topic (recursively).
285         * Traversal is informed by the "parent" and "child" role types.
286         * Traversal stops when no parent exists or when an association is met.
287         */
288        private Set<DeepaMehtaObject> getParents(Topic topic) {
289            Set<DeepaMehtaObject> parents = new LinkedHashSet();
290            //
291            List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child",
292                "dm4.core.parent", null, false, false, 0).getItems();
293            List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child",
294                "dm4.core.parent", null, false, false);
295            parents.addAll(parentTopics);
296            parents.addAll(parentAssocs);
297            //
298            for (Topic parentTopic : parentTopics) {
299                parents.addAll(getParents(parentTopic));
300            }
301            //
302            return parents;
303        }
304    }