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

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