UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

1,095 lines (1,005 loc) 81.6 kB
'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