Index: /trunk/autoquest-htmlmonitor/src/main/js/autoquest-htmlmonitor.js
===================================================================
--- /trunk/autoquest-htmlmonitor/src/main/js/autoquest-htmlmonitor.js	(revision 858)
+++ /trunk/autoquest-htmlmonitor/src/main/js/autoquest-htmlmonitor.js	(revision 858)
@@ -0,0 +1,460 @@
+/**
+ * AutoQUEST - HTML Monitor
+ * 
+ * Description: This script records the interactions done by an user on an
+ * HTML-Website and sends them to a server. It does not register actions on
+ * Flash, Java, or other special inputs. This script is tested on Firefox
+ * 15.0.1.
+ * 
+ * To insert it on your HTML-side, you need to write <script
+ * language="JavaScript" type="text/javascript" src="autoquest-htmlmonitor.js"></script> in the
+ * head and change the src-attribute to the location, you have chosen for this
+ * script.
+ * 
+ * To change the recorded events, edit the action config array. If you want to change
+ * the server where the data is send to, rewrite the destination variable. The
+ * records are send to the server, JSON-formatted, if there are 10 inputs or the
+ * user changes/closes the site.
+ * 
+ * Authors: Simon Leidenbach, Simon Reuss, Patrick Harms
+ * 
+ * Version: 0.1
+ */
+
+/**
+ * the server to send the recorded data to
+ */
+var destination = "http://patrick-prog-VM:8090"; // change to the location of your server
+
+/**
+ * the maximum number of recorded events to be put into one package sent to the server
+ */
+var packageSize = 10;
+
+/**
+ * this variable defines the tags for which event handling shall be added, as well as the
+ * event handling action to be monitored
+ */
+var actionConfig = [
+    { "tag": "body", "actions": [ "onunload", "onscroll" ] },
+    { "tag": "a", "actions": [ "onclick", "ondblclick", "onfocus" ] },
+    { "tag": "input", "actions": [ "onclick", "ondblclick", "onfocus" ] }
+];
+
+/**
+ * a possibility to trace, what is going on
+ */
+var doLog = false;
+
+/*var matchedTags = ["A", "ABBR", "ACRONYM", "ADDRESS", "AREA", "B", "BIG", "BLOCKQUOTE", "BODY",
+                   "BUTTON", "CAPTION", "CENTER", "CITE", "CODE", "COL", "COLGROUP", "DD", "DEL",
+                   "DFN", "DIR", "DIV", "DL", "DT", "EM", "FIELDSET", "FORM", "H1", "H2", "H3",
+                   "H4", "H5", "H6", "HR", "I", "IMG", "INPUT", "INS", "KBD", "LABEL", "LEGEND",
+                   "LI", "LINK", "MAP", "MENU", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL",
+                   "OPTGROUP", "OPTION", "P", "PRE", "Q", "S", "SAMP", "SELECT", "SMALL", "SPAN",
+                   "STRIKE", "STRONG", "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT",
+                   "TH", "THEAD", "TR", "TT", "U", "UL", "VAR"];*/
+/*var actions = ['onclick', 'ondblclick', 'onkeypress', 'onkeydown', 'onkeyup',
+'onmouseout' , 'onmousemove' ,'onfocus','onscroll'];  // edit*/
+
+
+var recordedEvents = [];
+
+/**
+ * is automatically executed to initialize the event handling
+ */
+(function() {
+    initEventHandling();
+}());
+
+
+/**
+ * initializes the event handling after the document is loaded completely
+ */
+function initEventHandling() {
+    if (document.body) {
+        log("adding event handling attributes");
+        addEventHandlingAttributes(document.documentElement, "");
+        
+        if (document.readyState !== "complete") {
+            // if the document is not loaded yet, try to add further event handling later
+            setTimeout(initEventHandling, 200);
+        }
+    }
+    else {
+        setTimeout(initEventHandling, 200);
+    }         
+}
+
+/**
+ * traverses the DOM-structure of the HTML-side, and adds event handling attributes to each
+ * relevant node
+ * 
+ * @param node the node of the DOM structure that shall be adapted and whose children shall be
+ *             traversed
+ */
+function addEventHandlingAttributes(node, parentPath) {
+    var nodePath = getNodePath(node, parentPath);
+    var i;
+    var k;
+    var value;
+    
+    if (node.nodeType === Node.ELEMENT_NODE) {
+        for (i = 0; i < actionConfig.length; i++) {
+            if (node.tagName.toLowerCase() === actionConfig[i].tag.toLowerCase()) {
+                for (k = 0; k < actionConfig[i].actions.length; k++) {
+                    value = "handleEvent('" + actionConfig[i].actions[k] + "', '" + nodePath +
+                        "', event);";
+
+                    if (!node.getAttribute(actionConfig[i].actions[k])) {
+                        node.setAttribute(actionConfig[i].actions[k], value);
+                    }
+                    else {
+                        var oldValue = node.getAttribute(actionConfig[i].actions[k]);
+                        if (oldValue.indexOf(value) < 0) {
+                            node.setAttribute(actionConfig[i].actions[k], value + ' ' + oldValue);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    for (i = 0; i < node.childNodes.length; i++) {
+        addEventHandlingAttributes(node.childNodes[i], nodePath);
+    }
+}
+
+/**
+ * 
+ */
+function getNodePath(node, parentPath) {
+    var nodePath = parentPath + "/";
+    var index = -1;
+    var i = 0;
+    
+    if (node.nodeType === Node.ELEMENT_NODE) {
+        nodePath += node.tagName.toLowerCase();
+    }
+    else {
+        nodePath += "undefined";
+    }
+    
+    if ((node.id) && (node.id !== "")) {
+        nodePath += "(id=" + node.id + ")";
+    }
+    else {
+        
+        if (node.parentNode) {
+            for (i = 0; i < node.parentNode.childNodes.length; i++) {
+                if (node.parentNode.childNodes[i].tagName === node.tagName) {
+                    index++;
+                    // if === also returns true if the nodes are not identical but only equal,
+                    // this may fail.
+                    if (node.parentNode.childNodes[i] === node) {
+                        break;
+                    }
+                }
+            }
+            
+        }
+        else {
+            index = 0;
+        }
+        
+        nodePath += "[" + index + "]";
+    }
+    
+    return nodePath;
+}
+
+/**
+ * handles an event that happened on a node
+ * 
+ * @param node
+ * @returns {Boolean}
+ */
+function handleEvent(eventName, nodePath, event) {
+    var eventType = eventName.toLowerCase();
+    
+    var eventObj = recordedEvents.pop();
+    
+    // reuse previous on scroll events to prevent too many events
+    if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) {
+        if (eventObj) {
+            recordedEvents.push(eventObj);
+        }
+        eventObj = new Event(eventType, nodePath);
+    }
+    
+    if ((eventType === "onclick") || (eventType === "ondblclick")) {
+        eventObj.setClickCoordinates(getEventCoordinates(event));
+    }
+
+    if ((eventType === "onkeypress") || (eventType === "onkeydown") || (eventType === "onkeyup")) {
+        eventObj.setKey(event.keyCode);
+    }
+    
+    if (eventType === "onscroll") {
+        if (window.pageYOffset) {
+            eventObj.setScrollPosition(window.pageYOffset);
+        }
+    }
+
+    recordedEvents.push(eventObj);
+
+    if ((recordedEvents.length >= packageSize) || (eventType === "onunload")) {
+        setTimeout(sendRequest(), 100);
+    }
+
+}
+
+/**
+ * sends the collected data to the server, named in the destination-variable
+ */
+function sendRequest() {
+    var eventList;
+    var message;
+    var clientId;
+    var i = 0;
+    var request;
+    
+    if (recordedEvents.length > 0) {
+        eventList = recordedEvents;
+        recordedEvents = [];
+        
+        message = "{\"message\":{\"clientInfos\":{";
+        
+        log("reading client id");
+        clientId = readClientId();
+        if ((clientId) && (clientId !== "")) {
+            message += "\"clientId\":\"" + clientId + "\",";
+        }
+        
+        log("adding other infos");
+        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
+        message += "\"title\":\"" + document.title + "\",";
+        message += "\"url\":\"" + document.URL + "\"},";
+        
+        
+        message += "\"events\":[";
+        
+        for (i = 0; i < eventList.length; i++) {
+            if (i > 0) {
+                message += ",";
+            }
+            message += eventList[i].toJSON();
+        }
+        
+        message += "]}}";
+        
+        request = null;
+        
+        // Mozilla
+        if (window.XMLHttpRequest) {
+            request = new XMLHttpRequest();
+        }
+        // IE
+        else if (window.ActiveXObject) {
+            request = new ActiveXObject("Microsoft.XMLHTTP");
+        }
+        
+        request.open("POST", destination, true);
+
+        log("sending " + message);
+        request.send(message);
+    }
+}
+
+/**
+ * determines the coordinates of an onclick or ondblclick event
+ */
+function getEventCoordinates(event) {
+    if (event.layerX) {
+        return [event.layerX, event.layerY];
+    }
+    else if (event.offsetX) {
+        return [event.offsetX, event.offsetY];
+    }
+
+    var obj = event.target || event.srcElement;
+    var objOffset = getPageOffset(obj);
+
+    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
+}
+
+/**
+ * determines the page offset of an object using the parent objects offset
+ */
+function getPageOffset(object) {
+    var top = 0;
+    var left = 0;
+    var obj = object;
+
+    while (obj.offsetParent) {
+        left += obj.offsetLeft;
+        top += obj.offsetTop;
+        obj = obj.offsetParent;
+    }
+
+    return [left, top];
+}
+
+/**
+ * 
+ */
+function readClientId() {
+    var cookie = document.cookie;
+    
+    var expectedCookieName = getClientIdCookieName();
+    
+    var cookiename = null;
+    var startIndex = 0;
+    
+    var clientId = null;
+
+    do {
+        cookie = cookie.substring(startIndex, cookie.length);
+        cookiename = cookie.substr(0, cookie.search('='));
+        startIndex = cookie.search(';') + 1;
+        
+        while (cookie.charAt(startIndex) === ' ') {
+            startIndex++;
+        }
+    }
+    while ((startIndex > 0) && (cookiename !== expectedCookieName));
+    
+    if (cookiename === expectedCookieName) {
+        clientId = cookie.substr(cookie.search('=') + 1, cookie.search(';'));
+        if (clientId === "") {
+            clientId = cookie.substr(cookie.search('=') + 1, cookie.length);
+        }
+    }
+    
+    if ((!clientId) || (clientId === "") || (clientId.search(getClientIdPrefix()) !== 0)) {
+        clientId = generateClientId();
+        storeClientId(clientId);
+    }
+    
+    return clientId;
+}
+
+
+/**
+ * 
+ */
+function storeClientId(clientId) {
+    if ((clientId) && (clientId !== "")) {
+        var expiry = new Date();
+        // 10 years should be sufficient :-)
+        expiry = new Date(expiry.getTime() + 1000*60*60*24*365*10);
+        document.cookie = getClientIdCookieName() + '=' + clientId +
+            '; expires=' + expiry.toGMTString()+';';
+    }
+}
+
+/**
+ * 
+ */
+function getClientIdCookieName() {
+    return document.URL + "/quest-htmlmonitor/quest-client-id";
+}
+
+/**
+ * 
+ */
+function generateClientId() {
+    return getClientIdPrefix() + new Date().getTime();
+}
+
+/**
+ * 
+ */
+function getClientIdPrefix() {
+    // create something like a more or less unique checksum. It is sufficient, if it differs
+    // only often, but not always, because it is concatenated with a time stamp, which differs
+    // much more often.
+    var prefixStr = navigator.userAgent + "_" + navigator.platform;
+    var prefixId = prefixStr.length;
+    var i = 0;
+    
+    for (i = 0; i < prefixStr.length; i++) {
+        prefixId += prefixStr.charCodeAt(i);
+    }
+    
+    // ensure, that a string is created and not a long. Otherwise, it can not be checked, if an
+    // existing client id starts with the client id prefix and can therefore be reused.
+    return prefixId.toString();
+}
+
+/**
+ * performs a simple logging
+ */
+function log(text) {
+    if (doLog) {
+        var loggingInfo = document.getElementById("loggingInfoParagraph");
+        
+        if (!loggingInfo) {
+            var div = document.createElement("div");
+            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
+                             "border:1px black solid; position:absolute; " +
+                             "top:10px; right:10px; width:350px; height:auto");
+            
+            loggingInfo = document.createElement("div");
+            loggingInfo.id = "loggingInfoParagraph";
+            div.appendChild(loggingInfo);
+            
+            var body = document.getElementsByTagName("body")[0];
+            if (!body) {
+                alert("could not enable logging");
+            }
+            else {
+                body.appendChild(div);
+            }
+        }
+        
+        loggingInfo.appendChild(document.createTextNode(text));
+        loggingInfo.appendChild(document.createElement("br"));
+    }
+}
+
+function Event(type, nodePath) {
+    this.type = type;
+    this.nodePath = nodePath;
+    this.time = new Date().getTime();
+    
+    this.setClickCoordinates = function(coordinates) {
+          this.clickCoordinates = coordinates;
+    };
+    
+    this.setKey = function(key) {
+          this.key = key;
+    };
+    
+    this.setScrollPosition = function(scrollPosition) {
+          this.scrollPosition = scrollPosition;
+    };
+    
+    this.toJSON = function() {
+        var eventInJSON =
+            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
+            this.type + "\"";
+
+        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
+            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
+        }
+
+        if (this.key) {
+            eventInJSON += ",\"key\":" + this.key;
+        }
+        
+        if (this.scrollPosition) {
+            eventInJSON += ",\"scrollPosition\":" + this.scrollPosition;
+        }
+
+        eventInJSON += "}";
+        
+        return eventInJSON;
+    };
+} 
+
