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

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