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

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