001package de.deepamehta.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 dm4.hasProperty(objectId, PROP_CREATED) ? (Long) dm4.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 dm4.hasProperty(objectId, PROP_MODIFIED) ? (Long) dm4.getProperty(objectId, PROP_MODIFIED) : 0;
093    }
094
095    // ---
096
097    @Override
098    public void setModified(DeepaMehtaObject object) {
099        storeTimestamp(object);
100    }
101
102
103
104    // === Retrieval ===
105
106    @GET
107    @Path("/from/{from}/to/{to}/topics/created")
108    @Override
109    public Collection<Topic> getTopicsByCreationTime(@PathParam("from") long from,
110                                                     @PathParam("to") long to) {
111        return dm4.getTopicsByPropertyRange(PROP_CREATED, from, to);
112    }
113
114    @GET
115    @Path("/from/{from}/to/{to}/topics/modified")
116    @Override
117    public Collection<Topic> getTopicsByModificationTime(@PathParam("from") long from,
118                                                         @PathParam("to") long to) {
119        return dm4.getTopicsByPropertyRange(PROP_MODIFIED, from, to);
120    }
121
122    @GET
123    @Path("/from/{from}/to/{to}/assocs/created")
124    @Override
125    public Collection<Association> getAssociationsByCreationTime(@PathParam("from") long from,
126                                                                 @PathParam("to") long to) {
127        return dm4.getAssociationsByPropertyRange(PROP_CREATED, from, to);
128    }
129
130    @GET
131    @Path("/from/{from}/to/{to}/assocs/modified")
132    @Override
133    public Collection<Association> getAssociationsByModificationTime(@PathParam("from") long from,
134                                                                     @PathParam("to") long to) {
135        return dm4.getAssociationsByPropertyRange(PROP_MODIFIED, from, to);
136    }
137
138
139
140    // ****************************
141    // *** Hook Implementations ***
142    // ****************************
143
144
145
146    @Override
147    public void init() {
148        // create the date format used in HTTP date/time headers, see:
149        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3
150        rfc2822 = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.ENGLISH);
151        rfc2822.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));
152        ((SimpleDateFormat) rfc2822).applyPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'");
153    }
154
155
156
157    // ********************************
158    // *** Listener Implementations ***
159    // ********************************
160
161
162
163    @Override
164    public void postCreateTopic(Topic topic) {
165        storeTimestamps(topic);
166    }
167
168    @Override
169    public void postCreateAssociation(Association assoc) {
170        storeTimestamps(assoc);
171    }
172
173    @Override
174    public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
175        storeTimestamp(topic);
176    }
177
178    @Override
179    public void postUpdateAssociation(Association assoc, AssociationModel newModel, AssociationModel oldModel) {
180        storeTimestamp(assoc);
181    }
182
183    // ---
184
185    @Override
186    public void postUpdateTopicRequest(Topic topic) {
187        storeParentsTimestamp(topic);
188    }
189
190    // ---
191
192    @Override
193    public void preSendTopic(Topic topic) {
194        enrichWithTimestamp(topic);
195    }
196
197    @Override
198    public void preSendAssociation(Association assoc) {
199        enrichWithTimestamp(assoc);
200    }
201
202    // ---
203
204    @Override
205    public void serviceResponseFilter(ContainerResponse response) {
206        DeepaMehtaObject object = responseObject(response);
207        if (object != null) {
208            setLastModifiedHeader(response, getModificationTime(object.getId()));
209        }
210    }
211
212
213
214    // ------------------------------------------------------------------------------------------------- Private Methods
215
216    private void storeTimestamps(DeepaMehtaObject object) {
217        long time = System.currentTimeMillis();
218        storeCreationTime(object, time);
219        storeModificationTime(object, time);
220    }
221
222    private void storeTimestamp(DeepaMehtaObject object) {
223        long time = System.currentTimeMillis();
224        storeModificationTime(object, time);
225    }
226
227    // ---
228
229    private void storeParentsTimestamp(Topic topic) {
230        for (DeepaMehtaObject object : getParents(topic)) {
231            storeTimestamp(object);
232        }
233    }
234
235    // ---
236
237    private void storeCreationTime(DeepaMehtaObject object, long time) {
238        storeTime(object, PROP_CREATED, time);
239    }
240
241    private void storeModificationTime(DeepaMehtaObject object, long time) {
242        storeTime(object, PROP_MODIFIED, time);
243    }
244
245    // ---
246
247    private void storeTime(DeepaMehtaObject object, String propUri, long time) {
248        object.setProperty(propUri, time, true);    // addToIndex=true
249    }
250
251    // ===
252
253    // ### FIXME: copy in CachingPlugin
254    private DeepaMehtaObject responseObject(ContainerResponse response) {
255        Object entity = response.getEntity();
256        return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null;
257    }
258
259    private void enrichWithTimestamp(DeepaMehtaObject object) {
260        long objectId = object.getId();
261        ChildTopicsModel childTopics = object.getChildTopics().getModel()
262            .put(PROP_CREATED, getCreationTime(objectId))
263            .put(PROP_MODIFIED, getModificationTime(objectId));
264    }
265
266    // ---
267
268    private void setLastModifiedHeader(ContainerResponse response, long time) {
269        setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time));
270    }
271
272    // ### FIXME: copy in CachingPlugin
273    private void setHeader(ContainerResponse response, String header, String value) {
274        MultivaluedMap headers = response.getHttpHeaders();
275        //
276        if (headers.containsKey(header)) {
277            throw new RuntimeException("Response already has a \"" + header + "\" header");
278        }
279        //
280        headers.putSingle(header, value);
281    }
282
283    // ---
284
285    /**
286     * Returns all parent topics/associations of the given topic (recursively).
287     * Traversal is informed by the "parent" and "child" role types.
288     * Traversal stops when no parent exists or when an association is met.
289     */
290    private Set<DeepaMehtaObject> getParents(Topic topic) {
291        Set<DeepaMehtaObject> parents = new LinkedHashSet();
292        //
293        List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child",
294            "dm4.core.parent", null);
295        List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child",
296            "dm4.core.parent", null);
297        parents.addAll(parentTopics);
298        parents.addAll(parentAssocs);
299        //
300        for (Topic parentTopic : parentTopics) {
301            parents.addAll(getParents(parentTopic));
302        }
303        //
304        return parents;
305    }
306}