Added error-reporting for the case where a HTTP response is 200 OK but
[pazpar2-moved-to-github.git] / js / pz2.js
1 /*
2 ** pz2.js - pazpar2's javascript client library.
3 */
4
5 //since explorer is flawed
6 if (!window['Node']) {
7     window.Node = new Object();
8     Node.ELEMENT_NODE = 1;
9     Node.ATTRIBUTE_NODE = 2;
10     Node.TEXT_NODE = 3;
11     Node.CDATA_SECTION_NODE = 4;
12     Node.ENTITY_REFERENCE_NODE = 5;
13     Node.ENTITY_NODE = 6;
14     Node.PROCESSING_INSTRUCTION_NODE = 7;
15     Node.COMMENT_NODE = 8;
16     Node.DOCUMENT_NODE = 9;
17     Node.DOCUMENT_TYPE_NODE = 10;
18     Node.DOCUMENT_FRAGMENT_NODE = 11;
19     Node.NOTATION_NODE = 12;
20 }
21
22 // prevent execution of more than once
23 if(typeof window.pz2 == "undefined") {
24 window.undefined = window.undefined;
25
26 var pz2 = function ( paramArray )
27 {
28     
29     // at least one callback required
30     if ( !paramArray )
31         throw new Error("Pz2.js: Array with parameters has to be suplied."); 
32
33     //supported pazpar2's protocol version
34     this.suppProtoVer = '1';
35     if (typeof paramArray.pazpar2path != "undefined")
36         this.pz2String = paramArray.pazpar2path;
37     else
38         this.pz2String = "/pazpar2/search.pz2";
39     this.useSessions = true;
40     
41     this.stylesheet = paramArray.detailstylesheet || null;
42     //load stylesheet if required in async mode
43     if( this.stylesheet ) {
44         var context = this;
45         var request = new pzHttpRequest( this.stylesheet );
46         request.get( {}, function ( doc ) { context.xslDoc = doc; } );
47     }
48     
49     this.errorHandler = paramArray.errorhandler || null;
50     
51     // function callbacks
52     this.initCallback = paramArray.oninit || null;
53     this.statCallback = paramArray.onstat || null;
54     this.showCallback = paramArray.onshow || null;
55     this.termlistCallback = paramArray.onterm || null;
56     this.recordCallback = paramArray.onrecord || null;
57     this.bytargetCallback = paramArray.onbytarget || null;
58     this.resetCallback = paramArray.onreset || null;
59
60     // termlist keys
61     this.termKeys = paramArray.termlist || "subject";
62     
63     // some configurational stuff
64     this.keepAlive = 50000;
65     
66     if ( paramArray.keepAlive < this.keepAlive )
67         this.keepAlive = paramArray.keepAlive;
68
69     this.sessionID = null;
70     this.initStatusOK = false;
71     this.pingStatusOK = false;
72     this.searchStatusOK = false;
73     
74     // for sorting
75     this.currentSort = "relevance";
76
77     // where are we?
78     this.currentStart = 0;
79     this.currentNum = 20;
80
81     // last full record retrieved
82     this.currRecID = null;
83     
84     // current query
85     this.currQuery = null;
86
87     //current raw record offset
88     this.currRecOffset = null;
89
90     //timers
91     this.statTime = paramArray.stattime || 1000;
92     this.statTimer = null;
93     this.termTime = paramArray.termtime || 1000;
94     this.termTimer = null;
95     this.showTime = paramArray.showtime || 1000;
96     this.showTimer = null;
97     this.showFastCount = 4;
98     this.bytargetTime = paramArray.bytargettime || 1000;
99     this.bytargetTimer = null;
100     this.recordTime = paramArray.recordtime || 500;
101     this.recordTimer = null;
102
103     // counters for each command and applied delay
104     this.dumpFactor = 500;
105     this.showCounter = 0;
106     this.termCounter = 0;
107     this.statCounter = 0;
108     this.bytargetCounter = 0;
109     this.recordCounter = 0;
110
111     // active clients, updated by stat and show
112     // might be an issue since bytarget will poll accordingly
113     this.activeClients = 1;
114
115     // if in proxy mode no need to init
116     if (paramArray.usesessions != undefined) {
117          this.useSessions = paramArray.usesessions;
118         this.initStatusOK = true;
119     }
120     // else, auto init session or wait for a user init?
121     if (this.useSessions && paramArray.autoInit !== false) {
122         this.init();
123     }
124 };
125
126 pz2.prototype = 
127 {
128     //error handler for async error throws
129    throwError: function (errMsg, errCode)
130    {
131         var err = new Error(errMsg);
132         if (errCode) err.code = errCode;
133                 
134         if (this.errorHandler) {
135             this.errorHandler(err);
136         }
137         else {
138             throw err;
139         }
140    },
141
142     // stop activity by clearing tiemouts 
143    stop: function ()
144    {
145        clearTimeout(this.statTimer);
146        clearTimeout(this.showTimer);
147        clearTimeout(this.termTimer);
148        clearTimeout(this.bytargetTimer);
149     },
150     
151     // reset status variables
152     reset: function ()
153     {   
154         if ( this.useSessions ) {
155             this.sessionID = null;
156             this.initStatusOK = false;
157             this.pingStatusOK = false;
158         }
159         this.searchStatusOK = false;
160         this.stop();
161             
162         if ( this.resetCallback )
163                 this.resetCallback();
164     },
165
166     init: function ( sessionId ) 
167     {
168         this.reset();
169         
170         // session id as a param
171         if ( sessionId != undefined && this.useSessions ) {
172             this.initStatusOK = true;
173             this.sessionID = sessionId;
174             this.ping();
175         // old school direct pazpar2 init
176         } else if (this.useSessions) {
177             var context = this;
178             var request = new pzHttpRequest(this.pz2String, this.errorHandler);
179             request.safeGet(
180                 { "command": "init" },
181                 function(data) {
182                     if ( data.getElementsByTagName("status")[0]
183                             .childNodes[0].nodeValue == "OK" ) {
184                         if ( data.getElementsByTagName("protocol")[0]
185                                 .childNodes[0].nodeValue 
186                             != context.suppProtoVer )
187                             throw new Error(
188                                 "Server's protocol not supported by the client"
189                             );
190                         context.initStatusOK = true;
191                         context.sessionID = 
192                             data.getElementsByTagName("session")[0]
193                                 .childNodes[0].nodeValue;
194                         setTimeout(
195                             function () {
196                                 context.ping();
197                             },
198                             context.keepAlive
199                         );
200                         if ( context.initCallback )
201                             context.initCallback();
202                     }
203                     else
204                         context.throwError('Init failed. Malformed WS resonse.',
205                                             110);
206                 }
207             );
208         // when through proxy no need to init
209         } else {
210             this.initStatusOK = true;
211         }
212     },
213     // no need to ping explicitly
214     ping: function () 
215     {
216         // pinging only makes sense when using pazpar2 directly
217         if( !this.initStatusOK || !this.useSessions )
218             throw new Error(
219             'Pz2.js: Ping not allowed (proxy mode) or session not initialized.'
220             );
221         var context = this;
222         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
223         request.safeGet(
224             { "command": "ping", "session": this.sessionID },
225             function(data) {
226                 if ( data.getElementsByTagName("status")[0]
227                         .childNodes[0].nodeValue == "OK" ) {
228                     context.pingStatusOK = true;
229                     setTimeout(
230                         function () {
231                             context.ping();
232                         }, 
233                         context.keepAlive
234                     );
235                 }
236                 else
237                     context.throwError('Ping failed. Malformed WS resonse.',
238                                         111);
239             }
240         );
241     },
242     search: function (query, num, sort, filter, showfrom, addParamsArr)
243     {
244         clearTimeout(this.statTimer);
245         clearTimeout(this.showTimer);
246         clearTimeout(this.termTimer);
247         clearTimeout(this.bytargetTimer);
248         
249         this.showCounter = 0;
250         this.termCounter = 0;
251         this.bytargetCounter = 0;
252         this.statCounter = 0;
253         
254         // no proxy mode
255         if( !this.initStatusOK )
256             throw new Error('Pz2.js: session not initialized.');
257         
258         if( query !== undefined )
259             this.currQuery = query;
260         else
261             throw new Error("Pz2.js: no query supplied to the search command.");
262         
263         if ( showfrom !== undefined )
264             var start = showfrom;
265         else
266             var start = 0;
267
268         var searchParams = { 
269             "command": "search",
270             "query": this.currQuery, 
271             "session": this.sessionID 
272         };
273         
274         if (filter !== undefined)
275             searchParams["filter"] = filter;
276
277         // copy additional parmeters, do not overwrite
278         if (addParamsArr != undefined) {
279             for (var prop in addParamsArr) {
280                 if (!searchParams.hasOwnProperty(prop))
281                     searchParams[prop] = addParamsArr[prop];
282             }
283         }
284         
285         var context = this;
286         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
287         request.safeGet(
288             searchParams,
289             function(data) {
290                 if ( data.getElementsByTagName("status")[0]
291                         .childNodes[0].nodeValue == "OK" ) {
292                     context.searchStatusOK = true;
293                     //piggyback search
294                     context.show(start, num, sort);
295                     if ( context.statCallback )
296                         context.stat();
297                     if ( context.termlistCallback )
298                         context.termlist();
299                     if ( context.bytargetCallback )
300                         context.bytarget();
301                 }
302                 else
303                     context.throwError('Search failed. Malformed WS resonse.',
304                                         112);
305             }
306         );
307     },
308     stat: function()
309     {
310         if( !this.initStatusOK )
311             throw new Error('Pz2.js: session not initialized.');
312         
313         // if called explicitly takes precedence
314         clearTimeout(this.statTimer);
315         
316         var context = this;
317         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
318         request.safeGet(
319             { "command": "stat", "session": this.sessionID },
320             function(data) {
321                 if ( data.getElementsByTagName("stat") ) {
322                     var activeClients = 
323                         Number( data.getElementsByTagName("activeclients")[0]
324                                     .childNodes[0].nodeValue );
325                     context.activeClients = activeClients;
326
327                     var stat = Element_parseChildNodes(data.documentElement);
328
329                     context.statCounter++;
330                     var delay = context.statTime 
331                         + context.statCounter * context.dumpFactor;
332                     
333                     if ( activeClients > 0 )
334                         context.statTimer = 
335                             setTimeout( 
336                                 function () {
337                                     context.stat();
338                                 },
339                                 delay
340                             );
341                     context.statCallback(stat);
342                 }
343                 else
344                     context.throwError('Stat failed. Malformed WS resonse.',
345                                         113);
346             }
347         );
348     },
349     show: function(start, num, sort)
350     {
351         if( !this.searchStatusOK && this.useSessions )
352             throw new Error(
353                 'Pz2.js: show command has to be preceded with a search command.'
354             );
355         
356         // if called explicitly takes precedence
357         clearTimeout(this.showTimer);
358         
359         if( sort !== undefined )
360             this.currentSort = sort;
361         if( start !== undefined )
362             this.currentStart = Number( start );
363         if( num !== undefined )
364             this.currentNum = Number( num );
365
366         var context = this;
367         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
368         request.safeGet(
369             { 
370                 "command": "show", 
371                 "session": this.sessionID, 
372                 "start": this.currentStart,
373                 "num": this.currentNum, 
374                 "sort": this.currentSort, 
375                 "block": 1 
376             },
377             function(data) {
378                 if ( data.getElementsByTagName("status")[0]
379                         .childNodes[0].nodeValue == "OK" ) {
380                     // first parse the status data send along with records
381                     // this is strictly bound to the format
382                     var activeClients = 
383                         Number( data.getElementsByTagName("activeclients")[0]
384                                     .childNodes[0].nodeValue );
385                     context.activeClients = activeClients; 
386                     var show = {
387                         "activeclients": activeClients,
388                         "merged": 
389                             Number( data.getElementsByTagName("merged")[0]
390                                         .childNodes[0].nodeValue ),
391                         "total": 
392                             Number( data.getElementsByTagName("total")[0]
393                                         .childNodes[0].nodeValue ),
394                         "start": 
395                             Number( data.getElementsByTagName("start")[0]
396                                         .childNodes[0].nodeValue ),
397                         "num": 
398                             Number( data.getElementsByTagName("num")[0]
399                                         .childNodes[0].nodeValue ),
400                         "hits": []
401                     };
402                     // parse all the first-level nodes for all <hit> tags
403                     var hits = data.getElementsByTagName("hit");
404                     for (i = 0; i < hits.length; i++)
405                         show.hits[i] = Element_parseChildNodes(hits[i]);
406                     
407                     context.showCounter++;
408                     var delay = context.showTime;
409                     if (context.showCounter > context.showFastCount)
410                             delay += context.showCounter * context.dumpFactor;
411                     if ( activeClients > 0 )
412                         context.showTimer = setTimeout(
413                             function () {
414                                 context.show();
415                             }, 
416                             delay);
417                     global_show = show;
418                     context.showCallback(show);
419                 }
420                 else
421                     context.throwError('Show failed. Malformed WS resonse.',
422                                         114);
423             }
424         );
425     },
426     record: function(id, offset, syntax, handler)
427     {
428         // we may call record with no previous search if in proxy mode
429         if(!this.searchStatusOK && this.useSessions)
430            throw new Error(
431             'Pz2.js: record command has to be preceded with a search command.'
432             );
433         
434         if( id !== undefined )
435             this.currRecID = id;
436         
437         var recordParams = { 
438             "command": "record", 
439             "session": this.sessionID,
440             "id": this.currRecID 
441         };
442         
443         this.currRecOffset = null;
444         if (offset != undefined) {
445             recordParams["offset"] = offset;
446             this.currRecOffset = offset;
447         }
448
449         if (syntax != undefined)
450             recordParams['syntax'] = syntax;
451
452         //overwrite default callback id needed
453         var callback = this.recordCallback;
454         var args = undefined;
455         if (handler != undefined) {
456             callback = handler['callback'];
457             args = handler['args'];
458         }
459         
460         var context = this;
461         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
462
463         request.safeGet(
464             recordParams,
465             function(data) {
466                 var recordNode;
467                 var record;                                
468                 //raw record
469                 if (context.currRecOffset !== null) {
470                     record = new Array();
471                     record['xmlDoc'] = data;
472                     record['offset'] = context.currRecOffset;
473                     callback(record, args);
474                 //pz2 record
475                 } else if ( recordNode = 
476                     data.getElementsByTagName("record")[0] ) {
477                     // if stylesheet was fetched do not parse the response
478                     if ( context.xslDoc ) {
479                         record = new Array();
480                         record['xmlDoc'] = data;
481                         record['xslDoc'] = context.xslDoc;
482                         record['recid'] = 
483                             recordNode.getElementsByTagName("recid")[0]
484                                 .firstChild.nodeValue;
485                     //parse record
486                     } else {
487                         record = Element_parseChildNodes(recordNode);
488                     }    
489                     var activeClients = 
490                        Number( data.getElementsByTagName("activeclients")[0]
491                                 .childNodes[0].nodeValue );
492                     context.activeClients = activeClients; 
493                     context.recordCounter++;
494                     var delay = context.recordTime + context.recordCounter * context.dumpFactor;
495                     if ( activeClients > 0 )
496                         context.recordTimer = 
497                            setTimeout ( 
498                                function() {
499                                   context.record(id, offset, syntax, handler);
500                                   },
501                                   delay
502                                );                                    
503                     callback(record, args);
504                 }
505                 else
506                     context.throwError('Record failed. Malformed WS resonse.',
507                                         115);
508             }
509         );
510     },
511
512     termlist: function()
513     {
514         if( !this.searchStatusOK && this.useSessions )
515             throw new Error(
516             'Pz2.js: termlist command has to be preceded with a search command.'
517             );
518
519         // if called explicitly takes precedence
520         clearTimeout(this.termTimer);
521         
522         var context = this;
523         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
524         request.safeGet(
525             { 
526                 "command": "termlist", 
527                 "session": this.sessionID, 
528                 "name": this.termKeys 
529             },
530             function(data) {
531                 if ( data.getElementsByTagName("termlist") ) {
532                     var activeClients = 
533                         Number( data.getElementsByTagName("activeclients")[0]
534                                     .childNodes[0].nodeValue );
535                     context.activeClients = activeClients;
536                     var termList = { "activeclients":  activeClients };
537                     var termLists = data.getElementsByTagName("list");
538                     //for each termlist
539                     for (i = 0; i < termLists.length; i++) {
540                         var listName = termLists[i].getAttribute('name');
541                         termList[listName] = new Array();
542                         var terms = termLists[i].getElementsByTagName('term');
543                         //for each term in the list
544                         for (j = 0; j < terms.length; j++) { 
545                             var term = {
546                                 "name": 
547                                     (terms[j].getElementsByTagName("name")[0]
548                                         .childNodes.length 
549                                     ? terms[j].getElementsByTagName("name")[0]
550                                         .childNodes[0].nodeValue
551                                     : 'ERROR'),
552                                 "freq": 
553                                     terms[j]
554                                     .getElementsByTagName("frequency")[0]
555                                     .childNodes[0].nodeValue || 'ERROR'
556                             };
557
558                             var termIdNode = 
559                                 terms[j].getElementsByTagName("id");
560                             if(terms[j].getElementsByTagName("id").length)
561                                 term["id"] = 
562                                     termIdNode[0].childNodes[0].nodeValue;
563                             termList[listName][j] = term;
564                         }
565                     }
566
567                     context.termCounter++;
568                     var delay = context.termTime 
569                         + context.termCounter * context.dumpFactor;
570                     if ( activeClients > 0 )
571                         context.termTimer = 
572                             setTimeout(
573                                 function () {
574                                     context.termlist();
575                                 }, 
576                                 delay
577                             );
578                    
579                    context.termlistCallback(termList);
580                 }
581                 else
582                     context.throwError('Termlist failed. Malformed WS resonse.',
583                                         116);
584             }
585         );
586
587     },
588     bytarget: function()
589     {
590         if( !this.initStatusOK && this.useSessions )
591             throw new Error(
592             'Pz2.js: bytarget command has to be preceded with a search command.'
593             );
594         
595         // no need to continue
596         if( !this.searchStatusOK )
597             return;
598
599         // if called explicitly takes precedence
600         clearTimeout(this.bytargetTimer);
601         
602         var context = this;
603         var request = new pzHttpRequest(this.pz2String, this.errorHandler);
604         request.safeGet(
605             { "command": "bytarget", "session": this.sessionID },
606             function(data) {
607                 if ( data.getElementsByTagName("status")[0]
608                         .childNodes[0].nodeValue == "OK" ) {
609                     var targetNodes = data.getElementsByTagName("target");
610                     var bytarget = new Array();
611                     for ( i = 0; i < targetNodes.length; i++) {
612                         bytarget[i] = new Array();
613                         for( j = 0; j < targetNodes[i].childNodes.length; j++ ) {
614                             if ( targetNodes[i].childNodes[j].nodeType 
615                                 == Node.ELEMENT_NODE ) {
616                                 var nodeName = 
617                                     targetNodes[i].childNodes[j].nodeName;
618                                 var nodeText = 
619                                     targetNodes[i].childNodes[j]
620                                         .firstChild.nodeValue;
621                                 bytarget[i][nodeName] = nodeText;
622                             }
623                         }
624                     }
625                     
626                     context.bytargetCounter++;
627                     var delay = context.bytargetTime 
628                         + context.bytargetCounter * context.dumpFactor;
629                     if ( context.activeClients > 0 )
630                         context.bytargetTimer = 
631                             setTimeout(
632                                 function () {
633                                     context.bytarget();
634                                 }, 
635                                 delay
636                             );
637
638                     context.bytargetCallback(bytarget);
639                 }
640                 else
641                     context.throwError('Bytarget failed. Malformed WS resonse.',
642                                         117);
643             }
644         );
645     },
646     
647     // just for testing, probably shouldn't be here
648     showNext: function(page)
649     {
650         var step = page || 1;
651         this.show( ( step * this.currentNum ) + this.currentStart );     
652     },
653
654     showPrev: function(page)
655     {
656         if (this.currentStart == 0 )
657             return false;
658         var step = page || 1;
659         var newStart = this.currentStart - (step * this.currentNum );
660         this.show( newStart > 0 ? newStart : 0 );
661     },
662
663     showPage: function(pageNum)
664     {
665         //var page = pageNum || 1;
666         this.show(pageNum * this.currentNum);
667     }
668 };
669
670 /*
671 ********************************************************************************
672 ** AJAX HELPER CLASS ***********************************************************
673 ********************************************************************************
674 */
675 var pzHttpRequest = function ( url, errorHandler ) {
676         this.maxUrlLength = 2048;
677         this.request = null;
678         this.url = url;
679         this.errorHandler = errorHandler || null;
680         this.async = true;
681         this.requestHeaders = {};
682         
683         if ( window.XMLHttpRequest ) {
684             this.request = new XMLHttpRequest();
685         } else if ( window.ActiveXObject ) {
686             try {
687                 this.request = new ActiveXObject( 'Msxml2.XMLHTTP' );
688             } catch (err) {
689                 this.request = new ActiveXObject( 'Microsoft.XMLHTTP' );
690             }
691         }
692 };
693
694 pzHttpRequest.prototype = 
695 {
696     safeGet: function ( params, callback )
697     {
698         var encodedParams =  this.encodeParams(params);
699         var url = this._urlAppendParams(encodedParams);
700         if (url.length >= this.maxUrlLength) {
701             this.requestHeaders["Content-Type"]
702                 = "application/x-www-form-urlencoded";
703             this._send( 'POST', this.url, encodedParams, callback );
704         } else {
705             this._send( 'GET', url, '', callback );
706         }
707     },
708
709     get: function ( params, callback ) 
710     {
711         this._send( 'GET', this._urlAppendParams(this.encodeParams(params)), 
712             '', callback );
713     },
714
715     post: function ( params, data, callback )
716     {
717         this._send( 'POST', this._urlAppendParams(this.encodeParams(params)), 
718             data, callback );
719     },
720
721     load: function ()
722     {
723         this.async = false;
724         this.request.open( 'GET', this.url, this.async );
725         this.request.send('');
726         if ( this.request.status == 200 )
727             return this.request.responseXML;
728     },
729
730     encodeParams: function (params)
731     {
732         var sep = "";
733         var encoded = "";
734         for (var key in params) {
735             if (params[key] != null) {
736                 encoded += sep + key + '=' + encodeURIComponent(params[key]);
737                 sep = '&';
738             }
739         }
740         return encoded;
741     },
742
743     _send: function ( type, url, data, callback)
744     {
745         var context = this;
746         this.callback = callback;
747         this.async = true;
748         this.request.open( type, url, this.async );
749         for (var key in this.requestHeaders)
750             this.request.setRequestHeader(key, this.requestHeaders[key]);
751         this.request.onreadystatechange = function () {
752             context._handleResponse(url); /// url used ONLY for error reporting
753         }
754         this.request.send(data);
755     },
756
757     _urlAppendParams: function (encodedParams)
758     {
759         if (encodedParams)
760             return this.url + "?" + encodedParams;
761         else
762             return this.url;
763     },
764
765     _handleResponse: function (savedUrlForErrorReporting)
766     {
767         if ( this.request.readyState == 4 ) { 
768             // pick up appplication errors first
769             var errNode = null;
770             if (this.request.responseXML &&
771                 (errNode = this.request.responseXML.documentElement)
772                 && errNode.nodeName == 'error') {
773                 var errMsg = errNode.getAttribute("msg");
774                 var errCode = errNode.getAttribute("code");
775                 var errAddInfo = '';
776                 if (errNode.childNodes.length)
777                     errAddInfo = ': ' + errNode.childNodes[0].nodeValue;
778                            
779                 var err = new Error(errMsg + errAddInfo);
780                 err.code = errCode;
781             
782                 if (this.errorHandler) {
783                     this.errorHandler(err);
784                 }
785                 else {
786                     throw err;
787                 }
788             } else if (this.request.status == 200 && 
789                        this.request.responseXML == null) {
790                 var err = new Error("XML response is empty but no error " +
791                                     "for " + savedUrlForErrorReporting);
792                 err.code = -1;
793                 if (this.errorHandler) {
794                     this.errorHandler(err);
795                 } else {
796                     throw err;
797                 }
798             } else if (this.request.status == 200) {
799                 this.callback(this.request.responseXML);
800             } else {
801                 var err = new Error("HTTP response not OK: " 
802                             + this.request.status + " - " 
803                             + this.request.statusText );
804                 err.code = '00' + this.request.status;
805                 
806                 if (this.errorHandler) {
807                     this.errorHandler(err);
808                 }
809                 else {
810                     throw err;
811                 }
812             }
813         }
814     }
815 };
816
817 /*
818 ********************************************************************************
819 ** XML HELPER FUNCTIONS ********************************************************
820 ********************************************************************************
821 */
822
823 // DOMDocument
824
825 if ( window.ActiveXObject) {
826     var DOMDoc = document;
827 } else {
828     var DOMDoc = Document.prototype;
829 }
830
831 DOMDoc.newXmlDoc = function ( root )
832 {
833     var doc;
834
835     if (document.implementation && document.implementation.createDocument) {
836         doc = document.implementation.createDocument('', root, null);
837     } else if ( window.ActiveXObject ) {
838         doc = new ActiveXObject("MSXML2.DOMDocument");
839         doc.loadXML('<' + root + '/>');
840     } else {
841         throw new Error ('No XML support in this browser');
842     }
843
844     return doc;
845 }
846
847    
848 DOMDoc.parseXmlFromString = function ( xmlString ) 
849 {
850     var doc;
851
852     if ( window.DOMParser ) {
853         var parser = new DOMParser();
854         doc = parser.parseFromString( xmlString, "text/xml");
855     } else if ( window.ActiveXObject ) {
856         doc = new ActiveXObject("MSXML2.DOMDocument");
857         doc.loadXML( xmlString );
858     } else {
859         throw new Error ("No XML parsing support in this browser.");
860     }
861
862     return doc;
863 }
864
865 DOMDoc.transformToDoc = function (xmlDoc, xslDoc)
866 {
867     if ( window.XSLTProcessor ) {
868         var proc = new XSLTProcessor();
869         proc.importStylesheet( xslDoc );
870         return proc.transformToDocument(xmlDoc);
871     } else if ( window.ActiveXObject ) {
872         return document.parseXmlFromString(xmlDoc.transformNode(xslDoc));
873     } else {
874         alert( 'Unable to perform XSLT transformation in this browser' );
875     }
876 }
877  
878 // DOMElement
879
880 Element_removeFromDoc = function (DOM_Element)
881 {
882     DOM_Element.parentNode.removeChild(DOM_Element);
883 }
884
885 Element_emptyChildren = function (DOM_Element)
886 {
887     while( DOM_Element.firstChild ) {
888         DOM_Element.removeChild( DOM_Element.firstChild )
889     }
890 }
891
892 Element_appendTransformResult = function ( DOM_Element, xmlDoc, xslDoc )
893 {
894     if ( window.XSLTProcessor ) {
895         var proc = new XSLTProcessor();
896         proc.importStylesheet( xslDoc );
897         var docFrag = false;
898         docFrag = proc.transformToFragment( xmlDoc, DOM_Element.ownerDocument );
899         DOM_Element.appendChild(docFrag);
900     } else if ( window.ActiveXObject ) {
901         DOM_Element.innerHTML = xmlDoc.transformNode( xslDoc );
902     } else {
903         alert( 'Unable to perform XSLT transformation in this browser' );
904     }
905 }
906  
907 Element_appendTextNode = function (DOM_Element, tagName, textContent )
908 {
909     var node = DOM_Element.ownerDocument.createElement(tagName);
910     var text = DOM_Element.ownerDocument.createTextNode(textContent);
911
912     DOM_Element.appendChild(node);
913     node.appendChild(text);
914
915     return node;
916 }
917
918 Element_setTextContent = function ( DOM_Element, textContent )
919 {
920     if (typeof DOM_Element.textContent !== "undefined") {
921         DOM_Element.textContent = textContent;
922     } else if (typeof DOM_Element.innerText !== "undefined" ) {
923         DOM_Element.innerText = textContent;
924     } else {
925         throw new Error("Cannot set text content of the node, no such method.");
926     }
927 }
928
929 Element_getTextContent = function (DOM_Element)
930 {
931     if ( typeof DOM_Element.textContent != 'undefined' ) {
932         return DOM_Element.textContent;
933     } else if (typeof DOM_Element.text != 'undefined') {
934         return DOM_Element.text;
935     } else {
936         throw new Error("Cannot get text content of the node, no such method.");
937     }
938 }
939
940 Element_parseChildNodes = function (node)
941 {
942     var parsed = {};
943     var hasChildElems = false;
944
945     if (node.hasChildNodes()) {
946         var children = node.childNodes;
947         for (var i = 0; i < children.length; i++) {
948             var child = children[i];
949             if (child.nodeType == Node.ELEMENT_NODE) {
950                 hasChildElems = true;
951                 var nodeName = child.nodeName; 
952                 if (!(nodeName in parsed))
953                     parsed[nodeName] = [];
954                 parsed[nodeName].push(Element_parseChildNodes(child));
955             }
956         }
957     }
958
959     var attrs = node.attributes;
960     for (var i = 0; i < attrs.length; i++) {
961         var attrName = '@' + attrs[i].nodeName;
962         var attrValue = attrs[i].nodeValue;
963         parsed[attrName] = attrValue;
964     }
965
966     // if no nested elements, get text content
967     if (node.hasChildNodes() && !hasChildElems) {
968         if (node.attributes.length) 
969             parsed['textContent'] = node.firstChild.nodeValue;
970         else
971             parsed = node.firstChild.nodeValue;
972     }
973     
974     return parsed;
975 }
976
977 /* do not remove trailing bracket */
978 }