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

Last change on this file since 998 was 998, checked in by pharms, 12 years ago
  • improved logging
  • added support for further event types
File size: 23.1 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            InputStream requestInputStream = request.getInputStream();
129           
130            value = JSONValue.parseWithException
131                (new InputStreamReader(requestInputStream, "UTF-8"));
132           
133            if (!(value instanceof JSONObject)) {
134                Console.printerrln("incoming data is not of the expected type --> discarding it");
135            }
136            else {
137                handleJSONObject((JSONObject) value);
138            }
139        }
140        catch (ParseException e) {
141            Console.printerrln
142                ("could not parse incoming data --> discarding it (" + e.toString() + ")");
143        }
144    }
145
146    /**
147     * <p>
148     * processes a received JSON object and validates it. If the message is ok, it is forwarded
149     * to the message listener
150     * </p>
151     *
152     * @param object the JSON object that contains a client message
153     */
154    private void handleJSONObject(JSONObject object) {
155        if (DO_TRACE) {
156            dumpJSONObject(object, "");
157        }
158       
159        JSONObject message = assertValue(object, "message", JSONObject.class);
160       
161        if (message == null) {
162            Console.printerrln("incoming data is no valid message --> discarding it");
163        }
164        else {
165            HtmlClientInfos clientInfos = extractClientInfos(message);
166
167            if (clientInfos == null) {
168                Console.printerrln
169                    ("incoming message does not contain valid client infos --> discarding it");
170            }
171            else {
172                HtmlEvent[] events = extractHtmlEvents(message, clientInfos);
173                if (events == null) {
174                    Console.printerrln
175                        ("incoming message does not contain valid events --> discarding it");
176                }
177                else {
178                    messageListener.handleMessage(clientInfos, events);
179                }
180            }
181        }
182    }
183
184    /**
185     * <p>
186     * tries to extract the client infos out of the received JSON object. If this is not fully
187     * possible, an appropriate message is dumped and the whole message is discarded (the method
188     * return null).
189     * </p>
190     *
191     * @param message the message to parse the client infos from
192     *
193     * @return the client infos, if the message is valid in this respect, or null if not
194     */
195    private HtmlClientInfos extractClientInfos(JSONObject message) {
196        HtmlClientInfos clientInfos = null;
197       
198        JSONObject infos = assertValue(message, "clientInfos", JSONObject.class);
199       
200        if (infos != null) {
201            String clientId = assertValue((JSONObject) infos, "clientId", String.class);
202            String userAgent = assertValue((JSONObject) infos, "userAgent", String.class);
203            URL url = assertValue((JSONObject) infos, "url", URL.class);
204            String title = assertValue((JSONObject) infos, "title", String.class);
205           
206            if (clientId == null) {
207                Console.printerrln("client infos do not contain a valid client id");
208            }
209            else if (userAgent == null) {
210                Console.printerrln("client infos do not contain a valid user agent");
211            }
212            else if (url == null) {
213                Console.printerrln("client infos do not contain a valid URL");
214            }
215            else if (title == null) {
216                Console.printerrln("client infos do not contain a valid title");
217            }
218            else {
219                clientInfos = new HtmlClientInfos(clientId, userAgent, url, title);
220            }
221        }
222       
223        return clientInfos;
224    }
225
226    /**
227     * <p>
228     * tries to extract the events out of the received JSON object. If this is not fully
229     * possible, an appropriate message is dumped and the errorprone event is discarded. If no
230     * valid event is found, the whole message is discarded.
231     * </p>
232     *
233     * @param object      the message to parse the events from
234     * @param clientInfos the infos about the client that send the events
235     * 
236     * @return the valid events stored in the message, or null if there are none
237     */
238    private HtmlEvent[] extractHtmlEvents(JSONObject object, HtmlClientInfos clientInfos) {
239        List<HtmlEvent> events = null;
240       
241        JSONArray eventArray = assertValue(object, "events", JSONArray.class);
242       
243        if (eventArray != null) {
244            events = new ArrayList<HtmlEvent>();
245           
246            for (int i = 0; i < eventArray.size(); i++) {
247                Object eventObj = eventArray.get(i);
248                if (!(eventObj instanceof JSONObject)) {
249                    Console.printerrln("event number " + (i + 1) + " is not a valid event object");
250                }
251                else {
252                    Long time = assertValue(((JSONObject) eventObj), "time", Long.class);
253                    String path = assertValue(((JSONObject) eventObj), "path", String.class);
254                    String eventType =
255                        assertValue(((JSONObject) eventObj), "eventType", String.class);
256                    Integer[] coordinates =
257                        assertValue(((JSONObject) eventObj), "coordinates", Integer[].class);
258                    Integer key = assertValue(((JSONObject) eventObj), "key", Integer.class);
259                    Integer[] scrollPosition =
260                        assertValue(((JSONObject) eventObj), "scrollPosition", Integer[].class);
261                    String selectedValue =
262                            assertValue(((JSONObject) eventObj), "selectedValue", String.class);
263                   
264                    if (eventType == null) {
265                        Console.printerrln("event number " + (i + 1) + " has no valid event type");
266                    }
267                    else if (time == null) {
268                        Console.printerrln(eventType + " event has no valid timestamp");
269                    }
270                    else if (path == null) {
271                        Console.printerrln(eventType + " event has no valid path");
272                    }
273                    else if ((coordinates != null) && (coordinates.length != 2)) {
274                        Console.printerrln(eventType + " event has no valid coordinates");
275                    }
276                    else if (checkEventParameterCombinations
277                                (eventType, coordinates, key, scrollPosition, selectedValue))
278                    {
279                        events.add(new HtmlEvent(clientInfos, time, path, eventType, coordinates,
280                                                 key, scrollPosition, selectedValue));
281                    }
282                    else {
283                        Console.printerrln(eventType + " event has no valid parameter combination");
284                    }
285                }
286            }
287           
288        }
289       
290        if ((events != null) && (events.size() > 0)) {
291            return events.toArray(new HtmlEvent[events.size()]);
292        }
293        else {
294            return null;
295        }
296    }
297
298    /**
299     * <p>
300     * validates if for the given event type the parameter combination of coordinates, key,
301     * scroll position, and selected value is valid. As an example, an onclick event should
302     * usually not have an associated scroll position.
303     * </p>
304     *
305     * @param eventType      the type of the event
306     * @param coordinates    the coordinates of the event
307     * @param key            the key of the event
308     * @param scrollPosition the scroll position of the event
309     * @param selectedValue  the value selected through a specific event
310     *
311     * @return true, if the combination of the parameters is valid, false else
312     */
313    private boolean checkEventParameterCombinations(String    eventType,
314                                                    Integer[] coordinates,
315                                                    Integer   key,
316                                                    Integer[] scrollPosition,
317                                                    String    selectedValue)
318    {
319        boolean result = false;
320       
321        if ("onscroll".equals(eventType)) {
322            if ((coordinates == null) && (key == null) &&
323                (scrollPosition != null) && (selectedValue == null))
324            {
325                result = true;
326            }
327            else {
328                Console.printerrln(eventType + " event has invalid parameters");
329            }
330        }
331        else if ("onclick".equals(eventType) || "ondblclick".equals(eventType)) {
332            if ((coordinates != null) && (key == null) &&
333                (scrollPosition == null) && (selectedValue == null))
334            {
335                result = true;
336            }
337            else {
338                Console.printerrln(eventType + " event has invalid parameters");
339            }
340        }
341        else if ("onchange".equals(eventType)) {
342            if ((coordinates == null) && (key == null) &&
343                (scrollPosition == null) && (selectedValue != null))
344            {
345                result = true;
346            }
347            else {
348                Console.printerrln(eventType + " event has invalid parameters");
349            }
350        }
351        else if ("onkeypress".equals(eventType) || "onkeydown".equals(eventType) ||
352                 "onkeyup".equals(eventType))
353        {
354            if ((coordinates == null) && (key != null) &&
355                (scrollPosition == null) && (selectedValue == null))
356            {
357                result = true;
358            }
359            else {
360                Console.printerrln(eventType + " event has invalid parameters");
361            }
362        }
363        else if ("onfocus".equals(eventType) || "onmouseout".equals(eventType) ||
364                 "onmousemove".equals(eventType) || "onload".equals(eventType) ||
365                 "onunload".equals(eventType) || "onbeforeunload".equals(eventType) ||
366                 "onpagehide".equals(eventType) || "onpageshow".equals(eventType) ||
367                 "onabort".equals(eventType) || "onsubmit".equals(eventType) ||
368                 "onplaying".equals(eventType) || "onpause".equals(eventType) ||
369                 "ontimeupdate".equals(eventType) || "onerror".equals(eventType) ||
370                 "onundo".equals(eventType) || "onreset".equals(eventType) ||
371                 "onselect".equals(eventType))
372        {
373            if ((coordinates == null) && (key == null) &&
374                (scrollPosition == null) && (selectedValue == null))
375            {
376                result = true;
377            }
378            else {
379                Console.printerrln(eventType + " event has invalid parameters");
380            }
381        }
382        else {
383            Console.printerrln("'" + eventType + "' is not a valid event type");
384        }
385       
386        return result;
387    }
388
389    /**
390     * <p>
391     * converts a value in the provided object matching the provided key to the provided type. If
392     * there is no value with the key or if the value can not be transformed to the provided type,
393     * the method returns null.
394     * </p>
395     *
396     * @param object the object to read the value from
397     * @param key    the key of the value
398     * @param clazz  the type to which the value should be transformed
399     *
400     * @return the value or null if either the value does not exist or if it can not be transformed
401     *         to the expected type
402     */
403    @SuppressWarnings("unchecked")
404    private <T> T assertValue(JSONObject object, String key, Class<T> clazz) {
405        Object value = object.get(key);
406        T result = null;
407       
408        if (clazz.isInstance(value)) {
409            result = (T) value;
410        }
411        else if (value instanceof String) {
412            if (URL.class.equals(clazz)) {
413                try {
414                    result = (T) new URL((String) value);
415                }
416                catch (MalformedURLException e) {
417                    e.printStackTrace();
418                    Console.printerrln("retrieved malformed URL for key '" + key + "': " + value +
419                                       " (" + e.toString() + ")");
420                }
421            }
422            else if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) {
423                try {
424                    result = (T) Integer.valueOf(Integer.parseInt((String) value));
425                }
426                catch (NumberFormatException e) {
427                    Console.printerrln
428                        ("retrieved malformed integer for key '" + key + "': " + value);
429                }
430            }
431            else if ((long.class.equals(clazz)) || (Long.class.equals(clazz))) {
432                try {
433                    result = (T) Long.valueOf(Long.parseLong((String) value));
434                }
435                catch (NumberFormatException e) {
436                    Console.printerrln
437                        ("retrieved malformed long for key '" + key + "': " + value);
438                }
439            }
440        }
441        else if (value instanceof Long) {
442            if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) {
443                result = (T) (Integer) ((Long) value).intValue();
444            }
445        }
446        else if (value instanceof JSONArray) {
447            if ((int[].class.equals(clazz)) || (Integer[].class.equals(clazz))) {
448                Integer[] resultArray = new Integer[((JSONArray) value).size()];
449                boolean allCouldBeParsed = true;
450               
451                for (int i = 0; i < ((JSONArray) value).size(); i++) {
452                    try {
453                        if (((JSONArray) value).get(i) instanceof Long) {
454                            resultArray[i] = (int) (long) (Long) ((JSONArray) value).get(i);
455                        }
456                        else if (((JSONArray) value).get(i) instanceof String) {
457                            try {
458                                resultArray[i] =
459                                    (int) Long.parseLong((String) ((JSONArray) value).get(i));
460                            }
461                            catch (NumberFormatException e) {
462                                Console.printerrln
463                                    ("retrieved malformed integer array for key '" + key + "': " +
464                                     value);
465                       
466                                allCouldBeParsed = false;
467                                break;
468                            }
469                        }
470                        else {
471                            Console.printerrln
472                                ("can not handle type of value in expected integer array '" + key +
473                                 "': " + value);
474                        }
475                    }
476                    catch (ClassCastException e) {
477                        e.printStackTrace();
478                        Console.printerrln("expected integer array for key '" + key +
479                                           "' but it was something else: " + value);
480                       
481                        allCouldBeParsed = false;
482                        break;
483                    }
484                }
485               
486                if (allCouldBeParsed) {
487                    result = (T) resultArray;
488                }
489            }
490        }
491       
492        return result;
493    }
494
495    /**
496     * <p>
497     * convenience method for dumping the content of a stream and returning a new stream
498     * containing the same data.
499     * </p>
500     *
501     * @param inputStream the stream to be dumped and copied
502     * @return the copy of the stream
503     *
504     * @throws IOException if the stream can not be read
505     */
506    /*private InputStream dumpStreamContent(ServletInputStream inputStream) throws IOException {
507        List<Byte> bytes = new ArrayList<Byte>();
508        int buf;
509       
510        while ((buf = inputStream.read()) >= 0) {
511            bytes.add((byte) buf);
512        }
513       
514        byte[] byteArray = new byte[bytes.size()];
515        for (int i = 0; i < bytes.size(); i++) {
516            byteArray[i] = bytes.get(i);
517        }
518       
519        System.out.println(new String(byteArray, "UTF-8"));
520       
521        return new ByteArrayInputStream(byteArray);
522    }*/
523
524    /**
525     * <p>
526     * convenience method for dumping an object to std out. If the object is a JSON object, it is
527     * deeply analyzed and its internal structure is dumped as well.
528     * </p>
529     *
530     * @param object the object to dump
531     * @param indent the indentation to be used.
532     */
533    private void dumpJSONObject(Object object, String indent) {
534        if (object instanceof JSONArray) {
535            boolean arrayContainsJSONObjects = false;
536            for (Object arrayElem : (JSONArray) object) {
537                if (arrayElem instanceof JSONObject) {
538                    arrayContainsJSONObjects = true;
539                    break;
540                }               
541            }
542           
543            if (arrayContainsJSONObjects) {
544                System.out.println();
545                System.out.print(indent);
546                System.out.println('[');
547                System.out.print(indent);
548                System.out.print(' ');
549            }
550            else {
551                System.out.print(' ');
552                System.out.print('[');
553            }
554           
555            int index = 0;
556            for (Object arrayElem : (JSONArray) object) {
557                if (index++ > 0) {
558                    System.out.print(",");
559                    if (arrayContainsJSONObjects) {
560                        System.out.println();
561                        System.out.print(indent);
562                    }
563
564                    System.out.print(' ');
565                }
566
567                dumpJSONObject(arrayElem, indent + "  ");
568            }
569           
570            if (arrayContainsJSONObjects) {
571                System.out.println();
572                System.out.print(indent);
573            }
574           
575            System.out.print(']');
576        }
577        else if (object instanceof JSONObject) {
578            System.out.println(" {");
579           
580            @SuppressWarnings("unchecked")
581            Set<Map.Entry<?,?>> entrySet = ((JSONObject) object).entrySet();
582           
583            int index = 0;
584            for (Map.Entry<?,?> entry : entrySet) {
585                if (index++ > 0) {
586                    System.out.println(",");
587                }
588                System.out.print(indent);
589                System.out.print("  \"");
590                System.out.print(entry.getKey());
591                System.out.print("\":");
592                dumpJSONObject(entry.getValue(), indent + "  ");
593            }
594           
595            System.out.println();
596            System.out.print(indent);
597            System.out.print('}');
598        }
599        else {
600            System.out.print('"');
601            System.out.print(object);
602            System.out.print('"');
603        }
604    }
605
606}
Note: See TracBrowser for help on using the repository browser.