package de.deepamehta.time; import de.deepamehta.core.Association; import de.deepamehta.core.DeepaMehtaObject; import de.deepamehta.core.Topic; import de.deepamehta.core.model.AssociationModel; import de.deepamehta.core.model.ChildTopicsModel; import de.deepamehta.core.model.TopicModel; import de.deepamehta.core.osgi.PluginActivator; import de.deepamehta.core.service.event.PostCreateAssociationListener; import de.deepamehta.core.service.event.PostCreateTopicListener; import de.deepamehta.core.service.event.PostUpdateAssociationListener; import de.deepamehta.core.service.event.PostUpdateTopicListener; import de.deepamehta.core.service.event.PostUpdateTopicRequestListener; import de.deepamehta.core.service.event.PreSendAssociationListener; import de.deepamehta.core.service.event.PreSendTopicListener; import de.deepamehta.core.service.event.ServiceResponseFilterListener; // ### TODO: hide Jersey internals. Move to JAX-RS 2.0. import com.sun.jersey.spi.container.ContainerResponse; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.Consumes; import javax.ws.rs.core.MultivaluedMap; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TimeZone; import java.util.logging.Logger; @Path("/time") @Consumes("application/json") @Produces("application/json") public class TimePlugin extends PluginActivator implements TimeService, PostCreateTopicListener, PostCreateAssociationListener, PostUpdateTopicListener, PostUpdateTopicRequestListener, PostUpdateAssociationListener, PreSendTopicListener, PreSendAssociationListener, ServiceResponseFilterListener { // ------------------------------------------------------------------------------------------------------- Constants private static String PROP_CREATED = "dm4.time.created"; private static String PROP_MODIFIED = "dm4.time.modified"; private static String HEADER_LAST_MODIFIED = "Last-Modified"; // ---------------------------------------------------------------------------------------------- Instance Variables private DateFormat rfc2822; private Logger logger = Logger.getLogger(getClass().getName()); // -------------------------------------------------------------------------------------------------- Public Methods // ********************************** // *** TimeService Implementation *** // ********************************** // === Timestamps === // Note: the timestamp getters must return 0 as default. Before we used -1 but Jersey's evaluatePreconditions() // does not work as expected when called with a negative value which is not dividable by 1000. @GET @Path("/object/{id}/created") @Override public long getCreationTime(@PathParam("id") long objectId) { try { return dm4.hasProperty(objectId, PROP_CREATED) ? (Long) dm4.getProperty(objectId, PROP_CREATED) : 0; } catch (Exception e) { throw new RuntimeException("Fetching creation time of object " + objectId + " failed", e); } } @GET @Path("/object/{id}/modified") @Override public long getModificationTime(@PathParam("id") long objectId) { try { return dm4.hasProperty(objectId, PROP_MODIFIED) ? (Long) dm4.getProperty(objectId, PROP_MODIFIED) : 0; } catch (Exception e) { throw new RuntimeException("Fetching modification time of object " + objectId + " failed", e); } } // --- @Override public void setModified(DeepaMehtaObject object) { storeTimestamp(object); } // === Retrieval === @GET @Path("/from/{from}/to/{to}/topics/created") @Override public Collection<Topic> getTopicsByCreationTime(@PathParam("from") long from, @PathParam("to") long to) { return dm4.getTopicsByPropertyRange(PROP_CREATED, from, to); } @GET @Path("/from/{from}/to/{to}/topics/modified") @Override public Collection<Topic> getTopicsByModificationTime(@PathParam("from") long from, @PathParam("to") long to) { return dm4.getTopicsByPropertyRange(PROP_MODIFIED, from, to); } @GET @Path("/from/{from}/to/{to}/assocs/created") @Override public Collection<Association> getAssociationsByCreationTime(@PathParam("from") long from, @PathParam("to") long to) { return dm4.getAssociationsByPropertyRange(PROP_CREATED, from, to); } @GET @Path("/from/{from}/to/{to}/assocs/modified") @Override public Collection<Association> getAssociationsByModificationTime(@PathParam("from") long from, @PathParam("to") long to) { return dm4.getAssociationsByPropertyRange(PROP_MODIFIED, from, to); } // **************************** // *** Hook Implementations *** // **************************** @Override public void init() { // create the date format used in HTTP date/time headers, see: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 rfc2822 = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.ENGLISH); rfc2822.setTimeZone(TimeZone.getTimeZone("GMT+00:00")); ((SimpleDateFormat) rfc2822).applyPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'"); } // ******************************** // *** Listener Implementations *** // ******************************** @Override public void postCreateTopic(Topic topic) { storeTimestamps(topic); } @Override public void postCreateAssociation(Association assoc) { storeTimestamps(assoc); } @Override public void postUpdateTopic(Topic topic, TopicModel updateModel, TopicModel oldTopic) { storeTimestamp(topic); } @Override public void postUpdateAssociation(Association assoc, AssociationModel updateModel, AssociationModel oldAssoc) { storeTimestamp(assoc); } // --- @Override public void postUpdateTopicRequest(Topic topic) { storeParentsTimestamp(topic); } // --- @Override public void preSendTopic(Topic topic) { enrichWithTimestamp(topic); } @Override public void preSendAssociation(Association assoc) { enrichWithTimestamp(assoc); } // --- @Override public void serviceResponseFilter(ContainerResponse response) { DeepaMehtaObject object = responseObject(response); if (object != null) { setLastModifiedHeader(response, getModificationTime(object.getId())); } } // ------------------------------------------------------------------------------------------------- Private Methods private void storeTimestamps(DeepaMehtaObject object) { long time = System.currentTimeMillis(); storeCreationTime(object, time); storeModificationTime(object, time); } private void storeTimestamp(DeepaMehtaObject object) { long time = System.currentTimeMillis(); storeModificationTime(object, time); } // --- private void storeParentsTimestamp(Topic topic) { for (DeepaMehtaObject object : getParents(topic)) { storeTimestamp(object); } } // --- private void storeCreationTime(DeepaMehtaObject object, long time) { storeTime(object, PROP_CREATED, time); } private void storeModificationTime(DeepaMehtaObject object, long time) { storeTime(object, PROP_MODIFIED, time); } // --- private void storeTime(DeepaMehtaObject object, String propUri, long time) { object.setProperty(propUri, time, true); // addToIndex=true } // === // ### FIXME: copy in CachingPlugin private DeepaMehtaObject responseObject(ContainerResponse response) { Object entity = response.getEntity(); return entity instanceof DeepaMehtaObject ? (DeepaMehtaObject) entity : null; } private void enrichWithTimestamp(DeepaMehtaObject object) { long objectId = object.getId(); ChildTopicsModel childTopics = object.getChildTopics().getModel() .put(PROP_CREATED, getCreationTime(objectId)) .put(PROP_MODIFIED, getModificationTime(objectId)); } // --- private void setLastModifiedHeader(ContainerResponse response, long time) { setHeader(response, HEADER_LAST_MODIFIED, rfc2822.format(time)); } // ### FIXME: copy in CachingPlugin private void setHeader(ContainerResponse response, String header, String value) { MultivaluedMap headers = response.getHttpHeaders(); // if (headers.containsKey(header)) { throw new RuntimeException("Response already has a \"" + header + "\" header"); } // headers.putSingle(header, value); } // --- /** * Returns all parent topics/associations of the given topic (recursively). * Traversal is informed by the "parent" and "child" role types. * Traversal stops when no parent exists or when an association is met. */ private Set<DeepaMehtaObject> getParents(Topic topic) { Set<DeepaMehtaObject> parents = new LinkedHashSet(); // List<? extends Topic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child", "dm4.core.parent", null); List<? extends Association> parentAssocs = topic.getRelatedAssociations(null, "dm4.core.child", "dm4.core.parent", null); parents.addAll(parentTopics); parents.addAll(parentAssocs); // for (Topic parentTopic : parentTopics) { parents.addAll(getParents(parentTopic)); } // return parents; } }