2.2.2. fejezet, Kliens JavaScript receptek

A böngésző oldalak előállítását XSL transzformációk és JQuery utófeldolgozók végzik. AJAX objektum gyártása az alábbi módon történik:

var XMLHTTPREQUEST_TIMEOUT = 10000;
 
//AJAX objektum gyártása
function createXMLHttpRequest() {
  if ($.browser.msie){
    try { return new XMLHttpRequest(); } catch(e) {}
    try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
    try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
  } else {
    try { return new XMLHttpRequest(); } catch(e) {}
  }
  alert("XMLHttpRequest not supported");
  return null;
}

Fontos megjegyezni, hogy a JQuery forms modulja szintén alkalmas AJAX kommunikációra, ám a két irányú XML kommunikáció nem az erőssége. Ezért ha a programounk oda-vissza XML formában kommunikál a szerverrel, keressünk más megoldást.

A Client osztály transzformálja az XML dokumentumot HTML kóddá:

function Client(owner){
    this.init(owner);
    $.extend(this, new ActionDrivenObject(this, owner, 'client'));
}
 
Client.prototype = {
    init: function(owner){
        this.className = 'Client';
        this.jComponent = owner;
        this.httpReqObj = createXMLHttpRequest();
        this.xStyleSheets = new Object();
        this.staticXMLRefs = new Object();
        this.session = this.jComponent.xmlServer.sessionHandlerFactory.sessionHandler;
        this.containers = new Object();
    },
    addXSLT: function(styleName, path){
        this.xStyleSheets[styleName] = new Object();
        if ($.browser.mozilla || $.browser.opera) {
            this.httpReqObj.open("GET", path, false); //syncronous call with last param false
            this.httpReqObj.send(null);
            this.xStyleSheets[styleName].xslRef = this.httpReqObj.responseXML;
        }
        else 
            if ($.browser.msie) {
                var xsltProcessor = new ActiveXObject("MSXML2.FreeThreadedDomDocument");
                xsltProcessor.async = false;
                xsltProcessor.load(path);
                this.xStyleSheets[styleName].xsltProcessor = xsltProcessor;
            }
    },
    addStaticXMLRef: function(refName, xmlRef){
        this.staticXMLRefs[refName] = xmlRef;
    },
    loadTemplate: function(targetId, path){
        var targetObj = $(targetId);
        if (targetObj != null) {
            this.httpReqObj.open("GET", path, false); //syncronous call with last param false
            this.httpReqObj.send(null);
            var tpl = this.httpReqObj.responseText;
            if (tpl != null) {
                $(targetObj).html(tpl);
            }
        }
    },
    transform: function(targetId, styleName, xmlRef, params){
        var containerDoc = this.createDocument();
        containerDoc.appendChild(xmlRef);
        var targetObj = $(targetId);
        var ready = true;
        if ((targetObj.length > 0) && (this.xStyleSheets[styleName] != undefined)) {
            if (($.browser.mozilla || $.browser.opera) && (this.xStyleSheets[styleName].xslRef != null)) {
                var xsltProcessor = new XSLTProcessor();
                xsltProcessor.reset();
                xsltProcessor.importStylesheet(this.xStyleSheets[styleName].xslRef);
 
                if (params != null) 
                    for (paramName in params) {
                        xsltProcessor.setParameter(null, paramName, params[paramName]);
                    }
                try {
                    fragment = xsltProcessor.transformToFragment(containerDoc, document);
                    $(targetObj).empty();
                    $(targetObj).html(fragment);
                } 
                catch (ex) {
                    ready = false;
                    alert("Exception in transformation (" + ex.name + ":" + ex.message + ")");
                }
            }
            else 
                if (($.browser.msie) && (this.xStyleSheets[styleName].xsltProcessor != null)) {
                    var objCompiled = new ActiveXObject("MSXML2.XSLTemplate");
                    objCompiled.stylesheet = this.xStyleSheets[styleName].xsltProcessor.documentElement;
                    var objXSLProc = objCompiled.createProcessor();
                    objXSLProc.input = containerDoc;
                    if (params != null) 
                        for (paramName in params) {
                            objXSLProc.addParameter(paramName, params[paramName]);
                        }
                    try {
                        objXSLProc.transform();
                        var fragmentText = objXSLProc.output;
                        $(targetObj).html(fragmentText);
                    } 
                    catch (ex) {
                        ready = false;
                        alert("Exception in transformation: (" + ex.name + ":" + ex.message + ")");
                    }
                }
        }
        return ready;
    },
    transform2Table: function(params){
        var pageContent = params.container.actions.getcontent.params.content;
        if ((pageContent != undefined) && (pageContent.value != undefined) && (pageContent.value != null)) {
            var xslParams = new Object();
            xslParams.editable = params.editable;
            xslParams.fielddefs = params.container.getTableDefDoc();
            xslParams.target = params.target;
            var items = $('> *', pageContent.value);
            if (items.length > 0) 
                if (JComponent.client.transform(params.target, 'table', items[0], xslParams)) 
                    if (params.onTransformReady != undefined) 
                        params.onTransformReady(params.containerName, params.target);
        }
 
    },
    transform2Editor: function(params){
        var pageContent = params.container.actions.getcontent.params.content;
        if ((pageContent != undefined) && (pageContent.value != undefined) && (pageContent.value != null)) {
            var xslParams = new Object();
            xslParams.fielddefs = params.container.getTableDefDoc();
            xslParams.iframesrc = params.iframesrc;
            var items = $('> *', pageContent.value);
            if (items.length > 0) 
                if (JComponent.client.transform(params.target, 'editor', items[0], xslParams)) 
                    if (params.onTransformReady != undefined) 
                        params.onTransformReady(params.containerName, params.target);
        }
 
    }
}

Látható, hogy a kliens hierarchiája követi a szerver felépítését. Így például a Session objektumig a this.jComponent.xmlServer.sessionHandlerFactory útvonal vezet. Minden objektum hivatkozik a szülőjére, és a tartalmazottjaira. Ezen felül minden esemény vezérelt osztály az ActionDrivenObject osztályból származik. Ez végzi a válasz XML-ek kezelését, és van egy targets tulajdonsága, ahol a tartalmazott objektumok általános formában elérhetők.

function ActionDrivenObject(superClass, owner, name){
    this._init(superClass, owner, name);
}
 
ActionDrivenObject.prototype = {
    _init: function(superClass, owner, name){
        this.owner = owner;
        this.name = name;
        this.targets = new Object();
        this.actions = new Object();
        this.actions.getdescription = new GetDescriptionAction(superClass);
    },
    createDocument: function(){
        var xmlDoc = null;
        if (document.implementation && document.implementation.createDocument) {
            xmlDoc = document.implementation.createDocument("", "", null);
        }
        else 
            if (window.ActiveXObject) {
                xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
            }
        return xmlDoc;
    },
    getCallGroup: function(){
        return new CallObj(this.getTargetPath(), null, null, false);
    },
    getTargetPath: function(){
        var result = this.name;
        if ((this.owner != null) && (this.owner.getTargetPath != undefined)) 
            result = this.owner.getTargetPath() + pathSeparator + result;
        return result;
    },
    handleResponse: function(request){
        if (request.pathArray.length > 0) {
            var target = request.pathArray.shift();
            if (this.targets[target] != null) {
                this.targets[target].handleResponse(request);
            }
            else 
                if (request.action == 'getdescription') {
                    this.buildTarget(target);
                    if (this.targets[target] != null) 
                        this.targets[target].handleResponse(request);
                }
        }
        else {
            var action = this.actions[request.action];
            if (action != null) 
                action.processResponse(request);
            if ((request.params != undefined) && (typeof request.params == 'object')) {
                if (request.params.subrequests != undefined) {
                    var selfObj = this;
                    $(request.params.subrequests).find('> request').each(function(index){
                        var subrequest = new Request(this);
                        selfObj.handleResponse(subrequest);
                    });
                };
                            };
                    };
            },
    importAction: function(actionName, params){
        if (this.actions[actionName] == undefined) 
            this.actions[actionName] = new Action(this, actionName, params);
        else 
            this.actions[actionName].importParams(params);
    }
}

Fontos megjegyezni itt a superClass változó használatát. A JavaScript objektum orientáltsága látszólag a C++ és Delphi osztályokhoz hasonló, ám itt egy ősosztályt a leszármaztatottban egy az abból létrehozott objektum reprezentál. Így a leszármaztatott objektum referenciájára hivatkozás nagyon fontos egy leszármaztatott osztály inicializálásakor felfűzött eseménynél. Ha az owner értéke a this objektum lenne a GetDescriptorAction konstruktoránál, az ActionDrivenObject leszármaztatottjaiban nem működne helyesen ez az esemény, mert nem a leszármazottba importálná az eseményeket, hanem az ősosztályba - jelen esetben az ActionDrivenObject-be -, ahonnan nem látszanak a leszármaztatott tulajdonságai és metódusai.

A GetDescriptorAction végzi az XML-ben leírt metódusok importálását

function GetDescriptionAction(owner){
  this.init(owner);
  $.extend(this,new GenericAction(owner,'getdescriptoraction',new Object()));
  this.params.descriptor = new Object();
  this.params.descriptor.direction="out";
  this.params.fulldescriptor = new Object();
  this.params.fulldescriptor.direction="in";
}
 
GetDescriptionAction.prototype = {
  init: function(owner){
    this.className='GetDescriptorAction';
  },
  generateCallObj: function(fulldescriptor){
    this.params.fulldescriptor.value=fulldescriptor;
    var callObj = new CallObj(this.owner.getTargetPath(),'getdescription',this.params,false);
    return callObj;
  },
  call: function(){
    var callObj = this.generateCallObj();
    var transactObj = new TransactObj();
    transactObj.addCall(callObj);
    this.owner.getXMLServer().commObj.addTransactObj(transactObj);
    this.owner.getXMLServer().commObj.sendAllTransactObj();
  },
  processResponse: function(request){
    if ((request.params!=null)){
      var selfObj = this;
      if (request.params.descriptor!=null){
	      this.params.descriptor.value = request.params.descriptor;
	      if (this.owner.actions == undefined)
	        this.owner.actions = new Object();
	      var actions = this.owner.actions;
	      if (selfObj.owner.className != undefined)
		      $(this.params.descriptor.value).find(selfObj.owner.className.toLowerCase()+'descriptor > actions > actiondescriptor').each(function(index){
		        var actionName = $(this).attr('name');
		        var tmpParams = new Object();
		        $(this).find('> params > param').each(function(index){
		          var paramName = $(this).attr('name');
		          tmpParams[paramName]=new Object();
		          tmpParams[paramName].direction = $(this).attr('direction');
		          tmpParams[paramName].optional = $(this).attr('optional');
		          tmpParams[paramName].type = $(this).attr('type');
		          tmpParams[paramName].order = parseInt($(this).attr('order'));
		          if (isNaN(tmpParams[paramName].order))
	                 tmpParams[paramName].order = -1;
		        });
		        selfObj.owner.importAction(actionName,tmpParams);
		        if (actions[actionName].generateCallObj==undefined){          
		          var tmpArray = new Array();
		          for(paramName in tmpParams){
		            if ((tmpParams[paramName].direction=="in")&&(tmpParams[paramName].order>-1)){
		              tmpArray[tmpParams[paramName].order]=paramName;
		            };
		          };
 
		          var inParams = new Array();
		          var cnt=0;
		          for (var i=0;i<tmpArray.length;i++)
		            if ((tmpArray[i]!=null)&&(tmpArray[i]!=undefined))
		              inParams[cnt++]=tmpArray[i];
 
		          var fnStr = new String();
		          for (var j=0;j<inParams.length;j++)
		            fnStr += "this.params."+inParams[j]+".value="+inParams[j]+";";
 
		          fnStr+="return new CallObj(this.owner.getTargetPath(),'"+actionName+"',this.params,false);";
		          actions[actionName].importMethod('generateCallObj',inParams,fnStr);
		        };
		      });
	    };
	  };
  }
}

Az Action ősosztálya egy általános eseménykezelő, ami a válasz XML feldolgozása közben a metódusok paramétereit importálja. A válasz XML kimenő paramétereit írja be az esemény paramétereibe. Az XSLT erőforrás igényes művelet, ha kiemeljük az XML-ből a lényeges részeket, gyorsíthatja a feldolgozást.

function Action(owner,name,params){
  this.init();
  $.extend(this,new GenericAction(owner,name,params));
}
 
Action.prototype = {
  init: function(){
    this.className='Action';
  },
  processResponse: function(request){
    if ((request.params!=null)){
      for(var paramName in request.params){
        if ((this.params != undefined)&&(this.params[paramName] != undefined)){
          if (this.params[paramName].type != undefined){
            var param = request.params[paramName];
            switch(this.params[paramName].type){
              case 'java.lang.String': this.params[paramName].value = $(param).text();break;
              case 'java.lang.Boolean': this.params[paramName].value = ($(param).text()=='true');break;
              case 'java.lang.Integer': this.params[paramName].value = parseInt($(param).text());break;
              case 'org.w3c.dom.Element': this.params[paramName].value = param.cloneNode(true);break;
            }
          } else
            this.params[paramName].value = request.params[paramName];
        } else if (paramName!='subrequests'){
            alert(paramName+" of "+this.name+"("+this.className+") of "+this.owner.name+"("+this.owner.className+") is undefined");
            this.params[paramName]=new Object();
            this.params[paramName].value = request.params[paramName];
        }
      }
    }
  }
}

A GenericAction mint az Action ősosztálya, metódusok importálására képes. Így jön létre a generateCallObject metódus, aminek a paramétereit az eseményt leíró XML-ből emeli be. Az esemény így más kliens nyelvekre - mint mondjuk a Delphi pascal-ja - egyszerűen portolható, a SOAP-hoz és a WSDL-hez hasonlóan.

function GenericAction(owner,name,params){
  this._init(owner,name,params);
}
 
GenericAction.prototype = {
  _init: function(owner,name,params){
    this.owner = owner;
    this.name = name;
    this.params = params;
  },
  importMethod: function(methodName,inParams,fnStr){
    if ($.browser.mozilla){
     var tmpStr = "function("+(inParams!=null?inParams.join(','):"")+"){";
     tmpStr+=fnStr;
	 tmpStr+="}";
	 this[methodName]=eval(tmpStr,this);
    } else if ($.browser.msie){
      if (inParams==null)
        this[methodName]=new Function(fnStr);
      else
        this[methodName]=new Function(inParams,fnStr);
    };
  },
  importParams: function(allParams){
    for(paramName in allParams){
      if ((this.params[paramName]==undefined)||(this.params[paramName]==null)){
        this.params[paramName]=new Object();
        $.extend(true,this.params[paramName],allParams[paramName]);
      } else {
        for(paramPropName in allParams[paramName])
          this.params[paramName][paramPropName]=allParams[paramName][paramPropName];
      }
    }
  }
}

JavaScript-ben vagy egy új Function objektum létrehozásánál kerüljük az alábbi függvénytörzs tartalmat:

actions.delete.generateCallObj()

A delete helyette használjunk más metódusnevet. Például:

actions.rmrecord.generateCallObj()

A Firefox és Mozilla helyesen értelmezi, ám az Internet Explorer a delete-et mint kulcsszót fordítja. Ezért meglepődhetünk, mikor a "várt elem azonosító" hibaüzenetet kapjuk fordításkor.

A tartalom elküldése előtt azt általánosan feldolgozhatóvá kell tenni, mivel az AJAX komponensek böngészőnként eltérő kódolást használnak. Erre alkalmas a JavaScript encodeURI függvénye.

function CallObj(targetPath,actionName,params,checkPrior){
  this.init(targetPath,actionName,params,checkPrior);
}
 
CallObj.prototype = {
  init: function(targetPath,actionName,paramsObj,checkPrior){
    this.className='CallObj';
    this.targetPath = targetPath;
    this.actionName = actionName;
    this.paramsObj = paramsObj;
    this.checkPrior = checkPrior;
    this.subCalls = new Array();
  },
  xmlNorm: function(input){
    input = input.replace('&','&#38;');
    input = input.replace('>','&#62;');
    input = input.replace('<','&#60;');
    input = input.replace('©','&#169;');
    input = input.replace('®','&#174;');
    input = input.replace('™','&#x2122;');
    input = encodeURI(input);
    return input;
  },
  toString: function(){
    return this.getString(0);
  },
  addSubCall: function(callObj){
    if (callObj.targetPath.indexOf(this.targetPath)==0)
      callObj.targetPath = callObj.targetPath.substr(this.targetPath.length+1);
    this.subCalls.push(callObj);
  },
  getString: function(index){
    var result = "<call target='"+this.targetPath+"'"+(this.actionName!=null ? " action='"+this.actionName+"'" : "")+" order='"+index+"'"+(this.checkPrior?" checkprior='true'":"")+">";
    var params = new String()
    if (this.paramsObj!=null){
      for(var paramName in this.paramsObj)
        if(this.paramsObj[paramName].direction=="in"){
          var value = this.paramsObj[paramName].value;
          params += "<param name='"+paramName+"'>"+(value!=null?this.xmlNorm(value.toString()):"")+"</param>";
        }
      if (params!="")
        result += "<params>"+params+"</params>";
    }
    for(var i = 0; i<this.subCalls.length; i++){
      result += this.subCalls[i].getString(i);
    }
    result += "</call>";
    return result;
  }
}

Érdemes néhány speciális karaktert még bekódolni, mert a szerver XML felépítője érzékeny lehet ezekre. Így került be az & karakter átalakítása az xmlNorm függvényben. Szerver oldalon a visszaalakítást szintén meg kell tennünk. Erre használható az URLDecoder osztály.

String result = URLDecoder.decode(param.toString(), "utf-8");

JavaScript esetén is előfordulhat memória szivárgás (memory leak). Ezek megértésére mindenképp szenteljünk időt, mert sok memóriát elfogyaszthat egy figyelmetlenül megtervezett alkalmazás. Mi esetünkben ez a beérkezett XML feldolgozásánál jelentkezett. Ezért az XML feldolgozását, átalakítását JavaScript helyett XSLT-vel végezzük.