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.PreSendAssociationListener;
019    import de.deepamehta.core.service.event.PreSendTopicListener;
020    import de.deepamehta.core.service.event.ServiceResponseFilterListener;
021    
022    // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
023    import com.sun.jersey.spi.container.ContainerResponse;
024    
025    import javax.ws.rs.GET;
026    import javax.ws.rs.PUT;
027    import javax.ws.rs.POST;
028    import javax.ws.rs.DELETE;
029    import javax.ws.rs.HeaderParam;
030    import javax.ws.rs.Path;
031    import javax.ws.rs.PathParam;
032    import javax.ws.rs.Produces;
033    import javax.ws.rs.Consumes;
034    import javax.ws.rs.core.MultivaluedMap;
035    
036    import java.text.DateFormat;
037    import java.text.SimpleDateFormat;
038    import java.util.Collection;
039    import java.util.Date;
040    import java.util.Locale;
041    import java.util.TimeZone;
042    import java.util.logging.Logger;
043    
044    
045    
046    @Path("/time")
047    @Consumes("application/json")
048    @Produces("application/json")
049    public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener,
050                                                                            PostCreateAssociationListener,
051                                                                            PostUpdateTopicListener,
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 preSendTopic(Topic topic, ClientState clientState) {
182            enrichWithTimestamp(topic);
183        }
184    
185        @Override
186        public void preSendAssociation(Association assoc, ClientState clientState) {
187            enrichWithTimestamp(assoc);
188        }
189    
190        // ---
191    
192        @Override
193        public void serviceResponseFilter(ContainerResponse response) {
194            DeepaMehtaObject object = responseObject(response);
195            if (object != null) {
196                long modified = enrichWithTimestamp(object);
197                setLastModifiedHeader(response, modified);
198            }
199        }
200    
201    
202    
203        // ------------------------------------------------------------------------------------------------- Private Methods
204    
205        private void storeTimestamps(DeepaMehtaObject object) {
206            long time = System.currentTimeMillis();
207            storeCreationTime(object, time);
208            storeModificationTime(object, time);
209        }
210    
211        private void storeTimestamp(DeepaMehtaObject object) {
212            long time = System.currentTimeMillis();
213            storeModificationTime(object, time);
214        }
215    
216        // ---
217    
218        private void storeCreationTime(DeepaMehtaObject object, long time) {
219            storeTime(object, PROP_URI_CREATED, time);
220        }
221    
222        private void storeModificationTime(DeepaMehtaObject object, long time) {
223            storeTime(object, PROP_URI_MODIFIED, time);
224        }
225    
226        // ---
227    
228        private void storeTime(DeepaMehtaObject object, String propUri, long time) {
229            object.setProperty(propUri, time, true);    // addToIndex=true
230        }
231    
232        // ===
233    
234        // ### FIXME: copy in CachingPlugin
235        private DeepaMehtaObject responseObject(ContainerResponse response) {
236            Object entity = response.getEntity();
237            return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null;
238        }
239    
240        private long enrichWithTimestamp(DeepaMehtaObject object) {
241            long created = getCreationTime(object);
242            long modified = getModificationTime(object);
243            CompositeValueModel comp = object.getCompositeValue().getModel();
244            comp.put(PROP_URI_CREATED, created);
245            comp.put(PROP_URI_MODIFIED, modified);
246            return modified;
247        }
248    
249        // ---
250    
251        private void setLastModifiedHeader(ContainerResponse response, long time) {
252            setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time));
253        }
254    
255        // ### FIXME: copy in CachingPlugin
256        private void setHeader(ContainerResponse response, String header, String value) {
257            MultivaluedMap headers = response.getHttpHeaders();
258            //
259            if (headers.containsKey(header)) {
260                throw new RuntimeException("Response already has a \"" + header + "\" header");
261            }
262            //
263            headers.putSingle(header, value);
264        }
265    }