@sap/odata-v4
Version:
OData V4.0 server library
282 lines (249 loc) • 12.7 kB
JavaScript
;
const TokenKind = require('./UriTokenizer').TokenKind;
const EdmTypeKind = require('../edm/EdmType').TypeKind;
const FullQualifiedName = require('../FullQualifiedName');
const UriResource = require('./UriResource');
const ResourceKind = UriResource.ResourceKind;
const SelectItem = require('./SelectItem');
const UriQueryOptionSemanticError = require('../errors/UriQueryOptionSemanticError');
const UriSemanticError = require('../errors/UriSemanticError');
const UriSyntaxError = require('../errors/UriSyntaxError');
const FeatureSupport = require('../FeatureSupport');
class SelectParser {
/**
* Create a select parser.
* @param {Edm} edm entity data model
*/
constructor(edm) {
this._edm = edm;
}
/**
* Parse a select string into an array of select items.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {?EdmType} referencedType type the expression references
* @param {boolean} referencedIsCollection whether a collection is referenced
* @returns {SelectItem[]} the parsed items
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
*/
parse(tokenizer, referencedType, referencedIsCollection) {
let selectItems = [];
do {
selectItems.push(this._parseItem(tokenizer, referencedType, referencedIsCollection));
} while (tokenizer.next(TokenKind.COMMA));
return selectItems;
}
/**
* Parse a select string into a select item.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {?EdmType} referencedType type the expression references
* @param {boolean} referencedIsCollection whether a collection is referenced
* @returns {SelectItem} the parsed item
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
* @private
*/
_parseItem(tokenizer, referencedType, referencedIsCollection) {
let item = new SelectItem();
let pathSegments = [];
if (tokenizer.next(TokenKind.STAR)) {
item.setAll(true);
} else if (tokenizer.next(TokenKind.QualifiedName)) {
// The namespace or its alias could consist of dot-separated OData identifiers.
const allOperationsInSchema = this._parseAllOperationsInSchema(tokenizer);
if (allOperationsInSchema) {
item.setAllOperationsInSchema(allOperationsInSchema);
} else {
if (referencedType == null) {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.REFERENCED_NOT_TYPED);
}
const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText());
const type = this._edm.getEntityType(qualifiedName) || this._edm.getComplexType(qualifiedName);
if (type == null) {
pathSegments.push(
this._parseBoundOperation(tokenizer, qualifiedName, referencedType, referencedIsCollection));
} else {
/** **********************************************************
* Type cast in $select is currently not supported
*************************************************************/
FeatureSupport
.failUnsupported(FeatureSupport.features.TypeCast, qualifiedName, tokenizer.getPosition());
// ************************************************************
if (type.compatibleTo(referencedType)) {
pathSegments.push(new UriResource().setKind(ResourceKind.TYPE_CAST).setTypeCast(type));
if (tokenizer.next(TokenKind.SLASH)) {
tokenizer.requireNext(TokenKind.ODataIdentifier);
this._addSelectPath(tokenizer, type, pathSegments);
}
} else {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE,
qualifiedName.toString(), referencedType.getFullQualifiedName().toString());
}
}
}
} else {
tokenizer.requireNext(TokenKind.ODataIdentifier);
// The namespace or its alias could be a single OData identifier.
let allOperationsInSchema = this._parseAllOperationsInSchema(tokenizer);
if (allOperationsInSchema) {
item.setAllOperationsInSchema(allOperationsInSchema);
} else {
if (referencedType == null) {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.REFERENCED_NOT_TYPED);
}
this._addSelectPath(tokenizer, referencedType, pathSegments);
}
}
item.setPathSegments(pathSegments);
return item;
}
/**
* Parse a namespace plus dot plus star indicating all operations in the schema are selected.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @returns {?string} the parsed namespace
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
* @private
*/
_parseAllOperationsInSchema(tokenizer) {
const namespace = tokenizer.getText();
if (tokenizer.next(TokenKind.DOT)) {
tokenizer.requireNext(TokenKind.STAR);
// Validate the namespace. Currently a namespace from a non-default schema is not supported.
// There is no direct access to the namespace without loading the whole schema;
// however, the default entity container should always be there, so its access methods can be used.
if (this._edm.getEntityContainer(
new FullQualifiedName(namespace, this._edm.getEntityContainer().getName())) == null) {
throw new UriQueryOptionSemanticError(UriQueryOptionSemanticError.Message.WRONG_NAMESPACE,
namespace);
}
return namespace;
}
return null;
}
/**
* Parse a bound operation.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {FullQualifiedName} qualifiedName the qualified name of the operation
* @param {?EdmType} referencedType type the expression references
* @param {boolean} referencedIsCollection whether a collection is referenced
* @returns {UriResource} a resource element describing the parsed operation
* @throws {UriQueryOptionSemanticError}
* @private
*/
_parseBoundOperation(tokenizer, qualifiedName, referencedType, referencedIsCollection) {
const boundAction =
this._edm.getBoundAction(qualifiedName, referencedType.getFullQualifiedName(), referencedIsCollection);
if (boundAction) {
return new UriResource()
.setKind(ResourceKind.BOUND_ACTION)
.setAction(boundAction);
}
const parameterNames = this._parseFunctionParameterNames(tokenizer);
const boundFunction = this._edm.getBoundFunction(qualifiedName,
referencedType.getFullQualifiedName(), referencedIsCollection, parameterNames);
if (boundFunction == null) {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.FUNCTION_NOT_FOUND,
qualifiedName, parameterNames.join(', '));
}
return new UriResource()
.setKind(ResourceKind.BOUND_FUNCTION)
.setFunction(boundFunction)
.setFunctionParameters(null);
}
/**
* Parse a comma-separated list of function-parameter names enclosed in parentheses.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @returns {string[]} the parsed names
* @throws {UriSyntaxError}
* @private
*/
_parseFunctionParameterNames(tokenizer) {
let names = [];
if (tokenizer.next(TokenKind.OPEN)) {
do {
tokenizer.requireNext(TokenKind.ODataIdentifier);
names.push(tokenizer.getText());
} while (tokenizer.next(TokenKind.COMMA));
tokenizer.requireNext(TokenKind.CLOSE);
}
return names;
}
/**
* Add more to the select path.
* @param {UriTokenizer} tokenizer tokenizer containing the string to be parsed
* @param {?EdmType} referencedType type the expression references
* @param {UriResource[]} path the path segments parsed so far where more will be added to
* @throws {UriSyntaxError}
* @throws {UriQueryOptionSemanticError}
* @private
*/
_addSelectPath(tokenizer, referencedType, path) {
const name = tokenizer.getText();
const property = referencedType.getStructuralProperty(name);
if (property == null) {
const navigationProperty = referencedType.getNavigationProperty(name);
if (navigationProperty == null) {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.PROPERTY_NOT_FOUND,
name, referencedType.getName());
}
path.push(new UriResource().setNavigationProperty(navigationProperty)
.setKind(ResourceKind.NAVIGATION_TO_ONE)
.setIsCollection(navigationProperty.isCollection()));
} else {
let kind = null;
switch (property.getType().getKind()) {
case EdmTypeKind.PRIMITIVE:
case EdmTypeKind.ENUM:
case EdmTypeKind.DEFINITION:
kind = property.isCollection() ?
ResourceKind.PRIMITIVE_COLLECTION_PROPERTY :
ResourceKind.PRIMITIVE_PROPERTY;
break;
case EdmTypeKind.COMPLEX:
kind = property.isCollection() ?
ResourceKind.COMPLEX_COLLECTION_PROPERTY :
ResourceKind.COMPLEX_PROPERTY;
break;
default:
}
path.push(new UriResource().setProperty(property).setKind(kind).setIsCollection(property.isCollection()));
if (property.getType().getKind() === EdmTypeKind.COMPLEX && tokenizer.next(TokenKind.SLASH)) {
if (tokenizer.next(TokenKind.QualifiedName)) {
const qualifiedName = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText());
const type = this._edm.getComplexType(qualifiedName);
if (type == null) {
throw new UriQueryOptionSemanticError(
UriQueryOptionSemanticError.Message.COMPLEX_TYPE_NOT_FOUND,
qualifiedName.toString());
}
/** **********************************************************
* Type cast in $select is currently not supported
*************************************************************/
FeatureSupport
.failUnsupported(FeatureSupport.features.TypeCast, qualifiedName, tokenizer.getPosition());
// ************************************************************
if (type.compatibleTo(property.getType())) {
path.push(new UriResource()
.setKind(ResourceKind.TYPE_CAST)
.setTypeCast(type));
if (tokenizer.next(TokenKind.SLASH)) {
tokenizer.requireNext(TokenKind.ODataIdentifier);
this._addSelectPath(tokenizer, type, path);
}
} else {
throw new UriQueryOptionSemanticError(UriSemanticError.Message.INCOMPATIBLE_TYPE,
qualifiedName.toString(), property.getType().getFullQualifiedName().toString());
}
} else if (tokenizer.next(TokenKind.ODataIdentifier)) {
this._addSelectPath(tokenizer, property.getType(), path);
} else {
throw new UriSyntaxError(UriSyntaxError.Message.TOKEN_KINDS_EXPECTED,
[TokenKind.QualifiedName, TokenKind.ODataIdentifier].join(', '),
tokenizer.getParseString(), tokenizer.getPosition());
}
}
}
}
}
module.exports = SelectParser;