@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