@sap/odata-v4
Version:
OData V4.0 server library
354 lines (317 loc) • 16.5 kB
JavaScript
'use strict';
const FilterParser = require('./FilterParser');
const OrderByParser = require('./OrderByParser');
const SearchParser = require('./SearchParser');
const SelectParser = require('./SelectParser');
const TokenKind = require('./UriTokenizer').TokenKind;
const EdmTypeKind = require('../edm/EdmType').TypeKind;
const FullQualifiedName = require('../FullQualifiedName');
const UriResource = require('./UriResource');
const ResourceKind = UriResource.ResourceKind;
const ExpandItem = require('./ExpandItem');
const UriSyntaxError = require('../errors/UriSyntaxError');
const UriQueryOptionSemanticError = require('../errors/UriQueryOptionSemanticError');
const UriSemanticError = require('../errors/UriSemanticError');
const FeatureSupport = require('../FeatureSupport');
class ExpandParser {
/**
* Create an expand parser.
* @param {Edm} edm entity data model
*/
constructor(edm) {
this._edm = edm;
}
/**
* Parse a string into an array of expand items.
* @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 {ExpandItem[]} the parsed items
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
*/
parse(tokenizer, referencedType, crossjoinEntitySets, aliases) {
let expandItems = [];
let navigationProperties = [];
do {
const expandItem = this._parseItem(tokenizer, referencedType, crossjoinEntitySets, aliases);
const navigationSegment = expandItem.getPathSegments().find(segment =>
segment.getKind() === ResourceKind.NAVIGATION_TO_ONE
|| segment.getKind() === ResourceKind.NAVIGATION_TO_MANY);
if (navigationSegment) {
const navigationProperty = navigationSegment.getNavigationProperty();
if (navigationProperties.includes(navigationProperty)) {
throw new UriSyntaxError(UriSyntaxError.Message.EXPAND_DUPLICATED_NAVIGATION_PROPERTY,
navigationProperty.getName());
}
navigationProperties.push(navigationProperty);
}
expandItems.push(expandItem);
} while (tokenizer.next(TokenKind.COMMA));
return expandItems;
}
/**
* Parse a string into an expand item.
* @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 {ExpandItem} the parsed item
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
* @private
*/
_parseItem(tokenizer, referencedType, crossjoinEntitySets, aliases) {
let item = new ExpandItem();
if (tokenizer.next(TokenKind.STAR)) {
item.setAll(true);
if (tokenizer.next(TokenKind.SLASH)) {
tokenizer.requireNext(TokenKind.REF);
FeatureSupport.failUnsupported(FeatureSupport.features.Ref);
item.setPathSegments([new UriResource().setKind(ResourceKind.REF)]);
} else if (tokenizer.next(TokenKind.OPEN)) {
tokenizer.requireNext(TokenKind.LEVELS);
const name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
item.setOption(name, this._parseLevels(tokenizer));
tokenizer.requireNext(TokenKind.CLOSE);
FeatureSupport.failUnsupported(FeatureSupport.features.Levels);
}
} else {
let path = this.parseExpandPath(tokenizer, referencedType, crossjoinEntitySets);
if (path.length === 0) {
throw new UriSyntaxError(UriSyntaxError.Message.EXPAND_NO_VALID_PATH,
tokenizer.getParseString(), tokenizer.getPosition());
}
let lastPart = path[path.length - 1];
let hasSlash = tokenizer.getText() === '/';
if (hasSlash || tokenizer.next(TokenKind.SLASH)) {
hasSlash = true;
if (lastPart.getKind() === ResourceKind.NAVIGATION_TO_ONE
|| lastPart.getKind() === ResourceKind.NAVIGATION_TO_MANY) {
const navigationProperty = lastPart.getNavigationProperty();
const typeCastSuffix = this._parseTypeCast(tokenizer, navigationProperty.getEntityType());
if (typeCastSuffix != null) {
lastPart = new UriResource()
.setKind(ResourceKind.TYPE_CAST)
.setTypeCast(typeCastSuffix)
.setIsCollection(navigationProperty.isCollection());
path.push(lastPart);
hasSlash = false;
}
}
}
const isCollection = lastPart.isCollection();
let isRef = false;
let isCount = false;
if (hasSlash || tokenizer.next(TokenKind.SLASH)) {
if (tokenizer.next(TokenKind.REF)) {
FeatureSupport.failUnsupported(FeatureSupport.features.Ref);
path.push(new UriResource()
.setKind(isCollection ? ResourceKind.REF_COLLECTION : ResourceKind.REF)
.setIsCollection(isCollection));
isRef = true;
} else if (isCollection && tokenizer.next(TokenKind.COUNT)) {
path.push(new UriResource().setKind(ResourceKind.COUNT));
isCount = true;
} else {
tokenizer.requireNext(TokenKind.STAR);
item.setAll(true);
}
}
const type = lastPart.getEdmType();
const isCyclic = item.isAll() || referencedType && type.compatibleTo(referencedType);
this._parseOptions(tokenizer, type, isCollection, aliases, item, isRef, isCount, isCyclic);
item.setPathSegments(path);
}
return item;
}
/**
* Parse a string into an expand path.
* @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
* @returns {UriResource[]} the parsed path
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
*/
parseExpandPath(tokenizer, referencedType, crossjoinEntitySets) {
let path = [];
let type = referencedType;
let name = null;
// In the crossjoin case the start has to be an EntitySet name which will dictate the reference type.
if (type == null && crossjoinEntitySets != null && crossjoinEntitySets.length > 0) {
tokenizer.requireNext(TokenKind.ODataIdentifier);
name = tokenizer.getText();
const crossjoinEntitySet = crossjoinEntitySets.find(entitySet => entitySet.getName() === name);
if (crossjoinEntitySet == null) {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.ENTITY_SET_NOT_FOUND, name);
} else {
path.push(new UriResource()
.setKind(ResourceKind.ENTITY_COLLECTION)
.setEntitySet(crossjoinEntitySet));
type = crossjoinEntitySet.getEntityType();
tokenizer.requireNext(TokenKind.SLASH);
}
}
const typeCast = this._parseTypeCast(tokenizer, type);
if (typeCast != null) {
path.push(new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(typeCast));
type = typeCast;
tokenizer.requireNext(TokenKind.SLASH);
}
while (tokenizer.next(TokenKind.ODataIdentifier)) {
name = tokenizer.getText();
const property = type.getStructuralProperty(name);
if (property != null && property.getType().getKind() === EdmTypeKind.COMPLEX) {
name = null;
type = property.getType();
path.push(new UriResource()
.setKind(ResourceKind.COMPLEX_PROPERTY)
.setProperty(property)
.setIsCollection(property.isCollection()));
tokenizer.requireNext(TokenKind.SLASH);
const typeCastSuffix = this._parseTypeCast(tokenizer, type);
if (typeCastSuffix != null) {
path.push(new UriResource()
.setKind(ResourceKind.TYPE_CAST)
.setTypeCast(typeCastSuffix)
.setIsCollection(property.isCollection()));
tokenizer.requireNext(TokenKind.SLASH);
type = typeCastSuffix;
}
}
}
const navigationProperty = type.getNavigationProperty(name);
if (navigationProperty == null) {
if (name) {
throw new UriQueryOptionSemanticError(
UriQueryOptionSemanticError.Message.NAVIGATION_PROPERTY_NOT_FOUND,
name, type.getFullQualifiedName().toString());
}
} else {
const isCollection = navigationProperty.isCollection();
path.push(new UriResource()
.setKind(isCollection ? ResourceKind.NAVIGATION_TO_MANY : ResourceKind.NAVIGATION_TO_ONE)
.setNavigationProperty(navigationProperty)
.setIsCollection(isCollection));
}
return path;
}
/**
* Parse a string supposed to contain a type cast.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {?EdmType} referencedType type the expression references
* @returns {?(EdmEntityType|EdmComplexType)} the parsed type cast (or null if nothing found)
* @throws {UriQueryOptionSemanticError}
* @private
*/
_parseTypeCast(tokenizer, referencedType) {
if (tokenizer.next(TokenKind.QualifiedName)) {
const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText());
const isEntityType = referencedType.getKind() === EdmTypeKind.ENTITY;
const type = isEntityType ?
this._edm.getEntityType(qualifiedName) :
this._edm.getComplexType(qualifiedName);
if (type == null) {
throw new UriQueryOptionSemanticError(
isEntityType ?
UriSemanticError.Message.ENTITY_TYPE_NOT_FOUND :
UriQueryOptionSemanticError.Message.COMPLEX_TYPE_NOT_FOUND,
tokenizer.getText());
} else if (type.compatibleTo(referencedType)) {
return type;
} else {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE,
qualifiedName.toString(), referencedType.getFullQualifiedName().toString());
}
}
return null;
}
/**
* Parse a string with expand options.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {?EdmType} referencedType type the options reference
* @param {boolean} referencedIsCollection whether the referenced expression is a collection
* @param {?Object} aliases alias definitions
* @param {ExpandItem} item the expand item where the parsed options will be set
* @param {boolean} forRef whether the referenced expression ends with $ref
* @param {boolean} forCount whether the referenced expression ends with $count
* @param {boolean} isCyclic whether the expansion is of the same or a compatible type
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
* @private
*/
_parseOptions(tokenizer, referencedType, referencedIsCollection, aliases, item, forRef, forCount, isCyclic) {
if (tokenizer.next(TokenKind.OPEN)) {
do {
let name = null;
let value = null;
if (referencedIsCollection && !forCount && tokenizer.next(TokenKind.COUNT)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
tokenizer.requireNext(TokenKind.BooleanValue);
value = tokenizer.getText() === 'true';
} else if (!forRef && !forCount && tokenizer.next(TokenKind.EXPAND)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
value = new ExpandParser(this._edm).parse(tokenizer, referencedType, null, aliases);
} else if (referencedIsCollection && tokenizer.next(TokenKind.FILTER)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
value = new FilterParser(this._edm).parse(tokenizer, referencedType, null, aliases);
} else if (isCyclic && !forRef && !forCount && tokenizer.next(TokenKind.LEVELS)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
value = this._parseLevels(tokenizer);
FeatureSupport.failUnsupported(FeatureSupport.features.Levels);
} else if (referencedIsCollection && !forCount && tokenizer.next(TokenKind.ORDERBY)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
value = new OrderByParser(this._edm).parse(tokenizer, referencedType, null, aliases);
} else if (referencedIsCollection && tokenizer.next(TokenKind.SEARCH)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
value = new SearchParser().parse(tokenizer);
} else if (!forRef && !forCount && tokenizer.next(TokenKind.SELECT)) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
value = new SelectParser(this._edm).parse(tokenizer, referencedType, referencedIsCollection);
} else if (referencedIsCollection && !forCount
&& (tokenizer.next(TokenKind.SKIP) || tokenizer.next(TokenKind.TOP))) {
name = tokenizer.getText();
tokenizer.requireNext(TokenKind.EQ);
tokenizer.requireNext(TokenKind.UnsignedIntegerValue);
value = Number.parseInt(tokenizer.getText(), 10);
if (!Number.isSafeInteger(value)) {
throw new UriSyntaxError(UriSyntaxError.Message.OPTION_NON_NEGATIVE_INTEGER, name);
}
} else {
throw new UriSyntaxError(UriSyntaxError.Message.WRONG_OPTION_NAME);
}
if (item.getOption(name) == null) {
item.setOption(name, value);
} else {
throw new UriSyntaxError(UriSyntaxError.Message.DUPLICATED_OPTION, name);
}
} while (tokenizer.next(TokenKind.SEMI));
tokenizer.requireNext(TokenKind.CLOSE);
}
}
/**
* Parse a string supposed to contain a value for the $levels option.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @returns {string} the parsed $levels value
* @throws {UriSyntaxError}
* @private
*/
_parseLevels(tokenizer) {
if (tokenizer.next(TokenKind.MAX)) return tokenizer.getText();
tokenizer.requireNext(TokenKind.UnsignedIntegerValue);
const result = Number.parseInt(tokenizer.getText(), 10);
if (Number.isSafeInteger(result)) return result;
throw new UriSyntaxError(UriSyntaxError.Message.OPTION_NON_NEGATIVE_INTEGER, '$levels');
}
}
module.exports = ExpandParser;