// Copyright 2012 Georg-August-Universität Göttingen, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package de.ugoe.cs.autoquest.genericeventmonitor; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.servlet.DefaultServlet; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.json.simple.parser.ParseException; import de.ugoe.cs.util.console.Console; /** *

* the servlet deployed in the web server that receives all generic events. The messages are parsed, * validated, and forwarded to the provided message listener. If a message is not valid, it is * discarded. If an event in a message is not valid, it is discarded. Messages are only received via * the POST HTTP method. *

* * @author Patrick Harms */ class GenericEventMonitorServlet extends DefaultServlet { /** */ private static final long serialVersionUID = 1L; /** must be true to do some detailed logging of what the server does */ private static final boolean DO_TRACE = false; /** * the map of event target ids to the concrete target objects. */ private Map eventTargets = new HashMap<>(); /** * the message listener to forward received messages to. */ private GenericEventMonitorMessageListener messageListener; /** *

* initializes the servlet with the message listener to which all events shall be forwarded *

* * @param messageListener the message listener that shall receive all events */ GenericEventMonitorServlet(GenericEventMonitorMessageListener messageListener) { this.messageListener = messageListener; } /** * this implements handling of doPost. For this servlet this means that * the data from the post request will be parsed and validated. * * (non-Javadoc) * @see org.mortbay.jetty.servlet.DefaultServlet#doPost(HttpServletRequest, HttpServletResponse) */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Object value = null; try { //InputStream requestInputStream = dumpStreamContent(request.getInputStream()); InputStream requestInputStream = request.getInputStream(); value = JSONValue.parseWithException (new InputStreamReader(requestInputStream, "UTF-8")); if (!(value instanceof JSONObject)) { Console.printerrln("incoming data is not of the expected type --> discarding it"); } else { handleJSONObject((JSONObject) value); } } catch (ParseException e) { Console.printerrln ("could not parse incoming data --> discarding it (" + e.toString() + ")"); } // this must be done to prevent firefox from notifying the "element not found" error response.setContentType("text/plain"); response.getOutputStream().write(' '); } /** *

* processes a received JSON object and validates it. If the message is ok, it is forwarded * to the message listener *

* * @param object the JSON object that contains a client message */ private void handleJSONObject(JSONObject object) { if (DO_TRACE) { dumpJSONObject(object, ""); } JSONObject message = assertValue(object, "message", JSONObject.class); if (message == null) { Console.printerrln("incoming data is no valid message --> discarding it"); } else { ClientInfos clientInfos = extractClientInfos(message); if (clientInfos == null) { Console.printerrln ("incoming message does not contain valid client infos --> discarding it"); } else { extractGenericEventTargets(message, clientInfos); GenericEvent[] events = extractGenericEvents(message, clientInfos); if (events == null) { Console.printerrln ("incoming message does not contain valid events --> discarding it"); } else { messageListener.handleEvents(clientInfos, events); Console.println ("handled message of " + clientInfos.getClientId() + " for app " + clientInfos.getAppId() + " (" + events.length + " events)"); } } } } /** *

* tries to extract the client infos out of the received JSON object. If this is not fully * possible, an appropriate message is dumped and the whole message is discarded (the method * return null). *

* * @param message the message to parse the client infos from * * @return the client infos, if the message is valid in this respect, or null if not */ private ClientInfos extractClientInfos(JSONObject message) { ClientInfos clientInfos = null; JSONObject infos = assertValue(message, "clientInfos", JSONObject.class); if (infos != null) { String clientId = assertValue((JSONObject) infos, "clientId", String.class); String appId = assertValue((JSONObject) infos, "appId", String.class); if (clientId == null) { Console.printerrln("client infos do not contain a valid client id"); } else if (appId == null) { Console.printerrln("client infos do not contain a valid application id"); } else { clientInfos = new ClientInfos(clientId, appId); } } return clientInfos; } /** *

* tries to extract the events out of the received JSON object. If this is not fully * possible, an appropriate message is dumped and the errorprone event is discarded. If no * valid event is found, the whole message is discarded. *

* * @param object the message to parse the events from * @param clientInfos the infos about the client that send the events * * @return the valid events stored in the message, or null if there are none */ private GenericEvent[] extractGenericEvents(JSONObject object, ClientInfos clientInfos) { List events = null; JSONArray eventArray = assertValue(object, "events", JSONArray.class); if (eventArray != null) { events = new ArrayList(); for (int i = 0; i < eventArray.size(); i++) { Object eventObj = eventArray.get(i); if (!(eventObj instanceof JSONObject)) { Console.printerrln("event number " + (i + 1) + " is not a valid event object"); } else { Long time = assertValue(((JSONObject) eventObj), "time", Long.class); String eventType = assertValue(((JSONObject) eventObj), "type", String.class); String eventTargetId = assertValue(((JSONObject) eventObj), "targetId", String.class); if (eventType == null) { Console.printerrln("event number " + (i + 1) + " has no valid event type"); } else if (eventTargetId == null) { Console.printerrln(eventType + " event has no valid target id"); } else if (time == null) { Console.printerrln(eventType + " event has no valid timestamp"); } else { GenericEventTarget target = eventTargets.get(eventTargetId); Map parameters = getParameters (((JSONObject) eventObj), "time", "type", "targetId"); if (target != null) { events.add(new GenericEvent(clientInfos, time, target, eventType, parameters)); } else { Console.printerrln(eventType + " event has no known target"); } } } } } if ((events != null) && (events.size() > 0)) { return events.toArray(new GenericEvent[events.size()]); } else { return null; } } /** *

* extracts the event target structure from the provided JSON object. *

* * @param object the JSON object to extract the event target structure from * @param clientInfos infos about the client who send the data * * @return the event target structure extracted from the JSON object of which the root nodes * are provided */ private GenericEventTarget[] extractGenericEventTargets(JSONObject object, ClientInfos clientInfos) { JSONObject[] jsonTargets = assertValue(object, "targetStructure", JSONObject[].class); if (jsonTargets != null) { GenericEventTarget[] result = new GenericEventTarget[jsonTargets.length]; for (int i = 0; i < jsonTargets.length; i++) { result[i] = convert(jsonTargets[i], null); } return result; } else { return null; } } /** *

* converts a JSON object representing a target to the generic target. Calls * itself recursively to also convert the children of the element, if any. *

* * @param jsonTarget the JSON object to be converted * @param parent the parent target of the converted element, or null, if none * is present. * * @return as described. */ private GenericEventTarget convert(JSONObject jsonTarget, GenericEventTarget parent) { GenericEventTarget result = null; if (jsonTarget != null) { String targetId = assertValue(jsonTarget, "targetId", String.class); Map parameters = getParameters(jsonTarget, "targetId", "children"); result = new GenericEventTarget(targetId, parameters, parent); JSONArray childElements = assertValue(jsonTarget, "children", JSONArray.class); if (childElements != null) { Object jsonChild; for (int i = 0; i < childElements.size(); i++) { jsonChild = childElements.get(i); if (!(jsonChild instanceof JSONObject)) { Console.printerrln("child " + (i + 1) + " of target " + targetId + " is no valid event target"); } else { result.addChild(convert((JSONObject) jsonChild, result)); } } } } if (result != null) { eventTargets.put(result.getId(), result); } return result; } /** *

* converts a value in the provided object matching the provided key to the provided type. If * there is no value with the key or if the value can not be transformed to the provided type, * the method returns null. *

* * @param object the object to read the value from * @param key the key of the value * @param clazz the type to which the value should be transformed * * @return the value or null if either the value does not exist or if it can not be transformed * to the expected type */ @SuppressWarnings("unchecked") private T assertValue(JSONObject object, String key, Class clazz) { Object value = object.get(key); T result = null; if (clazz.isInstance(value)) { result = (T) value; } else if (value instanceof String) { if (URL.class.equals(clazz)) { try { result = (T) new URL((String) value); } catch (MalformedURLException e) { e.printStackTrace(); Console.printerrln("retrieved malformed URL for key '" + key + "': " + value + " (" + e.toString() + ")"); } } else if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) { try { result = (T) Integer.valueOf(Integer.parseInt((String) value)); } catch (NumberFormatException e) { Console.printerrln ("retrieved malformed integer for key '" + key + "': " + value); } } else if ((long.class.equals(clazz)) || (Long.class.equals(clazz))) { try { result = (T) Long.valueOf(Long.parseLong((String) value)); } catch (NumberFormatException e) { Console.printerrln ("retrieved malformed long for key '" + key + "': " + value); } } } else if (value instanceof Long) { if ((int.class.equals(clazz)) || (Integer.class.equals(clazz))) { result = (T) (Integer) ((Long) value).intValue(); } } else if (value instanceof JSONArray) { if ((int[].class.equals(clazz)) || (Integer[].class.equals(clazz))) { Integer[] resultArray = new Integer[((JSONArray) value).size()]; boolean allCouldBeParsed = true; for (int i = 0; i < ((JSONArray) value).size(); i++) { try { if (((JSONArray) value).get(i) instanceof Long) { resultArray[i] = (int) (long) (Long) ((JSONArray) value).get(i); } else if (((JSONArray) value).get(i) instanceof String) { try { resultArray[i] = (int) Long.parseLong((String) ((JSONArray) value).get(i)); } catch (NumberFormatException e) { Console.printerrln ("retrieved malformed integer array for key '" + key + "': " + value); allCouldBeParsed = false; break; } } else { Console.printerrln ("can not handle type of value in expected integer array '" + key + "': " + value); } } catch (ClassCastException e) { e.printStackTrace(); Console.printerrln("expected integer array for key '" + key + "' but it was something else: " + value); allCouldBeParsed = false; break; } } if (allCouldBeParsed) { result = (T) resultArray; } } else if (JSONObject[].class.equals(clazz)) { JSONObject[] resultArray = new JSONObject[((JSONArray) value).size()]; boolean allCouldBeParsed = true; for (int i = 0; i < ((JSONArray) value).size(); i++) { try { if (((JSONArray) value).get(i) instanceof JSONObject) { resultArray[i] = (JSONObject) ((JSONArray) value).get(i); } } catch (ClassCastException e) { e.printStackTrace(); Console.printerrln("expected JSON Object array for key '" + key + "' but it was something else: " + value); allCouldBeParsed = false; break; } } if (allCouldBeParsed) { result = (T) resultArray; } } } return result; } /** *

* determines other parameters provided in the JSON object. The parameters with the provided * keys to ignore are not put into the resulting map. *

* * @param object the object, from which parameters shall be read * @param ignoredKeys the parameters not to be put into the resulting map * * @return a map with the parameters contained in the provided JSON object */ private Map getParameters(JSONObject object, String ... ignoredKeys) { Map result = new HashMap<>(); NEXT_KEY: for (Object key : object.keySet()) { if (!(key instanceof String)) { continue NEXT_KEY; } for (String ignoredKey : ignoredKeys) { if (ignoredKey.equals(key)) { continue NEXT_KEY; } } String value = assertValue(object, (String) key, String.class); result.put((String) key, value); } return result; } /** *

* convenience method for dumping the content of a stream and returning a new stream * containing the same data. *

* * @param inputStream the stream to be dumped and copied * @return the copy of the stream * * @throws IOException if the stream can not be read */ /* private InputStream dumpStreamContent(ServletInputStream inputStream) throws IOException { List bytes = new ArrayList(); int buf; while ((buf = inputStream.read()) >= 0) { bytes.add((byte) buf); } byte[] byteArray = new byte[bytes.size()]; for (int i = 0; i < bytes.size(); i++) { byteArray[i] = bytes.get(i); } System.out.println(new String(byteArray, "UTF-8")); return new ByteArrayInputStream(byteArray); }*/ /** *

* convenience method for dumping an object to std out. If the object is a JSON object, it is * deeply analyzed and its internal structure is dumped as well. *

* * @param object the object to dump * @param indent the indentation to be used. */ private void dumpJSONObject(Object object, String indent) { if (object instanceof JSONArray) { boolean arrayContainsJSONObjects = false; for (Object arrayElem : (JSONArray) object) { if (arrayElem instanceof JSONObject) { arrayContainsJSONObjects = true; break; } } if (arrayContainsJSONObjects) { System.out.println(); System.out.print(indent); System.out.println('['); System.out.print(indent); System.out.print(' '); } else { System.out.print(' '); System.out.print('['); } int index = 0; for (Object arrayElem : (JSONArray) object) { if (index++ > 0) { System.out.print(","); if (arrayContainsJSONObjects) { System.out.println(); System.out.print(indent); } System.out.print(' '); } dumpJSONObject(arrayElem, indent + " "); } if (arrayContainsJSONObjects) { System.out.println(); System.out.print(indent); } System.out.print(']'); } else if (object instanceof JSONObject) { System.out.println(" {"); @SuppressWarnings("unchecked") Set> entrySet = ((JSONObject) object).entrySet(); int index = 0; for (Map.Entry entry : entrySet) { if (index++ > 0) { System.out.println(","); } System.out.print(indent); System.out.print(" \""); System.out.print(entry.getKey()); System.out.print("\":"); dumpJSONObject(entry.getValue(), indent + " "); } System.out.println(); System.out.print(indent); System.out.print('}'); } else { System.out.print('"'); System.out.print(object); System.out.print('"'); } } }