001package systems.dmx.core.impl; 002 003import systems.dmx.core.Association; 004import systems.dmx.core.AssociationType; 005import systems.dmx.core.DMXObject; 006import systems.dmx.core.ChildTopics; 007import systems.dmx.core.RelatedTopic; 008import systems.dmx.core.Topic; 009import systems.dmx.core.TopicType; 010import systems.dmx.core.service.Directives; 011import systems.dmx.core.service.DirectivesResponse; 012import systems.dmx.core.service.WebSocketsService; 013 014import com.sun.jersey.spi.container.ContainerRequest; 015import com.sun.jersey.spi.container.ContainerResponse; 016import com.sun.jersey.spi.container.ContainerResponseFilter; 017 018import org.codehaus.jettison.json.JSONException; 019import org.codehaus.jettison.json.JSONObject; 020 021import javax.servlet.http.HttpServletRequest; 022import javax.ws.rs.core.Context; 023 024import java.lang.reflect.Type; 025import java.lang.reflect.ParameterizedType; 026import java.util.List; 027import java.util.logging.Logger; 028 029 030 031/** 032 * Response post-processing. 033 * Post-processing takes place <i>after</i> a request is processed, <i>before</i> the response is sent to the client. 034 * <p> 035 * Post-processing includes 5 steps: 036 * <ol> 037 * <li>Fire the <code>CoreEvent.SERVICE_RESPONSE_FILTER</code> event to let plugins operate on the response, e.g. 038 * - the Caching plugin sets the <code>Cache-Control</code> response header 039 * - the Time plugin sets the <code>Last-Modified</code> response header 040 * <li>Load child topics of the response object(s) if requested with the <code>include_childs</code> and 041 * <code>include_assoc_childs</code> query parameters. 042 * <li>Fire the <code>CoreEvent.PRE_SEND_XXX</code> events for all response object(s) and objects contained in response 043 * directives. This let plugins operate on the response on a per-object basis, e.g. 044 * - the Geomaps plugin enriches an Address topic with its geo coordinate 045 * - the Time plugin enriches topics/associations with creation/modification timestamps 046 * <li>Broadcast directives. 047 * <li>Remove the (thread-local) directives assembled while request processing. 048 * </ol> 049 */ 050class JerseyResponseFilter implements ContainerResponseFilter { 051 052 // ---------------------------------------------------------------------------------------------- Instance Variables 053 054 private EventManager em; 055 private WebSocketsService ws; 056 057 @Context 058 private HttpServletRequest request; 059 060 private Logger logger = Logger.getLogger(getClass().getName()); 061 062 // ---------------------------------------------------------------------------------------------------- Constructors 063 064 JerseyResponseFilter(EventManager em, WebSocketsService ws) { 065 this.em = em; 066 this.ws = ws; 067 } 068 069 // -------------------------------------------------------------------------------------------------- Public Methods 070 071 @Override 072 public ContainerResponse filter(ContainerRequest request, ContainerResponse response) { 073 try { 074 em.fireEvent(CoreEvent.SERVICE_RESPONSE_FILTER, response); 075 // 076 Object entity = response.getEntity(); 077 if (entity != null) { 078 // 079 // 1) Loading child topics 080 boolean includeChilds = getIncludeChilds(request); 081 boolean includeAssocChilds = getIncludeAssocChilds(request); 082 if (entity instanceof DMXObject) { 083 loadChildTopics((DMXObject) entity, includeChilds, includeAssocChilds); 084 } else if (isIterable(response, DMXObject.class)) { 085 loadChildTopics((Iterable<DMXObject>) entity, includeChilds, includeAssocChilds); 086 } 087 // 088 // 2) Firing PRE_SEND events 089 Directives directives = null; 090 if (entity instanceof DMXObject) { 091 firePreSend((DMXObject) entity); 092 } else if (isIterable(response, DMXObject.class)) { 093 firePreSend((Iterable<DMXObject>) entity); 094 } else if (entity instanceof DirectivesResponse) { 095 firePreSend(((DirectivesResponse) entity).getObject()); 096 // 097 // Note: some plugins rely on the PRE_SEND event to be fired for the individual DMX 098 // objects contained in the set of directives. E.g. the Time plugin enriches updated objects 099 // with timestamps. The timestamps in turn are needed at client-side by the Caching plugin 100 // in order to issue conditional PUT requests. 101 // ### TODO: don't fire PRE_SEND events for the individual directives but only for the wrapped 102 // DMXObject? Let the update() Core Service calls return the updated object? 103 directives = ((DirectivesResponse) entity).getDirectives(); 104 firePreSend(directives); 105 } 106 // 107 // 3) Broadcast directives 108 if (directives != null) { 109 broadcast(directives); 110 } 111 } 112 // 113 Directives.remove(); 114 // 115 return response; 116 } catch (Exception e) { 117 throw new RuntimeException("Response filtering failed", e); 118 } 119 } 120 121 // ------------------------------------------------------------------------------------------------- Private Methods 122 123 124 125 // === Loading child topics === 126 127 private void loadChildTopics(DMXObject object, boolean includeChilds, boolean includeAssocChilds) { 128 if (includeChilds) { 129 object.loadChildTopics(); 130 if (includeAssocChilds) { 131 loadRelatingAssociationChildTopics(object); 132 } 133 } 134 } 135 136 private void loadChildTopics(Iterable<DMXObject> objects, boolean includeChilds, boolean includeAssocChilds) { 137 for (DMXObject object : objects) { 138 loadChildTopics(object, includeChilds, includeChilds); 139 } 140 } 141 142 // --- 143 144 private void loadRelatingAssociationChildTopics(DMXObject object) { 145 ChildTopics childTopics = object.getChildTopics(); 146 for (String childTypeUri : childTopics) { 147 Object value = childTopics.get(childTypeUri); 148 if (value instanceof RelatedTopic) { 149 RelatedTopic childTopic = (RelatedTopic) value; 150 childTopic.getRelatingAssociation().loadChildTopics(); 151 loadRelatingAssociationChildTopics(childTopic); // recursion 152 } else if (value instanceof List) { 153 for (RelatedTopic childTopic : (List<RelatedTopic>) value) { 154 childTopic.getRelatingAssociation().loadChildTopics(); 155 loadRelatingAssociationChildTopics(childTopic); // recursion 156 } 157 } else { 158 throw new RuntimeException("Unexpected \"" + childTypeUri + "\" value in ChildTopics: " + value); 159 } 160 } 161 } 162 163 164 165 // === Firing PRE_SEND events === 166 167 private void firePreSend(DMXObject object) { 168 if (object instanceof TopicType) { // Note: must take precedence over topic 169 em.fireEvent(CoreEvent.PRE_SEND_TOPIC_TYPE, object); 170 } else if (object instanceof AssociationType) { // Note: must take precedence over topic 171 em.fireEvent(CoreEvent.PRE_SEND_ASSOCIATION_TYPE, object); 172 } else if (object instanceof Topic) { 173 em.fireEvent(CoreEvent.PRE_SEND_TOPIC, object); 174 } else if (object instanceof Association) { 175 em.fireEvent(CoreEvent.PRE_SEND_ASSOCIATION, object); 176 } 177 } 178 179 private void firePreSend(Iterable<DMXObject> objects) { 180 for (DMXObject object : objects) { 181 firePreSend(object); 182 } 183 } 184 185 private void firePreSend(Directives directives) { 186 for (Directives.Entry entry : directives) { 187 switch (entry.dir) { 188 case UPDATE_TOPIC: 189 case UPDATE_ASSOCIATION: 190 case UPDATE_TOPIC_TYPE: 191 case UPDATE_ASSOCIATION_TYPE: 192 firePreSend((DMXObject) entry.arg); 193 break; 194 } 195 } 196 } 197 198 199 200 // === Broadcast === 201 202 private void broadcast(Directives directives) throws JSONException { 203 JSONObject message = new JSONObject() 204 .put("type", "processDirectives") 205 .put("args", directives.toJSONArray()); 206 ws.messageToAllButOne(request, "systems.dmx.webclient", message.toString()); 207 } 208 209 210 211 // === Helper === 212 213 private boolean isIterable(ContainerResponse response, Class elementType) { 214 Type genericType = response.getEntityType(); 215 if (genericType instanceof ParameterizedType) { 216 Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments(); 217 Class<?> type = response.getEntity().getClass(); 218 if (typeArgs.length == 1 && Iterable.class.isAssignableFrom(type) && 219 elementType.isAssignableFrom((Class) typeArgs[0])) { 220 return true; 221 } 222 } 223 return false; 224 } 225 226 // --- 227 228 private boolean getIncludeChilds(ContainerRequest request) { 229 return getBooleanQueryParameter(request, "include_childs"); 230 } 231 232 private boolean getIncludeAssocChilds(ContainerRequest request) { 233 return getBooleanQueryParameter(request, "include_assoc_childs"); 234 } 235 236 // --- 237 238 private boolean getBooleanQueryParameter(ContainerRequest request, String param) { 239 return Boolean.parseBoolean(request.getQueryParameters().getFirst(param)); 240 } 241}