<@doc> Implements a predictive parser for dynamic expressions (c) SAP AG 2003-2006. All rights reserved. #INCLUDE[defs.inc] /////////////////////////////////////////////////////////////////////// // CLASS DEFINITION class DynExpParserFS inherit dev:DynExpParser; readonly property contextNode = null; readonly property relativeNode = null; readonly property parseOldFieldFormat = false; //this is used only for freestyle, and is set to true only for migration of old models from readonly property translations = {}; readonly property translationScope = ''; readonly property translationKeys = null; //FORMAT in CE is not translatable, so there is only 1 obligatory arg - the text itself override readonly property FORMAT_OBLIGATORY_ARGS = 1; <@doc>Lists the supported function groups override readonly property supportedFuncGroups = {s:true, n:true, d:true, t:true, c:true, b:true, m:true, o:true}; /////////////////////////////////////////////////////////////////////// // CONSTRUCTOR <@doc scope="private"> Parser constructor. constructor() //add support for NULL token this.addRule(this.isNull); this.addFuncValidation('DATETIME', this.validateFuncDATETIME); this.supercall(); //change description for FORMAT macro, it's different for CE and A1S var format = this.Class.metadata.funcTable['FORMAT']; if(!format) return; format.label = 'FORMAT(text,parameter list)'; format.args = ['s','s*']; format.descr = "#TEXT[XTOL_FUNCTION_FORMAT_FOR_CE]"; end /////////////////////////////////////////////////////////////////////// // PUBLIC METHODS <@doc scope="public"> Sets the parser's context node The context node full name method setContextNode(nodeFullName) this.contextNode = nodeFullName; end <@doc scope="public"> Skip checking implicit conversion or not skip checking implicit conversion or not override method setCheckForceExpectedType(bool) this.checkForceExpected = bool; end <@doc scope="public"> Sets the parser's context node only for xglPrint() function TODO: remove this as soon as the mapping will be fixed (use setContextNode in expressions) The context node full name method setRelativeNode(nodeFullName) this.relativeNode = nodeFullName; end <@doc scope="public"> Clears the parser's context node method clearContextNode() this.contextNode = null; end <@doc scope="public"> Clears the parser's relative node - see setRelativeNode for moer information method clearRelativeNode() this.relativeNode = null; end method setParsingOldFieldFormatByID(parseOldFieldFormat) //in patterns - this argument is not relevant // in freestyle - set to true in migration case, where field parse should be done by ID // must be called after set context, becasue set context sets this flag to false. this.parseOldFieldFormat = parseOldFieldFormat; end // Resets the translation collection for XGL use method resetTranslations() this.translations = {user:{},'default':{}}; end // Resets the translation scope to 'user'/'default' for XGL use method setTranslationScope(scope) this.translationScope = scope; end <@doc scope="protected"> reset translation properties override method resetTranslation() this.translationKeys = {}; end // add NULL Rule override method isNull(expr) if (expr.match(/^(null)/)!= null) { this.lexpos=this.lexpos+4; return {token:'VAL',type:'u', value:RegExp.lastMatch}; } return null; end <@doc scope="public"> Prepares a literal for printing method formatLiteral(value,type) if (undefined === value) value = this.source; type = type || 's'; switch (type) { case 's': return '"' + value.replace(/\\/g,'\\\\').replace(/\"/g,'\\"') + '"'; case 'c': case 'b': case 'u': case 'n': case 'd': case 't': return value + ''; default : return ''; } end /////////////////////////////////////////////////////////////////////// // OVERRIDE METHODS override method normalizeStringToken(str) str = str.slice(1,-1).replace(/\\\\/g,'\\').replace(/\\\'/g,'\'').replace(/\\\"/g,'"'); return str; end override method setContextField(field) this.field = ISA(field, 'dev:IField') ? field : null; this.parseOldFieldFormat = false; ///always!!! end <@doc scope="public"> Sets the parser context Notice! If you'll call the setParseById method - call it after setContext (or the setDynExpContext rule) because setContext will override the parseById flag. This overriding if to clear ContextNode or RelativeNode. The context infoset enumerator The context infoset The context field optional - true by default - if the expression is by field ID override method setContext(infosetEnum, infoset, field, parseByID) this.supercall(infosetEnum, infoset, field, parseByID); this.clearRelativeNode(); this.clearContextNode(); end <@doc scope="protected"> Validates the syntax of TRANSLATE/FORMAT TRANLSATE and FORMAT behaves differently in CE and in A1S FORMAT in CE ============= 1)1 obligatory arg which is the text. the text might contains place holders for the rest of FORMAT's arguments (FORMAT in A1S has 2 obligatory args). 2)The first FORMAT arg can be either a text or a TRANSLATE macro. 3)FORMAT is not translatable in CE thus there's no translation type parameter TRANSLATE in CE ============== 1)since the macro is persisted to the gml file as it is displayed to user (not so in A1S) we always validating its arguments (in A1S validating only in parseByName mode). 2)if the trans type is missing it is added automatically. so, for the user the second arg for TRANSLATE is optional. the func handled override method validateTransFunc(token) if(token.token == this.TOKEN_FUNC) { var tokenValue = token.value; var child0 = token.children && token.children[0]; var child1 = token.children && token.children[1] && token.children[1].value; if(tokenValue == this.TOKEN_FUNC_TRANSLATE) { //first argument for TRANSLATE should be literal. if(child0 && child0.token != 'VAL') throw new Error(-1, '#TEXT[XMSG_ERROR_INVALID_ARG_FORMAT] ' + tokenValue); //validate translation type for TRANSLATE, if missing add it automatically if(child1==null || child1==undefined) { token.children[1] = {token:'VAL', value:this.transType, type:'s'}; token.size=2; this.warning= '#TEXT[XMSG_INVALID_TRANSLATION_TYPE] '.replace('{0}',this.transType); } //if trans type is missing - add the deafult else if(!this.checkTranslationType(child1)) { token.children[1].value = this.transType; this.warning= '#TEXT[XMSG_INVALID_TRANSLATION_TYPE] '.replace('{0}',this.transType); } } if(tokenValue == this.TOKEN_FUNC_FORMAT) { //first arg for FORMAT need to be text - either literal or by TRANSLATE macro if(child0 && child0.token != 'VAL' && child0.value != this.TOKEN_FUNC_TRANSLATE) throw new Error(-1, '#TEXT[XMSG_ERROR_FIRST_ARG_FOR_FORMAT]'); //validate no. of argument vs. place holders for FORMAT var text = (child0.token == 'VAL')? child0.value : child0.children[0].value; var total = this.handleDynamicArgs(token, text); if (token.size > total) throw new Error(-1, '#TEXT[XMSG_ERROR_TOO_MANY_ARGS] '+tokenValue); if (token.size < total ) throw new Error(-1, '#TEXT[XMSG_ERROR_MISSING_ARG_FUNC] '+tokenValue); } } end // DATETIME LITERAL override method isDateTimeRule(expr) if (this.isDateTime(expr)) { this.lexpos += RegExp.lastIndex; var date = this.formatDate(RegExp.$1, RegExp.$2, RegExp.$3); if (date == this.VALIDATION_ERROR) { throw new Error(-1, '#TEXT[XMSG_INVALID_DATETIME_DATE]'.replace("{1}", RegExp.lastMatch)); } var time = this.formatTime(RegExp.$4, RegExp.$5, (RegExp.$6||'').slice(1)); if (time == this.VALIDATION_ERROR) { throw new Error(-1, '#TEXT[XMSG_INVALID_DATETIME_TIME]'.replace("{1}", RegExp.lastMatch)); } var dt = RegExp.lastMatch; var tmz = this.formatTMZ(TRIM(RegExp.$7)); if (tmz == this.VALIDATION_ERROR) { throw new Error(-1, '#TEXT[XMSG_INVALID_DATETIME_TMZ]'.replace("{1}", dt)); } var datetime = TRIM(date + ' ' + time + ' ' + tmz); return {token:'VAL', value:datetime, type:'c'}; } return null; end <@doc scope="public"> set the rule for validating field expression field full name token represents the field override method isBOField(expr) if(expr.indexOf("@")>=0 && expr.match(/^(\[([^\]]+)\]|([\w\.^\]]+))?\@(\[([^\]]+)\]|(\@)|([\w\.]+))+/)) { this.lexpos += RegExp.lastIndex; return this.validateField(RegExp.lastMatch); } return null; end <@doc scope="private"> checks if field name is valid the field name structure: infoshape1@infoshape2.field1.field2 when parsing by id - the structure is: AH@infoshape2.field1.field2 where AH is the infoshape 1 id (only the main infoshape is persistent with its id. the full name after @ remains the sme for persistence) field full name token represents the field method validateField(expression) var theInfosets = [], infoHelpers = [], pair = []; var identifier = null; var fieldName="", fieldId="", InfosetNameforErr=""; //split into 2 sections: infoshapes names and fields names //this.split(expression, '@', pair, true); pair = expression.split('@'); //root infoset //======================= if(pair[0].length > 0) //foreign field { try { identifier = this.handleReservedChar(pair[0], true); //get the infoshape obj var allInfosets = this.infosetEnum.getInfosetList(false); if(this.parseByID && allInfosets[identifier]){ theInfosets.push(allInfosets[identifier]); infoHelpers.push(allInfosets[identifier].getInfoHelper()); } else { //get list of all infosets with that name for(var set in allInfosets) if(allInfosets[set].getInfosetAlias() == identifier) { theInfosets.push(allInfosets[set]); infoHelpers.push(allInfosets[set].getInfoHelper()); } } } catch(e) //catch the error of wrong infoshape name { } //not found the infoset sent if (!theInfosets || theInfosets.length == 0){ throw new Error(-1, '#TEXT[XMSG_ERROR_MISSING_ITERACTOR]'.replace('{0}',pair[0]||''));} } else { //local field if (!this.currInfoset) //can happen with VC-debugger - they set the currInfoset to null because they are context-unaware throw new Error(-1, '#TEXT[XMSG_ERROR_MISSING_ITERACTOR_PART]'.replace(/\{0\}/g,expression)); //check if sub infoshape of current infoset theInfosets.push(this.currInfoset); infoHelpers.push(this.currInfoset && this.currInfoset.getInfoHelper()); InfosetNameforErr = this.handleReservedChar(this.currInfoset && this.currInfoset.getInfosetAlias() || ''); } //child infoshapes & fields //============= var infoHelper = null; var theField = null; var tmpField = null var theInfoset = null; for(var i=0, len=infoHelpers.length; i1? self.formatLiteral(c[1].value,'s') : "XFLD"); buf.push('"' + self.genTranslationKey(text, tType) + '"'); //MD5 } else for (var i=0,len=token.size; i Add the text and its tType into the translation map The text to be added into the translation map The tType of the text The text key method genTranslationKey(text, tType) text = text.replace(/^\s*[\'\"](.*?)[\'\"]\s*$/,'$1'); tType = tType.replace(/^\s*[\'\"](.*?)[\'\"]\s*$/,'$1'); tType = this.infosetEnum.printTransType && this.Class.metadata.translationTypes[tType] || "label"; var uniqueId = text + tType; var scope = this.translationScope || 'user'; var mt = this.translations[scope][uniqueId], key; if (!mt) { key = MD5(uniqueId); mt = {key:key, text:text, tType:tType, scope:scope}; this.translations[scope][uniqueId] = mt; } this.translationKeys[mt.key] = mt; return mt.key; end <@method name="addTranslationKey"> Add the given translation parameters into the translation map The MD5 unique id The text to be translated The tType of the text The scope of the text - "default" or "user" method addTranslationKey(key, text, tType, scope) var uniqueId = text + tType; var mt = this.translations[scope][uniqueId]; if (!mt) { mt = {key:key, text:text, tType:tType, scope:scope}; this.translations[scope][uniqueId] = mt; } end <@doc scope="public"> check if the translation type is correct If not valid, an exception is thrown translation type override method checkTranslationType(ttype) if(!this.Class.metadata.translationTypes[ttype]) //throw new Error(-1, '#TEXT[XMSG_ERROR_MISSING_TRANS_TYPE]'.replace('{0}','')); return false; return true; end /////////////////////////////////////////////////////////////////////// // PRIVATE METHODS <@doc scope="private"> validate DATETIME() the parser token that contains the function method validateFuncDATETIME(fToken) var childs = fToken.children; // Date - children 0-2 must be numeric literals to enable validation if (childs[0].token == 'VAL' && childs[0].type == 'n' && childs[1].token == 'VAL' && childs[1].type == 'n' && childs[2].token == 'VAL' && childs[2].type == 'n') if (this.formatDate(childs[2].value, childs[1].value, childs[0].value) == this.VALIDATION_ERROR) return(-1, '#TEXT[XMSG_INVALID_DATETIME_DATE]'.replace("{1}", this.genericPrint(true, fToken))); // Time - children 3-5 must be numeric literals to enable validation, note that child 6 is optional if (childs[3].token == 'VAL' && childs[3].type == 'n' && childs[4].token == 'VAL' && childs[4].type == 'n' && (!childs[5] || childs[5].token == 'VAL' && childs[5].type == 'n')) if (this.formatTime(childs[3].value, childs[4].value, childs[5] && childs[5].value || "0") == this.VALIDATION_ERROR) return(-1, '#TEXT[XMSG_INVALID_DATETIME_TIME]'.replace("{1}", this.genericPrint(true, fToken))); // child 6 - which is optional - must be a string literal to enable validation if (!childs[6] || childs[6].token != 'VAL' || childs[6].type != 's') return(''); if (this.formatTMZ(childs[6].value) == this.VALIDATION_ERROR) return(-1, '#TEXT[XMSG_INVALID_DATETIME_TMZ]'.replace("{1}", this.genericPrint(true, fToken))); return(''); end <@doc scope="private"> validate TIME() the defined timezone method formatTMZ(tmz) if (tmz.length == 0) return(''); var error = false; if (tmz.match(/^[A-Z]{3}$/)) { //TMZ code ... if (!this.Class.metadata.tmzToOffset[tmz]) //... but unrecognized one error = true; } else if (tmz.match(/^([+-])(\d+)[:](\d+)$/)) { if (parseInt(RegExp.$2+RegExp.$3) > 2300 || RegExp.$1.length > 2 || (RegExp.$3 !="00" && RegExp.$3 !="30")) error = true; if (RegExp.$2.length == 1) tmz=RegExp.$1+"0"+RegExp.$2+":"+RegExp.$3; } else if ("Z" != tmz) { error = true; } if (error) return(this.VALIDATION_ERROR); return(tmz); end /////////////////////////////////////////////////////////////////////////// // END OF CLASS