source: trunk/autoquest-htmlmonitor/src/main/java/de/ugoe/cs/autoquest/htmlmonitor/HtmlMonitorServlet.java @ 872

Last change on this file since 872 was 872, checked in by pharms, 12 years ago
  • removed find bug warnings
File size: 18.2 KB
Line 
1package de.ugoe.cs.autoquest.htmlmonitor;
2
3import java.io.IOException;
4import java.io.InputStreamReader;
5import java.net.MalformedURLException;
6import java.net.URL;
7import java.util.ArrayList;
8import java.util.List;
9import java.util.Map;
10import java.util.Set;
11
12import javax.servlet.ServletException;
13import javax.servlet.http.HttpServletRequest;
14import javax.servlet.http.HttpServletResponse;
15
16import org.json.simple.JSONArray;
17import org.json.simple.JSONObject;
18import org.json.simple.JSONValue;
19import org.json.simple.parser.ParseException;
20import org.mortbay.jetty.servlet.DefaultServlet;
21
22import de.ugoe.cs.util.console.Console;
23
24/**
25 * <p>
26 * the servlet deployed in the web server that receives all client messages. The messages are
27 * parsed, validated, and forwarded to the provided message listener. If a message is not valid,
28 * it is discarded. If an event in a message is not valid, it is discarded. Messages are only
29 * received via the POST HTTP method.
30 * </p>
31 *
32 * @author Patrick Harms
33 */
34class HtmlMonitorServlet extends DefaultServlet {
35
36    /**  */
37    private static final long serialVersionUID = 1L;
38   
39    /**
40     * the message listener to forward received messages to.
41     */
42    private HtmlMonitorMessageListener messageListener;
43
44    /**
45     * <p>
46     * initializes the servlet with the message listener to which all events shall be forwarded
47     * </p>
48     *
49     * @param messageListener the message listener that shall receive all client events
50     */
51    HtmlMonitorServlet(HtmlMonitorMessageListener messageListener) {
52        this.messageListener = messageListener;
53    }
54
55    /* (non-Javadoc)
56     * @see org.mortbay.jetty.servlet.DefaultServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
57     */
58    @Override
59    protected void doPost(HttpServletRequest request, HttpServletResponse response)
60        throws ServletException, IOException
61    {
62        Object value = null;
63        try {
64            value = JSONValue.parseWithException
65                (new InputStreamReader(request.getInputStream(), "UTF-8"));
66           
67            if (!(value instanceof JSONObject)) {
68                Console.printerrln("incoming data is not of the expected type --> discarding it");
69            }
70            else {
71                handleJSONObject((JSONObject) value);
72            }
73        }
74        catch (ParseException e) {
75            Console.printerrln
76                ("could not parse incoming data --> discarding it (" + e.toString() + ")");
77        }
78    }
79
80    /**
81     * <p>
82     * processes a received JSON object and validates it. If the message is ok, it is forwarded
83     * to the message listener
84     * </p>
85     *
86     * @param object the JSON object that contains a client message
87     */
88    private void handleJSONObject(JSONObject object) {
89        dumpJSONObject(object, "");
90       
91        JSONObject message = assertValue(object, "message", JSONObject.class);
92       
93        if (message == null) {
94            Console.printerrln("incoming data is no valid message --> discarding it");
95        }
96        else {
97            HtmlClientInfos clientInfos = extractClientInfos(message);
98
99            if (clientInfos == null) {
100                Console.printerrln
101                    ("incoming message does not contain valid client infos --> discarding it");
102            }
103            else {
104                HtmlEvent[] events = extractHtmlEvents(message, clientInfos);
105                if (events == null) {
106                    Console.printerrln
107                    ("incoming message does not contain valid events --> discarding it");
108                }
109                else {
110                    messageListener.handleMessage(clientInfos, events);
111                }
112            }
113        }
114    }
115
116    /**
117     * <p>
118     * tries to extract the client infos out of the received JSON object. If this is not fully
119     * possible, an appropriate message is dumped and the whole message is discarded (the method
120     * return null).
121     * </p>
122     *
123     * @param message the message to parse the client infos from
124     *
125     * @return the client infos, if the message is valid in this respect, or null if not
126     */
127    private HtmlClientInfos extractClientInfos(JSONObject message) {
128        HtmlClientInfos clientInfos = null;
129       
130        JSONObject infos = assertValue(message, "clientInfos", JSONObject.class);
131       
132        if (infos != null) {
133            String clientId = assertValue((JSONObject) infos, "clientId", String.class);
134            String userAgent = assertValue((JSONObject) infos, "userAgent", String.class);
135            URL url = assertValue((JSONObject) infos, "url", URL.class);
136            String title = assertValue((JSONObject) infos, "title", String.class);
137           
138            if (clientId == null) {
139                Console.printerrln("client infos do not contain a valid client id");
140            }
141            else if (userAgent == null) {
142                Console.printerrln("client infos do not contain a valid user agent");
143            }
144            else if (url == null) {
145                Console.printerrln("client infos do not contain a valid URL");
146            }
147            else if (title == null) {
148                Console.printerrln("client infos do not contain a valid title");
149            }
150            else {
151                clientInfos = new HtmlClientInfos(clientId, userAgent, url, title);
152            }
153        }
154       
155        return clientInfos;
156    }
157
158    /**
159     * <p>
160     * tries to extract the events out of the received JSON object. If this is not fully
161     * possible, an appropriate message is dumped and the errorprone event is discarded. If no
162     * valid event is found, the whole message is discarded.
163     * </p>
164     *
165     * @param object      the message to parse the events from
166     * @param clientInfos the infos about the client that send the events
167     * 
168     * @return the valid events stored in the message, or null if there are none
169     */
170    private HtmlEvent[] extractHtmlEvents(JSONObject object, HtmlClientInfos clientInfos) {
171        List<HtmlEvent> events = null;
172       
173        JSONArray eventArray = assertValue(object, "events", JSONArray.class);
174       
175        if (eventArray != null) {
176            events = new ArrayList<HtmlEvent>();
177           
178            for (int i = 0; i < eventArray.size(); i++) {
179                Object eventObj = eventArray.get(i);
180                if (!(eventObj instanceof JSONObject)) {
181                    Console.printerrln("event number " + (i + 1) + " is not a valid event object");
182                }
183                else {
184                    Long time = assertValue(((JSONObject) eventObj), "time", Long.class);
185                    String path = assertValue(((JSONObject) eventObj), "path", String.class);
186                    String eventType =
187                        assertValue(((JSONObject) eventObj), "eventType", String.class);
188                    Integer[] coordinates =
189                        assertValue(((JSONObject) eventObj), "coordinates", Integer[].class);
190                    Integer key = assertValue(((JSONObject) eventObj), "key", Integer.class);
191                    Integer scrollPosition =
192                        assertValue(((JSONObject) eventObj), "scrollPosition", Integer.class);
193                   
194                    if (time == null) {
195                        Console.printerrln("event number " + (i + 1) + " has no valid timestamp");
196                    }
197                    else if (path == null) {
198                        Console.printerrln("event number " + (i + 1) + " has no valid path");
199                    }
200                    else if (eventType == null) {
201                        Console.printerrln("event number " + (i + 1) + " has no valid event type");
202                    }
203                    else if ((coordinates != null) && (coordinates.length != 2)) {
204                        Console.printerrln("event number " + (i + 1) + " has no valid coordinates");
205                    }
206                    else if (checkEventParameterCombinations
207                                (eventType, coordinates, key, scrollPosition))
208                    {
209                        events.add(new HtmlEvent(clientInfos, time, path, eventType, coordinates,
210                                                 key, scrollPosition));
211                    }
212                    else {
213                        Console.printerrln
214                            ("event number " + (i + 1) + " has no valid parameter combination");
215                    }
216                }
217            }
218           
219        }
220       
221        if ((events != null) && (events.size() > 0)) {
222            return events.toArray(new HtmlEvent[events.size()]);
223        }
224        else {
225            return null;
226        }
227    }
228
229    /**
230     * <p>
231     * validates if for the given event type the parameter combination of coordinates, key, and
232     * scroll position is valid. As an example, an onclick event should usually not have an
233     * associated scroll position.
234     * </p>
235     *
236     * @param eventType      the type of the event
237     * @param coordinates    the coordinates of the event
238     * @param key            the key of the event
239     * @param scrollPosition the scroll position of the event
240     *
241     * @return true, if the combination of the parameters is valid, false else
242     */
243    private boolean checkEventParameterCombinations(String    eventType,
244                                                    Integer[] coordinates,
245                                                    Integer   key,
246                                                    Integer   scrollPosition)
247    {
248        boolean result = false;
249       
250        if ("onscroll".equals(eventType)) {
251            if ((coordinates == null) && (key == null) && (scrollPosition != null)) {
252                result = true;
253            }
254            else {
255                Console.printerrln(eventType + " event has invalid parameters");
256            }
257        }
258        else if ("onclick".equals(eventType) || "ondblclick".equals(eventType)) {
259            if ((coordinates != null) && (key == null) && (scrollPosition == null)) {
260                result = true;
261            }
262            else {
263                Console.printerrln(eventType + " event has invalid parameters");
264            }
265        }
266        else if ("onkeypress".equals(eventType) || "onkeydown".equals(eventType) ||
267                 "onkeyup".equals(eventType))
268        {
269            if ((coordinates == null) && (key != null) && (scrollPosition == null)) {
270                result = true;
271            }
272            else {
273                Console.printerrln(eventType + " event has invalid parameters");
274            }
275        }
276        else if ("onfocus".equals(eventType) || "onmouseout".equals(eventType) ||
277                 "onmousemove".equals(eventType) || "onunload".equals(eventType))
278        {
279            if ((coordinates == null) && (key == null) && (scrollPosition == null)) {
280                result = true;
281            }
282            else {
283                Console.printerrln(eventType + " event has invalid parameters");
284            }
285        }
286        else {
287            Console.printerrln("'" + eventType + "' is not a valid event type");
288        }
289       
290        return result;
291    }
292
293    /**
294     * <p>
295     * converts a value in the provided object matching the provided key to the provided type. If
296     * there is no value with the key or if the value can not be transformed to the provided type,
297     * the method returns null.
298     * </p>
299     *
300     * @param object the object to read the value from
301     * @param key    the key of the value
302     * @param clazz  the type to which the value should be transformed
303     *
304     * @return the value or null if either the value does not exist or if it can not be transformed
305     *         to the expected type
306     */
307    @SuppressWarnings("unchecked")
308    private <T> T assertValue(JSONObject object, String key, Class<T> clazz) {
309        Object value = object.get(key);
310        T result = null;
311       
312        if (clazz.isInstance(value)) {
313            result = (T) value;
314        }
315        else if (value instanceof String) {
316            if (URL.class.equals(clazz)) {
317                try {
318                    result = (T) new URL((String) value);
319                }
320                catch (MalformedURLException e) {
321                    e.printStackTrace();
322                    Console.printerrln("retrieved malformed URL for key '" + key + "': " + value +
323                                       " (" + e.toString() + ")");
324                }
325            }
326            else if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) {
327                try {
328                    result = (T) Integer.valueOf(Integer.parseInt((String) value));
329                }
330                catch (NumberFormatException e) {
331                    Console.printerrln
332                        ("retrieved malformed integer for key '" + key + "': " + value);
333                }
334            }
335            else if ((long.class.equals(clazz)) || (Long.class.equals(clazz))) {
336                try {
337                    result = (T) Long.valueOf(Long.parseLong((String) value));
338                }
339                catch (NumberFormatException e) {
340                    Console.printerrln
341                        ("retrieved malformed long for key '" + key + "': " + value);
342                }
343            }
344        }
345        else if (value instanceof Long) {
346            if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) {
347                result = (T) (Integer) ((Long) value).intValue();
348            }
349        }
350        else if (value instanceof JSONArray) {
351            if ((int[].class.equals(clazz)) || (Integer[].class.equals(clazz))) {
352                Integer[] resultArray = new Integer[((JSONArray) value).size()];
353                boolean allCouldBeParsed = true;
354               
355                for (int i = 0; i < ((JSONArray) value).size(); i++) {
356                    try {
357                        if (((JSONArray) value).get(i) instanceof Long) {
358                            resultArray[i] = (int) (long) (Long) ((JSONArray) value).get(i);
359                        }
360                        else if (((JSONArray) value).get(i) instanceof String) {
361                            try {
362                                resultArray[i] =
363                                    (int) Long.parseLong((String) ((JSONArray) value).get(i));
364                            }
365                            catch (NumberFormatException e) {
366                                Console.printerrln
367                                    ("retrieved malformed integer array for key '" + key + "': " +
368                                     value);
369                       
370                                allCouldBeParsed = false;
371                                break;
372                            }
373                        }
374                        else {
375                            Console.printerrln
376                                ("can not handle type of value in expected integer array '" + key +
377                                 "': " + value);
378                        }
379                    }
380                    catch (ClassCastException e) {
381                        e.printStackTrace();
382                        Console.printerrln("expected integer array for key '" + key +
383                                           "' but it was something else: " + value);
384                       
385                        allCouldBeParsed = false;
386                        break;
387                    }
388                }
389               
390                if (allCouldBeParsed) {
391                    result = (T) resultArray;
392                }
393            }
394        }
395       
396        return result;
397    }
398
399    /**
400     * <p>
401     * convenience method for dumping an object to std out. If the object is a JSON object, it is
402     * deeply analyzed and its internal structure is dumped as well.
403     * </p>
404     *
405     * @param object the object to dump
406     * @param indent the indentation to be used.
407     */
408    private void dumpJSONObject(Object object, String indent) {
409        if (object instanceof JSONArray) {
410            boolean arrayContainsJSONObjects = false;
411            for (Object arrayElem : (JSONArray) object) {
412                if (arrayElem instanceof JSONObject) {
413                    arrayContainsJSONObjects = true;
414                    break;
415                }               
416            }
417           
418            if (arrayContainsJSONObjects) {
419                System.out.println();
420                System.out.print(indent);
421                System.out.println('[');
422                System.out.print(indent);
423                System.out.print(' ');
424            }
425            else {
426                System.out.print(' ');
427                System.out.print('[');
428            }
429           
430            int index = 0;
431            for (Object arrayElem : (JSONArray) object) {
432                if (index++ > 0) {
433                    System.out.print(",");
434                    if (arrayContainsJSONObjects) {
435                        System.out.println();
436                        System.out.print(indent);
437                    }
438
439                    System.out.print(' ');
440                }
441
442                dumpJSONObject(arrayElem, indent + "  ");
443            }
444           
445            if (arrayContainsJSONObjects) {
446                System.out.println();
447                System.out.print(indent);
448            }
449           
450            System.out.print(']');
451        }
452        else if (object instanceof JSONObject) {
453            System.out.println(" {");
454           
455            @SuppressWarnings("unchecked")
456            Set<Map.Entry<?,?>> entrySet = ((JSONObject) object).entrySet();
457           
458            int index = 0;
459            for (Map.Entry<?,?> entry : entrySet) {
460                if (index++ > 0) {
461                    System.out.println(",");
462                }
463                System.out.print(indent);
464                System.out.print("  \"");
465                System.out.print(entry.getKey());
466                System.out.print("\":");
467                dumpJSONObject(entry.getValue(), indent + "  ");
468            }
469           
470            System.out.println();
471            System.out.print(indent);
472            System.out.print('}');
473        }
474        else {
475            System.out.print('"');
476            System.out.print(object);
477            System.out.print('"');
478        }
479    }
480
481}
Note: See TracBrowser for help on using the repository browser.