Allow predictable sessions PAZ-1030
[pazpar2-moved-to-github.git] / js / pz2.js
index e2335da..1ee8096 100644 (file)
--- a/js/pz2.js
+++ b/js/pz2.js
@@ -1,5 +1,5 @@
 /*
- * Mine
+ * $Id$
 ** pz2.js - pazpar2's javascript client library.
 */
 
@@ -29,9 +29,10 @@ var pz2 = function ( paramArray )
     
     // at least one callback required
     if ( !paramArray )
-        throw new Error("Pz2.js: Array with parameters has to be suplied."); 
+        throw new Error("Pz2.js: Array with parameters has to be supplied."); 
 
     //supported pazpar2's protocol version
+    this.windowid = paramArray.windowid || window.name;
     this.suppProtoVer = '1';
     if (typeof paramArray.pazpar2path != "undefined")
         this.pz2String = paramArray.pazpar2path;
@@ -73,6 +74,8 @@ var pz2 = function ( paramArray )
     this.initStatusOK = false;
     this.pingStatusOK = false;
     this.searchStatusOK = false;
+    this.mergekey = paramArray.mergekey || null;
+    this.rank = paramArray.rank || null;
     
     // for sorting
     this.currentSort = "relevance";
@@ -124,7 +127,7 @@ var pz2 = function ( paramArray )
     }
     // else, auto init session or wait for a user init?
     if (this.useSessions && paramArray.autoInit !== false) {
-        this.init(this.sessionId, this.serviceId);
+        this.init(this.sessionID, this.serviceId);
     }
     // Version parameter
     this.version = paramArray.version || null;
@@ -168,7 +171,7 @@ pz2.prototype =
         this.stop();
             
         if ( this.resetCallback )
-                this.resetCallback();
+                this.resetCallback(this.windowid);
     },
 
     init: function (sessionId, serviceId) 
@@ -201,6 +204,9 @@ pz2.prototype =
                         context.sessionID = 
                             data.getElementsByTagName("session")[0]
                                 .childNodes[0].nodeValue;
+                        if (data.getElementsByTagName("keepAlive").length > 0) {
+                            context.keepAlive = data.getElementsByTagName("keepAlive")[0].childNodes[0].nodeValue;
+                        }
                         context.pingTimer =
                             setTimeout(
                                 function () {
@@ -209,7 +215,7 @@ pz2.prototype =
                                 context.keepAlive
                             );
                         if ( context.initCallback )
-                            context.initCallback();
+                            context.initCallback(context.windowid);
                     }
                     else
                         context.throwError('Init failed. Malformed WS resonse.',
@@ -235,7 +241,7 @@ pz2.prototype =
 
         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
         request.safeGet(
-            { "command": "ping", "session": this.sessionID, "windowid" : window.name },
+            { "command": "ping", "session": this.sessionID, "windowid" : context.windowid },
             function(data) {
                 if ( data.getElementsByTagName("status")[0]
                         .childNodes[0].nodeValue == "OK" ) {
@@ -281,15 +287,20 @@ pz2.prototype =
         else
             var start = 0;
 
-             var searchParams = { 
+       var searchParams = { 
           "command": "search",
           "query": this.currQuery, 
           "session": this.sessionID,
-          "windowid" : window.name
+          "windowid" : this.windowid
         };
        
-        if (filter !== undefined)
-               searchParams["filter"] = filter;
+        if( sort !== undefined ) {
+            this.currentSort = sort;
+           searchParams["sort"] = sort;
+       }
+        if (filter !== undefined) searchParams["filter"] = filter;
+        if (this.mergekey) searchParams["mergekey"] = this.mergekey;
+        if (this.rank) searchParams["rank"] = this.rank;
 
         // copy additional parmeters, do not overwrite
         if (addParamsArr != undefined) {
@@ -308,7 +319,8 @@ pz2.prototype =
                         .childNodes[0].nodeValue == "OK" ) {
                     context.searchStatusOK = true;
                     //piggyback search
-                    context.show(start, num, sort);
+                    if (context.showCallback)
+                        context.show(start, num, sort);
                     if (context.statCallback)
                         context.stat();
                     if (context.termlistCallback)
@@ -333,7 +345,7 @@ pz2.prototype =
         var context = this;
         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
         request.safeGet(
-            { "command": "stat", "session": this.sessionID, "windowid" : window.name },
+            { "command": "stat", "session": this.sessionID, "windowid" : context.windowid },
             function(data) {
                 if ( data.getElementsByTagName("stat") ) {
                     var activeClients = 
@@ -355,7 +367,7 @@ pz2.prototype =
                                 },
                                 delay
                             );
-                    context.statCallback(stat);
+                    context.statCallback(stat, context.windowid);
                 }
                 else
                     context.throwError('Stat failed. Malformed WS resonse.',
@@ -391,7 +403,7 @@ pz2.prototype =
               "sort": this.currentSort, 
               "block": 1,
               "type": this.showResponseType,
-              "windowid" : window.name,
+              "windowid" : this.windowid
           };
         if (query_state)
           requestParameters["query-state"] = query_state;
@@ -442,8 +454,9 @@ pz2.prototype =
               context.throwError('Show failed. Malformed WS resonse.',
                   114);
             };
+           
            var approxNode = data.getElementsByTagName("approximation");
-           if (approxNode)
+           if (approxNode && approxNode[0] && approxNode[0].childNodes[0] && approxNode[0].childNodes[0].nodeValue)
                show['approximation'] = 
                  Number( approxNode[0].childNodes[0].nodeValue);
              
@@ -460,7 +473,7 @@ pz2.prototype =
                   context.show();
                 }, 
                 delay);
-            context.showCallback(show);
+              context.showCallback(show, context.windowid);
           }
         );
     },
@@ -479,7 +492,7 @@ pz2.prototype =
             "command": "record", 
             "session": this.sessionID,
             "id": this.currRecID,
-            "windowid" : window.name
+            "windowid" : this.windowid
         };
        
        this.currRecOffset = null;
@@ -512,7 +525,7 @@ pz2.prototype =
                     record = new Array();
                     record['xmlDoc'] = data;
                     record['offset'] = context.currRecOffset;
-                    callback(record, args);
+                    callback(record, args, context.windowid);
                 //pz2 record
                 } else if ( recordNode = 
                     data.getElementsByTagName("record")[0] ) {
@@ -542,7 +555,7 @@ pz2.prototype =
                                   },
                                   delay
                                );                                    
-                    callback(record, args);
+                    callback(record, args, context.windowid);
                 }
                 else
                     context.throwError('Record failed. Malformed WS resonse.',
@@ -568,7 +581,7 @@ pz2.prototype =
                 "command": "termlist", 
                 "session": this.sessionID, 
                 "name": this.termKeys,
-                "windowid" : window.name, 
+                "windowid" : this.windowid, 
                "version" : this.version
        
             },
@@ -631,7 +644,7 @@ pz2.prototype =
                                 delay
                             );
                    
-                   context.termlistCallback(termList);
+                    context.termlistCallback(termList, context.windowid);
                 }
                 else
                     context.throwError('Termlist failed. Malformed WS resonse.',
@@ -661,7 +674,7 @@ pz2.prototype =
                "command": "bytarget", 
                "session": this.sessionID, 
                "block": 1,
-               "windowid" : window.name,
+               "windowid" : this.windowid,
                "version" : this.version
            },
             function(data) {
@@ -696,11 +709,6 @@ pz2.prototype =
                         } else if (bytarget[i]["state"]=="Client_Working") {
                           bytarget[i]["hits"] = "...";
                         }
-                        if (bytarget[i].diagnostic == "1") {
-                          bytarget[i].diagnostic = "Permanent system error";
-                        } else if (bytarget[i].diagnostic == "2") {
-                          bytarget[i].diagnostic = "Temporary system error";
-                        } 
                         var targetsSuggestions = targetNodes[i].getElementsByTagName("suggestions");
                         if (targetsSuggestions != undefined && targetsSuggestions.length>0) {
                           var suggestions = targetsSuggestions[0];
@@ -720,7 +728,7 @@ pz2.prototype =
                                 delay
                             );
 
-                    context.bytargetCallback(bytarget);
+                    context.bytargetCallback(bytarget, context.windowid);
                 }
                 else
                     context.throwError('Bytarget failed. Malformed WS resonse.',
@@ -757,23 +765,31 @@ pz2.prototype =
 ** AJAX HELPER CLASS ***********************************************************
 ********************************************************************************
 */
-var pzHttpRequest = function ( url, errorHandler ) {
+var pzHttpRequest = function (url, errorHandler, cookieDomain, windowId) {
         this.maxUrlLength = 2048;
         this.request = null;
         this.url = url;
         this.errorHandler = errorHandler || null;
         this.async = true;
         this.requestHeaders = {};
-        
-        if ( window.XMLHttpRequest ) {
-            this.request = new XMLHttpRequest();
-        } else if ( window.ActiveXObject ) {
-            try {
-                this.request = new ActiveXObject( 'Msxml2.XMLHTTP' );
-            } catch (err) {
-                this.request = new ActiveXObject( 'Microsoft.XMLHTTP' );
-            }
+        this.isXDR = false;
+        this.domainRegex = /https?:\/\/([^:/]+).*/;
+        this.cookieDomain = cookieDomain || null;
+        this.windowId = windowId || window.name;
+
+        var xhr = new XMLHttpRequest();
+        var domain = this._getDomainFromUrl(url);
+        if ("withCredentials" in xhr) {
+          // XHR for Chrome/Firefox/Opera/Safari.
+        } else if (domain && this._isCrossDomain(domain) &&
+            typeof XDomainRequest != "undefined") {
+          // use XDR (IE7/8) when no other way
+          xhr = new XDomainRequest();
+          this.isXDR = true;
+        } else {
+          // CORS not supported.
         }
+        this.request = xhr;
 };
 
 
@@ -826,16 +842,102 @@ pzHttpRequest.prototype =
         return encoded;
     },
 
+    _getDomainFromUrl: function (url)
+    {
+      if (this.cookieDomain) return this.cookieDomain; //explicit cookie domain
+      var m = this.domainRegex.exec(url);
+      return (m && m.length > 1) ? m[1] : null;
+    },
+
+    _strEndsWith: function (str, suffix) 
+    {
+      return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    },
+
+    _isCrossDomain: function (domain)
+    {
+      if (this.cookieDomain) return true; //assume xdomain is cookie domain set
+      return !this._strEndsWith(domain, document.domain); 
+    },
+
+    getCookie: function (sKey) {
+      return decodeURI(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" 
+        + encodeURI(sKey).replace(/[\-\.\+\*]/g, "\\$&") 
+        + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
+    },
+
+    setCookie: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
+      if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { 
+        return false; 
+      }
+      var sExpires = "";
+      if (vEnd) {
+        switch (vEnd.constructor) {
+          case Number:
+            sExpires = vEnd === Infinity 
+              ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" 
+              : "; max-age=" + vEnd;
+            break;
+          case String:
+            sExpires = "; expires=" + vEnd;
+            break;
+          case Date:
+            sExpires = "; expires=" + vEnd.toGMTString();
+            break;
+        }
+      }
+      document.cookie = encodeURI(sKey) + "=" + encodeURI(sValue) 
+        + sExpires 
+        + (sDomain ? "; domain=" + sDomain : "") 
+        + (sPath ? "; path=" + sPath : "") 
+        + (bSecure ? "; secure" : "");
+      return true;
+    },
+    
     _send: function ( type, url, data, callback)
     {
         var context = this;
         this.callback = callback;
         this.async = true;
+        //we never do withCredentials, so if it's CORS and we have
+        //session cookie, resend it
+        var domain = this._getDomainFromUrl(url);
+        if (domain && this._isCrossDomain(domain) &&
+            this.getCookie(domain+":"+this.windowId+":SESSID")) {
+          //rewrite the URL
+          var sessparam = ';jsessionid=' + this.getCookie(domain+":"+this.windowId+":SESSID");
+          var q = url.indexOf('?');
+          if (q == -1) {
+            url += sessparam;            
+          } else {
+            url = url.substring(0, q) + sessparam + url.substring(q);
+          }
+        }
         this.request.open( type, url, this.async );
-        for (var key in this.requestHeaders)
+        if (!this.isXDR) {
+          //setting headers is only allowed with XHR
+          for (var key in this.requestHeaders)
             this.request.setRequestHeader(key, this.requestHeaders[key]);
-        this.request.onreadystatechange = function () {
+        }
+        if (this.isXDR) {
+          this.request.onload = function () {
+            //fake XHR props
+            context.request.status = 200;
+            context.request.readyState = 4;
+            //handle
+            context._handleResponse(url);
+          }
+          this.request.onerror = function () {
+            //fake XHR props
+            context.request.status = 417; //not really, but what can we do
+            context.request.readyState = 4;
+            //handle
+            context._handleResponse(url);
+          }
+        } else {
+          this.request.onreadystatechange = function () {
             context._handleResponse(url); /// url used ONLY for error reporting
+          }
         }
         this.request.send(data);
     },
@@ -848,11 +950,22 @@ pzHttpRequest.prototype =
             return this.url;
     },
 
-    _handleResponse: function (savedUrlForErrorReporting)
+    _handleResponse: function (requestUrl)
     {
         if ( this.request.readyState == 4 ) { 
             // pick up appplication errors first
             var errNode = null;
+            // xdomainreq does not have responseXML
+            if (this.isXDR) {
+              if (this.request.contentType.match(/\/xml/)){                
+                var dom = new ActiveXObject('Microsoft.XMLDOM');
+                dom.async = false;                
+                dom.loadXML(this.request.responseText);
+                this.request.responseXML = dom;
+              } else {
+                this.request.responseXML = null;
+              }
+            }
             if (this.request.responseXML &&
                 (errNode = this.request.responseXML.documentElement)
                 && errNode.nodeName == 'error') {
@@ -871,40 +984,25 @@ pzHttpRequest.prototype =
                 else {
                     throw err;
                 }
-            } else if (this.request.status == 200 && 
-                       this.request.responseXML == null) {
-              if (this.request.responseText != null) {
+            } 
+            else if (this.request.status == 200 && 
+                     this.request.responseXML === null) {
+              if (this.request.responseText !== null) {
                 //assume JSON
-               
-               var json = null; 
-               var text = this.request.responseText;
-               if (typeof window.JSON == "undefined") 
-                   json = eval("(" + text + ")");
-               else { 
-                   try {
-                       json = JSON.parse(text);
-                   }
-                   catch (e) {
-                       // Safari: eval will fail as well. Considering trying JSON2 (non-native implementation) instead
-                       /* DEBUG only works in mk2-mobile
-                       if (document.getElementById("log")) 
-                           document.getElementById("log").innerHTML = "" + e + " " + length + ": " + text;
-                       */
-                       try {
-                           json = eval("(" + text + ")");
-                       }
-                       catch (e) {
-                           /* DEBUG only works in mk2-mobile
-                           if (document.getElementById("log")) 
-                               document.getElementById("log").innerHTML = "" + e + " " + length + ": " + text;
-                           */
-                       }
-                   }
-               } 
-               this.callback(json, "json");
+                       var json = null; 
+                       var text = this.request.responseText;
+                       if (typeof window.JSON == "undefined") {
+                         json = eval("(" + text + ")");
+                } else { 
+                         try {
+                           json = JSON.parse(text);
+                         } catch (e) {
+                  }
+                       } 
+                       this.callback(json, "json");
               } else {
-                var err = new Error("XML response is empty but no error " +
-                                    "for " + savedUrlForErrorReporting);
+                var err = new Error("XML/Text response is empty but no error " +
+                                    "for " + requestUrl);
                 err.code = -1;
                 if (this.errorHandler) {
                     this.errorHandler(err);
@@ -913,6 +1011,14 @@ pzHttpRequest.prototype =
                 }
               }
             } else if (this.request.status == 200) {
+                //set cookie manually only if cross-domain
+                var domain = this._getDomainFromUrl(requestUrl);
+                if (domain && this._isCrossDomain(domain)) {
+                  var jsessionId = this.request.responseXML
+                    .documentElement.getAttribute('jsessionId');
+                  if (jsessionId)                  
+                    this.setCookie(domain+":"+this.windowId+":SESSID", jsessionId);
+                }
                 this.callback(this.request.responseXML);
             } else {
                 var err = new Error("HTTP response not OK: " 
@@ -1057,35 +1163,43 @@ Element_parseChildNodes = function (node)
 {
     var parsed = {};
     var hasChildElems = false;
+    var textContent = '';
 
     if (node.hasChildNodes()) {
         var children = node.childNodes;
         for (var i = 0; i < children.length; i++) {
             var child = children[i];
-            if (child.nodeType == Node.ELEMENT_NODE) {
+            switch (child.nodeType) {
+              case Node.ELEMENT_NODE:
                 hasChildElems = true;
                 var nodeName = child.nodeName; 
                 if (!(nodeName in parsed))
                     parsed[nodeName] = [];
                 parsed[nodeName].push(Element_parseChildNodes(child));
+                break;
+              case Node.TEXT_NODE:
+                textContent += child.nodeValue;
+                break;
+              case Node.CDATA_SECTION_NODE:
+                textContent += child.nodeValue;
+                break;
             }
         }
     }
 
     var attrs = node.attributes;
     for (var i = 0; i < attrs.length; i++) {
+        hasChildElems = true;
         var attrName = '@' + attrs[i].nodeName;
         var attrValue = attrs[i].nodeValue;
         parsed[attrName] = attrValue;
     }
 
-    // if no nested elements, get text content
-    if (node.hasChildNodes() && !hasChildElems) {
-        if (node.attributes.length) 
-            parsed['#text'] = node.firstChild.nodeValue;
-        else
-            parsed = node.firstChild.nodeValue;
-    }
+    // if no nested elements/attrs set value to text
+    if (hasChildElems)
+      parsed['#text'] = textContent;
+    else
+      parsed = textContent;
     
     return parsed;
 }