@sap/odata-v4
Version:
OData V4.0 server library
306 lines (276 loc) • 14.6 kB
JavaScript
;
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;