source: trunk/autoquest-generic-event-monitor/src/main/java/de/ugoe/cs/autoquest/genericeventmonitor/GenericEventMonitorServlet.java @ 2163

Last change on this file since 2163 was 2163, checked in by pharms, 7 years ago
  • changes for first VR oriented usability evaluation
  • Property svn:mime-type set to text/plain
File size: 22.5 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.genericeventmonitor;
16
17import java.io.IOException;
18import java.io.InputStream;
19import java.io.InputStreamReader;
20import java.net.MalformedURLException;
21import java.net.URL;
22import java.util.ArrayList;
23import java.util.HashMap;
24import java.util.List;
25import java.util.Map;
26import java.util.Set;
27
28import javax.servlet.ServletException;
29import javax.servlet.http.HttpServletRequest;
30import javax.servlet.http.HttpServletResponse;
31
32import org.eclipse.jetty.servlet.DefaultServlet;
33import org.json.simple.JSONArray;
34import org.json.simple.JSONObject;
35import org.json.simple.JSONValue;
36import org.json.simple.parser.ParseException;
37
38import de.ugoe.cs.util.console.Console;
39
40/**
41 * <p>
42 * the servlet deployed in the web server that receives all generic events. The messages are parsed,
43 * validated, and forwarded to the provided message listener. If a message is not valid, it is
44 * discarded. If an event in a message is not valid, it is discarded. Messages are only received via
45 * the POST HTTP method.
46 * </p>
47 *
48 * @author Patrick Harms
49 */
50class GenericEventMonitorServlet extends DefaultServlet {
51
52    /**  */
53    private static final long serialVersionUID = 1L;
54   
55    /** must be true to do some detailed logging of what the server does */
56    private static final boolean DO_TRACE = false;
57
58    /**
59     * the map of event target ids to the concrete target objects.
60     */
61    private transient Map<String, GenericEventTarget> eventTargets = new HashMap<>();
62
63    /**
64     * the message listener to forward received messages to.
65     */
66    private transient GenericEventMonitorMessageListener messageListener;
67
68    /**
69     * <p>
70     * initializes the servlet with the message listener to which all events shall be forwarded
71     * </p>
72     *
73     * @param messageListener the message listener that shall receive all events
74     */
75    GenericEventMonitorServlet(GenericEventMonitorMessageListener messageListener) {
76        this.messageListener = messageListener;
77    }
78
79    /**
80     * this implements handling of doPost. For this servlet this means that
81     * the data from the post request will be parsed and validated.
82     *
83     * (non-Javadoc)
84     * @see org.mortbay.jetty.servlet.DefaultServlet#doPost(HttpServletRequest, HttpServletResponse)
85     */
86    @Override
87    protected void doPost(HttpServletRequest request, HttpServletResponse response)
88        throws ServletException, IOException
89    {
90        Object value = null;
91        try {
92            //InputStream requestInputStream = dumpStreamContent(request.getInputStream());
93            InputStream requestInputStream = request.getInputStream();
94
95            value = JSONValue.parseWithException
96                (new InputStreamReader(requestInputStream, "UTF-8"));
97           
98            if (!(value instanceof JSONObject)) {
99                Console.printerrln("incoming data is not of the expected type --> discarding it");
100            }
101            else {
102                handleJSONObject((JSONObject) value);
103            }
104        }
105        catch (ParseException e) {
106            Console.printerrln
107                ("could not parse incoming data --> discarding it (" + e.toString() + ")");
108        }
109       
110        // this must be done to prevent firefox from notifying the "element not found" error
111        response.setContentType("text/plain");
112        response.getOutputStream().write(' ');
113    }
114
115    /**
116     * <p>
117     * processes a received JSON object and validates it. If the message is ok, it is forwarded
118     * to the message listener
119     * </p>
120     *
121     * @param object the JSON object that contains a client message
122     */
123    private void handleJSONObject(JSONObject object) {
124        if (DO_TRACE) {
125            dumpJSONObject(object, "");
126        }
127       
128        JSONObject message = assertValue(object, "message", JSONObject.class);
129       
130        if (message == null) {
131            Console.printerrln("incoming data is no valid message --> discarding it");
132        }
133        else {
134            ClientInfos clientInfos = extractClientInfos(message);
135
136            if (clientInfos == null) {
137                Console.printerrln
138                    ("incoming message does not contain valid client infos --> discarding it");
139            }
140            else {
141                extractGenericEventTargets(message, clientInfos);
142                GenericEvent[] events = extractGenericEvents(message, clientInfos);
143               
144                if (events == null) {
145                    Console.printerrln
146                        ("incoming message does not contain valid events --> discarding it");
147                }
148                else {
149                    messageListener.handleEvents(clientInfos, events);
150                    Console.println
151                        ("handled message of " + clientInfos.getClientId() + " for app " +
152                         clientInfos.getAppId() + " (" + events.length + " events)");
153                }
154            }
155        }
156    }
157
158    /**
159     * <p>
160     * tries to extract the client infos out of the received JSON object. If this is not fully
161     * possible, an appropriate message is dumped and the whole message is discarded (the method
162     * return null).
163     * </p>
164     *
165     * @param message the message to parse the client infos from
166     *
167     * @return the client infos, if the message is valid in this respect, or null if not
168     */
169    private ClientInfos extractClientInfos(JSONObject message) {
170        ClientInfos clientInfos = null;
171       
172        JSONObject infos = assertValue(message, "clientInfos", JSONObject.class);
173       
174        if (infos != null) {
175            String clientId = assertValue((JSONObject) infos, "clientId", String.class);
176            String appId = assertValue((JSONObject) infos, "appId", String.class);
177           
178            if (clientId == null) {
179                Console.printerrln("client infos do not contain a valid client id");
180            }
181            else if (appId == null) {
182                Console.printerrln("client infos do not contain a valid application id");
183            }
184            else {
185                clientInfos = new ClientInfos(clientId, appId);
186            }
187        }
188       
189        return clientInfos;
190    }
191
192    /**
193     * <p>
194     * tries to extract the events out of the received JSON object. If this is not fully
195     * possible, an appropriate message is dumped and the errorprone event is discarded. If no
196     * valid event is found, the whole message is discarded.
197     * </p>
198     *
199     * @param object      the message to parse the events from
200     * @param clientInfos the infos about the client that send the events
201     * 
202     * @return the valid events stored in the message, or null if there are none
203     */
204    private GenericEvent[] extractGenericEvents(JSONObject object, ClientInfos clientInfos) {
205        List<GenericEvent> events = null;
206       
207        JSONArray eventArray = assertValue(object, "events", JSONArray.class);
208       
209        if (eventArray != null) {
210            events = new ArrayList<GenericEvent>();
211           
212            for (int i = 0; i < eventArray.size(); i++) {
213                Object eventObj = eventArray.get(i);
214                if (!(eventObj instanceof JSONObject)) {
215                    Console.printerrln("event number " + (i + 1) + " is not a valid event object");
216                }
217                else {
218                    Long time = assertValue(((JSONObject) eventObj), "time", Long.class);
219                    String eventType =
220                        assertValue(((JSONObject) eventObj), "type", String.class);
221                    String eventTargetId =
222                        assertValue(((JSONObject) eventObj), "targetId", String.class);
223                   
224                    if (eventType == null) {
225                        Console.printerrln("event number " + (i + 1) + " has no valid event type");
226                    }
227                    else if (eventTargetId == null) {
228                        Console.printerrln(eventType + " event has no valid target id");
229                    }
230                    else if (time == null) {
231                        Console.printerrln(eventType + " event has no valid timestamp");
232                    }
233                    else {
234                        GenericEventTarget target = eventTargets.get(eventTargetId);
235                        Map<String, String> parameters = getParameters
236                            (((JSONObject) eventObj), "time", "type", "targetId");
237                       
238                        if (target != null) {
239                            events.add(new GenericEvent(clientInfos, time, target, eventType,
240                                                        parameters));
241                        }
242                        else {
243                            Console.printerrln(eventType + " event has no known target");
244                        }
245                    }
246                }
247            }
248           
249        }
250       
251        if ((events != null) && (events.size() > 0)) {
252            return events.toArray(new GenericEvent[events.size()]);
253        }
254        else {
255            return null;
256        }
257    }
258
259    /**
260     * <p>
261     * extracts the event target structure from the provided JSON object.
262     * </p>
263     *
264     * @param object      the JSON object to extract the event target structure from
265     * @param clientInfos infos about the client who send the data
266     *
267     * @return the event target structure extracted from the JSON object of which the root nodes
268     *         are provided
269     */
270    private GenericEventTarget[] extractGenericEventTargets(JSONObject  object,
271                                                            ClientInfos clientInfos)
272    {
273        JSONObject[] jsonTargets = assertValue(object, "targetStructure", JSONObject[].class);
274       
275        if (jsonTargets != null) {
276            GenericEventTarget[] result = new GenericEventTarget[jsonTargets.length];
277
278            for (int i = 0; i < jsonTargets.length; i++) {
279                result[i] = convert(jsonTargets[i], null);
280            }
281
282            return result;
283        }
284        else {
285            return null;
286        }
287    }
288
289    /**
290     * <p>
291     * converts a JSON object representing a target to the generic target. Calls
292     * itself recursively to also convert the children of the element, if any.
293     * </p>
294     *
295     * @param jsonTarget      the JSON object to be converted
296     * @param parent          the parent target of the converted element, or null, if none
297     *                        is present.
298     *                       
299     * @return as described.
300     */
301    private GenericEventTarget convert(JSONObject         jsonTarget,
302                                       GenericEventTarget parent)
303    {
304        GenericEventTarget result = null;
305
306        if (jsonTarget != null) {
307            String targetId = assertValue(jsonTarget, "targetId", String.class);
308            Map<String, String> parameters = getParameters(jsonTarget, "targetId", "children");
309
310            result = new GenericEventTarget(targetId, parameters, parent);
311
312            JSONArray childElements = assertValue(jsonTarget, "children", JSONArray.class);
313           
314            if (childElements != null) {
315                Object jsonChild;
316
317                for (int i = 0; i < childElements.size(); i++) {
318                    jsonChild = childElements.get(i);
319                    if (!(jsonChild instanceof JSONObject)) {
320                        Console.printerrln("child " + (i + 1) + " of target " + targetId +
321                                           " is no valid event target");
322                    }
323                    else {
324                        result.addChild(convert((JSONObject) jsonChild, result));
325                    }
326                }
327            }
328           
329        }
330       
331        if (result != null) {
332            eventTargets.put(result.getId(), result);
333        }
334       
335        return result;   
336    }
337
338    /**
339     * <p>
340     * converts a value in the provided object matching the provided key to the provided type. If
341     * there is no value with the key or if the value can not be transformed to the provided type,
342     * the method returns null.
343     * </p>
344     *
345     * @param object the object to read the value from
346     * @param key    the key of the value
347     * @param clazz  the type to which the value should be transformed
348     *
349     * @return the value or null if either the value does not exist or if it can not be transformed
350     *         to the expected type
351     */
352    @SuppressWarnings("unchecked")
353    private <T> T assertValue(JSONObject object, String key, Class<T> clazz) {
354        Object value = object.get(key);
355        T result = null;
356       
357        if (clazz.isInstance(value)) {
358            result = (T) value;
359        }
360        else if (value instanceof String) {
361            if (URL.class.equals(clazz)) {
362                try {
363                    result = (T) new URL((String) value);
364                }
365                catch (MalformedURLException e) {
366                    e.printStackTrace();
367                    Console.printerrln("retrieved malformed URL for key '" + key + "': " + value +
368                                       " (" + e.toString() + ")");
369                }
370            }
371            else if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) {
372                try {
373                    result = (T) Integer.valueOf(Integer.parseInt((String) value));
374                }
375                catch (NumberFormatException e) {
376                    Console.printerrln
377                        ("retrieved malformed integer for key '" + key + "': " + value);
378                }
379            }
380            else if ((long.class.equals(clazz)) || (Long.class.equals(clazz))) {
381                try {
382                    result = (T) Long.valueOf(Long.parseLong((String) value));
383                }
384                catch (NumberFormatException e) {
385                    Console.printerrln
386                        ("retrieved malformed long for key '" + key + "': " + value);
387                }
388            }
389        }
390        else if (value instanceof Long) {
391            if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) {
392                result = (T) (Integer) ((Long) value).intValue();
393            }
394        }
395        else if (value instanceof JSONArray) {
396            if ((int[].class.equals(clazz)) || (Integer[].class.equals(clazz))) {
397                Integer[] resultArray = new Integer[((JSONArray) value).size()];
398                boolean allCouldBeParsed = true;
399               
400                for (int i = 0; i < ((JSONArray) value).size(); i++) {
401                    try {
402                        if (((JSONArray) value).get(i) instanceof Long) {
403                            resultArray[i] = (int) (long) (Long) ((JSONArray) value).get(i);
404                        }
405                        else if (((JSONArray) value).get(i) instanceof String) {
406                            try {
407                                resultArray[i] =
408                                    (int) Long.parseLong((String) ((JSONArray) value).get(i));
409                            }
410                            catch (NumberFormatException e) {
411                                Console.printerrln
412                                    ("retrieved malformed integer array for key '" + key + "': " +
413                                     value);
414                       
415                                allCouldBeParsed = false;
416                                break;
417                            }
418                        }
419                        else {
420                            Console.printerrln
421                                ("can not handle type of value in expected integer array '" + key +
422                                 "': " + value);
423                        }
424                    }
425                    catch (ClassCastException e) {
426                        e.printStackTrace();
427                        Console.printerrln("expected integer array for key '" + key +
428                                           "' but it was something else: " + value);
429                       
430                        allCouldBeParsed = false;
431                        break;
432                    }
433                }
434               
435                if (allCouldBeParsed) {
436                    result = (T) resultArray;
437                }
438            }
439            else if (JSONObject[].class.equals(clazz)) {
440                JSONObject[] resultArray = new JSONObject[((JSONArray) value).size()];
441                boolean allCouldBeParsed = true;
442               
443                for (int i = 0; i < ((JSONArray) value).size(); i++) {
444                    try {
445                        if (((JSONArray) value).get(i) instanceof JSONObject) {
446                            resultArray[i] = (JSONObject) ((JSONArray) value).get(i);
447                        }
448                    }
449                    catch (ClassCastException e) {
450                        e.printStackTrace();
451                        Console.printerrln("expected JSON Object array for key '" + key +
452                                           "' but it was something else: " + value);
453                       
454                        allCouldBeParsed = false;
455                        break;
456                    }
457                }
458               
459                if (allCouldBeParsed) {
460                    result = (T) resultArray;
461                }
462            }
463        }
464       
465        return result;
466    }
467
468    /**
469     * <p>
470     * determines other parameters provided in the JSON object. The parameters with the provided
471     * keys to ignore are not put into the resulting map.
472     * </p>
473     *
474     * @param object      the object, from which parameters shall be read
475     * @param ignoredKeys the parameters not to be put into the resulting map
476     *
477     * @return a map with the parameters contained in the provided JSON object
478     */
479    private Map<String, String> getParameters(JSONObject object, String ... ignoredKeys) {
480        Map<String, String> result = new HashMap<>();
481       
482        NEXT_KEY:
483        for (Object key : object.keySet()) {
484            if (!(key instanceof String)) {
485                continue NEXT_KEY;
486            }
487           
488            for (String ignoredKey : ignoredKeys) {
489                if (ignoredKey.equals(key)) {
490                    continue NEXT_KEY;
491                }
492            }
493           
494            String value = assertValue(object, (String) key, String.class);
495           
496            result.put((String) key, value);
497        }
498       
499        return result;
500    }
501
502    /**
503     * <p>
504     * convenience method for dumping the content of a stream and returning a new stream
505     * containing the same data.
506     * </p>
507     *
508     * @param inputStream the stream to be dumped and copied
509     * @return the copy of the stream
510     *
511     * @throws IOException if the stream can not be read
512     */
513/*    private InputStream dumpStreamContent(ServletInputStream inputStream) throws IOException {
514        List<Byte> bytes = new ArrayList<Byte>();
515        int buf;
516       
517        while ((buf = inputStream.read()) >= 0) {
518            bytes.add((byte) buf);
519        }
520       
521        byte[] byteArray = new byte[bytes.size()];
522        for (int i = 0; i < bytes.size(); i++) {
523            byteArray[i] = bytes.get(i);
524        }
525       
526        System.out.println(new String(byteArray, "UTF-8"));
527       
528        return new ByteArrayInputStream(byteArray);
529    }*/
530
531    /**
532     * <p>
533     * convenience method for dumping an object to std out. If the object is a JSON object, it is
534     * deeply analyzed and its internal structure is dumped as well.
535     * </p>
536     *
537     * @param object the object to dump
538     * @param indent the indentation to be used.
539     */
540    private void dumpJSONObject(Object object, String indent) {
541        if (object instanceof JSONArray) {
542            boolean arrayContainsJSONObjects = false;
543            for (Object arrayElem : (JSONArray) object) {
544                if (arrayElem instanceof JSONObject) {
545                    arrayContainsJSONObjects = true;
546                    break;
547                }               
548            }
549           
550            if (arrayContainsJSONObjects) {
551                System.out.println();
552                System.out.print(indent);
553                System.out.println('[');
554                System.out.print(indent);
555                System.out.print(' ');
556            }
557            else {
558                System.out.print(' ');
559                System.out.print('[');
560            }
561           
562            int index = 0;
563            for (Object arrayElem : (JSONArray) object) {
564                if (index++ > 0) {
565                    System.out.print(",");
566                    if (arrayContainsJSONObjects) {
567                        System.out.println();
568                        System.out.print(indent);
569                    }
570
571                    System.out.print(' ');
572                }
573
574                dumpJSONObject(arrayElem, indent + "  ");
575            }
576           
577            if (arrayContainsJSONObjects) {
578                System.out.println();
579                System.out.print(indent);
580            }
581           
582            System.out.print(']');
583        }
584        else if (object instanceof JSONObject) {
585            System.out.println(" {");
586           
587            @SuppressWarnings("unchecked")
588            Set<Map.Entry<?,?>> entrySet = ((JSONObject) object).entrySet();
589           
590            int index = 0;
591            for (Map.Entry<?,?> entry : entrySet) {
592                if (index++ > 0) {
593                    System.out.println(",");
594                }
595                System.out.print(indent);
596                System.out.print("  \"");
597                System.out.print(entry.getKey());
598                System.out.print("\":");
599                dumpJSONObject(entry.getValue(), indent + "  ");
600            }
601           
602            System.out.println();
603            System.out.print(indent);
604            System.out.print('}');
605        }
606        else {
607            System.out.print('"');
608            System.out.print(object);
609            System.out.print('"');
610        }
611    }
612
613}
Note: See TracBrowser for help on using the repository browser.