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); } } } } }