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}