UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

1,224 lines (1,132 loc) 48.2 kB
'use strict'; const Character = require('../utils/Character'); const UriSyntaxError = require('../errors/UriSyntaxError'); const BOOLEAN_VALUE_REGEXP = new RegExp('^(?:true|false)', 'i'); const STRING_REGEXP = new RegExp("^'(?:''|[^'])*'"); const HEX_DIGIT = '[A-Fa-f0-9]'; const GUID_VALUE_REGEXP = new RegExp('^(?:' + HEX_DIGIT + '{8}-' + HEX_DIGIT + '{4}-' + HEX_DIGIT + '{4}-' + HEX_DIGIT + '{4}-' + HEX_DIGIT + '{12})'); const BASE64 = '[-_A-Za-z0-9]'; const BASE64B16 = BASE64 + '{2}[AEIMQUYcgkosw048]=?'; const BASE64B8 = BASE64 + '[AQgw](?:==)?'; const BINARY = '(?:' + BASE64 + '{4})*(?:' + BASE64B16 + '|' + BASE64B8 + ')?'; const BINARY_VALUE_REGEXP = new RegExp("^[Bb][Ii][Nn][Aa][Rr][Yy]'" + BINARY + "'"); const UNSIGNED_INTEGER_VALUE_REGEXP = new RegExp('^\\d+'); const INTEGER_VALUE_REGEXP = new RegExp('^[-+]?\\d+'); const DECIMAL_VALUE_REGEXP = new RegExp('^[-+]?\\d+\\.\\d+'); const DOUBLE_VALUE_REGEXP = new RegExp('^(?:(?:[-+]?\\d+(?:\\.\\d+)?[Ee][-+]?\\d+)|NaN|-?INF)'); const DURATION_REGEXP = new RegExp("^duration'[-+]?P(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?'", 'i'); const YEAR = '-?(?:0\\d{3}|[1-9]\\d{3,})'; const MONTH = '(?:0[1-9]|1[012])'; const DAY = '(?:0[1-9]|[12]\\d|3[01])'; const HOUR = '(?:[01]\\d|2[0123])'; const MINUTE = '[012345]\\d'; const SECOND = '[012345]\\d'; const DATE = YEAR + '-' + MONTH + '-' + DAY; const TIME = HOUR + ':' + MINUTE + '(?::' + SECOND + '(?:\\.\\d{1,12})?)?'; const DATE_REGEXP = new RegExp('^' + DATE); const TIME_OF_DAY_REGEXP = new RegExp('^' + TIME); const DATE_TIME_OFFSET_REGEXP = new RegExp('^' + DATE + 'T' + TIME + '(?:Z|[-+]' + HOUR + ':' + MINUTE + ')', 'i'); const SRID = 'SRID=(\\d{1,8});'; const NUMBER = '[-+]?\\d+(?:\\.\\d+)?(?:[Ee][-+]?\\d+)?'; // A geo position is given by two space-separated numbers, like "1.23 4.56E-1". const POSITION = '(?:' + NUMBER + ' ' + NUMBER + ')'; // A geo line is a comma-separated list of positions, like "1 2,3 4,5 6". const LINE = '(?:' + POSITION + '?(?:,' + POSITION + ')*)'; // A geo multiposition is a comma-separated list of positions, each in parentheses, like "(1 2),(3 4),(5 6)". const MULTI_POSITION = '(?:(?:\\(' + POSITION + '\\))?(?:,\\(' + POSITION + '\\))*)'; // A geo multiline is a comma-separated list of lines, each in parentheses, like "(1 1,2 2),(3 3,4 4)". // A geo polygon has exactly the same coordinate representation as a geo multiline. const MULTI_LINE = '(?:(?:\\(' + LINE + '\\))?(?:,\\(' + LINE + '\\))*)'; // A geo multipolygon is a comma-separated list of multilines, each in parentheses, like // "((-1 -2,1 -2,1 2,-1 2,-1 -2),(-5 -10,-5 10,5 10,5 -10,-5 -10)),((-1 -2,-3 -4,-5 -6,-1 -2))". const MULTI_POLYGON = '(?:(?:\\(' + MULTI_LINE + '\\))?(?:,\\(' + MULTI_LINE + '\\))*)'; // A geo literal is one of position, line, multiposition, multiline, multipolygon, // enclosed in parentheses and prefixed with a type name. const GEO_LITERAL = '(?:(?:Point\\(' + POSITION + '\\))' + '|(?:LineString\\(' + LINE + '\\))' + '|(?:Polygon\\(' + MULTI_LINE + '\\))' + '|(?:MultiPoint\\(' + MULTI_POSITION + '\\))' + '|(?:MultiLineString\\(' + MULTI_LINE + '\\))' + '|(?:MultiPolygon\\(' + MULTI_POLYGON + '\\)))'; // A multigeoliteral (used for a collection) is a comma-separated list of geo literals. const MULTI_GEO_LITERAL = '(?:' + GEO_LITERAL + '?(?:,' + GEO_LITERAL + ')*)'; // The validation regular expressions for geo literals must be all case-insensitive. // They are built as sequence of an SRID definition, a type name, and the coordinates; // the coordinates are enclosed in parentheses. const GEO_POINT_REGEXP = new RegExp('^' + SRID + 'Point\\(' + POSITION + '\\)', 'i'); const GEO_LINE_STRING_REGEXP = new RegExp('^' + SRID + 'LineString\\(' + LINE + '\\)', 'i'); const GEO_POLYGON_REGEXP = new RegExp('^' + SRID + 'Polygon\\(' + MULTI_LINE + '\\)', 'i'); const GEO_MULTI_POINT_REGEXP = new RegExp('^' + SRID + 'MultiPoint\\(' + MULTI_POSITION + '\\)', 'i'); const GEO_MULTI_LINE_STRING_REGEXP = new RegExp('^' + SRID + 'MultiLineString\\(' + MULTI_LINE + '\\)', 'i'); const GEO_MULTI_POLYGON_REGEXP = new RegExp('^' + SRID + 'MultiPolygon\\(' + MULTI_POLYGON + '\\)', 'i'); const GEO_COLLECTION_REGEXP = new RegExp('^' + SRID + 'Collection\\(' + MULTI_GEO_LITERAL + '\\)', 'i'); const JSON_STRING_REGEXP = new RegExp('^"(?:(?:\\\\(?:[btnfr"/\\\\]|u' + HEX_DIGIT + '{4}))|[^\\\\"])*"'); // A search phrase is a doublequoted string with backslash-escaped backslashes and doublequotes. const PHRASE_REGEXP = new RegExp('^"(?:[^\\\\"]|\\\\[\\\\"])*"'); /** * <p>Simple OData URI tokenizer that works on a given string by keeping an index.</p> * <p>As far as feasible, it tries to work on character basis, * assuming this to be faster than string operations. * Since only the index is 'moved', backing out while parsing a token is easy and used throughout. * There is intentionally no method to push back tokens * (although it would be easy to add such a method) * because this tokenizer should behave like a classical token-consuming tokenizer. * There is, however, the possibility to save the current state and return to it later.</p> * <p>Whitespace is not an extra token but consumed with the tokens that require whitespace. * Optional whitespace is not supported.</p> */ class UriTokenizer { /** * Constructor which accepts the uri string. * * @param {string} parseString The uri string */ constructor(parseString) { this._index = 0; this._parseString = parseString || ''; this._startIndex = 0; this._savedStartIndex = null; this._savedIndex = null; } /** * Save the current state. * * @see #returnToSavedState() */ saveState() { this._savedStartIndex = this._startIndex; this._savedIndex = this._index; } /** * Return to the previously saved state. * * @see #saveState() */ returnToSavedState() { this._startIndex = this._savedStartIndex; this._index = this._savedIndex; } /** * Return the whole parse string. * @returns {string} the parse string */ getParseString() { return this._parseString; } /** * Return the current position in the parse string. * @returns {number} position, starting at 1 */ getPosition() { return this._index + 1; } /** * Return the string value corresponding to the last successful {@link #next(TokenKind)} call. * @returns {string} the token text */ getText() { return this._parseString.substring(this._startIndex, this._index); } /** * Try to find a token of the given token kind at the current index. * The order in which this method is called with different token kinds is important, * not only for performance reasons but also if tokens can start with the same characters * (e.g., a qualified name starts with an OData identifier). * The index is advanced to the end of this token if the token is found. * * @param {UriTokenizer.TokenKind} allowedTokenKind the kind of token to expect * @returns {boolean} <code>true</code> if the token is found; <code>false</code> otherwise * @see #getText() */ next(allowedTokenKind) { if (!allowedTokenKind) { return false; } const previousIndex = this._index; let found = false; switch (allowedTokenKind) { case UriTokenizer.TokenKind.EOF: found = this._nextEOF(); break; // Constants case UriTokenizer.TokenKind.REF: found = this._nextConstant('$ref'); break; case UriTokenizer.TokenKind.VALUE: found = this._nextConstant('$value'); break; case UriTokenizer.TokenKind.COUNT: found = this._nextConstant('$count'); break; case UriTokenizer.TokenKind.METADATA: found = this._nextConstant('$metadata'); break; case UriTokenizer.TokenKind.BATCH: found = this._nextConstant('$batch'); break; case UriTokenizer.TokenKind.CROSSJOIN: found = this._nextConstant('$crossjoin'); break; case UriTokenizer.TokenKind.ALL: found = this._nextConstant('$all'); break; case UriTokenizer.TokenKind.ENTITY: found = this._nextConstant('$entity'); break; case UriTokenizer.TokenKind.ROOT: found = this._nextConstant('$root'); break; case UriTokenizer.TokenKind.IT: found = this._nextConstant('$it'); break; case UriTokenizer.TokenKind.APPLY: found = this._nextConstant('$apply'); break; case UriTokenizer.TokenKind.EXPAND: found = this._nextConstant('$expand'); break; case UriTokenizer.TokenKind.FILTER: found = this._nextConstant('$filter'); break; case UriTokenizer.TokenKind.LEVELS: found = this._nextConstant('$levels'); break; case UriTokenizer.TokenKind.ORDERBY: found = this._nextConstant('$orderby'); break; case UriTokenizer.TokenKind.SEARCH: found = this._nextConstant('$search'); break; case UriTokenizer.TokenKind.SELECT: found = this._nextConstant('$select'); break; case UriTokenizer.TokenKind.SKIP: found = this._nextConstant('$skip'); break; case UriTokenizer.TokenKind.TOP: found = this._nextConstant('$top'); break; case UriTokenizer.TokenKind.LAMBDA_ANY: found = this._nextConstant('any'); break; case UriTokenizer.TokenKind.LAMBDA_ALL: found = this._nextConstant('all'); break; case UriTokenizer.TokenKind.OPEN: found = this._nextCharacter('('); break; case UriTokenizer.TokenKind.CLOSE: found = this._nextCharacter(')'); break; case UriTokenizer.TokenKind.COMMA: found = this._nextCharacter(','); break; case UriTokenizer.TokenKind.SEMI: found = this._nextCharacter(';'); break; case UriTokenizer.TokenKind.COLON: found = this._nextCharacter(':'); break; case UriTokenizer.TokenKind.DOT: found = this._nextCharacter('.'); break; case UriTokenizer.TokenKind.SLASH: found = this._nextCharacter('/'); break; case UriTokenizer.TokenKind.EQ: found = this._nextCharacter('='); break; case UriTokenizer.TokenKind.STAR: found = this._nextCharacter('*'); break; case UriTokenizer.TokenKind.PLUS: found = this._nextCharacter('+'); break; case UriTokenizer.TokenKind.NULL: found = this._nextConstant('null'); break; case UriTokenizer.TokenKind.MAX: found = this._nextConstant('max'); break; case UriTokenizer.TokenKind.AVERAGE: found = this._nextConstant('average'); break; case UriTokenizer.TokenKind.COUNTDISTINCT: found = this._nextConstant('countdistinct'); break; case UriTokenizer.TokenKind.IDENTITY: found = this._nextConstant('identity'); break; case UriTokenizer.TokenKind.MIN: found = this._nextConstant('min'); break; case UriTokenizer.TokenKind.SUM: found = this._nextConstant('sum'); break; // Identifiers case UriTokenizer.TokenKind.ODataIdentifier: found = this._nextODataIdentifier(); break; case UriTokenizer.TokenKind.QualifiedName: found = this._nextQualifiedName(); break; case UriTokenizer.TokenKind.ParameterAliasName: found = this._nextParameterAliasName(); break; // Primitive Values case UriTokenizer.TokenKind.BooleanValue: found = this._nextWithRegularExpression(BOOLEAN_VALUE_REGEXP); break; case UriTokenizer.TokenKind.StringValue: found = this._nextWithRegularExpression(STRING_REGEXP); break; case UriTokenizer.TokenKind.IntegerValue: found = this._nextIntegerValue(); break; case UriTokenizer.TokenKind.UnsignedIntegerValue: found = this._nextWithRegularExpression(UNSIGNED_INTEGER_VALUE_REGEXP); break; case UriTokenizer.TokenKind.GuidValue: found = this._nextWithRegularExpression(GUID_VALUE_REGEXP); break; case UriTokenizer.TokenKind.DateValue: found = this._nextWithRegularExpression(DATE_REGEXP); break; case UriTokenizer.TokenKind.DateTimeOffsetValue: found = this._nextWithRegularExpression(DATE_TIME_OFFSET_REGEXP); break; case UriTokenizer.TokenKind.TimeOfDayValue: found = this._nextWithRegularExpression(TIME_OF_DAY_REGEXP); break; case UriTokenizer.TokenKind.DecimalValue: found = this._nextDecimalValue(); break; case UriTokenizer.TokenKind.DoubleValue: found = this._nextDoubleValue(); break; case UriTokenizer.TokenKind.DurationValue: found = this._nextWithRegularExpression(DURATION_REGEXP); break; case UriTokenizer.TokenKind.BinaryValue: found = this._nextWithRegularExpression(BINARY_VALUE_REGEXP); break; case UriTokenizer.TokenKind.EnumValue: found = this._nextEnumValue(); break; // Geo Values case UriTokenizer.TokenKind.GeographyPoint: found = this._nextGeoValue(true, GEO_POINT_REGEXP); break; case UriTokenizer.TokenKind.GeometryPoint: found = this._nextGeoValue(false, GEO_POINT_REGEXP); break; case UriTokenizer.TokenKind.GeographyLineString: found = this._nextGeoValue(true, GEO_LINE_STRING_REGEXP); break; case UriTokenizer.TokenKind.GeometryLineString: found = this._nextGeoValue(false, GEO_LINE_STRING_REGEXP); break; case UriTokenizer.TokenKind.GeographyPolygon: found = this._nextGeoValue(true, GEO_POLYGON_REGEXP); break; case UriTokenizer.TokenKind.GeometryPolygon: found = this._nextGeoValue(false, GEO_POLYGON_REGEXP); break; case UriTokenizer.TokenKind.GeographyMultiPoint: found = this._nextGeoValue(true, GEO_MULTI_POINT_REGEXP); break; case UriTokenizer.TokenKind.GeometryMultiPoint: found = this._nextGeoValue(false, GEO_MULTI_POINT_REGEXP); break; case UriTokenizer.TokenKind.GeographyMultiLineString: found = this._nextGeoValue(true, GEO_MULTI_LINE_STRING_REGEXP); break; case UriTokenizer.TokenKind.GeometryMultiLineString: found = this._nextGeoValue(false, GEO_MULTI_LINE_STRING_REGEXP); break; case UriTokenizer.TokenKind.GeographyMultiPolygon: found = this._nextGeoValue(true, GEO_MULTI_POLYGON_REGEXP); break; case UriTokenizer.TokenKind.GeometryMultiPolygon: found = this._nextGeoValue(false, GEO_MULTI_POLYGON_REGEXP); break; case UriTokenizer.TokenKind.GeographyCollection: found = this._nextGeoValue(true, GEO_COLLECTION_REGEXP); break; case UriTokenizer.TokenKind.GeometryCollection: found = this._nextGeoValue(false, GEO_COLLECTION_REGEXP); break; // Complex or Collection Value case UriTokenizer.TokenKind.jsonArrayOrObject: found = this._nextJsonArrayOrObject(); break; // Search case UriTokenizer.TokenKind.Word: found = this._nextWord(); break; case UriTokenizer.TokenKind.Phrase: found = this._nextWithRegularExpression(PHRASE_REGEXP); break; // Operators in Search Expressions case UriTokenizer.TokenKind.OrOperatorSearch: found = this._nextBinaryOperator('OR'); break; case UriTokenizer.TokenKind.AndOperatorSearch: found = this._nextAndOperatorSearch(); break; case UriTokenizer.TokenKind.NotOperatorSearch: found = this._nextUnaryOperator('NOT'); break; // Operators case UriTokenizer.TokenKind.OrOperator: found = this._nextBinaryOperator('or'); break; case UriTokenizer.TokenKind.AndOperator: found = this._nextBinaryOperator('and'); break; case UriTokenizer.TokenKind.EqualsOperator: found = this._nextBinaryOperator('eq'); break; case UriTokenizer.TokenKind.NotEqualsOperator: found = this._nextBinaryOperator('ne'); break; case UriTokenizer.TokenKind.GreaterThanOperator: found = this._nextBinaryOperator('gt'); break; case UriTokenizer.TokenKind.GreaterThanOrEqualsOperator: found = this._nextBinaryOperator('ge'); break; case UriTokenizer.TokenKind.LessThanOperator: found = this._nextBinaryOperator('lt'); break; case UriTokenizer.TokenKind.LessThanOrEqualsOperator: found = this._nextBinaryOperator('le'); break; case UriTokenizer.TokenKind.HasOperator: found = this._nextBinaryOperator('has'); break; case UriTokenizer.TokenKind.AddOperator: found = this._nextBinaryOperator('add'); break; case UriTokenizer.TokenKind.SubOperator: found = this._nextBinaryOperator('sub'); break; case UriTokenizer.TokenKind.MulOperator: found = this._nextBinaryOperator('mul'); break; case UriTokenizer.TokenKind.DivOperator: found = this._nextBinaryOperator('div'); break; case UriTokenizer.TokenKind.ModOperator: found = this._nextBinaryOperator('mod'); break; case UriTokenizer.TokenKind.MinusOperator: found = this._nextMinusOperator(); break; case UriTokenizer.TokenKind.NotOperator: found = this._nextUnaryOperator('not'); break; // Operators for the aggregation extension case UriTokenizer.TokenKind.AsOperator: found = this._nextBinaryOperator('as'); break; case UriTokenizer.TokenKind.FromOperator: found = this._nextBinaryOperator('from'); break; case UriTokenizer.TokenKind.WithOperator: found = this._nextBinaryOperator('with'); break; // Methods case UriTokenizer.TokenKind.CastMethod: found = this._nextMethod('cast'); break; case UriTokenizer.TokenKind.CeilingMethod: found = this._nextMethod('ceiling'); break; case UriTokenizer.TokenKind.ConcatMethod: found = this._nextMethod('concat'); break; case UriTokenizer.TokenKind.ContainsMethod: found = this._nextMethod('contains'); break; case UriTokenizer.TokenKind.DateMethod: found = this._nextMethod('date'); break; case UriTokenizer.TokenKind.DayMethod: found = this._nextMethod('day'); break; case UriTokenizer.TokenKind.EndswithMethod: found = this._nextMethod('endswith'); break; case UriTokenizer.TokenKind.FloorMethod: found = this._nextMethod('floor'); break; case UriTokenizer.TokenKind.FractionalsecondsMethod: found = this._nextMethod('fractionalseconds'); break; case UriTokenizer.TokenKind.GeoDistanceMethod: found = this._nextMethod('geo.distance'); break; case UriTokenizer.TokenKind.GeoIntersectsMethod: found = this._nextMethod('geo.intersects'); break; case UriTokenizer.TokenKind.GeoLengthMethod: found = this._nextMethod('geo.length'); break; case UriTokenizer.TokenKind.HourMethod: found = this._nextMethod('hour'); break; case UriTokenizer.TokenKind.IndexofMethod: found = this._nextMethod('indexof'); break; case UriTokenizer.TokenKind.IsofMethod: found = this._nextMethod('isof'); break; case UriTokenizer.TokenKind.LengthMethod: found = this._nextMethod('length'); break; case UriTokenizer.TokenKind.MaxdatetimeMethod: found = this._nextMethod('maxdatetime'); break; case UriTokenizer.TokenKind.MindatetimeMethod: found = this._nextMethod('mindatetime'); break; case UriTokenizer.TokenKind.MinuteMethod: found = this._nextMethod('minute'); break; case UriTokenizer.TokenKind.MonthMethod: found = this._nextMethod('month'); break; case UriTokenizer.TokenKind.NowMethod: found = this._nextMethod('now'); break; case UriTokenizer.TokenKind.RoundMethod: found = this._nextMethod('round'); break; case UriTokenizer.TokenKind.SecondMethod: found = this._nextMethod('second'); break; case UriTokenizer.TokenKind.StartswithMethod: found = this._nextMethod('startswith'); break; case UriTokenizer.TokenKind.SubstringMethod: found = this._nextMethod('substring'); break; case UriTokenizer.TokenKind.TimeMethod: found = this._nextMethod('time'); break; case UriTokenizer.TokenKind.TolowerMethod: found = this._nextMethod('tolower'); break; case UriTokenizer.TokenKind.TotaloffsetminutesMethod: found = this._nextMethod('totaloffsetminutes'); break; case UriTokenizer.TokenKind.TotalsecondsMethod: found = this._nextMethod('totalseconds'); break; case UriTokenizer.TokenKind.ToupperMethod: found = this._nextMethod('toupper'); break; case UriTokenizer.TokenKind.TrimMethod: found = this._nextMethod('trim'); break; case UriTokenizer.TokenKind.YearMethod: found = this._nextMethod('year'); break; // Method for the aggregation extension case UriTokenizer.TokenKind.IsDefinedMethod: found = this._nextMethod('isdefined'); break; // Transformations for the aggregation extension case UriTokenizer.TokenKind.AggregateTrafo: found = this._nextMethod('aggregate'); break; case UriTokenizer.TokenKind.BottomCountTrafo: found = this._nextMethod('bottomcount'); break; case UriTokenizer.TokenKind.BottomPercentTrafo: found = this._nextMethod('bottompercent'); break; case UriTokenizer.TokenKind.BottomSumTrafo: found = this._nextMethod('bottomsum'); break; case UriTokenizer.TokenKind.ComputeTrafo: found = this._nextMethod('compute'); break; case UriTokenizer.TokenKind.ExpandTrafo: found = this._nextMethod('expand'); break; case UriTokenizer.TokenKind.FilterTrafo: found = this._nextMethod('filter'); break; case UriTokenizer.TokenKind.GroupByTrafo: found = this._nextMethod('groupby'); break; case UriTokenizer.TokenKind.OrderByTrafo: found = this._nextMethod('orderby'); break; case UriTokenizer.TokenKind.SearchTrafo: found = this._nextMethod('search'); break; case UriTokenizer.TokenKind.SkipTrafo: found = this._nextMethod('skip'); break; case UriTokenizer.TokenKind.TopTrafo: found = this._nextMethod('top'); break; case UriTokenizer.TokenKind.TopCountTrafo: found = this._nextMethod('topcount'); break; case UriTokenizer.TokenKind.TopPercentTrafo: found = this._nextMethod('toppercent'); break; case UriTokenizer.TokenKind.TopSumTrafo: found = this._nextMethod('topsum'); break; // Roll-up specification for the aggregation extension case UriTokenizer.TokenKind.RollUpSpec: found = this._nextMethod('rollup'); break; // Suffixes case UriTokenizer.TokenKind.AscSuffix: found = this._nextSuffix('asc'); break; case UriTokenizer.TokenKind.DescSuffix: found = this._nextSuffix('desc'); break; default: } if (found) { this._startIndex = previousIndex; } else { this._index = previousIndex; } return found; } /** * Determine whether the index is at the end of the string to be parsed; leave the index unchanged. * * @returns {boolean} whether the current index is at the end of the string to be parsed * @private */ _nextEOF() { return this._index >= this._parseString.length; } /** * Move past the given string constant if found; otherwise leave the index unchanged. * * @param {string} constant the string constant * @returns {boolean} whether the constant has been found at the current index * @private */ _nextConstant(constant) { if (this._parseString.startsWith(constant, this._index)) { this._index += constant.length; return true; } return false; } /** * Move past the given regular expression, if found; otherwise leave the index unchanged. * * @param {RegExp} regexp the regular expression * @returns {boolean} whether the regular expression has been found at the current index * @private */ _nextWithRegularExpression(regexp) { const parsed = regexp.exec(this._parseString.substring(this._index)); if (!parsed) return false; this._index += parsed[0].length; return true; } /** * Move past the given character if found; otherwise leave the index unchanged. * * @param {string} character the character * @returns {boolean} whether the given character has been found at the current index * @private */ _nextCharacter(character) { if (this._index < this._parseString.length && this._parseString.charAt(this._index) === character) { this._index++; return true; } return false; } /** * Move past a digit character ('0' to '9') if found; otherwise leave the index unchanged. * * @returns {boolean} whether a digit character has been found at the current index * @private */ _nextDigit() { if (this._index < this._parseString.length) { const code = this._parseString.charAt(this._index); if (code >= '0' && code <= '9') { this._index++; return true; } } return false; } /** * Move past whitespace (space or horizontal tabulator) characters if found; otherwise leave the index unchanged. * * @returns {boolean} whether whitespace characters have been found at the current index * @private */ _nextWhitespace() { let count = 0; while (this._nextCharacter(' ') || this._nextCharacter('\t')) count++; return count > 0; } /** * Move past an OData identifier if found; otherwise leave the index unchanged. * * @returns {boolean} whether an OData identifier has been found at the current index * @private */ _nextODataIdentifier() { let count = 0; if (this._index < this._parseString.length) { let code = this._parseString.codePointAt(this._index); if (Character.isUnicodeIdentifierStart(code) || code === 0x005F /* LOW LINE */) { count++; // Unicode characters outside of the Basic Multilingual Plane are // represented as two Javascript characters. this._index += Character.isSupplementaryCodePoint(code) ? 2 : 1; while (this._index < this._parseString.length && count < 128) { code = this._parseString.codePointAt(this._index); if (Character.isUnicodeIdentifierPart(code)) { count++; // Unicode characters outside of the Basic Multilingual // Plane are represented as two Javascript characters. this._index += Character.isSupplementaryCodePoint(code) ? 2 : 1; } else { break; } } } } return count > 0; } /** * Move past a qualified name if found; otherwise leave the index unchanged. * * @returns {boolean} whether a qualified name has been found at the current index * @private */ _nextQualifiedName() { const lastGoodIndex = this._index; if (!this._nextODataIdentifier()) return false; let count = 1; while (this._nextCharacter('.')) { if (this._nextODataIdentifier()) { count++; } else { this._index--; break; } } if (count >= 2) return true; this._index = lastGoodIndex; return false; } /** * Move past the given whitespace-surrounded operator constant if found. * * @param {string} operator the name of the operator * @returns {boolean} whether the operator has been found at the current index * @private */ _nextBinaryOperator(operator) { return this._nextWhitespace() && this._nextConstant(operator) && this._nextWhitespace(); } /** * Move past the given whitespace-suffixed operator constant if found. * * @param {string} operator the name of the operator * @returns {boolean} whether the operator has been found at the current index * @private */ _nextUnaryOperator(operator) { return this._nextConstant(operator) && this._nextWhitespace(); } /** * Move past the minus operator if found. * * @returns {boolean} whether the operator has been found at the current index * @private */ _nextMinusOperator() { // In order to avoid unnecessary minus operators for negative numbers, // we have to check what follows the minus sign. return this._nextCharacter('-') && !this._nextDigit() && !this._nextConstant('INF'); } /** * Move past the given method name and its immediately following opening parenthesis if found. * * @param {string} methodName the name of the method * @returns {boolean} whether the method has been found at the current index * @private */ _nextMethod(methodName) { return this._nextConstant(methodName) && this._nextCharacter('('); } /** * Move past (required) whitespace and the given suffix name if found. * * @param {string} suffixName the name of the suffix * @returns {boolean} whether the suffix has been found at the current index * @private */ _nextSuffix(suffixName) { return this._nextWhitespace() && this._nextConstant(suffixName); } /** * Move past a parameter-alias name if found. * @returns {boolean} whether the parameter-alias name has been found at the current index * @private */ _nextParameterAliasName() { return this._nextCharacter('@') && this._nextODataIdentifier(); } /** * Move past an integer value if found; otherwise leave the index unchanged. * @returns {boolean} whether an integer value has been found at the current index * @private */ _nextIntegerValue() { return this._nextWithRegularExpression(INTEGER_VALUE_REGEXP); } /** * Move past a decimal value with a fractional part if found; otherwise leave the index unchanged. * Whole numbers must be found with {@link #nextIntegerValue()}. * * @returns {boolean} whether a decimal value has been found at the current index * @private */ _nextDecimalValue() { return this._nextWithRegularExpression(DECIMAL_VALUE_REGEXP); } /** * Move past a floating-point-number value with an exponential part * or one of the special constants 'NaN', '-INF', and 'INF' if found; * otherwise leave the index unchanged. * Whole numbers must be found with {@link #nextIntegerValue()}. * Decimal numbers must be found with {@link #nextDecimalValue()}. * * @returns {boolean} whether a double value has been found at the current index * @private */ _nextDoubleValue() { return this._nextWithRegularExpression(DOUBLE_VALUE_REGEXP); } /** * Move past an enumeration-type value if found; otherwise leave the index unchanged. * @returns {boolean} whether an enumeration-type value has been found at the current index * @private */ _nextEnumValue() { const lastGoodIndex = this._index; if (this._nextQualifiedName() && this._nextCharacter('\'')) { do { if (!(this._nextODataIdentifier() || this._nextIntegerValue(true))) { this._index = lastGoodIndex; return false; } } while (this._nextCharacter(',')); if (this._nextCharacter('\'')) return true; } this._index = lastGoodIndex; return false; } /** * Move past a geography or geometry value if found. * @param {boolean} isGeography if true the suffix must be 'geography', if false it must be 'geometry' * @param {RegExp} regexp the regular expression for the value within the quotes * @returns {boolean} whether a geography/geometry value has been found at the current index * @private */ _nextGeoValue(isGeography, regexp) { const prefix = isGeography ? 'geography' : 'geometry'; if (this._index + prefix.length - 1 < this._parseString.length && this._parseString.substring(this._index, this._index + prefix.length).toLowerCase() === prefix) { this._index += prefix.length; } else { return false; } return this._nextCharacter("'") && this._nextWithRegularExpression(regexp) && this._nextCharacter("'"); } /** * Move past a JSON string if found; otherwise leave the index unchanged. * @returns {boolean} whether a JSON string has been found at the current index * @private */ _nextJsonString() { return this._nextWithRegularExpression(JSON_STRING_REGEXP); } /** * Move past a JSON value if found; otherwise leave the index unchanged. * @returns {boolean} whether a JSON value has been found at the current index * @private */ _nextJsonValue() { return this._nextConstant('null') || this._nextConstant('true') || this._nextConstant('false') || this._nextDoubleValue() || this._nextDecimalValue() || this._nextIntegerValue() || this._nextJsonString() || this._nextJsonArrayOrObject(); } /** * Move past a JSON object member if found; otherwise leave the index unchanged. * @returns {boolean} whether a JSON object member has been found at the current index * @private */ _nextJsonMember() { const lastGoodIndex = this._index; if (this._nextJsonString() && this._nextCharacter(':') && this._nextJsonValue()) { return true; } this._index = lastGoodIndex; return false; } /** * Move past a JSON array or object if found; otherwise leave the index unchanged. * @returns {boolean} whether a JSON array or object has been found at the current index * @private */ _nextJsonArrayOrObject() { const lastGoodIndex = this._index; if (this._nextCharacter('[')) { if (this._nextJsonValue()) { while (this._nextCharacter(',')) { if (!this._nextJsonValue()) { this._index = lastGoodIndex; return false; } } } if (this._nextCharacter(']')) return true; this._index = lastGoodIndex; return false; } else if (this._nextCharacter('{')) { if (this._nextJsonMember()) { while (this._nextCharacter(',')) { if (!this._nextJsonMember()) { this._index = lastGoodIndex; return false; } } } if (this._nextCharacter('}')) return true; this._index = lastGoodIndex; return false; } return false; } /** * Move past a search operator AND if found. * @returns {boolean} whether the search operator AND has been found at the current index * @private */ _nextAndOperatorSearch() { if (this._nextWhitespace()) { const lastGoodIndex = this._index; if (this._nextUnaryOperator('OR')) return false; if (!this._nextUnaryOperator('AND')) this._index = lastGoodIndex; // implicit AND return true; } return false; } /** * Move past a search word if found. * @returns {boolean} whether a search word has been found at the current index * @private */ _nextWord() { let count = 0; while (this._index < this._parseString.length) { const code = this._parseString.codePointAt(this._index); // From "OData ABNF Construction Rules Version 4.01 and 4.0": // "A searchWord is a sequence of one or more letters, digits, commas, or dots. // This includes Unicode characters of categories L or N [...] // The words AND, OR, and NOT are not a valid searchWord." if (Character.isLetter(code) || Character.isDigit(code) || code === 0x002C /* COMMA */ || code === 0x002E /* FULL STOP */) { count++; // Unicode characters outside of the Basic Multilingual Plane are represented // as two Javascript characters. this._index += Character.isSupplementaryCodePoint(code) ? 2 : 1; } else { break; } } const word = this._parseString.substring(this._index - count, this._index); return count > 0 && !(word === 'OR' || word === 'AND' || word === 'NOT'); } /** * Require the next requested token. * If the next token is not of the requested kind an exception is thrown. * * @param {UriTokenizer.TokenKind} tokenKind next token * @returns {boolean} true if reading next token was successful * @throws {UriSyntaxError} if the token has not been found */ requireNext(tokenKind) { if (this.next(tokenKind)) return true; throw new UriSyntaxError(UriSyntaxError.Message.TOKEN_REQUIRED, tokenKind, this._parseString, this.getPosition()); } } UriTokenizer.TokenKind = { EOF: 'EOF', // signals the end of the string to be parsed // constant-value tokens (convention: uppercase) REF: 'REF', VALUE: 'VALUE', COUNT: 'COUNT', METADATA: 'METADATA', BATCH: 'BATCH', CROSSJOIN: 'CROSSJOIN', ALL: 'ALL', ENTITY: 'ENTITY', ROOT: 'ROOT', IT: 'IT', APPLY: 'APPLY', // for the aggregation extension EXPAND: 'EXPAND', FILTER: 'FILTER', LEVELS: 'LEVELS', ORDERBY: 'ORDERBY', SEARCH: 'SEARCH', SELECT: 'SELECT', SKIP: 'SKIP', TOP: 'TOP', LAMBDA_ANY: 'LAMBDA_ANY', LAMBDA_ALL: 'LAMBDA_ALL', OPEN: 'OPEN', CLOSE: 'CLOSE', COMMA: 'COMMA', SEMI: 'SEMI', COLON: 'COLON', DOT: 'DOT', SLASH: 'SLASH', EQ: 'EQ', STAR: 'STAR', PLUS: 'PLUS', NULL: 'NULL', MAX: 'MAX', AVERAGE: 'AVERAGE', // for the aggregation extension COUNTDISTINCT: 'COUNTDISTINCT', // for the aggregation extension IDENTITY: 'IDENTITY', // for the aggregation extension MIN: 'MIN', // for the aggregation extension SUM: 'SUM', // for the aggregation extension // variable-value tokens (convention: mixed case) ODataIdentifier: 'ODataIdentifier', QualifiedName: 'QualifiedName', ParameterAliasName: 'ParameterAliasName', BooleanValue: 'BooleanValue', StringValue: 'StringValue', UnsignedIntegerValue: 'UnsignedIntegerValue', IntegerValue: 'IntegerValue', GuidValue: 'GuidValue', DateValue: 'DateValue', DateTimeOffsetValue: 'DateTimeOffsetValue', TimeOfDayValue: 'TimeOfDayValue', DecimalValue: 'DecimalValue', DoubleValue: 'DoubleValue', DurationValue: 'DurationValue', BinaryValue: 'BinaryValue', EnumValue: 'EnumValue', GeographyPoint: 'GeographyPoint', GeometryPoint: 'GeometryPoint', GeographyLineString: 'GeographyLineString', GeometryLineString: 'GeometryLineString', GeographyPolygon: 'GeographyPolygon', GeometryPolygon: 'GeometryPolygon', GeographyMultiPoint: 'GeographyMultiPoint', GeometryMultiPoint: 'GeometryMultiPoint', GeographyMultiLineString: 'GeographyMultiLineString', GeometryMultiLineString: 'GeometryMultiLineString', GeographyMultiPolygon: 'GeographyMultiPolygon', GeometryMultiPolygon: 'GeometryMultiPolygon', GeographyCollection: 'GeographyCollection', GeometryCollection: 'GeometryCollection', jsonArrayOrObject: 'jsonArrayOrObject', Word: 'Word', Phrase: 'Phrase', OrOperatorSearch: 'OrOperatorSearch', AndOperatorSearch: 'AndOperatorSearch', NotOperatorSearch: 'NotOperatorSearch', OrOperator: 'OrOperator', AndOperator: 'AndOperator', EqualsOperator: 'EqualsOperator', NotEqualsOperator: 'NotEqualsOperator', GreaterThanOperator: 'GreaterThanOperator', GreaterThanOrEqualsOperator: 'GreaterThanOrEqualsOperator', LessThanOperator: 'LessThanOperator', LessThanOrEqualsOperator: 'LessThanOrEqualsOperator', HasOperator: 'HasOperator', AddOperator: 'AddOperator', SubOperator: 'SubOperator', MulOperator: 'MulOperator', DivOperator: 'DivOperator', ModOperator: 'ModOperator', MinusOperator: 'MinusOperator', NotOperator: 'NotOperator', AsOperator: 'AsOperator', // for the aggregation extension FromOperator: 'FromOperator', // for the aggregation extension WithOperator: 'WithOperator', // for the aggregation extension CastMethod: 'CastMethod', CeilingMethod: 'CeilingMethod', ConcatMethod: 'ConcatMethod', ContainsMethod: 'ContainsMethod', DateMethod: 'DateMethod', DayMethod: 'DayMethod', EndswithMethod: 'EndswithMethod', FloorMethod: 'FloorMethod', FractionalsecondsMethod: 'FractionalsecondsMethod', GeoDistanceMethod: 'GeoDistanceMethod', GeoIntersectsMethod: 'GeoIntersectsMethod', GeoLengthMethod: 'GeoLengthMethod', HourMethod: 'HourMethod', IndexofMethod: 'IndexofMethod', IsofMethod: 'IsofMethod', LengthMethod: 'LengthMethod', MaxdatetimeMethod: 'MaxdatetimeMethod', MindatetimeMethod: 'MindatetimeMethod', MinuteMethod: 'MinuteMethod', MonthMethod: 'MonthMethod', NowMethod: 'NowMethod', RoundMethod: 'RoundMethod', SecondMethod: 'SecondMethod', StartswithMethod: 'StartswithMethod', SubstringMethod: 'SubstringMethod', TimeMethod: 'TimeMethod', TolowerMethod: 'TolowerMethod', TotaloffsetminutesMethod: 'TotaloffsetminutesMethod', TotalsecondsMethod: 'TotalsecondsMethod', ToupperMethod: 'ToupperMethod', TrimMethod: 'TrimMethod', YearMethod: 'YearMethod', IsDefinedMethod: 'IsDefinedMethod', // for the aggregation extension AggregateTrafo: 'AggregateTrafo', // for the aggregation extension BottomCountTrafo: 'BottomCountTrafo', // for the aggregation extension BottomPercentTrafo: 'BottomPercentTrafo', // for the aggregation extension BottomSumTrafo: 'BottomSumTrafo', // for the aggregation extension ComputeTrafo: 'ComputeTrafo', // for the aggregation extension ExpandTrafo: 'ExpandTrafo', // for the aggregation extension FilterTrafo: 'FilterTrafo', // for the aggregation extension GroupByTrafo: 'GroupByTrafo', // for the aggregation extension OrderByTrafo: 'OrderByTrafo', // for the aggregation extension SearchTrafo: 'SearchTrafo', // for the aggregation extension SkipTrafo: 'SkipTrafo', // for the aggregation extension TopTrafo: 'TopTrafo', // for the aggregation extension TopCountTrafo: 'TopCountTrafo', // for the aggregation extension TopPercentTrafo: 'TopPercentTrafo', // for the aggregation extension TopSumTrafo: 'TopSumTrafo', // for the aggregation extension RollUpSpec: 'RollUpSpec', // for the aggregation extension AscSuffix: 'AscSuffix', DescSuffix: 'DescSuffix' }; module.exports = UriTokenizer;