@sap/odata-v4
Version:
OData V4.0 server library
1,095 lines (1,005 loc) • 81.6 kB
JavaScript
'use strict';
const UriHelper = require('./UriHelper');
const UriTokenizer = require('./UriTokenizer');
const TokenKind = UriTokenizer.TokenKind;
const KeyPredicateParser = require('./KeyPredicateParser');
const ExpressionKind = require('./Expression').ExpressionKind;
const AliasExpression = require('./AliasExpression');
const BinaryExpression = require('./BinaryExpression');
const BinaryOperatorKind = BinaryExpression.OperatorKind;
const LiteralExpression = require('./LiteralExpression');
const MemberExpression = require('./MemberExpression');
const MethodExpression = require('./MethodExpression');
const MethodKind = MethodExpression.MethodKind;
const TypeLiteralExpression = require('./TypeLiteralExpression');
const UnaryExpression = require('./UnaryExpression');
const UnaryOperatorKind = UnaryExpression.OperatorKind;
const EdmTypeKind = require('../edm/EdmType').TypeKind;
const EdmPrimitiveType = require('../edm/EdmPrimitiveType');
const EdmPrimitiveTypeKind = require('../edm/EdmPrimitiveTypeKind');
const FullQualifiedName = require('../FullQualifiedName');
const UriResource = require('./UriResource');
const ResourceKind = UriResource.ResourceKind;
const UriParameter = require('./UriParameter');
const ValueValidator = require('../validator/ValueValidator');
const valueValidator = new ValueValidator();
const UriSyntaxError = require('../errors/UriSyntaxError');
const UriQueryOptionSemanticError = require('../errors/UriQueryOptionSemanticError');
const UriSemanticError = require('../errors/UriSemanticError');
const ErrorNames = require('../errors/AbstractError').ErrorNames;
const FeatureSupport = require('../FeatureSupport');
const tokenToPrimitiveType = new Map([
// Enum and null are not present in the map. These have to be handled differently.
[TokenKind.BooleanValue, EdmPrimitiveTypeKind.Boolean],
[TokenKind.StringValue, EdmPrimitiveTypeKind.String],
// Very large integer values are of type Edm.Decimal but this is handled elsewhere.
[TokenKind.IntegerValue, EdmPrimitiveTypeKind.Int64],
[TokenKind.GuidValue, EdmPrimitiveTypeKind.Guid],
[TokenKind.DateValue, EdmPrimitiveTypeKind.Date],
[TokenKind.DateTimeOffsetValue, EdmPrimitiveTypeKind.DateTimeOffset],
[TokenKind.TimeOfDayValue, EdmPrimitiveTypeKind.TimeOfDay],
[TokenKind.DecimalValue, EdmPrimitiveTypeKind.Decimal],
[TokenKind.DoubleValue, EdmPrimitiveTypeKind.Double],
[TokenKind.DurationValue, EdmPrimitiveTypeKind.Duration],
[TokenKind.BinaryValue, EdmPrimitiveTypeKind.Binary],
[TokenKind.GeographyPoint, EdmPrimitiveTypeKind.GeographyPoint],
[TokenKind.GeometryPoint, EdmPrimitiveTypeKind.GeometryPoint],
[TokenKind.GeographyLineString, EdmPrimitiveTypeKind.GeographyLineString],
[TokenKind.GeometryLineString, EdmPrimitiveTypeKind.GeometryLineString],
[TokenKind.GeographyPolygon, EdmPrimitiveTypeKind.GeographyPolygon],
[TokenKind.GeometryPolygon, EdmPrimitiveTypeKind.GeometryPolygon],
[TokenKind.GeographyMultiPoint, EdmPrimitiveTypeKind.GeographyMultiPoint],
[TokenKind.GeometryMultiPoint, EdmPrimitiveTypeKind.GeometryMultiPoint],
[TokenKind.GeographyMultiLineString, EdmPrimitiveTypeKind.GeographyMultiLineString],
[TokenKind.GeometryMultiLineString, EdmPrimitiveTypeKind.GeometryMultiLineString],
[TokenKind.GeographyMultiPolygon, EdmPrimitiveTypeKind.GeographyMultiPolygon],
[TokenKind.GeometryMultiPolygon, EdmPrimitiveTypeKind.GeometryMultiPolygon],
[TokenKind.GeographyCollection, EdmPrimitiveTypeKind.GeographyCollection],
[TokenKind.GeometryCollection, EdmPrimitiveTypeKind.GeometryCollection]]);
const tokenToBinaryOperator = new Map([
[TokenKind.OrOperator, BinaryOperatorKind.OR],
[TokenKind.AndOperator, BinaryOperatorKind.AND],
[TokenKind.EqualsOperator, BinaryOperatorKind.EQ],
[TokenKind.NotEqualsOperator, BinaryOperatorKind.NE],
[TokenKind.GreaterThanOperator, BinaryOperatorKind.GT],
[TokenKind.GreaterThanOrEqualsOperator, BinaryOperatorKind.GE],
[TokenKind.LessThanOperator, BinaryOperatorKind.LT],
[TokenKind.LessThanOrEqualsOperator, BinaryOperatorKind.LE],
[TokenKind.AddOperator, BinaryOperatorKind.ADD],
[TokenKind.SubOperator, BinaryOperatorKind.SUB],
[TokenKind.MulOperator, BinaryOperatorKind.MUL],
[TokenKind.DivOperator, BinaryOperatorKind.DIV],
[TokenKind.ModOperator, BinaryOperatorKind.MOD]]);
const tokenToMethod = new Map([
// 'cast' and 'isof' are handled specially.
[TokenKind.CeilingMethod, MethodKind.CEILING],
[TokenKind.ConcatMethod, MethodKind.CONCAT],
[TokenKind.ContainsMethod, MethodKind.CONTAINS],
[TokenKind.DateMethod, MethodKind.DATE],
[TokenKind.DayMethod, MethodKind.DAY],
[TokenKind.EndswithMethod, MethodKind.ENDSWITH],
[TokenKind.FloorMethod, MethodKind.FLOOR],
[TokenKind.FractionalsecondsMethod, MethodKind.FRACTIONALSECONDS],
[TokenKind.GeoDistanceMethod, MethodKind.GEODISTANCE],
[TokenKind.GeoIntersectsMethod, MethodKind.GEOINTERSECTS],
[TokenKind.GeoLengthMethod, MethodKind.GEOLENGTH],
[TokenKind.HourMethod, MethodKind.HOUR],
[TokenKind.IndexofMethod, MethodKind.INDEXOF],
[TokenKind.LengthMethod, MethodKind.LENGTH],
[TokenKind.MaxdatetimeMethod, MethodKind.MAXDATETIME],
[TokenKind.MindatetimeMethod, MethodKind.MINDATETIME],
[TokenKind.MinuteMethod, MethodKind.MINUTE],
[TokenKind.MonthMethod, MethodKind.MONTH],
[TokenKind.NowMethod, MethodKind.NOW],
[TokenKind.RoundMethod, MethodKind.ROUND],
[TokenKind.SecondMethod, MethodKind.SECOND],
[TokenKind.StartswithMethod, MethodKind.STARTSWITH],
[TokenKind.SubstringMethod, MethodKind.SUBSTRING],
[TokenKind.TimeMethod, MethodKind.TIME],
[TokenKind.TolowerMethod, MethodKind.TOLOWER],
[TokenKind.TotaloffsetminutesMethod, MethodKind.TOTALOFFSETMINUTES],
[TokenKind.TotalsecondsMethod, MethodKind.TOTALSECONDS],
[TokenKind.ToupperMethod, MethodKind.TOUPPER],
[TokenKind.TrimMethod, MethodKind.TRIM],
[TokenKind.YearMethod, MethodKind.YEAR]]);
class ExpressionParser {
/**
* Create an expression parser.
* @param {Edm} edm entity data model
*/
constructor(edm) {
this._edm = edm;
}
/**
* Parse a string into an expression tree.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {?EdmType} referencedType type the expression references
* @param {?(EdmEntitySet[])} crossjoinEntitySets entity sets in case of a $crossjoin request
* @param {?Object} aliases alias definitions
* @returns {Expression} the root of the expression tree
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
*/
parse(tokenizer, referencedType, crossjoinEntitySets, aliases) {
this._tokenizer = tokenizer;
this._referringType = referencedType;
this._crossjoinEntitySets = crossjoinEntitySets;
this._aliases = aliases;
this._lambdaVariables = [];
return this._parseExpression();
}
/**
* Top-level parser method.
* @returns {Expression} the root of the expression tree
* @private
*/
_parseExpression() {
let left = this._parseAnd();
while (this._tokenizer.next(TokenKind.OrOperator)) {
this.checkType(left, EdmPrimitiveTypeKind.Boolean);
this.checkNoCollection(left);
let right = this._parseAnd();
this.checkType(right, EdmPrimitiveTypeKind.Boolean);
this.checkNoCollection(right);
left = new BinaryExpression(left, BinaryOperatorKind.OR, right, EdmPrimitiveTypeKind.Boolean);
}
return left;
}
/**
* Parse expression with zero or more 'and' operators.
* @returns {Expression} expression
* @private
*/
_parseAnd() {
let left = this._parseExprEquality();
while (this._tokenizer.next(TokenKind.AndOperator)) {
this.checkType(left, EdmPrimitiveTypeKind.Boolean);
this.checkNoCollection(left);
let right = this._parseExprEquality();
this.checkType(right, EdmPrimitiveTypeKind.Boolean);
this.checkNoCollection(right);
left = new BinaryExpression(left, BinaryOperatorKind.AND, right, EdmPrimitiveTypeKind.Boolean);
}
return left;
}
/**
* Parse expression with zero or more 'eq' or 'ne' operators.
* @returns {Expression} expression
* @private
*/
_parseExprEquality() {
let left = this._parseExprRel();
let operatorTokenKind = [TokenKind.EqualsOperator, TokenKind.NotEqualsOperator]
.find(kind => this._tokenizer.next(kind));
// Null for everything other than EQ or NE
while (operatorTokenKind != null) {
let right = this._parseExprEquality();
this._checkEqualityTypes(left, right);
left = new BinaryExpression(left, tokenToBinaryOperator.get(operatorTokenKind), right,
EdmPrimitiveTypeKind.Boolean);
operatorTokenKind = [TokenKind.EqualsOperator, TokenKind.NotEqualsOperator]
.find(kind => this._tokenizer.next(kind));
}
return left;
}
/**
* Parse expression with 'isof' method or with zero or more 'gt' or 'ge' or 'lt' or 'le' operators.
* @returns {Expression} expression
* @private
*/
_parseExprRel() {
if (this._tokenizer.next(TokenKind.IsofMethod)) {
// The isof method is a terminal. So no further operators are allowed.
return this._parseIsOfOrCastMethod(MethodKind.ISOF);
}
let left = this._parseExprAdd();
let operatorTokenKind =
[
TokenKind.GreaterThanOperator, TokenKind.GreaterThanOrEqualsOperator,
TokenKind.LessThanOperator, TokenKind.LessThanOrEqualsOperator
].find(kind => this._tokenizer.next(kind));
// Null for everything other than GT or GE or LT or LE
while (operatorTokenKind != null) {
let right = this._parseExprAdd();
this._checkRelationTypes(left, right);
left = new BinaryExpression(left, tokenToBinaryOperator.get(operatorTokenKind), right,
EdmPrimitiveTypeKind.Boolean);
operatorTokenKind =
[
TokenKind.GreaterThanOperator, TokenKind.GreaterThanOrEqualsOperator,
TokenKind.LessThanOperator, TokenKind.LessThanOrEqualsOperator
].find(kind => this._tokenizer.next(kind));
}
return left;
}
/**
* Parse expression with 'isof' or 'cast' method.
* @param {UriTokenizer.TokenKind} kind token kind
* @returns {MethodExpression} method expression
* @private
*/
_parseIsOfOrCastMethod(kind) {
// The TokenKind 'IsOfMethod' consumes also the opening parenthesis.
// The first parameter could be an expression or a type literal.
let parameters = [this._parseExpression()];
if (!(parameters[0].getKind() === ExpressionKind.TYPE_LITERAL)) {
// The first parameter is not a type literal, so there must be a second parameter.
this._tokenizer.requireNext(TokenKind.COMMA);
parameters.push(this._parseExpression());
// The second parameter must be a type literal.
if (!(parameters[1].getKind() === ExpressionKind.TYPE_LITERAL)) {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.TYPE_LITERAL,
this._tokenizer.getParseString(), this._tokenizer.getPosition());
}
}
this._tokenizer.requireNext(TokenKind.CLOSE);
return new MethodExpression(kind, parameters);
}
/**
* Parse expression with zero or more 'add' or 'sub' operators.
* @returns {Expression} expression
* @private
*/
_parseExprAdd() {
let left = this._parseExprMul();
let operatorTokenKind = [TokenKind.AddOperator, TokenKind.SubOperator]
.find(kind => this._tokenizer.next(kind));
// Null for everything other than ADD or SUB
while (operatorTokenKind != null) {
let right = this._parseExprMul();
let resultType = this._getAddSubTypeAndCheckLeftAndRight(left, right,
operatorTokenKind === TokenKind.SubOperator);
left = new BinaryExpression(left, tokenToBinaryOperator.get(operatorTokenKind), right, resultType);
operatorTokenKind = [TokenKind.AddOperator, TokenKind.SubOperator]
.find(kind => this._tokenizer.next(kind));
}
return left;
}
/**
* Parse expression with zero or more 'mul' or 'div' or 'mod' operators.
* @returns {Expression} expression
* @private
*/
_parseExprMul() {
let left = this._parseExprUnary();
let operatorTokenKind = [TokenKind.MulOperator, TokenKind.DivOperator, TokenKind.ModOperator]
.find(kind => this._tokenizer.next(kind));
// Null for everything other than MUL or DIV or MOD
while (operatorTokenKind != null) {
const operatorKind = tokenToBinaryOperator.get(operatorTokenKind);
let right = this._parseExprUnary();
let resultType = this._getMulDivModTypeAndCheckLeftAndRight(left, right, operatorKind);
left = new BinaryExpression(left, operatorKind, right, resultType);
operatorTokenKind = [TokenKind.MulOperator, TokenKind.DivOperator, TokenKind.ModOperator]
.find(kind => this._tokenizer.next(kind));
}
return left;
}
/**
* Parse expression with zero or more '-' or 'not' operators.
* @returns {Expression} expression
* @private
*/
_parseExprUnary() {
if (this._tokenizer.next(TokenKind.MinusOperator)) {
let expression = this._parseExprPrimary();
if (this._getType(expression) !== EdmPrimitiveTypeKind.Duration) {
this.checkNumericType(expression);
}
return new UnaryExpression(UnaryOperatorKind.MINUS, expression);
} else if (this._tokenizer.next(TokenKind.NotOperator)) {
let expression = this._parseExprPrimary();
this.checkType(expression, EdmPrimitiveTypeKind.Boolean);
this.checkNoCollection(expression);
return new UnaryExpression(UnaryOperatorKind.NOT, expression);
} else if (this._tokenizer.next(TokenKind.CastMethod)) {
return this._parseIsOfOrCastMethod(MethodKind.CAST);
}
return this._parseExprPrimary();
}
/**
* Parse expression with a value and optionally a 'has' operator with its right-hand value.
* @returns {Expression} expression
* @private
*/
_parseExprPrimary() {
const left = this._parseExprValue();
if (this._isType(this._getType(left),
EdmPrimitiveTypeKind.Int64, EdmPrimitiveTypeKind.Int32, EdmPrimitiveTypeKind.Int16,
EdmPrimitiveTypeKind.Byte, EdmPrimitiveTypeKind.SByte)
&& this._tokenizer.next(TokenKind.HasOperator)) {
this._tokenizer.requireNext(TokenKind.EnumValue);
const right = this._parsePrimitive(TokenKind.EnumValue);
return new BinaryExpression(left, BinaryOperatorKind.HAS, right, EdmPrimitiveTypeKind.Boolean);
}
return left;
}
/**
* Parse expression with a value.
* @returns {Expression} expression
* @private
*/
_parseExprValue() {
if (this._tokenizer.next(TokenKind.OPEN)) {
const expression = this._parseExpression();
this._tokenizer.requireNext(TokenKind.CLOSE);
return expression;
}
if (this._tokenizer.next(TokenKind.ParameterAliasName)) {
const name = this._tokenizer.getText();
if (this._aliases && this._aliases[name]) {
return new AliasExpression(name, this.parseAlias(name, null, this._aliases));
}
throw new UriSyntaxError(UriSyntaxError.Message.ALIAS_NOT_FOUND, name);
}
if (this._tokenizer.next(TokenKind.jsonArrayOrObject)) {
// There is no obvious way how the type could be determined from the JSON literal.
return new LiteralExpression(this._tokenizer.getText(), undefined);
}
let firstTokenKind = [TokenKind.ROOT, TokenKind.IT].find(kind => this._tokenizer.next(kind));
if (firstTokenKind) {
return this._parseFirstMemberExpr(firstTokenKind);
}
const nextPrimitive = this._nextPrimitiveValue(this._tokenizer);
if (nextPrimitive != null) {
return this._parsePrimitive(nextPrimitive);
}
const nextMethod = Array.from(tokenToMethod.keys()).find(kind => this._tokenizer.next(kind));
if (nextMethod != null) {
// The method token text includes the opening parenthesis so that method calls can be recognized
// unambiguously. OData identifiers have to be considered after that.
const methodKind = tokenToMethod.get(nextMethod);
return new MethodExpression(methodKind, this._parseMethodParameters(methodKind));
}
firstTokenKind = [TokenKind.QualifiedName, TokenKind.ODataIdentifier].find(kind => this._tokenizer.next(kind));
if (firstTokenKind) {
return this._parseFirstMemberExpr(firstTokenKind);
}
throw new UriSyntaxError(UriSyntaxError.Message.TOKEN_KINDS_EXPECTED,
[TokenKind.QualifiedName, TokenKind.ODataIdentifier].join(', '),
this._tokenizer.getParseString(), this._tokenizer.getPosition());
}
/**
* Advance the tokenizer past a primitive value.
* @param {UriTokenizer} tokenizer tokenizer
* @returns {?UriTokenizer.TokenKind} the token kind of the primitive value
* @private
*/
_nextPrimitiveValue(tokenizer) {
const tokenKind = [
TokenKind.NULL,
TokenKind.BooleanValue,
TokenKind.StringValue,
// The order of the next seven expressions is important in order to avoid
// finding partly parsed tokens (counter-intuitive as it may be, even a GUID may start with digits ...).
TokenKind.GuidValue,
TokenKind.DoubleValue,
TokenKind.DecimalValue,
TokenKind.DateTimeOffsetValue,
TokenKind.DateValue,
TokenKind.TimeOfDayValue,
TokenKind.IntegerValue,
TokenKind.DurationValue,
TokenKind.BinaryValue,
TokenKind.EnumValue,
// Geography and geometry literals are defined to be primitive,
// although they contain several parts with their own meaning.
TokenKind.GeographyPoint,
TokenKind.GeometryPoint,
TokenKind.GeographyLineString,
TokenKind.GeometryLineString,
TokenKind.GeographyPolygon,
TokenKind.GeometryPolygon,
TokenKind.GeographyMultiPoint,
TokenKind.GeometryMultiPoint,
TokenKind.GeographyMultiLineString,
TokenKind.GeometryMultiLineString,
TokenKind.GeographyMultiPolygon,
TokenKind.GeometryMultiPolygon,
TokenKind.GeographyCollection,
TokenKind.GeometryCollection
].find(kind => tokenizer.next(kind));
return tokenKind;
}
/**
* Parse an alias value.
* @param {string} alias alias name
* @param {EdmType} edmType expected EDM type
* @param {Object} aliases alias definitions
* @returns {Expression} parsed expression
* @package
*/
parseAlias(alias, edmType, aliases) {
const value = aliases[alias];
if (!value) {
throw new UriSyntaxError(UriSyntaxError.Message.ALIAS_NOT_FOUND, alias);
}
let newAliases = Object.assign({}, aliases);
delete newAliases[alias];
let newTokenizer = new UriTokenizer(value);
let expression = null;
try {
expression = new ExpressionParser(this._edm)
.parse(newTokenizer, this._referringType, this._crossjoinEntitySets, newAliases);
} catch (e) {
throw new (e.name === ErrorNames.URI_QUERY_OPTION_SEMANTIC ? UriQueryOptionSemanticError : UriSyntaxError)(
UriSyntaxError.Message.WRONG_ALIAS_VALUE, alias)
.setRootCause(e);
}
const type = this._getType(expression);
if (!newTokenizer.next(TokenKind.EOF)) {
throw new UriSyntaxError(UriSyntaxError.Message.WRONG_ALIAS_VALUE, alias);
}
if (type !== null && !this.isCompatible(edmType, type)) {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE,
type.getFullQualifiedName().toString(), edmType.getFullQualifiedName().toString());
}
return expression;
}
/**
* Parse expression with a primitive literal.
* @param {UriTokenizer.TokenKind} primitiveTokenKind token kind
* @returns {LiteralExpression} literal expression
* @private
*/
_parsePrimitive(primitiveTokenKind) {
const primitiveValueLiteral = this._tokenizer.getText();
if (primitiveTokenKind === TokenKind.NULL) return new LiteralExpression(null, null);
let type = tokenToPrimitiveType.get(primitiveTokenKind);
if (type === EdmPrimitiveTypeKind.Int64) type = this._determineIntegerType(primitiveValueLiteral);
if (primitiveTokenKind === TokenKind.EnumValue) {
const typeName = primitiveValueLiteral.substring(0, primitiveValueLiteral.indexOf("'"));
type = this._edm.getEnumType(FullQualifiedName.createFromNameSpaceAndName(typeName));
if (!type) {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.ENUM_TYPE_NOT_FOUND,
typeName);
}
// Only pre-defined values (and their combinations, for flags) are allowed.
const literalValue = primitiveValueLiteral.substring(typeName.length + 1, primitiveValueLiteral.length - 1);
let enumValue = null;
for (const value of literalValue.split(',')) {
let memberValue = null;
for (const [name, member] of type.getMembers()) {
if (value === name || value === member.getValue().toString()) {
memberValue = member.getValue();
break;
}
}
if (memberValue === null || enumValue !== null && !type.isFlags()) {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.WRONG_VALUE, typeName, literalValue);
}
enumValue = enumValue === null ? memberValue : enumValue | memberValue;
}
}
return new LiteralExpression(UriHelper.fromUriLiteral(primitiveValueLiteral, type), type);
}
/**
* Determines the EDM type of a whole number given as string literal.
* @param {string} intValueAsString the number as string
* @returns {EdmPrimitiveType} EDM primitive type
* @private
*/
_determineIntegerType(intValueAsString) {
// To check the value range, we convert the input string to a Javascript number.
// For numbers with an absolute value larger than 2^53 - 1
// (outside the "safe" number range in Javascript),
// the resulting number is changed to another number in the safe range.
// This is not relevant here because the resulting number is still of the same OData type.
const value = Number(intValueAsString);
if (valueValidator.isSByte(value)) return EdmPrimitiveTypeKind.SByte;
if (valueValidator.isByte(value)) return EdmPrimitiveTypeKind.Byte;
if (valueValidator.isInt16(value)) return EdmPrimitiveTypeKind.Int16;
if (valueValidator.isInt32(value)) return EdmPrimitiveTypeKind.Int32;
if (valueValidator.isInt64(intValueAsString)) return EdmPrimitiveTypeKind.Int64;
// The number cannot be formatted wrongly because the tokenizer already checked the format
// but it is too large for Edm.Int64.
return EdmPrimitiveTypeKind.Decimal;
}
/**
* Parse method parameters.
* @param {MethodKind} methodKind method
* @returns {Expression[]} parameters
* @private
*/
_parseMethodParameters(methodKind) {
let parameters = [];
switch (methodKind) {
// Must have no parameter.
case MethodKind.NOW:
case MethodKind.MAXDATETIME:
case MethodKind.MINDATETIME:
break;
// Must have one parameter.
case MethodKind.LENGTH:
case MethodKind.TOLOWER:
case MethodKind.TOUPPER:
case MethodKind.TRIM: {
const stringParameter = this._parseExpression();
this.checkType(stringParameter, EdmPrimitiveTypeKind.String);
this.checkNoCollection(stringParameter);
parameters.push(stringParameter);
}
break;
case MethodKind.YEAR:
case MethodKind.MONTH:
case MethodKind.DAY: {
const dateParameter = this._parseExpression();
this.checkType(dateParameter, EdmPrimitiveTypeKind.Date, EdmPrimitiveTypeKind.DateTimeOffset);
this.checkNoCollection(dateParameter);
parameters.push(dateParameter);
}
break;
case MethodKind.HOUR:
case MethodKind.MINUTE:
case MethodKind.SECOND:
case MethodKind.FRACTIONALSECONDS: {
const timeParameter = this._parseExpression();
this.checkType(timeParameter, EdmPrimitiveTypeKind.TimeOfDay, EdmPrimitiveTypeKind.DateTimeOffset);
this.checkNoCollection(timeParameter);
parameters.push(timeParameter);
}
break;
case MethodKind.DATE:
case MethodKind.TIME:
case MethodKind.TOTALOFFSETMINUTES: {
const dateTimeParameter = this._parseExpression();
this.checkType(dateTimeParameter, EdmPrimitiveTypeKind.DateTimeOffset);
this.checkNoCollection(dateTimeParameter);
parameters.push(dateTimeParameter);
}
break;
case MethodKind.TOTALSECONDS: {
const durationParameter = this._parseExpression();
this.checkType(durationParameter, EdmPrimitiveTypeKind.Duration);
this.checkNoCollection(durationParameter);
parameters.push(durationParameter);
}
break;
case MethodKind.ROUND:
case MethodKind.FLOOR:
case MethodKind.CEILING: {
const decimalParameter = this._parseExpression();
this.checkType(decimalParameter,
EdmPrimitiveTypeKind.Decimal, EdmPrimitiveTypeKind.Single, EdmPrimitiveTypeKind.Double);
this.checkNoCollection(decimalParameter);
parameters.push(decimalParameter);
}
break;
case MethodKind.GEOLENGTH: {
const geoParameter = this._parseExpression();
this.checkType(geoParameter,
EdmPrimitiveTypeKind.GeographyLineString, EdmPrimitiveTypeKind.GeometryLineString);
this.checkNoCollection(geoParameter);
parameters.push(geoParameter);
}
break;
// Must have two parameters.
case MethodKind.CONTAINS:
case MethodKind.ENDSWITH:
case MethodKind.STARTSWITH:
case MethodKind.INDEXOF:
case MethodKind.CONCAT: {
const stringParameter1 = this._parseExpression();
this.checkType(stringParameter1, EdmPrimitiveTypeKind.String);
this.checkNoCollection(stringParameter1);
parameters.push(stringParameter1);
this._tokenizer.requireNext(TokenKind.COMMA);
const stringParameter2 = this._parseExpression();
this.checkType(stringParameter2, EdmPrimitiveTypeKind.String);
this.checkNoCollection(stringParameter2);
parameters.push(stringParameter2);
}
break;
case MethodKind.GEODISTANCE: {
const geoParameter1 = this._parseExpression();
this.checkType(geoParameter1, EdmPrimitiveTypeKind.GeographyPoint, EdmPrimitiveTypeKind.GeometryPoint);
this.checkNoCollection(geoParameter1);
parameters.push(geoParameter1);
this._tokenizer.requireNext(TokenKind.COMMA);
const geoParameter2 = this._parseExpression();
this.checkType(geoParameter2, this._getType(geoParameter1));
this.checkNoCollection(geoParameter2);
parameters.push(geoParameter2);
}
break;
case MethodKind.GEOINTERSECTS: {
const geoPointParameter = this._parseExpression();
this.checkType(geoPointParameter,
EdmPrimitiveTypeKind.GeographyPoint, EdmPrimitiveTypeKind.GeometryPoint);
this.checkNoCollection(geoPointParameter);
parameters.push(geoPointParameter);
this._tokenizer.requireNext(TokenKind.COMMA);
const geoPolygonParameter = this._parseExpression();
this.checkType(geoPolygonParameter,
this._getType(geoPointParameter) === EdmPrimitiveTypeKind.GeographyPoint ?
EdmPrimitiveTypeKind.GeographyPolygon : EdmPrimitiveTypeKind.GeometryPolygon);
this.checkNoCollection(geoPolygonParameter);
parameters.push(geoPolygonParameter);
}
break;
// Can have two or three parameters.
case MethodKind.SUBSTRING: {
const parameterFirst = this._parseExpression();
this.checkType(parameterFirst, EdmPrimitiveTypeKind.String);
this.checkNoCollection(parameterFirst);
parameters.push(parameterFirst);
this._tokenizer.requireNext(TokenKind.COMMA);
const parameterSecond = this._parseExpression();
this.checkIntegerType(parameterSecond);
parameters.push(parameterSecond);
if (this._tokenizer.next(TokenKind.COMMA)) {
const parameterThird = this._parseExpression();
this.checkIntegerType(parameterThird);
parameters.push(parameterThird);
}
}
break;
// Can have one or two parameters. These methods are handled elsewhere.
case MethodKind.CAST:
case MethodKind.ISOF:
break;
default:
break;
}
this._tokenizer.requireNext(TokenKind.CLOSE);
return parameters;
}
/**
* Parse member expression at start.
* @param {UriTokenizer.TokenKind} lastTokenKind token kind
* @returns {Expression} expression
* @private
*/
_parseFirstMemberExpr(lastTokenKind) {
let pathSegments = [];
if (lastTokenKind === TokenKind.ROOT) {
this._parseDollarRoot(pathSegments);
} else if (lastTokenKind === TokenKind.IT) {
this._parseDollarIt(pathSegments, this._referringType);
} else if (lastTokenKind === TokenKind.QualifiedName) {
// Special handling for leading type casts and type literals
const fullQualifiedName = FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText());
const castType = this._edm.getEntityType(fullQualifiedName)
|| this._edm.getComplexType(fullQualifiedName)
|| this._getPrimitiveType(fullQualifiedName)
|| this._edm.getTypeDefinition(fullQualifiedName)
|| this._edm.getEnumType(fullQualifiedName);
if (castType != null) {
if (this._tokenizer.next(TokenKind.SLASH)) {
// Leading type cast
this._checkStructuredTypeCast(this._referringType, castType);
const castResource = new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(castType);
pathSegments.push(castResource);
const tokenKind = [TokenKind.QualifiedName, TokenKind.ODataIdentifier]
.find(kind => this._tokenizer.next(kind));
this._parseMemberExpression(tokenKind, pathSegments, castResource, false);
} else {
// Type literal
return new TypeLiteralExpression(castType);
}
} else {
// Must be bound or unbound function.
this._parseFunction(fullQualifiedName, pathSegments, this._referringType, false, true);
}
} else {
// Must be TokenKind.ODataIdentifier (see callers).
this._parseFirstMemberODataIdentifier(pathSegments);
}
return new MemberExpression(pathSegments);
}
/**
* Get primitive type from full-qualified name.
* @param {FullQualifiedName} fullQualifiedName full-qualified name
* @returns {?EdmPrimitiveType} primitive type or null if nothing found
* @private
*/
_getPrimitiveType(fullQualifiedName) {
return EdmPrimitiveType.EDM_NAMESPACE === fullQualifiedName.namespace ?
EdmPrimitiveTypeKind.fromName(fullQualifiedName.name) :
null;
}
/**
* Parse key predicates.
* @param {UriResource} resource current resource
* @param {EdmEntityType} type entity type for which the keys are parsed
* @returns {UriParameter[]} key predicates
* @private
*/
_parseKeyPredicates(resource, type) {
try {
return new KeyPredicateParser(this._edm).parse(resource, type, this._tokenizer);
} catch (err) {
throw err.name === ErrorNames.URI_SEMANTIC ?
new UriQueryOptionSemanticError(err.message).setRootCause(err) :
err;
}
}
/**
* Parse $root expression.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @private
*/
_parseDollarRoot(pathSegments) {
pathSegments.push(new UriResource().setKind(ResourceKind.ROOT_EXPRESSION));
this._tokenizer.requireNext(TokenKind.SLASH);
this._tokenizer.requireNext(TokenKind.ODataIdentifier);
const name = this._tokenizer.getText();
let resource = new UriResource();
const entitySet = this._edm.getEntityContainer().getEntitySet(name);
if (entitySet == null) {
const singleton = this._edm.getEntityContainer().getSingleton(name);
if (singleton == null) {
throw new UriQueryOptionSemanticError(
UriQueryOptionSemanticError.Message.ENTITY_SET_OR_SINGLETON_NOT_FOUND, name);
}
resource.setKind(ResourceKind.SINGLETON).setSingleton(singleton);
} else {
resource.setKind(ResourceKind.ENTITY).setEntitySet(entitySet);
this._tokenizer.requireNext(TokenKind.OPEN);
const keyPredicates = this._parseKeyPredicates(resource, entitySet.getEntityType());
this._tokenizer.requireNext(TokenKind.CLOSE);
resource.setKeyPredicates(keyPredicates);
}
pathSegments.push(resource);
this._parseSingleNavigationExpr(pathSegments, resource);
}
/**
* Parse $it expression.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {EdmType} referringType referring type
* @private
*/
_parseDollarIt(pathSegments, referringType) {
let itResource = new UriResource()
.setKind(ResourceKind.IT_EXPRESSION)
.setExpressionVariableEdmType(referringType);
pathSegments.push(itResource);
if (this._tokenizer.next(TokenKind.SLASH)) {
const tokenKind = [TokenKind.QualifiedName, TokenKind.ODataIdentifier]
.find(kind => this._tokenizer.next(kind));
this._parseMemberExpression(tokenKind, pathSegments, itResource, true);
}
}
/**
* Parse member expression at start with an OData identifier.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @private
*/
_parseFirstMemberODataIdentifier(pathSegments) {
const name = this._tokenizer.getText();
// For a crossjoin, the identifier must be an entity-set name.
if (this._crossjoinEntitySets != null && this._crossjoinEntitySets.length > 0) {
const crossjoinEntitySet = this._crossjoinEntitySets.find(entitySet => entitySet.getName() === name);
if (crossjoinEntitySet != null) {
let resource = new UriResource()
.setKind(ResourceKind.ENTITY_COLLECTION)
.setEntitySet(crossjoinEntitySet);
pathSegments.push(resource);
if (this._tokenizer.next(TokenKind.SLASH)) {
const tokenKind = [TokenKind.QualifiedName, TokenKind.ODataIdentifier]
.find(kind => this._tokenizer.next(kind));
this._parseMemberExpression(tokenKind, pathSegments, resource, true);
}
return;
}
throw new UriQueryOptionSemanticError(UriSemanticError.Message.ENTITY_SET_NOT_FOUND, name);
}
// Check if the OData identifier is a lambda variable, otherwise it must be a property.
const lambdaVariable = this._lambdaVariables.find(variable => variable.getExpressionVariableName() === name);
if (lambdaVariable != null) {
pathSegments.push(lambdaVariable);
if (this._tokenizer.next(TokenKind.SLASH)) {
const tokenKind = [TokenKind.QualifiedName, TokenKind.ODataIdentifier]
.find(kind => this._tokenizer.next(kind));
this._parseMemberExpression(tokenKind, pathSegments, lambdaVariable, true);
}
} else {
// Must be a property.
this._parseMemberExpression(TokenKind.ODataIdentifier, pathSegments, null, true);
}
}
/**
* Parse member expression.
* @param {UriTokenizer.TokenKind} lastTokenKind last token kind
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {UriResource} lastResource last resource
* @param {boolean} allowTypeCast whether a type cast is allowed
* @private
*/
_parseMemberExpression(lastTokenKind, pathSegments, lastResource, allowTypeCast) {
if (lastTokenKind === TokenKind.QualifiedName) {
// Type cast or bound function
const fullQualifiedName = FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText());
const edmEntityType = this._edm.getEntityType(fullQualifiedName);
if (edmEntityType != null) {
if (allowTypeCast) {
this._checkStructuredTypeCast(lastResource.getEdmType(), edmEntityType);
const castResource = new UriResource()
.setKind(ResourceKind.TYPE_CAST)
.setTypeCast(edmEntityType)
.setIsCollection(lastResource.isCollection());
pathSegments.push(castResource);
if (this._tokenizer.next(TokenKind.SLASH)) {
if (this._tokenizer.next(TokenKind.QualifiedName)) {
this._parseFunction(
FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText()),
pathSegments,
edmEntityType,
castResource.isCollection(),
false);
} else if (this._tokenizer.next(TokenKind.ODataIdentifier)) {
this._parsePropertyPathExpr(pathSegments, castResource);
} else {
throw new UriSyntaxError(UriSyntaxError.Message.TOKEN_KINDS_EXPECTED,
[TokenKind.QualifiedName, TokenKind.ODataIdentifier].join(', '),
this._tokenizer.getParseString(), this._tokenizer.getPosition());
}
}
} else {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.TYPE_CAST_NOT_CHAINABLE,
fullQualifiedName.toString(), lastResource.getEdmType().getFullQualifiedName().toString());
}
} else {
this._parseFunction(fullQualifiedName, pathSegments,
lastResource.getEdmType(), lastResource.isCollection(), false);
}
} else {
// TokenKind.ODataIdentifier
this._parsePropertyPathExpr(pathSegments, lastResource);
}
}
/**
* Parse property-path expression.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {UriResource} lastResource last resource
* @private
*/
_parsePropertyPathExpr(pathSegments, lastResource) {
const identifier = this._tokenizer.getText();
const lastType = lastResource == null ? this._referringType : lastResource.getEdmType();
if (lastType.getKind() !== EdmTypeKind.COMPLEX && lastType.getKind() !== EdmTypeKind.ENTITY) {
throw new UriQueryOptionSemanticError(
UriQueryOptionSemanticError.Message.PROPERTY_MUST_FOLLOW_STRUCTURED_TYPE, identifier);
}
const property = lastType.getStructuralProperty(identifier);
if (property == null) {
const navigationProperty = lastType.getNavigationProperty(identifier);
if (navigationProperty == null) {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.PROPERTY_NOT_FOUND,
identifier, lastType.getFullQualifiedName().toString());
}
// Navigation property; maybe a collection
let navigationResource = new UriResource()
.setNavigationProperty(navigationProperty)
.setIsCollection(navigationProperty.isCollection());
if (this._tokenizer.next(TokenKind.OPEN)) {
if (navigationProperty.isCollection()) {
const keyPredicates =
this._parseKeyPredicates(navigationResource, navigationProperty.getEntityType());
this._tokenizer.requireNext(TokenKind.CLOSE);
navigationResource.setKeyPredicates(keyPredicates).setIsCollection(false);
} else {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.KEY_NOT_ALLOWED);
}
}
pathSegments.push(navigationResource);
if (navigationResource.isCollection()) {
navigationResource.setKind(ResourceKind.NAVIGATION_TO_MANY);
this._parseCollectionNavigationExpr(pathSegments, navigationResource);
} else {
navigationResource.setKind(ResourceKind.NAVIGATION_TO_ONE);
this._parseSingleNavigationExpr(pathSegments, navigationResource);
}
} else {
const isCollection = property.isCollection();
let propertyResource = new UriResource().setProperty(property).setIsCollection(isCollection);
pathSegments.push(propertyResource);
if (property.getType().getKind() === EdmTypeKind.COMPLEX) {
if (isCollection) {
propertyResource.setKind(ResourceKind.COMPLEX_COLLECTION_PROPERTY);
this._parseComplexCollectionPathExpr(pathSegments, propertyResource);
} else {
propertyResource.setKind(ResourceKind.COMPLEX_PROPERTY);
this._parseComplexPathExpr(pathSegments, propertyResource);
}
} else {
// Primitive or type-definition type
if (isCollection) {
propertyResource.setKind(ResourceKind.PRIMITIVE_COLLECTION_PROPERTY);
if (this._tokenizer.next(TokenKind.SLASH)) {
this._parseCollectionPathExpr(pathSegments, propertyResource);
}
} else {
propertyResource.setKind(ResourceKind.PRIMITIVE_PROPERTY);
this._parsePrimitivePathExpr(pathSegments, propertyResource);
}
}
}
}
/**
* Parse navigation path after a non-collection.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {UriResource} lastResource last resource
* @private
*/
_parseSingleNavigationExpr(pathSegments, lastResource) {
if (this._tokenizer.next(TokenKind.SLASH)) {
const tokenKind = [TokenKind.QualifiedName, TokenKind.ODataIdentifier]
.find(kind => this._tokenizer.next(kind));
this._parseMemberExpression(tokenKind, pathSegments, lastResource, true);
}
}
/**
* Parse navigation path after a collection.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {UriResource} lastResourceParam last resource
* @private
*/
_parseCollectionNavigationExpr(pathSegments, lastResourceParam) {
let lastResource = lastResourceParam;
let hasSlash = this._tokenizer.next(TokenKind.SLASH);
if (hasSlash) {
if (this._tokenizer.next(TokenKind.QualifiedName)) {
const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText());
const edmEntityType = this._edm.getEntityType(qualifiedName);
if (edmEntityType == null) {
this._parseFunction(qualifiedName, pathSegments,
lastResource.getEdmType(), lastResource.isCollection(), false);
} else {
this._checkStructuredTypeCast(lastResource.getEdmType(), edmEntityType);
const castResource = new UriResource()
.setKind(ResourceKind.TYPE_CAST)
.setTypeCast(edmEntityType)
.setIsCollection(lastResource.isCollection());
pathSegments.push(castResource);
lastResource = castResource;
}
hasSlash = false;
}
}
if (!hasSlash && this._tokenizer.next(TokenKind.OPEN)) {
const keyPredicates = this._parseKeyPredicates(lastResource, lastResource.getEdmType());
this._tokenizer.requireNext(TokenKind.CLOSE);
lastResource.setKeyPredicates(keyPredicates).setIsCollection(false);
this._parseSingleNavigationExpr(pathSegments, lastResource);
}
if (hasSlash || this._tokenizer.next(TokenKind.SLASH)) {
this._parseCollectionPathExpr(pathSegments, lastResource);
}
}
/**
* Parse path after a non-collection primitive type.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {UriResource} lastResource last resource
* @private
*/
_parsePrimitivePathExpr(pathSegments, lastResource) {
if (this._tokenizer.next(TokenKind.SLASH)) {
this._tokenizer.requireNext(TokenKind.QualifiedName);
this._parseFunction(
FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText()),
pathSegments,
lastResource.getEdmType(), lastResource.isCollection(),
false);
}
}
/**
* Parse path after a non-collection complex type.
* @param {UriResource[]} pathSegments path segments where parsed information is added
* @param {UriResource} lastResource last resource
* @private
*/
_parseComplexPathExpr(pathSegments, lastResource) {
if (this._tokenizer.next(TokenKind.SLASH)) {
if (this._tokenizer.next(TokenKind.QualifiedName)) {
const fullQualifiedName = FullQualifiedName.createFromNameSpaceAndName(this._tokenizer.getText());
const edmComplexType = this._edm.getComplexType(fullQualifiedName);
if (edmComplexType != null) {
this._checkStructuredTypeCast(lastResource.getEdmType(), edmComplexType);
const castResource =
new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(edmComplexType);
pathSegments.push(castResource);
if (this._tokenizer.next(TokenKind.SLASH)) {
this._parseComplexPathRestExpr(pathSegments, castResource);
}
} else