Skip to content

Commit

Permalink
Support no-$ for system query parameters in OData library (Issue ODat…
Browse files Browse the repository at this point in the history
  • Loading branch information
Charlie authored and LaylaLiu committed Jul 28, 2016
1 parent 56ecd2b commit 71297a9
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 68 deletions.
43 changes: 32 additions & 11 deletions src/Microsoft.OData.Core/UriParser/ODataQueryOptionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Metadata;
using Microsoft.OData.UriParser.Aggregation;
using Microsoft.OData.Edm;

namespace Microsoft.OData.UriParser
{
Expand Down Expand Up @@ -543,21 +544,40 @@ private static SearchClause ParseSearchImplementation(string search, ODataUriPar
}

/// <summary>
/// Get query options according to case insensitive.
/// Gets query options according to case sensitivity and
/// whether no dollar query options is enabled.
/// </summary>
/// <param name="queryOptionName">The query option's name.</param>
/// <param name="value">The value for that query option.</param>
/// <param name="name">The query option name with $ prefix.</param>
/// <param name="value">The value of the query option.</param>
/// <returns>Whether value successfully retrived.</returns>
private bool TryGetQueryOption(string queryOptionName, out string value)
private bool TryGetQueryOption(string name, out string value)
{
if (!this.Resolver.EnableCaseInsensitive)
value = null;
if (name == null)
{
return this.queryOptions.TryGetValue(queryOptionName, out value);
return false;
}

value = null;
// Trim name to prevent caller from passing in untrimmed name for comparison with
// already trimmed keys in queryOptions dictionary.
string trimmedName = name.Trim();

bool isCaseInsensitiveEnabled = this.Resolver.EnableCaseInsensitive;
bool isNoDollarQueryOptionsEnabled = this.Configuration.EnableNoDollarQueryOptions;

if (!isCaseInsensitiveEnabled && !isNoDollarQueryOptionsEnabled)
{
return this.queryOptions.TryGetValue(trimmedName, out value);
}

StringComparison stringComparison = isCaseInsensitiveEnabled ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

string nameWithoutDollarPrefix = (isNoDollarQueryOptionsEnabled && trimmedName.StartsWith(UriQueryConstants.DollarSign, StringComparison.Ordinal)) ?
trimmedName.Substring(1) : null;

var list = this.queryOptions
.Where(pair => string.Equals(queryOptionName, pair.Key, StringComparison.OrdinalIgnoreCase))
.Where(pair => string.Equals(trimmedName, pair.Key, stringComparison)
|| (nameWithoutDollarPrefix != null && string.Equals(nameWithoutDollarPrefix, pair.Key, stringComparison)))
.ToList();

if (list.Count == 0)
Expand All @@ -570,8 +590,9 @@ private bool TryGetQueryOption(string queryOptionName, out string value)
return true;
}

throw new ODataException(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce(queryOptionName));
throw new ODataException(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce(
isNoDollarQueryOptionsEnabled ? string.Format(CultureInfo.InvariantCulture, "${0}/{0}", nameWithoutDollarPrefix ?? trimmedName) : trimmedName));
}
#endregion private methods
}
}
}
38 changes: 30 additions & 8 deletions src/Microsoft.OData.Core/UriParser/ODataUriParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.OData.UriParser
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser.Aggregation;
Expand Down Expand Up @@ -169,6 +170,17 @@ public Func<string, BatchReferenceSegment> BatchReferenceCallback
set { this.configuration.BatchReferenceCallback = value; }
}

/// <summary>
/// Whether no dollar query options is enabled.
/// If it is enabled, the '$' prefix of system query options becomes optional.
/// For example, "select" and "$select" are equivalent in this case.
/// </summary>
public bool EnableNoDollarQueryOptions
{
get { return this.configuration.EnableNoDollarQueryOptions; }
set { this.configuration.EnableNoDollarQueryOptions = value; }
}

/// <summary>
/// Whether Uri template parsing is enabled. Uri template for keys and function parameters are supported.
/// See <see cref="UriTemplateExpression"/> class for detail.
Expand Down Expand Up @@ -514,23 +526,33 @@ private void InitQueryOptionDic()
{
foreach (var queryOption in queryOptions)
{
if (queryOption.Name == null)
string queryOptionName = queryOption.Name;
if (queryOptionName == null)
{
continue;
}

if (IsODataQueryOption(queryOption.Name))
// If EnableNoDollarQueryOptions is true, we will treat all reserved OData query options without "$" prefix as odata query options.
// Or, they will be treated as custom query options which could be duplicated.

bool shouldAddDollarPrefix = this.EnableNoDollarQueryOptions && !queryOption.Name.StartsWith("$", StringComparison.Ordinal);
string fixedQueryOptionName = shouldAddDollarPrefix ? UriQueryConstants.DollarSign + queryOptionName : queryOptionName;

if (IsODataQueryOption(fixedQueryOptionName))
{
if (queryOptionDic.ContainsKey(queryOption.Name))
if (queryOptionDic.ContainsKey(fixedQueryOptionName))
{
throw new ODataException(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce(queryOption.Name));
throw new ODataException(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce(
this.EnableNoDollarQueryOptions
? string.Format(CultureInfo.InvariantCulture, "${0}/{0}", fixedQueryOptionName.TrimStart('$'))
: fixedQueryOptionName));
}

queryOptionDic.Add(queryOption.Name, queryOption.Value);
queryOptionDic.Add(fixedQueryOptionName, queryOption.Value);
}
else
{
customQueryOptions.Add(new KeyValuePair<string, string>(queryOption.Name, queryOption.Value));
customQueryOptions.Add(new KeyValuePair<string, string>(queryOptionName, queryOption.Value));
}
}
}
Expand All @@ -541,9 +563,9 @@ private void InitQueryOptionDic()
/// </summary>
/// <param name="optionName">The name of a query option.</param>
/// <returns>True if optionName is OData query option, vise versa.</returns>
private static bool IsODataQueryOption(string optionName)
private bool IsODataQueryOption(string optionName)
{
switch (optionName.ToLowerInvariant())
switch (this.Resolver.EnableCaseInsensitive ? optionName.ToLowerInvariant() : optionName)
{
case UriQueryConstants.FilterQueryOption:
case UriQueryConstants.ApplyQueryOption:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ internal bool EnableCaseInsensitiveUriFunctionIdentifier
}
}

/// <summary>
/// Whether no dollar query options is enabled.
/// If it is enabled, the '$' prefix of system query options becomes optional.
/// For example, "select" and "$select" are equivalent in this case.
/// </summary>
internal bool EnableNoDollarQueryOptions { get; set; }

/// <summary>
/// Whether Uri template parsing is enabled. See <see cref="UriTemplateExpression"/> class for detail.
/// </summary>
Expand Down
49 changes: 40 additions & 9 deletions src/Microsoft.OData.Core/UriParser/Parsers/ExpandOptionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,35 @@ internal sealed class ExpandOptionParser
private readonly int maxRecursionDepth;

/// <summary>
/// Lexer to parse over the optionsText for a single $expand term. This is NOT the same lexer used by <see cref="SelectExpandParser"/>
/// to parse over the entirety of $select or $expand.
/// Whether to enable no dollar query options.
/// </summary>
private ExpressionLexer lexer;
private readonly bool enableNoDollarQueryOptions;

/// <summary>
/// Whether to allow case insensitive for builtin identifier.
/// </summary>
private bool enableCaseInsensitiveBuiltinIdentifier;
private readonly bool enableCaseInsensitiveBuiltinIdentifier;

/// <summary>
/// Lexer to parse over the optionsText for a single $expand term. This is NOT the same lexer used by <see cref="SelectExpandParser"/>
/// to parse over the entirety of $select or $expand.
/// </summary>
private ExpressionLexer lexer;

/// <summary>
/// Creates an instance of this class to parse options.
/// </summary>
/// <param name="maxRecursionDepth">Max recursion depth left.</param>
/// <param name="enableCaseInsensitiveBuiltinIdentifier">Whether to allow case insensitive for builtin identifier.</param>
internal ExpandOptionParser(int maxRecursionDepth, bool enableCaseInsensitiveBuiltinIdentifier = false)
/// <param name="enableNoDollarQueryOptions">Whether to enable no dollar query options.</param>
internal ExpandOptionParser(
int maxRecursionDepth,
bool enableCaseInsensitiveBuiltinIdentifier = false,
bool enableNoDollarQueryOptions = false)
{
this.maxRecursionDepth = maxRecursionDepth;
this.enableCaseInsensitiveBuiltinIdentifier = enableCaseInsensitiveBuiltinIdentifier;
this.enableNoDollarQueryOptions = enableNoDollarQueryOptions;
}

/// <summary>
Expand All @@ -63,8 +73,14 @@ internal ExpandOptionParser(int maxRecursionDepth, bool enableCaseInsensitiveBui
/// <param name="parentEntityType">The parent entity type for expand option</param>
/// <param name="maxRecursionDepth">Max recursion depth left.</param>
/// <param name="enableCaseInsensitiveBuiltinIdentifier">Whether to allow case insensitive for builtin identifier.</param>
internal ExpandOptionParser(ODataUriResolver resolver, IEdmStructuredType parentEntityType, int maxRecursionDepth, bool enableCaseInsensitiveBuiltinIdentifier = false)
: this(maxRecursionDepth, enableCaseInsensitiveBuiltinIdentifier)
/// <param name="enableNoDollarQueryOptions">Whether to enable no dollar query options.</param>
internal ExpandOptionParser(
ODataUriResolver resolver,
IEdmStructuredType parentEntityType,
int maxRecursionDepth,
bool enableCaseInsensitiveBuiltinIdentifier = false,
bool enableNoDollarQueryOptions = false)
: this(maxRecursionDepth, enableCaseInsensitiveBuiltinIdentifier, enableNoDollarQueryOptions)
{
this.resolver = resolver;
this.parentEntityType = parentEntityType;
Expand Down Expand Up @@ -130,6 +146,13 @@ internal List<ExpandTermToken> BuildExpandTermToken(PathSegmentToken pathToken,
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:
Expand Down Expand Up @@ -257,14 +280,22 @@ internal List<ExpandTermToken> BuildExpandTermToken(PathSegmentToken pathToken,
{
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.
// 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, enableCaseInsensitiveBuiltinIdentifier);
SelectExpandParser innerExpandParser = new SelectExpandParser(
resolver,
expandText,
targetEntityType,
this.maxRecursionDepth - 1,
this.enableCaseInsensitiveBuiltinIdentifier,
this.enableNoDollarQueryOptions);
expandOption = innerExpandParser.ParseExpand();
break;
}
Expand Down
42 changes: 33 additions & 9 deletions src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ internal sealed class SelectExpandParser
/// </summary>
private readonly int maxRecursiveDepth;

/// <summary>
/// Whether to enable no dollar query options.
/// </summary>
private readonly bool enableNoDollarQueryOptions;

/// <summary>
/// Whether to allow case insensitive for builtin identifier.
/// </summary>
private readonly bool enableCaseInsensitiveBuiltinIdentifier;

/// <summary>
/// Object to handle the parsing of any nested expand options that we discover.
/// </summary>
Expand All @@ -47,19 +57,19 @@ internal sealed class SelectExpandParser
/// </summary>
private bool isSelect;

/// <summary>
/// Whether to allow case insensitive for builtin identifier.
/// </summary>
private bool enableCaseInsensitiveBuiltinIdentifier;

/// <summary>
/// Build the SelectOption strategy.
/// TODO: Really should not take the clauseToParse here. Instead it should be provided with a call to ParseSelect() or ParseExpand().
/// </summary>
/// <param name="clauseToParse">the clause to parse</param>
/// <param name="maxRecursiveDepth">max recursive depth</param>
/// <param name="enableCaseInsensitiveBuiltinIdentifier">Whether to allow case insensitive for builtin identifier.</param>
public SelectExpandParser(string clauseToParse, int maxRecursiveDepth, bool enableCaseInsensitiveBuiltinIdentifier = false)
/// <param name="enableNoDollarQueryOptions">Whether to enable no dollar query options.</param>
public SelectExpandParser(
string clauseToParse,
int maxRecursiveDepth,
bool enableCaseInsensitiveBuiltinIdentifier = false,
bool enableNoDollarQueryOptions = false)
{
this.maxRecursiveDepth = maxRecursiveDepth;

Expand All @@ -74,6 +84,8 @@ public SelectExpandParser(string clauseToParse, int maxRecursiveDepth, bool enab
this.lexer = clauseToParse != null ? new ExpressionLexer(clauseToParse, false /*moveToFirstToken*/, false /*useSemicolonDelimiter*/) : null;

this.enableCaseInsensitiveBuiltinIdentifier = enableCaseInsensitiveBuiltinIdentifier;

this.enableNoDollarQueryOptions = enableNoDollarQueryOptions;
}

/// <summary>
Expand All @@ -84,8 +96,15 @@ public SelectExpandParser(string clauseToParse, int maxRecursiveDepth, bool enab
/// <param name="parentEntityType">the parent entity type for expand option</param>
/// <param name="maxRecursiveDepth">max recursive depth</param>
/// <param name="enableCaseInsensitiveBuiltinIdentifier">Whether to allow case insensitive for builtin identifier.</param>
public SelectExpandParser(ODataUriResolver resolver, string clauseToParse, IEdmStructuredType parentEntityType, int maxRecursiveDepth, bool enableCaseInsensitiveBuiltinIdentifier = false)
: this(clauseToParse, maxRecursiveDepth, enableCaseInsensitiveBuiltinIdentifier)
/// <param name="enableNoDollarQueryOptions">Whether to enable no dollar query options.</param>
public SelectExpandParser(
ODataUriResolver resolver,
string clauseToParse,
IEdmStructuredType parentEntityType,
int maxRecursiveDepth,
bool enableCaseInsensitiveBuiltinIdentifier = false,
bool enableNoDollarQueryOptions = false)
: this(clauseToParse, maxRecursiveDepth, enableCaseInsensitiveBuiltinIdentifier, enableNoDollarQueryOptions)
{
this.resolver = resolver;
this.parentEntityType = parentEntityType;
Expand Down Expand Up @@ -165,7 +184,12 @@ private List<ExpandTermToken> ParseSingleExpandTerm()

if (expandOptionParser == null)
{
expandOptionParser = new ExpandOptionParser(this.resolver, this.parentEntityType, this.maxRecursiveDepth, enableCaseInsensitiveBuiltinIdentifier)
expandOptionParser = new ExpandOptionParser(
this.resolver,
this.parentEntityType,
this.maxRecursiveDepth,
this.enableCaseInsensitiveBuiltinIdentifier,
this.enableNoDollarQueryOptions)
{
MaxFilterDepth = MaxFilterDepth,
MaxOrderByDepth = MaxOrderByDepth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ public static void Parse(
};
selectTree = selectParser.ParseSelect();

SelectExpandParser expandParser = new SelectExpandParser(configuration.Resolver, expandClause, parentEntityType, configuration.Settings.SelectExpandLimit, configuration.EnableCaseInsensitiveUriFunctionIdentifier)
SelectExpandParser expandParser = new SelectExpandParser(
configuration.Resolver,
expandClause,
parentEntityType,
configuration.Settings.SelectExpandLimit,
configuration.EnableCaseInsensitiveUriFunctionIdentifier,
configuration.EnableNoDollarQueryOptions)
{
MaxPathDepth = configuration.Settings.PathLimit,
MaxFilterDepth = configuration.Settings.FilterLimit,
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.OData.Core/UriParser/UriQueryConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@ internal static class UriQueryConstants

/// <summary>A search query option name.</summary>
internal const string SearchQueryOption = "$search";

/// <summary>Dollar sign.</summary>
internal const string DollarSign = "$";
}
}
Loading

0 comments on commit 71297a9

Please sign in to comment.