/// <summary> /// Build a segment from a token. /// </summary> /// <param name="tokenIn">the token to bind</param> /// <param name="model">The model.</param> /// <param name="edmType">the type of the current scope based on type segments.</param> /// <param name="resolver">Resolver for uri parser.</param> /// <returns>The segment created from the token.</returns> public static ODataPathSegment ConvertNonTypeTokenToSegment(PathSegmentToken tokenIn, IEdmModel model, IEdmStructuredType edmType, ODataUriResolver resolver) { ExceptionUtils.CheckArgumentNotNull(resolver, "resolver"); ODataPathSegment nextSegment; if (TryBindAsDeclaredProperty(tokenIn, edmType, resolver, out nextSegment)) { return(nextSegment); } // Operations must be container-qualified, and because the token type indicates it was not a .-seperated identifier, we should not try to look up operations. if (tokenIn.IsNamespaceOrContainerQualified()) { if (TryBindAsOperation(tokenIn, model, edmType, out nextSegment)) { return(nextSegment); } // If an action or function is requested in a selectItem using a qualifiedActionName or a qualifiedFunctionName // and that operation cannot be bound to the entities requested, the service MUST ignore the selectItem. if (!edmType.IsOpen) { return(null); } } if (edmType.IsOpen) { return(new DynamicPathSegment(tokenIn.Identifier)); } throw ExceptionUtil.CreatePropertyNotFoundException(tokenIn.Identifier, edmType.FullTypeName()); }
/// <summary> /// Process a <see cref="SelectTermToken"/> to identify whether it's a Wildcard path. /// </summary> /// <param name="selectToken">the select token to process.</param> /// <param name="newSelectItem">the built select item to out.</param> private bool ProcessWildcardTokenPath(SelectTermToken selectToken, out SelectItem newSelectItem) { newSelectItem = null; if (selectToken == null || selectToken.PathToProperty == null) { return(false); } PathSegmentToken pathToken = selectToken.PathToProperty; if (SelectPathSegmentTokenBinder.TryBindAsWildcard(pathToken, this.Model, out newSelectItem)) { // * or Namespace.* if (pathToken.NextToken != null) { throw new ODataException(ODataErrorStrings.SelectExpandBinder_InvalidIdentifierAfterWildcard(pathToken.NextToken.Identifier)); } VerifyNoQueryOptionsNested(selectToken, pathToken.Identifier); return(true); } return(false); }
/// <summary> /// Parse the expand option in the select/expand option text. /// </summary> /// <param name="pathToken">The path segment token</param> /// <returns>The expand option for select/expand</returns> private ExpandToken ParseInnerExpand(PathSegmentToken pathToken) { // advance to the equal sign this.lexer.NextToken(); string expandText = this.ReadQueryOption(); IEdmStructuredType targetStructuredType = null; if (this.resolver != null && this.parentStructuredType != null) { var parentProperty = this.resolver.ResolveProperty(parentStructuredType, pathToken.Identifier); // it is a property, need to find the type. // Like $expand=Friends($expand=Trips($expand=*)), when expandText becomes "Trips($expand=*)", // find navigation property Trips of Friends, then get Entity type of Trips. // or for select query like: $select=Address($expand=City) if (parentProperty != null) { targetStructuredType = parentProperty.Type.ToStructuredType(); } } SelectExpandParser innerExpandParser = new SelectExpandParser( resolver, expandText, targetStructuredType, this.maxRecursionDepth - 1, this.enableCaseInsensitiveBuiltinIdentifier, this.enableNoDollarQueryOptions); return(innerExpandParser.ParseExpand()); }
/// <summary> /// Build a wildcard selection item /// </summary> /// <param name="tokenIn">the token to bind to a wildcard</param> /// <param name="model">the model to search for this wildcard</param> /// <param name="item">the new wildcard selection item, if we found one</param> /// <returns>true if we successfully bound to a wildcard, false otherwise</returns> public static bool TryBindAsWildcard(PathSegmentToken tokenIn, IEdmModel model, out SelectItem item) { bool isTypeToken = tokenIn.IsNamespaceOrContainerQualified(); bool wildcard = tokenIn.Identifier.EndsWith("*", StringComparison.Ordinal); if (isTypeToken && wildcard) { string namespaceName = tokenIn.Identifier.Substring(0, tokenIn.Identifier.Length - 2); if (model.DeclaredNamespaces.Any(declaredNamespace => declaredNamespace.Equals(namespaceName, StringComparison.Ordinal))) { item = new NamespaceQualifiedWildcardSelectItem(namespaceName); return(true); } } if (tokenIn.Identifier == "*") { item = new WildcardSelectItem(); return(true); } item = null; return(false); }
/// <summary> /// Invert the all of the paths in an expandToken, such that they are now in the same order as they are present in the /// base url /// </summary> /// <param name="treeToInvert">the tree to invert paths on</param> /// <returns>a new tree with all of its paths inverted</returns> public ExpandToken NormalizePaths(ExpandToken treeToInvert) { // iterate through each expand term token, and reverse the tree in its path property List <ExpandTermToken> updatedTerms = new List <ExpandTermToken>(); foreach (ExpandTermToken term in treeToInvert.ExpandTerms) { PathReverser pathReverser = new PathReverser(); PathSegmentToken reversedPath = term.PathToNavigationProp.Accept(pathReverser); // we also need to call the select token normalizer for this level to reverse the select paths SelectToken newSelectToken = term.SelectOption; if (term.SelectOption != null) { newSelectToken = SelectTreeNormalizer.NormalizeSelectTree(term.SelectOption); } ExpandToken subExpandTree; if (term.ExpandOption != null) { subExpandTree = this.NormalizePaths(term.ExpandOption); } else { subExpandTree = null; } ExpandTermToken newTerm = new ExpandTermToken(reversedPath, term.FilterOption, term.OrderByOptions, term.TopOption, term.SkipOption, term.CountQueryOption, term.LevelsOption, term.SearchOption, newSelectToken, subExpandTree, term.ComputeOption); updatedTerms.Add(newTerm); } return(new ExpandToken(updatedTerms)); }
/// <summary> /// Parses a single term in a comma separated list of things to expand. /// </summary> /// <returns>A token list representing thing to expand, the expand option star will have more than one items in the list.</returns> private List <ExpandTermToken> ParseSingleExpandTerm() { this.isSelect = false; var termParser = new SelectExpandTermParser(this.lexer, this.MaxPathDepth, this.isSelect); PathSegmentToken pathToken = termParser.ParseTerm(allowRef: true); string optionsText = null; if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.OpenParen) { optionsText = this.lexer.AdvanceThroughBalancedParentheticalExpression(); // Move lexer to what is after the parenthesis expression. Now CurrentToken will be the next thing. this.lexer.NextToken(); } if (expandOptionParser == null) { expandOptionParser = new ExpandOptionParser( this.resolver, this.parentEntityType, this.maxRecursiveDepth, this.enableCaseInsensitiveBuiltinIdentifier, this.enableNoDollarQueryOptions) { MaxFilterDepth = MaxFilterDepth, MaxOrderByDepth = MaxOrderByDepth, MaxSearchDepth = MaxSearchDepth }; } return(this.expandOptionParser.BuildExpandTermToken(pathToken, optionsText)); }
/// <summary> /// Parse the select option in the select/expand option text. /// </summary> /// <param name="pathToken">The path segment token</param> /// <returns>The select option for select/expand</returns> private SelectToken ParseInnerSelect(PathSegmentToken pathToken) { // advance to the equal sign this.lexer.NextToken(); string selectText = this.ReadQueryOption(); IEdmStructuredType targetStructuredType = null; if (this.resolver != null && this.parentStructuredType != null) { var parentProperty = this.resolver.ResolveProperty(parentStructuredType, pathToken.Identifier); // It is a property, need to find the type. // or for select query like: $select=Address($expand=City) if (parentProperty != null) { targetStructuredType = parentProperty.Type.ToStructuredType(); } } SelectExpandParser innerSelectParser = new SelectExpandParser( resolver, selectText, targetStructuredType, this.maxRecursionDepth - 1, this.enableCaseInsensitiveBuiltinIdentifier, this.enableNoDollarQueryOptions); return(innerSelectParser.ParseSelect()); }
/// <summary> /// Uses the ExpressionLexer to visit the next ExpressionToken, and delegates parsing of segments, type segments, identifiers, /// and the star token to other methods. /// </summary> /// <param name="previousSegment">Previously parsed PathSegmentToken, or null if this is the first token.</param> /// <param name="allowRef">Whether the $ref operation is valid in this token.</param> /// <returns>A parsed PathSegmentToken representing the next segment in this path.</returns> private PathSegmentToken ParseSegment(PathSegmentToken previousSegment, bool allowRef) { // TODO $count is defined in specification for expand, it is not supported now. Also note $count is not supported with star as expand option. if (this.lexer.CurrentToken.Text.StartsWith("$", StringComparison.Ordinal) && (!allowRef || this.lexer.CurrentToken.Text != UriQueryConstants.RefSegment) && this.lexer.CurrentToken.Text != UriQueryConstants.CountSegment ) { throw new ODataException(ODataErrorStrings.UriSelectParser_SystemTokenInSelectExpand(this.lexer.CurrentToken.Text, this.lexer.ExpressionText)); } // Some check here to throw exception, both prop1/*/prop2 and */$ref/prop will throw exception, both are for $expand cases if (!isSelect) { if (previousSegment != null && previousSegment.Identifier == UriQueryConstants.Star && this.lexer.CurrentToken.GetIdentifier() != UriQueryConstants.RefSegment) { // Star can only be followed with $ref throw new ODataException(ODataErrorStrings.ExpressionToken_OnlyRefAllowWithStarInExpand); } else if (previousSegment != null && previousSegment.Identifier == UriQueryConstants.RefSegment) { // $ref should not have more property followed. throw new ODataException(ODataErrorStrings.ExpressionToken_NoPropAllowedAfterRef); } } string propertyName; if (this.lexer.PeekNextToken().Kind == ExpressionTokenKind.Dot) { propertyName = this.lexer.ReadDottedIdentifier(this.isSelect); } else if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.Star) { // "*/$ref" is supported in expand if (this.lexer.PeekNextToken().Kind == ExpressionTokenKind.Slash && isSelect) { throw new ODataException(ODataErrorStrings.ExpressionToken_IdentifierExpected(this.lexer.Position)); } else if (previousSegment != null && !isSelect) { // expand option like "customer?$expand=VIPCustomer/*" is not allowed as specification does not allowed any property before *. throw new ODataException(ODataErrorStrings.ExpressionToken_NoSegmentAllowedBeforeStarInExpand); } propertyName = this.lexer.CurrentToken.Text; this.lexer.NextToken(); } else { propertyName = this.lexer.CurrentToken.GetIdentifier(); this.lexer.NextToken(); } return(new NonSystemToken(propertyName, null, previousSegment)); }
/// <summary> /// Build a segment from a token. /// </summary> /// <param name="tokenIn">the token to bind</param> /// <param name="model">The model.</param> /// <param name="edmType">the type of the current scope based on type segments.</param> /// <param name="resolver">Resolver for uri parser.</param> /// <returns>The segment created from the token.</returns> public static ODataPathSegment ConvertNonTypeTokenToSegment(PathSegmentToken tokenIn, IEdmModel model, IEdmStructuredType edmType, ODataUriResolver resolver) { ExceptionUtils.CheckArgumentNotNull(resolver, "resolver"); ODataPathSegment nextSegment; if (UriParserHelper.IsAnnotation(tokenIn.Identifier)) { if (TryBindAsDeclaredTerm(tokenIn, model, resolver, out nextSegment)) { return(nextSegment); } string qualifiedTermName = tokenIn.Identifier.Remove(0, 1); int separator = qualifiedTermName.LastIndexOf(".", StringComparison.Ordinal); string namespaceName = qualifiedTermName.Substring(0, separator); string termName = qualifiedTermName.Substring(separator == 0 ? 0 : separator + 1); // Don't allow selecting odata control information if (String.Compare(namespaceName, ODataConstants.ODataPrefix, StringComparison.OrdinalIgnoreCase) == 0) { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(tokenIn.Identifier)); } return(new AnnotationSegment(new EdmTerm(namespaceName, termName, EdmCoreModel.Instance.GetUntyped()))); } if (TryBindAsDeclaredProperty(tokenIn, edmType, resolver, out nextSegment)) { return(nextSegment); } // Operations must be container-qualified, and because the token type indicates it was not a .-seperated identifier, we should not try to look up operations. if (tokenIn.IsNamespaceOrContainerQualified()) { if (TryBindAsOperation(tokenIn, model, edmType, out nextSegment)) { return(nextSegment); } // If an action or function is requested in a selectItem using a qualifiedActionName or a qualifiedFunctionName // and that operation cannot be bound to the entities requested, the service MUST ignore the selectItem. if (!edmType.IsOpen) { return(null); } } if (edmType.IsOpen) { return(new DynamicPathSegment(tokenIn.Identifier)); } throw new ODataException(ODataErrorStrings.MetadataBinder_PropertyNotDeclared(edmType.FullTypeName(), tokenIn.Identifier)); }
private static void VerifySelectedPath(SelectTermToken selectedToken) { PathSegmentToken current = selectedToken.PathToProperty; while (current != null) { if (current is SystemToken) { // It's not allowed to set a system token in a select clause. throw new ODataException(ODataErrorStrings.SelectExpandBinder_SystemTokenInSelect(current.Identifier)); } current = current.NextToken; } }
/// <summary> /// Parses a single term in a comma separated list of things to expand. /// </summary> /// <returns>A token list representing thing to expand, the expand option star will have more than one items in the list.</returns> private List <ExpandTermToken> ParseSingleExpandTerm() { this.isSelect = false; var termParser = new SelectExpandTermParser(this.lexer, this.MaxPathDepth, this.isSelect); PathSegmentToken pathToken = termParser.ParseTerm(allowRef: true); string optionsText = null; if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.OpenParen) { optionsText = this.lexer.AdvanceThroughBalancedParentheticalExpression(); // Move lexer to what is after the parenthesis expression. Now CurrentToken will be the next thing. this.lexer.NextToken(); } return(this.SelectExpandOptionParser.BuildExpandTermToken(pathToken, optionsText)); }
/// <summary> /// Tries to bind a given token as a declared annotation term. /// </summary> /// <param name="tokenIn">Token to bind.</param> /// <param name="model">The model to search for this term</param> /// <param name="resolver">Resolver for uri parser.</param> /// <param name="segment">Bound segment if the token was bound to a declared term successfully, or null.</param> /// <returns>True if the token was bound successfully, or false otherwise.</returns> private static bool TryBindAsDeclaredTerm(PathSegmentToken tokenIn, IEdmModel model, ODataUriResolver resolver, out ODataPathSegment segment) { if (!UriParserHelper.IsAnnotation(tokenIn.Identifier)) { segment = null; return(false); } IEdmTerm term = resolver.ResolveTerm(model, tokenIn.Identifier.Remove(0, 1)); if (term == null) { segment = null; return(false); } segment = new AnnotationSegment(term); return(true); }
/// <summary> /// Get the path string for a path segment token. /// </summary> /// <param name="head">The head of the path</param> /// <returns>The path string.</returns> internal static string ToPathString(PathSegmentToken head) { StringBuilder sb = new StringBuilder(); PathSegmentToken curr = head; while (curr != null) { sb.Append(curr.Identifier); NonSystemToken nonSystem = curr as NonSystemToken; if (nonSystem != null && nonSystem.NamedValues != null) { sb.Append("("); bool first = true; foreach (var item in nonSystem.NamedValues) { if (first) { first = false; } else { sb.Append(","); } sb.Append(item.Name).Append("=").Append(item.Value.Value); } sb.Append(")"); } curr = curr.NextToken; if (curr != null) { sb.Append("/"); } } return(sb.ToString()); }
/// <summary> /// Follow any type segments on the path, stopping at the first segment that isn't a type token. /// </summary> /// <param name="firstTypeToken">the first type segment</param> /// <param name="model">the model these types are contained in.</param> /// <param name="maxDepth">the maximum recursive depth</param> /// <param name="resolver">Resolver for uri parser.</param> /// <param name="currentLevelType">the top level type, will be overwritten with the last entity type in the chain</param> /// <param name="firstNonTypeToken">the first non type token in the path</param> /// <returns>A path with type segments added to it.</returns> public static IEnumerable <ODataPathSegment> FollowTypeSegments(PathSegmentToken firstTypeToken, IEdmModel model, int maxDepth, ODataUriResolver resolver, ref IEdmStructuredType currentLevelType, out PathSegmentToken firstNonTypeToken) { ExceptionUtils.CheckArgumentNotNull(firstTypeToken, "firstTypeToken"); ExceptionUtils.CheckArgumentNotNull(model, "model"); if (!firstTypeToken.IsNamespaceOrContainerQualified()) { throw new ODataException(ODataErrorStrings.SelectExpandPathBinder_FollowNonTypeSegment(firstTypeToken.Identifier)); } int index = 0; List <ODataPathSegment> pathToReturn = new List <ODataPathSegment>(); PathSegmentToken currentToken = firstTypeToken; while (currentToken.IsNamespaceOrContainerQualified() && currentToken.NextToken != null) { IEdmType previousLevelEntityType = currentLevelType; currentLevelType = UriEdmHelpers.FindTypeFromModel(model, currentToken.Identifier, resolver) as IEdmStructuredType; if (currentLevelType == null) { // TODO: fix this error message? throw new ODataException(ODataErrorStrings.ExpandItemBinder_CannotFindType(currentToken.Identifier)); } UriEdmHelpers.CheckRelatedTo(previousLevelEntityType, currentLevelType); pathToReturn.Add(new TypeSegment(currentLevelType, /*entitySet*/ null)); index++; currentToken = currentToken.NextToken; if (index >= maxDepth) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_PathTooDeep); } } firstNonTypeToken = currentToken; return(pathToReturn); }
/// <summary> /// Parses a select or expand term into a PathSegmentToken. /// Assumes the lexer is positioned at the beginning of the term to parse. /// When done, the lexer will be positioned at whatever is after the identifier. /// </summary> /// <param name="allowRef">Whether the $ref operation is valid in this token.</param> /// <returns>parsed query token</returns> internal PathSegmentToken ParseTerm(bool allowRef = false) { int pathLength; PathSegmentToken token = this.ParseSegment(null, allowRef); if (token != null) { pathLength = 1; } else { return(null); } this.CheckPathLength(pathLength); // If this property was a path, walk that path. e.g. SomeComplex/SomeInnerComplex/SomeNavProp while (this.lexer.CurrentToken.Kind == ExpressionTokenKind.Slash) { // Move from '/' to the next segment this.lexer.NextToken(); // TODO: Could remove V4 if we don't want to allow a trailing '/' character // Allow a single trailing slash for backwards compatibility with the WCF DS Server parser. if (pathLength > 1 && this.lexer.CurrentToken.Kind == ExpressionTokenKind.End) { break; } token = this.ParseSegment(token, allowRef); if (token != null) { this.CheckPathLength(++pathLength); } } return(token); }
/// <summary> /// Tries to bind a given token as an a declared structural or navigation property. /// </summary> /// <param name="tokenIn">Token to bind.</param> /// <param name="edmType">the type to search for this property</param> /// <param name="resolver">Resolver for uri parser.</param> /// <param name="segment">Bound segment if the token was bound to a declared property successfully, or null.</param> /// <returns>True if the token was bound successfully, or false otherwise.</returns> private static bool TryBindAsDeclaredProperty(PathSegmentToken tokenIn, IEdmStructuredType edmType, ODataUriResolver resolver, out ODataPathSegment segment) { IEdmProperty prop = resolver.ResolveProperty(edmType, tokenIn.Identifier); if (prop == null) { segment = null; return(false); } if (prop.PropertyKind == EdmPropertyKind.Structural) { segment = new PropertySegment((IEdmStructuralProperty)prop); return(true); } if (prop.PropertyKind == EdmPropertyKind.Navigation) { segment = new NavigationPropertySegment((IEdmNavigationProperty)prop, null /*TODO: set*/); return(true); } throw new ODataException(ODataErrorStrings.SelectExpandBinder_UnknownPropertyType(prop.Name)); }
/// <summary> /// Building off a PathSegmentToken, continue parsing any select options (nested $filter, $expand, etc) /// to build up an SelectTermToken which fully represents the tree that makes up this select term. /// </summary> /// <param name="pathToken">The PathSegmentToken representing the parsed select path whose options we are now parsing.</param> /// <param name="optionsText">A string of the text between the parenthesis after a select option.</param> /// <returns>The select term token based on the path token, and all available select options.</returns> internal SelectTermToken BuildSelectTermToken(PathSegmentToken pathToken, string optionsText) { // Setup a new lexer for parsing the optionsText this.lexer = new ExpressionLexer(optionsText ?? "", true /*moveToFirstToken*/, true /*useSemicolonDelimiter*/); QueryToken filterOption = null; IEnumerable <OrderByToken> orderByOptions = null; long? topOption = null; long? skipOption = null; bool? countOption = null; QueryToken searchOption = null; SelectToken selectOption = null; ComputeToken computeOption = null; if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.OpenParen) { // advance past the '(' this.lexer.NextToken(); // Check for (), which is not allowed. if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.CloseParen) { throw new ODataException(ODataErrorStrings.UriParser_MissingSelectOption(pathToken.Identifier)); } // Look for all the supported query options while (this.lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen) { string text = this.enableCaseInsensitiveBuiltinIdentifier ? this.lexer.CurrentToken.Text.ToLowerInvariant() : this.lexer.CurrentToken.Text; // Prepend '$' prefix if needed. if (this.enableNoDollarQueryOptions && !text.StartsWith(UriQueryConstants.DollarSign, StringComparison.Ordinal)) { text = string.Format(CultureInfo.InvariantCulture, "{0}{1}", UriQueryConstants.DollarSign, text); } switch (text) { case ExpressionConstants.QueryOptionFilter: // inner $filter filterOption = ParseInnerFilter(); break; case ExpressionConstants.QueryOptionOrderby: // inner $orderby orderByOptions = ParseInnerOrderBy(); break; case ExpressionConstants.QueryOptionTop: // inner $top topOption = ParseInnerTop(); break; case ExpressionConstants.QueryOptionSkip: // innner $skip skipOption = ParseInnerSkip(); break; case ExpressionConstants.QueryOptionCount: // inner $count countOption = ParseInnerCount(); break; case ExpressionConstants.QueryOptionSearch: // inner $search searchOption = ParseInnerSearch(); break; case ExpressionConstants.QueryOptionSelect: // inner $select selectOption = ParseInnerSelect(pathToken); break; case ExpressionConstants.QueryOptionCompute: // inner $compute computeOption = ParseInnerCompute(); break; default: throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } } // Move past the ')' this.lexer.NextToken(); } // Either there was no '(' at all or we just read past the ')' so we should be at the end if (this.lexer.CurrentToken.Kind != ExpressionTokenKind.End) { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } return(new SelectTermToken(pathToken, filterOption, orderByOptions, topOption, skipOption, countOption, searchOption, selectOption, computeOption)); }
/// <summary> /// Building off of a PathSegmentToken, continue parsing any expand options (nested $filter, $expand, etc) /// to build up an ExpandTermToken which fully represents the tree that makes up this expand term. /// </summary> /// <param name="pathToken">The PathSegmentToken representing the parsed expand path whose options we are now parsing.</param> /// <param name="optionsText">A string of the text between the parenthesis after an expand option.</param> /// <returns>The list of expand term tokens based on the path token, and all available expand options.</returns> internal List <ExpandTermToken> BuildExpandTermToken(PathSegmentToken pathToken, string optionsText) { // Setup a new lexer for parsing the optionsText this.lexer = new ExpressionLexer(optionsText ?? "", true /*moveToFirstToken*/, true /*useSemicolonDelimiter*/); // $expand option with star only support $ref option, $expand option property could be "*" or "*/$ref", special logic will be adopted. if (pathToken.Identifier == UriQueryConstants.Star || (pathToken.Identifier == UriQueryConstants.RefSegment && pathToken.NextToken.Identifier == UriQueryConstants.Star)) { return(BuildStarExpandTermToken(pathToken)); } QueryToken filterOption = null; IEnumerable <OrderByToken> orderByOptions = null; long? topOption = null; long? skipOption = null; bool? countOption = null; long? levelsOption = null; QueryToken searchOption = null; SelectToken selectOption = null; ExpandToken expandOption = null; if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.OpenParen) { // advance past the '(' this.lexer.NextToken(); // Check for (), which is not allowed. if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.CloseParen) { throw new ODataException(ODataErrorStrings.UriParser_MissingExpandOption(pathToken.Identifier)); } // Look for all the supported query options while (this.lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen) { string text = this.enableCaseInsensitiveBuiltinIdentifier ? this.lexer.CurrentToken.Text.ToLowerInvariant() : this.lexer.CurrentToken.Text; // Prepend '$' prefix if needed. if (this.enableNoDollarQueryOptions && !text.StartsWith(UriQueryConstants.DollarSign, StringComparison.Ordinal)) { text = string.Format(CultureInfo.InvariantCulture, "{0}{1}", UriQueryConstants.DollarSign, text); } switch (text) { case ExpressionConstants.QueryOptionFilter: { // advance to the equal sign this.lexer.NextToken(); string filterText = this.ReadQueryOption(); UriQueryExpressionParser filterParser = new UriQueryExpressionParser(this.MaxFilterDepth, enableCaseInsensitiveBuiltinIdentifier); filterOption = filterParser.ParseFilter(filterText); break; } case ExpressionConstants.QueryOptionOrderby: { // advance to the equal sign this.lexer.NextToken(); string orderByText = this.ReadQueryOption(); UriQueryExpressionParser orderbyParser = new UriQueryExpressionParser(this.MaxOrderByDepth, enableCaseInsensitiveBuiltinIdentifier); orderByOptions = orderbyParser.ParseOrderBy(orderByText); break; } case ExpressionConstants.QueryOptionTop: { // advance to the equal sign this.lexer.NextToken(); string topText = this.ReadQueryOption(); // TryParse requires a non-nullable non-negative long. long top; if (!long.TryParse(topText, out top) || top < 0) { throw new ODataException(ODataErrorStrings.UriSelectParser_InvalidTopOption(topText)); } topOption = top; break; } case ExpressionConstants.QueryOptionSkip: { // advance to the equal sign this.lexer.NextToken(); string skipText = this.ReadQueryOption(); // TryParse requires a non-nullable non-negative long. long skip; if (!long.TryParse(skipText, out skip) || skip < 0) { throw new ODataException(ODataErrorStrings.UriSelectParser_InvalidSkipOption(skipText)); } skipOption = skip; break; } case ExpressionConstants.QueryOptionCount: { // advance to the equal sign this.lexer.NextToken(); string countText = this.ReadQueryOption(); switch (countText) { case ExpressionConstants.KeywordTrue: { countOption = true; break; } case ExpressionConstants.KeywordFalse: { countOption = false; break; } default: { throw new ODataException(ODataErrorStrings.UriSelectParser_InvalidCountOption(countText)); } } break; } case ExpressionConstants.QueryOptionLevels: { levelsOption = ResolveLevelOption(); break; } case ExpressionConstants.QueryOptionSearch: { // advance to the equal sign this.lexer.NextToken(); string searchText = this.ReadQueryOption(); SearchParser searchParser = new SearchParser(this.MaxSearchDepth); searchOption = searchParser.ParseSearch(searchText); break; } case ExpressionConstants.QueryOptionSelect: { // advance to the equal sign this.lexer.NextToken(); string selectText = this.ReadQueryOption(); SelectExpandParser innerSelectParser = new SelectExpandParser(selectText, this.maxRecursionDepth, enableCaseInsensitiveBuiltinIdentifier); selectOption = innerSelectParser.ParseSelect(); break; } case ExpressionConstants.QueryOptionExpand: { // advance to the equal sign this.lexer.NextToken(); string expandText = this.ReadQueryOption(); // As 2016/1/8, the navigation property is only supported in entity type, and will support in ComplexType in future. IEdmStructuredType targetEntityType = null; if (this.resolver != null && this.parentEntityType != null) { var parentProperty = this.resolver.ResolveProperty(parentEntityType, pathToken.Identifier) as IEdmNavigationProperty; // it is a navigation property, need to find the type. // Like $expand=Friends($expand=Trips($expand=*)), when expandText becomes "Trips($expand=*)", // find navigation property Trips of Friends, then get Entity type of Trips. if (parentProperty != null) { targetEntityType = parentProperty.ToEntityType(); } } SelectExpandParser innerExpandParser = new SelectExpandParser( resolver, expandText, targetEntityType, this.maxRecursionDepth - 1, this.enableCaseInsensitiveBuiltinIdentifier, this.enableNoDollarQueryOptions); expandOption = innerExpandParser.ParseExpand(); break; } default: { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } } } // Move past the ')' this.lexer.NextToken(); } // Either there was no '(' at all or we just read past the ')' so we should be at the end if (this.lexer.CurrentToken.Kind != ExpressionTokenKind.End) { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } // TODO, there should be some check here in case pathToken identifier is $ref, select, expand and levels options are not allowed. List <ExpandTermToken> expandTermTokenList = new List <ExpandTermToken>(); ExpandTermToken currentToken = new ExpandTermToken(pathToken, filterOption, orderByOptions, topOption, skipOption, countOption, levelsOption, searchOption, selectOption, expandOption); expandTermTokenList.Add(currentToken); return(expandTermTokenList); }
/// <summary> /// Tries to bind a given token as an Operation. /// </summary> /// <param name="pathToken">Token to bind.</param> /// <param name="model">The model.</param> /// <param name="entityType">the current entity type to use as the binding type when looking for operations.</param> /// <param name="segment">Bound segment if the token was bound to an operation successfully, or null.</param> /// <returns>True if the token was bound successfully, or false otherwise.</returns> internal static bool TryBindAsOperation(PathSegmentToken pathToken, IEdmModel model, IEdmStructuredType entityType, out ODataPathSegment segment) { Debug.Assert(pathToken != null, "pathToken != null"); Debug.Assert(entityType != null, "bindingType != null"); IEnumerable <IEdmOperation> possibleFunctions = Enumerable.Empty <IEdmOperation>(); IList <string> parameterNames = new List <string>(); // Catch all catchable exceptions as FindDeclaredBoundOperations is implemented by anyone. // If an exception occurs it will be suppressed and the possible functions will be empty and return false. try { int wildCardPos = pathToken.Identifier.IndexOf("*", StringComparison.Ordinal); if (wildCardPos > -1) { string namespaceName = pathToken.Identifier.Substring(0, wildCardPos - 1); possibleFunctions = model.FindBoundOperations(entityType).Where(o => o.Namespace == namespaceName); } else { NonSystemToken nonSystemToken = pathToken as NonSystemToken; if (nonSystemToken != null && nonSystemToken.NamedValues != null) { parameterNames = nonSystemToken.NamedValues.Select(s => s.Name).ToList(); } if (parameterNames.Count > 0) { // Always force to use fully qualified name when select operation possibleFunctions = model.FindBoundOperations(entityType).FilterByName(true, pathToken.Identifier).FilterOperationsByParameterNames(parameterNames, false); } else { possibleFunctions = model.FindBoundOperations(entityType).FilterByName(true, pathToken.Identifier); } } } catch (Exception exc) { if (!ExceptionUtils.IsCatchableExceptionType(exc)) { throw; } } // Only filter if there is more than one and its needed. if (possibleFunctions.Count() > 1) { possibleFunctions = possibleFunctions.FilterBoundOperationsWithSameTypeHierarchyToTypeClosestToBindingType(entityType); } // If more than one overload matches, try to select based on optional parameters if (possibleFunctions.Count() > 1 && parameterNames.Count > 0) { possibleFunctions = possibleFunctions.FilterOverloadsBasedOnParameterCount(parameterNames.Count); } if (!possibleFunctions.HasAny()) { segment = null; return(false); } possibleFunctions.EnsureOperationsBoundWithBindingParameter(); segment = new OperationSegment(possibleFunctions, null /*entitySet*/); return(true); }
/// <summary> /// Generate an expand item (and a select item for the implicit nav prop if necessary) based on an ExpandTermToken /// </summary> /// <param name="tokenIn">the expandTerm token to visit</param> /// <returns>the expand item for this expand term token.</returns> private SelectItem GenerateExpandItem(ExpandTermToken tokenIn) { ExceptionUtils.CheckArgumentNotNull(tokenIn, "tokenIn"); PathSegmentToken currentToken = tokenIn.PathToNavigationProp; IEdmStructuredType currentLevelEntityType = this.EdmType; List <ODataPathSegment> pathSoFar = new List <ODataPathSegment>(); PathSegmentToken firstNonTypeToken = currentToken; if (currentToken.IsNamespaceOrContainerQualified()) { pathSoFar.AddRange(SelectExpandPathBinder.FollowTypeSegments(currentToken, this.Model, this.Settings.SelectExpandLimit, this.configuration.Resolver, ref currentLevelEntityType, out firstNonTypeToken)); } IEdmProperty edmProperty = this.configuration.Resolver.ResolveProperty(currentLevelEntityType, firstNonTypeToken.Identifier); if (edmProperty == null) { throw new ODataException(ODataErrorStrings.MetadataBinder_PropertyNotDeclared(currentLevelEntityType.FullTypeName(), currentToken.Identifier)); } IEdmNavigationProperty currentNavProp = edmProperty as IEdmNavigationProperty; IEdmStructuralProperty currentComplexProp = edmProperty as IEdmStructuralProperty; if (currentNavProp == null && currentComplexProp == null) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_PropertyIsNotANavigationPropertyOrComplexProperty(currentToken.Identifier, currentLevelEntityType.FullTypeName())); } if (currentComplexProp != null) { currentNavProp = ParseComplexTypesBeforeNavigation(currentComplexProp, ref firstNonTypeToken, pathSoFar); } // ensure that we're always dealing with proper V4 syntax if (firstNonTypeToken.NextToken != null && firstNonTypeToken.NextToken.NextToken != null) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_TraversingMultipleNavPropsInTheSamePath); } bool isRef = false; if (firstNonTypeToken.NextToken != null) { // lastly... make sure that, since we're on a NavProp, that the next token isn't null. if (firstNonTypeToken.NextToken.Identifier == UriQueryConstants.RefSegment) { isRef = true; } else { throw new ODataException(ODataErrorStrings.ExpandItemBinder_TraversingMultipleNavPropsInTheSamePath); } } // Add the segments in select and expand to parsed segments List <ODataPathSegment> parsedPath = new List <ODataPathSegment>(this.parsedSegments); parsedPath.AddRange(pathSoFar); IEdmNavigationSource targetNavigationSource = null; if (this.NavigationSource != null) { IEdmPathExpression bindingPath; targetNavigationSource = this.NavigationSource.FindNavigationTarget(currentNavProp, BindingPathHelper.MatchBindingPath, parsedPath, out bindingPath); } NavigationPropertySegment navSegment = new NavigationPropertySegment(currentNavProp, targetNavigationSource); pathSoFar.Add(navSegment); parsedPath.Add(navSegment); // Add the navigation property segment to parsed segments for future usage. ODataExpandPath pathToNavProp = new ODataExpandPath(pathSoFar); // $apply ApplyClause applyOption = BindApply(tokenIn.ApplyOptions, targetNavigationSource); // $compute ComputeClause computeOption = BindCompute(tokenIn.ComputeOption, targetNavigationSource); var generatedProperties = GetGeneratedProperties(computeOption, applyOption); bool collapsed = applyOption?.Transformations.Any(t => t.Kind == TransformationNodeKind.Aggregate || t.Kind == TransformationNodeKind.GroupBy) ?? false; // $filter FilterClause filterOption = BindFilter(tokenIn.FilterOption, targetNavigationSource, null, generatedProperties, collapsed); // $orderby OrderByClause orderbyOption = BindOrderby(tokenIn.OrderByOptions, targetNavigationSource, null, generatedProperties, collapsed); // $search SearchClause searchOption = BindSearch(tokenIn.SearchOption, targetNavigationSource, null); if (isRef) { return(new ExpandedReferenceSelectItem(pathToNavProp, targetNavigationSource, filterOption, orderbyOption, tokenIn.TopOption, tokenIn.SkipOption, tokenIn.CountQueryOption, searchOption, computeOption, applyOption)); } // $select & $expand SelectExpandClause subSelectExpand = BindSelectExpand(tokenIn.ExpandOption, tokenIn.SelectOption, parsedPath, targetNavigationSource, null, generatedProperties, collapsed); // $levels LevelsClause levelsOption = ParseLevels(tokenIn.LevelsOption, currentLevelEntityType, currentNavProp); return(new ExpandedNavigationSelectItem(pathToNavProp, targetNavigationSource, subSelectExpand, filterOption, orderbyOption, tokenIn.TopOption, tokenIn.SkipOption, tokenIn.CountQueryOption, searchOption, levelsOption, computeOption, applyOption)); }
/// <summary> /// Building of a PathSegmentToken, continue parsing any expand options (nested $filter, $expand, etc) /// to build up an ExpandTermToken which fully represents the tree that makes up this expand term. /// </summary> /// <param name="pathToken">The PathSegmentToken representing the parsed expand path whose options we are now parsing.</param> /// <param name="optionsText">A string of the text between the parenthesis after an expand option.</param> /// <returns>The list of expand term tokens based on the path token, and all available expand options.</returns> internal List <ExpandTermToken> BuildExpandTermToken(PathSegmentToken pathToken, string optionsText) { // Setup a new lexer for parsing the optionsText this.lexer = new ExpressionLexer(optionsText ?? "", true /*moveToFirstToken*/, true /*useSemicolonDelimiter*/); // $expand option with star only support $ref option, $expand option property could be "*" or "*/$ref", special logic will be adopted. if (pathToken.Identifier == UriQueryConstants.Star || (pathToken.Identifier == UriQueryConstants.RefSegment && pathToken.NextToken.Identifier == UriQueryConstants.Star)) { return(BuildStarExpandTermToken(pathToken)); } QueryToken filterOption = null; IEnumerable <OrderByToken> orderByOptions = null; long? topOption = null; long? skipOption = null; bool? countOption = null; long? levelsOption = null; QueryToken searchOption = null; SelectToken selectOption = null; ExpandToken expandOption = null; ComputeToken computeOption = null; IEnumerable <QueryToken> applyOptions = null; if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.OpenParen) { // advance past the '(' this.lexer.NextToken(); // Check for (), which is not allowed. if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.CloseParen) { throw new ODataException(ODataErrorStrings.UriParser_MissingExpandOption(pathToken.Identifier)); } // Look for all the supported query options while (this.lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen) { string text = this.enableCaseInsensitiveBuiltinIdentifier ? this.lexer.CurrentToken.Text.ToLowerInvariant() : this.lexer.CurrentToken.Text; // Prepend '$' prefix if needed. if (this.enableNoDollarQueryOptions && !text.StartsWith(UriQueryConstants.DollarSign, StringComparison.Ordinal)) { text = string.Format(CultureInfo.InvariantCulture, "{0}{1}", UriQueryConstants.DollarSign, text); } switch (text) { case ExpressionConstants.QueryOptionFilter: // inner $filter filterOption = ParseInnerFilter(); break; case ExpressionConstants.QueryOptionOrderby: // inner $orderby orderByOptions = ParseInnerOrderBy(); break; case ExpressionConstants.QueryOptionTop: // inner $top topOption = ParseInnerTop(); break; case ExpressionConstants.QueryOptionSkip: // innner $skip skipOption = ParseInnerSkip(); break; case ExpressionConstants.QueryOptionCount: // inner $count countOption = ParseInnerCount(); break; case ExpressionConstants.QueryOptionSearch: // inner $search searchOption = ParseInnerSearch(); break; case ExpressionConstants.QueryOptionLevels: // inner $level levelsOption = ParseInnerLevel(); break; case ExpressionConstants.QueryOptionSelect: // inner $select selectOption = ParseInnerSelect(pathToken); break; case ExpressionConstants.QueryOptionExpand: // inner $expand expandOption = ParseInnerExpand(pathToken); break; case ExpressionConstants.QueryOptionCompute: // inner $compute computeOption = ParseInnerCompute(); break; case ExpressionConstants.QueryOptionApply: // inner $apply applyOptions = ParseInnerApply(); break; default: { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } } } // Move past the ')' this.lexer.NextToken(); } // Either there was no '(' at all or we just read past the ')' so we should be at the end if (this.lexer.CurrentToken.Kind != ExpressionTokenKind.End) { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } // TODO, there should be some check here in case pathToken identifier is $ref, select, expand and levels options are not allowed. List <ExpandTermToken> expandTermTokenList = new List <ExpandTermToken>(); ExpandTermToken currentToken = new ExpandTermToken(pathToken, filterOption, orderByOptions, topOption, skipOption, countOption, levelsOption, searchOption, selectOption, expandOption, computeOption, applyOptions); expandTermTokenList.Add(currentToken); return(expandTermTokenList); }
/// <summary> /// Building off of a PathSegmentToken whose value is star, only nested level options is allowed. /// </summary> /// <param name="pathToken">The PathSegmentToken representing the parsed expand path whose options we are now parsing.</param> /// <returns>An expand term token based on the path token, and all available expand options.</returns> private List <ExpandTermToken> BuildStarExpandTermToken(PathSegmentToken pathToken) { List <ExpandTermToken> expandTermTokenList = new List <ExpandTermToken>(); long?levelsOption = null; bool isRefExpand = (pathToken.Identifier == UriQueryConstants.RefSegment); // Based on the specification, // For star in expand, this will be supported, // $expand=* // $expand=EntitySet($expand=* ) // $expand=*/$ref // $expand=*,EntitySet // $expand=EntitySet, * // $expand=*/$ref,EntitySet // Parenthesized set of expand options for star expand option supported are $level per specification. // And this will throw exception, // $expand= * /$count // Parenthesized set of expand options for star expand option which will also cause exception are $filter, $select, $orderby, $skip, $top, $count, $search, and $expand per specification. // And level is not supported with "*/$ref". // As 2016/1/8, the navigation property is only supported in entity type, and will support in ComplexType in future. if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.OpenParen) { // advance past the '(' this.lexer.NextToken(); // Check for (), which is not allowed. if (this.lexer.CurrentToken.Kind == ExpressionTokenKind.CloseParen) { throw new ODataException(ODataErrorStrings.UriParser_MissingExpandOption(pathToken.Identifier)); } // Only level option is supported by expand. while (this.lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen) { string text = this.enableCaseInsensitiveBuiltinIdentifier ? this.lexer.CurrentToken.Text.ToLowerInvariant() : this.lexer.CurrentToken.Text; switch (text) { case ExpressionConstants.QueryOptionLevels: { if (!isRefExpand) { levelsOption = ParseInnerLevel(); } else { // no option is allowed when expand with star per specification throw new ODataException(ODataErrorStrings.UriExpandParser_TermIsNotValidForStarRef(this.lexer.ExpressionText)); } break; } default: { throw new ODataException(ODataErrorStrings.UriExpandParser_TermIsNotValidForStar(this.lexer.ExpressionText)); } } } // Move past the ')' this.lexer.NextToken(); } // Either there was no '(' at all or we just read past the ')' so we should be at the end if (this.lexer.CurrentToken.Kind != ExpressionTokenKind.End) { throw new ODataException(ODataErrorStrings.UriSelectParser_TermIsNotValid(this.lexer.ExpressionText)); } // As 2016/1/8, the navigation property is only supported in entity type, and will support in ComplexType in future. var entityType = this.parentStructuredType as IEdmEntityType; if (entityType == null) { throw new ODataException(ODataErrorStrings.UriExpandParser_ParentEntityIsNull(this.lexer.ExpressionText)); } foreach (var navigationProperty in entityType.NavigationProperties()) { var tmpPathToken = default(PathSegmentToken); // create path token for each navigation properties. if (pathToken.Identifier.Equals(UriQueryConstants.RefSegment)) { tmpPathToken = new NonSystemToken(navigationProperty.Name, null, pathToken.NextToken.NextToken); tmpPathToken = new NonSystemToken(UriQueryConstants.RefSegment, null, tmpPathToken); } else { tmpPathToken = new NonSystemToken(navigationProperty.Name, null, pathToken.NextToken); } ExpandTermToken currentToken = new ExpandTermToken(tmpPathToken, null, null, null, null, null, levelsOption, null, null, null, null, null); expandTermTokenList.Add(currentToken); } return(expandTermTokenList); }
/// <summary> /// Process a <see cref="PathSegmentToken"/> following any type segments if necessary. /// </summary> /// <param name="tokenIn">the path token to process.</param> private List <ODataPathSegment> ProcessSelectTokenPath(PathSegmentToken tokenIn) { Debug.Assert(tokenIn != null, "tokenIn != null"); List <ODataPathSegment> pathSoFar = new List <ODataPathSegment>(); IEdmStructuredType currentLevelType = this.edmType; // first, walk through all type segments in a row, converting them from tokens into segments. if (tokenIn.IsNamespaceOrContainerQualified() && !UriParserHelper.IsAnnotation(tokenIn.Identifier)) { PathSegmentToken firstNonTypeToken; pathSoFar.AddRange(SelectExpandPathBinder.FollowTypeSegments(tokenIn, this.Model, this.Settings.SelectExpandLimit, this.configuration.Resolver, ref currentLevelType, out firstNonTypeToken)); Debug.Assert(firstNonTypeToken != null, "Did not get last token."); tokenIn = firstNonTypeToken as NonSystemToken; if (tokenIn == null) { throw new ODataException(ODataErrorStrings.SelectExpandBinder_SystemTokenInSelect(firstNonTypeToken.Identifier)); } } // next, create a segment for the first non-type segment in the path. ODataPathSegment lastSegment = SelectPathSegmentTokenBinder.ConvertNonTypeTokenToSegment(tokenIn, this.Model, currentLevelType, this.configuration.Resolver, this.state); // next, create an ODataPath and add the segments to it. if (lastSegment != null) { pathSoFar.Add(lastSegment); // try create a complex type property path. while (true) { // no need to go on if the current property is not of complex type or collection of complex type, // unless the segment is a primitive type cast or a property on an open complex property. currentLevelType = lastSegment.EdmType as IEdmStructuredType; IEdmCollectionType collectionType = lastSegment.EdmType as IEdmCollectionType; IEdmPrimitiveType primitiveType = lastSegment.EdmType as IEdmPrimitiveType; DynamicPathSegment dynamicPath = lastSegment as DynamicPathSegment; if ((currentLevelType == null || currentLevelType.TypeKind != EdmTypeKind.Complex) && (collectionType == null || collectionType.ElementType.TypeKind() != EdmTypeKind.Complex) && (primitiveType == null || primitiveType.TypeKind != EdmTypeKind.Primitive) && (dynamicPath == null || tokenIn.NextToken == null)) { break; } NonSystemToken nextToken = tokenIn.NextToken as NonSystemToken; if (nextToken == null) { break; } if (UriParserHelper.IsAnnotation(nextToken.Identifier)) { lastSegment = SelectPathSegmentTokenBinder.ConvertNonTypeTokenToSegment(nextToken, this.Model, currentLevelType, this.configuration.Resolver, null); } else if (primitiveType == null && dynamicPath == null) { // This means last segment a collection of complex type, // current segment can only be type cast and cannot be property name. if (currentLevelType == null) { currentLevelType = collectionType.ElementType.Definition as IEdmStructuredType; } // If there is no collection type in the path yet, will try to bind property for the next token // first try bind the segment as property. lastSegment = SelectPathSegmentTokenBinder.ConvertNonTypeTokenToSegment(nextToken, this.Model, currentLevelType, this.configuration.Resolver, null); } else { // determine whether we are looking at a type cast or a dynamic path segment. EdmPrimitiveTypeKind nextTypeKind = EdmCoreModel.Instance.GetPrimitiveTypeKind(nextToken.Identifier); IEdmPrimitiveType castType = EdmCoreModel.Instance.GetPrimitiveType(nextTypeKind); if (castType != null) { lastSegment = new TypeSegment(castType, castType, null); } else if (dynamicPath != null) { lastSegment = new DynamicPathSegment(nextToken.Identifier); } else { throw new ODataException(ODataErrorStrings.SelectBinder_MultiLevelPathInSelect); } } // then try bind the segment as type cast. if (lastSegment == null) { IEdmStructuredType typeFromNextToken = UriEdmHelpers.FindTypeFromModel(this.Model, nextToken.Identifier, this.configuration.Resolver) as IEdmStructuredType; if (typeFromNextToken.IsOrInheritsFrom(currentLevelType)) { lastSegment = new TypeSegment(typeFromNextToken, /*entitySet*/ null); } } // type cast failed too. if (lastSegment == null) { break; } // try move to and add next path segment. tokenIn = nextToken; pathSoFar.Add(lastSegment); } } // non-navigation cases do not allow further segments in $select. if (tokenIn.NextToken != null) { throw new ODataException(ODataErrorStrings.SelectBinder_MultiLevelPathInSelect); } // Later, we can consider to create a "DynamicOperationSegment" to handle this. // But now, Let's throw exception. if (lastSegment == null) { throw new ODataException(ODataErrorStrings.MetadataBinder_InvalidIdentifierInQueryOption(tokenIn.Identifier)); } // navigation property is not allowed to append sub path in the selection. NavigationPropertySegment navPropSegment = pathSoFar.LastOrDefault() as NavigationPropertySegment; if (navPropSegment != null && tokenIn.NextToken != null) { throw new ODataException(ODataErrorStrings.SelectBinder_MultiLevelPathInSelect); } return(pathSoFar); }
private IEdmNavigationProperty ParseComplexTypesBeforeNavigation(IEdmStructuralProperty edmProperty, ref PathSegmentToken currentToken, List <ODataPathSegment> pathSoFar) { pathSoFar.Add(new PropertySegment(edmProperty)); if (currentToken.NextToken == null) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_PropertyIsNotANavigationPropertyOrComplexProperty(currentToken.Identifier, edmProperty.DeclaringType.FullTypeName())); } currentToken = currentToken.NextToken; IEdmType complexType = edmProperty.Type.Definition; IEdmCollectionType collectionType = complexType as IEdmCollectionType; if (collectionType != null) { complexType = collectionType.ElementType.Definition; } IEdmStructuredType currentType = complexType as IEdmStructuredType; if (currentType == null) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_InvaidSegmentInExpand(currentToken.Identifier)); } if (currentToken.IsNamespaceOrContainerQualified()) { pathSoFar.AddRange(SelectExpandPathBinder.FollowTypeSegments(currentToken, this.Model, this.Settings.SelectExpandLimit, this.configuration.Resolver, ref currentType, out currentToken)); } IEdmProperty property = this.configuration.Resolver.ResolveProperty(currentType, currentToken.Identifier); if (edmProperty == null) { throw new ODataException(ODataErrorStrings.MetadataBinder_PropertyNotDeclared(currentType.FullTypeName(), currentToken.Identifier)); } IEdmStructuralProperty complexProp = property as IEdmStructuralProperty; if (complexProp != null) { property = ParseComplexTypesBeforeNavigation(complexProp, ref currentToken, pathSoFar); } IEdmNavigationProperty navProp = property as IEdmNavigationProperty; if (navProp != null) { return(navProp); } else { throw new ODataException(ODataErrorStrings.ExpandItemBinder_PropertyIsNotANavigationPropertyOrComplexProperty(currentToken.Identifier, currentType.FullTypeName())); } }
/// <summary> /// Generate an expand item (and a select item for the implicit nav prop if necessary) based on an ExpandTermToken /// </summary> /// <param name="tokenIn">the expandTerm token to visit</param> /// <returns>the expand item for this expand term token.</returns> private SelectItem GenerateExpandItem(ExpandTermToken tokenIn) { ExceptionUtils.CheckArgumentNotNull(tokenIn, "tokenIn"); PathSegmentToken currentToken = tokenIn.PathToNavigationProp; IEdmStructuredType currentLevelEntityType = this.EdmType; List <ODataPathSegment> pathSoFar = new List <ODataPathSegment>(); PathSegmentToken firstNonTypeToken = currentToken; if (currentToken.IsNamespaceOrContainerQualified()) { pathSoFar.AddRange(SelectExpandPathBinder.FollowTypeSegments(currentToken, this.Model, this.Settings.SelectExpandLimit, this.configuration.Resolver, ref currentLevelEntityType, out firstNonTypeToken)); } IEdmProperty edmProperty = this.configuration.Resolver.ResolveProperty(currentLevelEntityType, firstNonTypeToken.Identifier); if (edmProperty == null) { throw new ODataException(ODataErrorStrings.MetadataBinder_PropertyNotDeclared(currentLevelEntityType.FullTypeName(), currentToken.Identifier)); } IEdmNavigationProperty currentNavProp = edmProperty as IEdmNavigationProperty; IEdmStructuralProperty currentComplexProp = edmProperty as IEdmStructuralProperty; if (currentNavProp == null && currentComplexProp == null) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_PropertyIsNotANavigationPropertyOrComplexProperty(currentToken.Identifier, currentLevelEntityType.FullTypeName())); } if (currentComplexProp != null) { currentNavProp = ParseComplexTypesBeforeNavigation(currentComplexProp, ref firstNonTypeToken, pathSoFar); } // ensure that we're always dealing with proper V4 syntax if (firstNonTypeToken.NextToken != null && firstNonTypeToken.NextToken.NextToken != null) { throw new ODataException(ODataErrorStrings.ExpandItemBinder_TraversingMultipleNavPropsInTheSamePath); } bool isRef = false; if (firstNonTypeToken.NextToken != null) { // lastly... make sure that, since we're on a NavProp, that the next token isn't null. if (firstNonTypeToken.NextToken.Identifier == UriQueryConstants.RefSegment) { isRef = true; } else { throw new ODataException(ODataErrorStrings.ExpandItemBinder_TraversingMultipleNavPropsInTheSamePath); } } // Add the segments in select and expand to parsed segments this.parsedSegments.AddRange(pathSoFar); IEdmNavigationSource targetNavigationSource = null; if (this.NavigationSource != null) { IEdmPathExpression bindingPath; targetNavigationSource = this.NavigationSource.FindNavigationTarget(currentNavProp, BindingPathHelper.MatchBindingPath, this.parsedSegments, out bindingPath); } NavigationPropertySegment navSegment = new NavigationPropertySegment(currentNavProp, targetNavigationSource); pathSoFar.Add(navSegment); this.parsedSegments.Add(navSegment); // Add the navigation property segment to parsed segments for future usage. ODataExpandPath pathToNavProp = new ODataExpandPath(pathSoFar); // call MetadataBinder to build the filter clause FilterClause filterOption = null; if (tokenIn.FilterOption != null) { MetadataBinder binder = this.BuildNewMetadataBinder(targetNavigationSource); FilterBinder filterBinder = new FilterBinder(binder.Bind, binder.BindingState); filterOption = filterBinder.BindFilter(tokenIn.FilterOption); } // call MetadataBinder again to build the orderby clause OrderByClause orderbyOption = null; if (tokenIn.OrderByOptions != null) { MetadataBinder binder = this.BuildNewMetadataBinder(targetNavigationSource); OrderByBinder orderByBinder = new OrderByBinder(binder.Bind); orderbyOption = orderByBinder.BindOrderBy(binder.BindingState, tokenIn.OrderByOptions); } SearchClause searchOption = null; if (tokenIn.SearchOption != null) { MetadataBinder binder = this.BuildNewMetadataBinder(targetNavigationSource); SearchBinder searchBinder = new SearchBinder(binder.Bind); searchOption = searchBinder.BindSearch(tokenIn.SearchOption); } if (isRef) { return(new ExpandedReferenceSelectItem(pathToNavProp, targetNavigationSource, filterOption, orderbyOption, tokenIn.TopOption, tokenIn.SkipOption, tokenIn.CountQueryOption, searchOption)); } SelectExpandClause subSelectExpand; if (tokenIn.ExpandOption != null) { subSelectExpand = this.GenerateSubExpand(tokenIn); } else { subSelectExpand = BuildDefaultSubExpand(); } subSelectExpand = this.DecorateExpandWithSelect(subSelectExpand, currentNavProp, tokenIn.SelectOption); LevelsClause levelsOption = this.ParseLevels(tokenIn.LevelsOption, currentLevelEntityType, currentNavProp); return(new ExpandedNavigationSelectItem(pathToNavProp, targetNavigationSource, subSelectExpand, filterOption, orderbyOption, tokenIn.TopOption, tokenIn.SkipOption, tokenIn.CountQueryOption, searchOption, levelsOption)); }