001package de.deepamehta.plugins.time;
002
003import de.deepamehta.core.Association;
004import de.deepamehta.core.DeepaMehtaObject;
005import de.deepamehta.core.Topic;
006import de.deepamehta.core.model.AssociationModel;
007import de.deepamehta.core.model.ChildTopicsModel;
008import de.deepamehta.core.model.TopicModel;
009import de.deepamehta.core.osgi.PluginActivator;
010import de.deepamehta.core.service.event.PostCreateAssociationListener;
011import de.deepamehta.core.service.event.PostCreateTopicListener;
012import de.deepamehta.core.service.event.PostUpdateAssociationListener;
013import de.deepamehta.core.service.event.PostUpdateTopicListener;
014import de.deepamehta.core.service.event.PostUpdateTopicRequestListener;
015import de.deepamehta.core.service.event.PreSendAssociationListener;
016import de.deepamehta.core.service.event.PreSendTopicListener;
017import de.deepamehta.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  = "dm4.time.created";
056    private static String PROP_MODIFIED = "dm4.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        return dms.hasProperty(objectId, PROP_CREATED) ? (Long) dms.getProperty(objectId, PROP_CREATED) : 0;
086    }
087
088    @GET
089    @Path("/object/{id}/modified")
090    @Override
091    public long getModificationTime(@PathParam("id") long objectId) {
092        return dms.hasProperty(objectId, PROP_MODIFIED) ? (Long) dms.getProperty(objectId, PROP_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_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_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_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_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) {
158        storeTimestamps(topic);
159    }
160
161    @Override
162    public void postCreateAssociation(Association assoc) {
163        storeTimestamps(assoc);
164    }
165
166    @Override
167    public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
168        storeTimestamp(topic);
169    }
170
171    @Override
172    public void postUpdateAssociation(Association assoc, AssociationModel oldModel) {
173        storeTimestamp(assoc);
174    }
175
176    // ---
177
178    @Override
179    public void postUpdateTopicRequest(Topic topic) {
180        storeParentsTimestamp(topic);
181    }
182
183    // ---
184
185    @Override
186    public void preSendTopic(Topic topic) {
187        enrichWithTimestamp(topic);
188    }
189
190    @Override
191    public void preSendAssociation(Association assoc) {
192        enrichWithTimestamp(assoc);
193    }
194
195    // ---
196
197    @Override
198    public void serviceResponseFilter(ContainerResponse response) {
199        DeepaMehtaObject object = responseObject(response);
200        if (object != null) {
201            long modified = enrichWithTimestamp(object);
202            setLastModifiedHeader(response, modified);
203        }
204    }
205
206
207
208    // ------------------------------------------------------------------------------------------------- Private Methods
209
210    private void storeTimestamps(DeepaMehtaObject object) {
211        long time = System.currentTimeMillis();
212        storeCreationTime(object, time);
213        storeModificationTime(object, time);
214    }
215
216    private void storeTimestamp(DeepaMehtaObject object) {
217        long time = System.currentTimeMillis();
218        storeModificationTime(object, time);
219    }
220
221    // ---
222
223    private void storeParentsTimestamp(Topic topic) {
224        for (DeepaMehtaObject object : getParents(topic)) {
225            storeTimestamp(object);
226        }
227    }
228
229    // ---
230
231    private void storeCreationTime(DeepaMehtaObject object, long time) {
232        storeTime(object, PROP_CREATED, time);
233    }
234
235    private void storeModificationTime(DeepaMehtaObject object, long time) {
236        storeTime(object, PROP_MODIFIED, time);
237    }
238
239    // ---
240
241    private void storeTime(DeepaMehtaObject object, String propUri, long time) {
242        object.setProperty(propUri, time, true);    // addToIndex=true
243    }
244
245    // ===
246
247    // ### FIXME: copy in CachingPlugin
248    private DeepaMehtaObject responseObject(ContainerResponse response) {
249        Object entity = response.getEntity();
250        return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null;
251    }
252
253    private long enrichWithTimestamp(DeepaMehtaObject object) {
254        long objectId = object.getId();
255        long created = getCreationTime(objectId);
256        long modified = getModificationTime(objectId);
257        ChildTopicsModel childTopics = object.getChildTopics().getModel();
258        childTopics.put(PROP_CREATED, created);
259        childTopics.put(PROP_MODIFIED, modified);
260        return modified;
261    }
262
263    // ---
264
265    private void setLastModifiedHeader(ContainerResponse response, long time) {
266        setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time));
267    }
268
269    // ### FIXME: copy in CachingPlugin
270    private void setHeader(ContainerResponse response, String header, String value) {
271        MultivaluedMap headers = response.getHttpHeaders();
272        //
273        if (headers.containsKey(header)) {
274            throw new RuntimeException("Response already has a \"" + header + "\" header");
275        }
276        //
277        headers.putSingle(header, value);
278    }
279
280    // ---
281
282    /**
283     * Returns all parent topics/associations of the given topic (recursively).
284     * Traversal is informed by the "parent" and "child" role types.
285     * Traversal stops when no parent exists or when an association is met.
286     */
287    private Set<DeepaMehtaObject> getParents(Topic topic) {
288        Set<DeepaMehtaObject> parents = new LinkedHashSet();
289        //
290        List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child",
291            "dm4.core.parent", null, 0).getItems();
292        List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child",
293            "dm4.core.parent", null).getItems();
294        parents.addAll(parentTopics);
295        parents.addAll(parentAssocs);
296        //
297        for (Topic parentTopic : parentTopics) {
298            parents.addAll(getParents(parentTopic));
299        }
300        //
301        return parents;
302    }
303}