src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs (220 lines of code) (raw):
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.WebJobs.Host.Bindings.Path
{
/// <summary>
/// Represents a semantic token as generated by <see cref="BindingTemplateParser"/> during parsing of parameterized
/// binding template string. Then generated token may be consumed to create instance of
/// <see cref="BindingTemplate"/> or <see cref="BindingTemplateSource"/>.
/// The token may be a literal value or an expression.
/// </summary>
internal abstract class BindingTemplateToken
{
// If this is expression, get the prameter name
// If it's a literal, return null.
public abstract string ParameterName { get; }
// If this is a literal, return the value. Else, return null.
public abstract string AsLiteral { get; }
public static BindingTemplateToken NewLiteral(string literalValue)
{
return new LiteralToken(literalValue);
}
public static BindingTemplateToken NewExpression(string expression)
{
// BindingData takes precedence over builtins.
BindingParameterResolver builtin;
BindingParameterResolver.TryGetResolver(expression, out builtin);
// check for formatter, which is applied to finale results.
string format = null;
if (builtin == null)
{
int indexColon = expression.IndexOf(':');
if (indexColon > 0)
{
format = expression.Substring(indexColon + 1);
expression = expression.Substring(0, indexColon);
}
}
if (!BindingTemplateParser.IsValidIdentifier(expression))
{
throw new FormatException($"Invalid template expression '{expression}");
}
// Expression is just a series of dot operators like: a.b.c
var parts = expression.Split('.');
// For backwards compat, first part can't have a '-'
if (builtin == null)
{
if (parts[0].IndexOf('-') >= 0)
{
throw new FormatException($"The parameter name '{parts[0]}' is invalid.");
}
}
foreach (var part in parts)
{
if (string.IsNullOrWhiteSpace(part))
{
throw new InvalidOperationException($"Illegal expression: {parts}");
}
}
return new ExpressionToken(parts, format, builtin);
}
public abstract string Evaluate(IReadOnlyDictionary<string, object> bindingData);
// Generate a regex segment to capture this token type.
protected abstract void BuildCapturePattern(StringBuilder builder);
/// <summary>
/// Utility method to build a regular expression to capture parameter values out of pre-parsed template tokens.
/// </summary>
/// <param name="tokens">Template tokens as generated and validated by
/// the <see cref="BindingTemplateParser"/>.</param>
/// <returns>Regex pattern to capture parameter values, containing named capturing groups, matching
/// structure and parameter names provided by the list of tokens.
/// Create a regex that will populate values of parameters by matching the template against an input string.
/// For example, [BlobTrigger("container/{name}.txt")] gets triggered on "container/foo.txt",
/// then create a binding data where name=foo.
/// </returns>
internal static string BuildCapturePattern(IEnumerable<BindingTemplateToken> tokens)
{
StringBuilder builder = new StringBuilder("^");
foreach (var token in tokens)
{
token.BuildCapturePattern(builder);
}
return builder.Append("$").ToString();
}
// Represents a literal segment.
private class LiteralToken : BindingTemplateToken
{
private readonly string _value;
public LiteralToken(string value)
{
_value = value;
}
public override string AsLiteral => _value;
public override string ParameterName
{
get
{
return null;
}
}
public override string Evaluate(IReadOnlyDictionary<string, object> bindingData)
{
return _value;
}
protected override void BuildCapturePattern(StringBuilder builder)
{
builder.Append(Regex.Escape(this._value));
}
}
// Represent an expression.
// "literal{expr}literal"
private class ExpressionToken : BindingTemplateToken
{
// If non-null, then this could be a builtin object.
private readonly BindingParameterResolver _builtin;
private readonly string _format;
// The parts of an expression, like a.b.c.
private readonly string[] _expressionParts;
public ExpressionToken(string[] expressionParts, string format, BindingParameterResolver builtin)
{
_expressionParts = expressionParts;
_builtin = builtin;
_format = format;
}
public override string AsLiteral => null;
public override string ParameterName
{
get
{
return this._expressionParts[0];
}
}
protected override void BuildCapturePattern(StringBuilder builder)
{
if (this._expressionParts.Length != 1)
{
throw new InvalidOperationException($"Capture expressions can't include dot operators");
}
var value = this._expressionParts[0];
builder.Append(String.Format(CultureInfo.InvariantCulture, "(?<{0}>.*)", value));
}
public override string Evaluate(IReadOnlyDictionary<string, object> bindingData)
{
object current;
if (bindingData.TryGetValue(this._expressionParts[0], out current))
{
for (int i = 1; i < _expressionParts.Length; i++)
{
var propName = _expressionParts[i];
try
{
current = GetProperty(current, propName);
}
catch (Exception e)
{
throw new InvalidOperationException($"Error while accessing '{propName}': {e.Message}");
}
}
}
else
{
// Not found in binding data. Is this a builtin?
if (this._builtin != null)
{
current = _builtin.Resolve(this.ParameterName);
}
if (current == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "No value for named parameter '{0}'.", this.ParameterName));
}
}
var strValue = BindingDataPathHelper.ConvertParameterValueToString(current, _format);
return strValue;
}
// Support:
// IDictionary<string, T>;
// IReadOnlyDictionary<string, T>
// There's no common base interface here, and T can be { string, object, poco },
// so we just directly reflect over 'bool TryGetValue(string, out T)' and invoke that.
private static bool TryGetValue(object o, string member, out object result)
{
Type scope = o.GetType();
var method = scope.GetMethod("TryGetValue", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
if (method.ReturnType == typeof(bool))
{
var ps = method.GetParameters();
if (ps.Length == 2)
{
if (ps[0].ParameterType == typeof(string))
{
if (ps[1].ParameterType.IsByRef)
{
object[] parameters = new object[2];
parameters[0] = member;
var hasValue = (bool)method.Invoke(o, parameters);
if (hasValue)
{
result = parameters[1];
return true;
}
else
{
throw new InvalidOperationException($"property doesn't exist.");
}
}
}
}
}
}
result = null;
return false;
}
// Case insensitive lookup.
// Helper to get a property from an object.
// This supports both static types and JObject (for dynamic)
public static object GetProperty(object o, string member)
{
Type scope = o.GetType();
// Try JObject. Call specific JObject lookup to do a case-insensitive lookup.
JObject jobj = o as JObject;
if (jobj != null)
{
JToken propValue;
// JObject's normal dot operator returns null for missing properties;
// we need to explicitly call TryGetValue.
if (!jobj.TryGetValue(member, StringComparison.OrdinalIgnoreCase, out propValue))
{
throw new InvalidOperationException($"property doesn't exist.");
}
return propValue;
}
// Handle IDictionary, IReadOnlyDictionary
// Casing is at the mercy of the dictionary implementation
object result;
if (TryGetValue(o, member, out result))
{
return result;
}
{
var prop = scope.GetProperty(member, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (prop == null)
{
throw new InvalidOperationException($"property doesn't exist.");
}
if (!prop.CanRead)
{
throw new InvalidOperationException($"property is not readable.");
}
return prop.GetValue(o, null);
}
}
}
}
}