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