UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

306 lines (276 loc) 14.6 kB
'use strict'; const ValueConverter = require('../utils/ValueConverter'); const ValueValidator = require('../validator/ValueValidator'); const validateThat = require('../validator/ParameterValidator').validateThat; const JsonContentTypeInfo = require('../format/JsonContentTypeInfo'); const EdmTypeKind = require('../edm/EdmType').TypeKind; const EdmPrimitiveTypeKind = require('../edm/EdmPrimitiveTypeKind'); const DeserializationError = require('../errors/DeserializationError'); const INTEGER_VALIDATION = new RegExp('^[-+]?\\d{1,10}$'); const NUMBER = '[-+]?\\d+(?:\\.\\d+)?(?:[Ee][-+]?\\d+)?'; const NUMBER_VALIDATION = new RegExp('^' + NUMBER + '$'); // Definitions for geo literals const SRID = 'SRID=(\\d{1,8});'; // 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. // Only the coordinates are grouped with a RegExp group; the code below relies on this fact. const POINT_VALIDATION = new RegExp('^' + SRID + 'Point\\((' + POSITION + ')\\)$', 'i'); const LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'LineString\\((' + LINE + ')\\)$', 'i'); const POLYGON_VALIDATION = new RegExp('^' + SRID + 'Polygon\\((' + MULTI_LINE + ')\\)$', 'i'); const MULTI_POINT_VALIDATION = new RegExp('^' + SRID + 'MultiPoint\\((' + MULTI_POSITION + ')\\)$', 'i'); const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\((' + MULTI_LINE + ')\\)$', 'i'); const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i'); const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i'); /** * Deserializes a provided payload containing a textual representation of a primitive value * into a Javascript object. */ class ValueTextDeserializer { /** * Creates an instance of ValueTextDeserializer. */ constructor() { this._converter = new ValueConverter(new ValueValidator(), // The input is a string, so the parameter to expect the format according to IEEE754 // can be set unconditionally. This is needed, e.g., for large Int64 values. new JsonContentTypeInfo().addParameter(JsonContentTypeInfo.FormatParameter.IEEE754, 'true')); } /** * Converts a provided value into the appropriate JavaScript value. * Available facets are taken into account. * * @param {EdmProperty} edmProperty the corresponding edm property * @param {string} propertyValue the string value of the primitive property * @returns {?(number|string|boolean|Buffer|Object)} the primitive JavaScript value * @throws {DeserializationError} if the conversion was not successful */ convertPrimitiveValue(edmProperty, propertyValue) { validateThat('edmProperty', edmProperty).truthy().typeOf('object'); validateThat('propertyValue', propertyValue).truthy().typeOf('string'); let parsed; let value; const type = edmProperty.getType(); switch (type) { case EdmPrimitiveTypeKind.Binary: value = Buffer.from(propertyValue); break; case EdmPrimitiveTypeKind.Boolean: if (propertyValue === 'true') value = true; if (propertyValue === 'false') value = false; break; case EdmPrimitiveTypeKind.Int16: case EdmPrimitiveTypeKind.Int32: case EdmPrimitiveTypeKind.Byte: case EdmPrimitiveTypeKind.SByte: if (!INTEGER_VALIDATION.test(propertyValue)) { throw new DeserializationError(`Wrong value for property '${edmProperty.getName()}'.`); } value = Number(propertyValue); break; case EdmPrimitiveTypeKind.Single: case EdmPrimitiveTypeKind.Double: if (!NUMBER_VALIDATION.test(propertyValue)) { throw new DeserializationError(`Wrong value for property '${edmProperty.getName()}'.`); } value = Number(propertyValue); break; case EdmPrimitiveTypeKind.GeographyPoint: case EdmPrimitiveTypeKind.GeometryPoint: parsed = this._parseGeoValue(edmProperty, propertyValue, POINT_VALIDATION); value = this._parsePoint(parsed.values); if (parsed.crs) value.crs = parsed.crs; break; case EdmPrimitiveTypeKind.GeographyLineString: case EdmPrimitiveTypeKind.GeometryLineString: parsed = this._parseGeoValue(edmProperty, propertyValue, LINE_STRING_VALIDATION); value = this._parseLineString(parsed.values); if (parsed.crs) value.crs = parsed.crs; break; case EdmPrimitiveTypeKind.GeographyPolygon: case EdmPrimitiveTypeKind.GeometryPolygon: parsed = this._parseGeoValue(edmProperty, propertyValue, POLYGON_VALIDATION); value = this._parsePolygon(parsed.values); if (parsed.crs) value.crs = parsed.crs; break; case EdmPrimitiveTypeKind.GeographyMultiPoint: case EdmPrimitiveTypeKind.GeometryMultiPoint: parsed = this._parseGeoValue(edmProperty, propertyValue, MULTI_POINT_VALIDATION); value = this._parseMultiPoint(parsed.values); if (parsed.crs) value.crs = parsed.crs; break; case EdmPrimitiveTypeKind.GeographyMultiLineString: case EdmPrimitiveTypeKind.GeometryMultiLineString: parsed = this._parseGeoValue(edmProperty, propertyValue, MULTI_LINE_STRING_VALIDATION); value = this._parseMultiLineString(parsed.values); if (parsed.crs) value.crs = parsed.crs; break; case EdmPrimitiveTypeKind.GeographyMultiPolygon: case EdmPrimitiveTypeKind.GeometryMultiPolygon: parsed = this._parseGeoValue(edmProperty, propertyValue, MULTI_POLYGON_VALIDATION); value = this._parseMultiPolygon(parsed.values); if (parsed.crs) value.crs = parsed.crs; break; case EdmPrimitiveTypeKind.GeographyCollection: case EdmPrimitiveTypeKind.GeometryCollection: parsed = this._parseGeoValue(edmProperty, propertyValue, COLLECTION_VALIDATION); value = { type: 'GeometryCollection' }; // Split at commas that are followed by first letters of type names. value.geometries = parsed.values.split(/,(?=[LMP])/i).map(geoLiteral => { const content = geoLiteral.slice(geoLiteral.indexOf('(') + 1, -1); if (/^Point/i.test(geoLiteral)) return this._parsePoint(content); if (/^LineString/i.test(geoLiteral)) return this._parseLineString(content); if (/^Polygon/i.test(geoLiteral)) return this._parsePolygon(content); if (/^MultiPoint/i.test(geoLiteral)) return this._parseMultiPoint(content); if (/^MultiLineString/i.test(geoLiteral)) return this._parseMultiLineString(content); if (/^MultiPolygon/i.test(geoLiteral)) return this._parseMultiPolygon(content); throw new DeserializationError('Unknown content in geometry collection: ' + geoLiteral); }); if (parsed.crs) value.crs = parsed.crs; break; default: value = propertyValue; } // The value converter asserts maxLength, scale, precision facets // and performs additional checks for geo types. try { this._converter.convert(edmProperty, value); } catch (error) { throw new DeserializationError("Wrong value for property '" + edmProperty.getName() + "'.", error); } return value; } /** * Parses a geo value. * @param {EdmProperty} edmProperty the corresponding EDM property * @param {string} propertyValue the string value of the EDM property * @param {RegExp} validationRegExp regular expression that must match * @returns {{ values: string, crs: ?Object }} a JavaScript object with values as string and CRS information * @throws {DeserializationError} if parsing was not successful * @private */ _parseGeoValue(edmProperty, propertyValue, validationRegExp) { const edmSrid = this._determineSrid(edmProperty); const match = validationRegExp.exec(propertyValue); if (match && (edmSrid === 'variable' || match[1] === String(edmSrid))) { let value = { values: match[2] }; if (edmSrid === 'variable') value.crs = { type: 'name', properties: { name: 'EPSG:' + match[1] } }; return value; } throw new DeserializationError(`Wrong value for property '${edmProperty.getName()}'.`); } /** * Returns value of the SRID facet or the default value (4326 for geography, 0 for geometry). * If the specified type is a TypeDefinition, then take also the type definition's facet into account. * * @param {EdmProperty} edmProperty object containing SRID facet * @returns {?(number|string)} value of SRID facet * @private */ _determineSrid(edmProperty) { let srid = edmProperty.getSrid(); let type = edmProperty.getType(); if (srid === null && type.getKind() === EdmTypeKind.DEFINITION) { srid = type.getSrid(); type = type.getUnderlyingType(); } if (srid === null) srid = type.getName().startsWith('Geography') ? 4326 : 0; return srid; } /** * Parses point values into a GeoJSON point object. * @param {string} content point values in well-known text format * @returns {{ type: string, coordinates: number[] }} a GeoJSON point object * @private */ _parsePoint(content) { return { type: 'Point', coordinates: content.split(' ').map(Number) }; } /** * Parses linestring values into a GeoJSON linestring object. * @param {string} content linestring values in well-known text format * @returns {{ type: string, coordinates: Array.<number[]> }} a GeoJSON linestring object * @private */ _parseLineString(content) { return { type: 'LineString', coordinates: content.split(',').map(position => position.split(' ').map(Number)) }; } /** * Parses polygon values into a GeoJSON polygon object. * @param {string} content polygon values in well-known text format * @returns {{ type: string, coordinates: Array.<Array.<number[]>> }} a GeoJSON polygon object * @private */ _parsePolygon(content) { return { type: 'Polygon', coordinates: content.slice(1, -1).split('),(') .map(ring => ring.split(',').map(position => position.split(' ').map(Number))) }; } /** * Parses multipoint values into a GeoJSON multipoint object. * @param {string} content multipoint values in well-known text format * @returns {{ type: string, coordinates: Array.<number[]> }} a GeoJSON multipoint object * @private */ _parseMultiPoint(content) { return { type: 'MultiPoint', coordinates: content.slice(1, -1).split('),(').map(position => position.split(' ').map(Number)) }; } /** * Parses multilinestring values into a GeoJSON multilinestring object. * @param {string} content multilinestring values in well-known text format * @returns {{ type: string, coordinates: Array.<Array.<number[]>> }} a GeoJSON multilinestring object * @private */ _parseMultiLineString(content) { return { type: 'MultiLineString', coordinates: content.slice(1, -1).split('),(') .map(line => line.split(',').map(position => position.split(' ').map(Number))) }; } /** * Parses multipolygon values into a GeoJSON multipolygon object. * @param {string} content multipolygon values in well-known text format * @returns {{ type: string, coordinates: Array.<Array.<Array.<number[]>>> }} a GeoJSON multipolygon object * @private */ _parseMultiPolygon(content) { return { type: 'MultiPolygon', coordinates: content.slice(2, -2).split(')),((') .map(polygon => polygon.split('),(') .map(line => line.split(',').map(position => position.split(' ').map(Number)))) }; } } module.exports = ValueTextDeserializer;