//****************************************************************************************************** // TemplateExpression.cs - Gbtc // // Copyright © 2010, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 06/13/2014 - J. Ritchie Carroll // Generated original version of source code. // 09/26/2015 - J. Ritchie Carroll // Added advanced expression evaluation option, i.e., eval{expression} // //****************************************************************************************************** using System; using System.CodeDom; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; #if DNF46 using ExpressionEvaluator; #endif using ParsedExpression = System.Tuple; using ParsedEvaluation = System.Tuple; namespace GSF.Parsing { /// /// Represents a template based token substitution parser that supports binary and advanced expressions. /// /// /// /// As an example, this parser can use a templated expression of the form: /// /// {CompanyAcronym}_{DeviceAcronym}[?{SignalType.Source}=Phasor[-{SignalType.Suffix}{SignalIndex}]]:{Signal.Acronymn} /// /// then replace the tokens with actual values and properly evaluate the expressions. /// Example results could look like: GPA_SHELBY-PA1:IPHA and GPA_SHELBY:FREQ /// /// /// Parser also supports more complex C# style expressions using the "eval{}" function, e.g.: /// /// eval{'{CompanyAcronym}'.Substring(0,3)}_{DeviceAcronym}eval{'[?{SignalType.Source}=Phasor[-{SignalType.Suffix}]]'.Length} /// /// /// public class TemplatedExpressionParser { #region [ Members ] // Nested Types // Operator symbol indices (see s_operatorSymbols definition) private struct OperatorType { // Using struct instead of enum to avoid casts public const int Equality = 0; public const int Inequality = 1; public const int Inequality2 = 2; public const int LessThanOrEqual = 3; public const int LessThan = 4; public const int GreaterThanOrEqual = 5; public const int GreaterThan = 6; //public const int Equality2 = 7; } // Constants private const string ExpressionParser = @"^[^\{0}\{1}]*(((?(?'Open'\{0})[^\{0}\{1}]*))+((?'Close-Open'\{1})[^\{0}\{1}]*)+)*(?(Open)(?!))$"; private const string EvaluationParser = @"eval\{0}([^\{0}]+)\{1}"; // Fields private readonly Regex m_expressionParser; private readonly Regex m_evaluationParser; private readonly string[] m_escapedReservedSymbols; private readonly string[] m_encodedReservedSymbols; private string m_templatedExpression; #endregion #region [ Constructors ] /// /// Creates a new . /// public TemplatedExpressionParser() : this('{', '}', '[', ']') { } /// /// Creates a new with desired delimiters. /// /// Character that identifies the beginning of a token. /// Character that identifies the end of a token. /// Character that identifies the beginning of an expression. /// Character that identifies the beginning of an expression. /// /// All delimiters must be unique -- or -- the symbol 'u' is reserved for encoding and cannot be used as a delimiter. /// public TemplatedExpressionParser(char startTokenDelimiter, char endTokenDelimiter, char startExpressionDelimiter, char endExpressionDelimiter) { if (startTokenDelimiter == endTokenDelimiter || startTokenDelimiter == startExpressionDelimiter || startTokenDelimiter == endExpressionDelimiter || endTokenDelimiter == startExpressionDelimiter || endTokenDelimiter == endExpressionDelimiter || startExpressionDelimiter == endExpressionDelimiter) throw new ArgumentException("All delimiters must be unique"); if (startTokenDelimiter == 'u' || endTokenDelimiter == 'u' || startExpressionDelimiter == 'u' || endExpressionDelimiter == 'u') throw new ArgumentException("The symbol 'u' is reserved for encoding and cannot be used as a delimiter."); // Define a regular expression that can parse nested binary expressions m_expressionParser = new Regex(string.Format(ExpressionParser, startExpressionDelimiter, endExpressionDelimiter), RegexOptions.Compiled | RegexOptions.CultureInvariant); // Define a regular expression that will find each "eval[]" expression m_evaluationParser = new Regex(string.Format(EvaluationParser, startTokenDelimiter, endTokenDelimiter), RegexOptions.Compiled); // Reserved symbols include all expression operators and delimiters ReservedSymbols = new[] { '\\', '<', '>', '=', '!', startTokenDelimiter, endTokenDelimiter, startExpressionDelimiter, endExpressionDelimiter }; m_escapedReservedSymbols = ReservedSymbols.Select(symbol => $"\\{symbol}").ToArray(); m_encodedReservedSymbols = ReservedSymbols.Select(symbol => symbol.RegexEncode()).ToArray(); StartTokenDelimiter = startTokenDelimiter; EndTokenDelimiter = endTokenDelimiter; StartExpressionDelimiter = startExpressionDelimiter; EndExpressionDelimiter = endExpressionDelimiter; } #endregion #region [ Properties ] /// /// Gets or sets the templated expression to use. /// public string TemplatedExpression { get => m_templatedExpression; set => m_templatedExpression = EncodeEscapedReservedSymbols(value); } /// /// Gets the reserved symbols - this includes all delimiters and expression operators. /// /// /// The default reserved symbol list is: '\', '<', '>', '=', '!', '{', '}', '[', ']'. /// public char[] ReservedSymbols { get; } /// /// Gets the character that identifies the beginning of a token. /// public char StartTokenDelimiter { get; } /// /// Gets the character that identifies the end of a token. /// public char EndTokenDelimiter { get; } /// /// Gets the character that identifies the start of an expression. /// public char StartExpressionDelimiter { get; } /// /// Gets the character that identifies the end of an expression. /// public char EndExpressionDelimiter { get; } #endregion #region [ Methods ] /// /// Executes replacements using provided dictionary on the currently defined /// value and optionally evaluates any expressions. /// /// Dictionary of substitutions. Dictionary keys are tokens to be replaced by the values. /// Determines if substitutions should be case insensitive. Defaults to true. /// Determines if expressions should be evaluated. Defaults to true. /// Determines if reserved symbols in substitution values should be automatically escaped. Defaults to true. /// A string that was based on with tokens replaced and expressions evaluated. /// /// /// The default token start and end delimiters are { and } but can be overridden in constructor. /// All substitution tokens surrounded by and (e.g., {token}) /// will be immediately replaced with their string equivalents before further expression evaluation. As a result of the expression /// syntax <, >, =, and ! are reserved expressions symbols; , , /// and are reserved delimiter symbols. To embed any reserved /// symbol into the so that it appears in the evaluated result, escape the symbol by prefixing it /// with a backslash, e.g., \{. /// /// /// The default expression start and end delimiters are [ and ] but can be overridden in constructor. /// Expressions are represented in the form of [?expression[result]] and can be nested, e.g. ([?expression1[?expression2[result]]]). /// Expressions should not contain extraneous white space for proper evaluation. Only simple boolean comparison operations are allowed /// in expressions, e.g., A=B (or A==B), A!=B (or A<>B), A>B, A>=B, A<B and A<=B - nothing more. Any expression that /// fails to evaluate will be evaluated as FALSE. Note that if both left (A) and right (B) operands can be parsed as a numeric value then /// the expression will be numerically evaluated otherwise expression will be a culture-invariant string comparison. Nested expressions /// are evaluated as cumulative AND operators. There is no defined nesting limit. /// /// /// Advanced expressions can be parsed using the eval function, e.g., eval{1 + 2}. Eval function expression is delimited by the currently /// defined token delimiters, { and } by default. The evaluation engine deals with numbers and strings. The typing of numeric literals /// follow C# standards, suffixes such as d, f and m may be used to explicitly specify the numeric type. If no numeric suffix is provided, /// the default type of a numeric literal is assumed to be . String literals should be specified using single quotes /// instead of double quotes, this includes substitution parameters, e.g., eval{'{DeviceAcronym}'.Length + 1}. Advanced evaluation /// expressions using the eval function are always parsed after common expressions. Eval functions cannot be nested. /// /// /// When is true and values in intentionally contain /// expressions to be evaluated, then should be set to false so that the reserved /// symbols in the values are not automatically escaped. /// /// public string Execute(IDictionary substitutions, bool ignoreCase = true, bool evaluateExpressions = true, bool escapeSubstitutionValues = true) { if (string.IsNullOrEmpty(m_templatedExpression)) return ""; string result = m_templatedExpression; // Execute substitutions foreach (KeyValuePair substitution in substitutions) { if (ignoreCase) result = result.ReplaceCaseInsensitive(substitution.Key, escapeSubstitutionValues ? EscapeAndEncodeReservedSymbols(substitution.Value) : EncodeEscapedReservedSymbols(substitution.Value)); else result = result.Replace(substitution.Key, escapeSubstitutionValues ? EscapeAndEncodeReservedSymbols(substitution.Value) : EncodeEscapedReservedSymbols(substitution.Value)); } if (evaluateExpressions) { // Parse common expressions, i.e., [?expression[result]] List parsedExpressions = ParseExpressions(result, ignoreCase); // Execute common expression replacements foreach (ParsedExpression parsedExpression in parsedExpressions) result = result.Replace(parsedExpression.Item1, parsedExpression.Item2 ? parsedExpression.Item3 : ""); // Parse evaluation expressions, i.e., eval{expression} List parsedEvaluations = ParseEvaluations(result); // Execute evaluation expression replacements foreach (ParsedEvaluation parsedEvaluation in parsedEvaluations) result = result.Replace(parsedEvaluation.Item1, parsedEvaluation.Item2); } // Decode any reserved symbols that were escaped in original templated expression return DecodeReservedSymbols(result); } // Encodes escaped reserved symbols in a value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private string EncodeEscapedReservedSymbols(string value) { if (string.IsNullOrWhiteSpace(value)) return value; for (int i = 0; i < m_escapedReservedSymbols.Length; i++) value = value.Replace(m_escapedReservedSymbols[i], m_encodedReservedSymbols[i]); return value; } // Escapes then encodes reserved symbols in a value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private string EscapeAndEncodeReservedSymbols(string value) { if (string.IsNullOrWhiteSpace(value)) return value; // Encode any values that have already been escaped value = EncodeEscapedReservedSymbols(value); // Escape any remaining reserved symbols, e.g., replace '{' with '\{' for (int i = 0; i < ReservedSymbols.Length; i++) value = value.Replace(ReservedSymbols[i].ToString(), m_escapedReservedSymbols[i]); return EncodeEscapedReservedSymbols(value); } // Decodes reserved symbols in a value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private string DecodeReservedSymbols(string value) { if (string.IsNullOrWhiteSpace(value)) return value; for (int i = 0; i < ReservedSymbols.Length; i++) value = value.Replace(m_encodedReservedSymbols[i], ReservedSymbols[i].ToString()); return value; } // Parses expressions of the form "[?expression[result]]". Expressions can be nested, e.g., "[?expression1[?expression2[result]]]". // Returns list of complete expressions (used as base replacement text), cumulative boolean expression evaluations and expression results [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] private List ParseExpressions(string fieldReplacedTemplatedExpression, bool ignoreCase) { List parsedExpressions = new List(); // Find all expressions using regular expression Match match = m_expressionParser.Match(fieldReplacedTemplatedExpression); if (match.Success) { Group capturedExpressions = match.Groups["Expressions"]; StringBuilder completeExpression = new StringBuilder(); List evaluations = new List(); int depth = 0, lastDepth = 0; foreach (Capture capture in capturedExpressions.Captures) { if (capture.Value.StartsWith($"{StartExpressionDelimiter}?", StringComparison.Ordinal)) { // Found binary operation expression depth++; completeExpression.Append(capture.Value); // Only apply cumulative AND logic for items at continued depth if (depth == lastDepth && evaluations.Count > 0) evaluations.RemoveAt(evaluations.Count - 1); lastDepth = depth; // Parse binary expression CodeBinaryOperatorExpression expression = ParseBinaryOperatorExpression(capture.Value.Substring(2), out TypeCode expressionType); // Evaluate binary expression if (expression != null) { IComparer comparer; // Select comparer based on expression type switch (expressionType) { case TypeCode.Int32: comparer = Comparer.Default; break; case TypeCode.Double: comparer = Comparer.Default; break; case TypeCode.String: comparer = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; break; default: throw new ArgumentOutOfRangeException(); } // Compare operands int result = comparer.Compare(((CodePrimitiveExpression)expression.Left).Value, ((CodePrimitiveExpression)expression.Right).Value); // Evaluate comparison result bool evaluation; switch (expression.Operator) { case CodeBinaryOperatorType.IdentityEquality: evaluation = result == 0; break; case CodeBinaryOperatorType.IdentityInequality: evaluation = result != 0; break; case CodeBinaryOperatorType.LessThan: evaluation = result < 0; break; case CodeBinaryOperatorType.LessThanOrEqual: evaluation = result <= 0; break; case CodeBinaryOperatorType.GreaterThan: evaluation = result > 0; break; case CodeBinaryOperatorType.GreaterThanOrEqual: evaluation = result >= 0; break; default: evaluation = false; break; } evaluations.Add(evaluation); } else { // Expression evaluation fails if there is not an expression evaluations.Add(false); } } else if (capture.Value.StartsWith(StartExpressionDelimiter.ToString(), StringComparison.Ordinal)) { if (depth > 0) { depth++; // Found expression result completeExpression.Append(capture.Value); // Complete closed expression int index = capture.Index + capture.Length; while (index < fieldReplacedTemplatedExpression.Length && fieldReplacedTemplatedExpression[index] == EndExpressionDelimiter) { completeExpression.Append(EndExpressionDelimiter); index++; depth--; } // Add complete expression, cumulative boolean expression evaluation and expression result to parsed expression list parsedExpressions.Add(new ParsedExpression(completeExpression.ToString(), evaluations.All(item => item), capture.Value.Substring(1))); // Reset for next expression completeExpression.Clear(); if (depth > 0) continue; evaluations.Clear(); depth = 0; } else { // Unbalanced expression - exception not expected since regex should already catch this throw new InvalidOperationException($"Unbalanced delimiters detected in field replaced templated expression \"{fieldReplacedTemplatedExpression}\""); } } } } return parsedExpressions; } private CodeBinaryOperatorExpression ParseBinaryOperatorExpression(string expression, out TypeCode expressionType) { if (!string.IsNullOrEmpty(expression)) { string[] operands = expression.Split(s_operatorSymbols, StringSplitOptions.None); // Expression is only valid if there are exactly two operands if (operands.Length == 2) { CodePrimitiveExpression leftOperand, rightOperand; CodeBinaryOperatorType operatorType; if (int.TryParse(operands[0], out int left) && int.TryParse(operands[1], out int right)) { // Both operands can be compared as integers leftOperand = new CodePrimitiveExpression(left); rightOperand = new CodePrimitiveExpression(right); expressionType = TypeCode.Int32; } else if (double.TryParse(operands[0], out double leftF) && double.TryParse(operands[1], out double rightF)) { // Both operands can be compared as doubles leftOperand = new CodePrimitiveExpression(leftF); rightOperand = new CodePrimitiveExpression(rightF); expressionType = TypeCode.Double; } else { // Default to string comparison of operands leftOperand = new CodePrimitiveExpression(operands[0]); rightOperand = new CodePrimitiveExpression(operands[1]); expressionType = TypeCode.String; } // Determine operator type - order of detection is critical Symbol: if (expression.Contains(s_operatorSymbols[OperatorType.Equality])) operatorType = CodeBinaryOperatorType.IdentityEquality; // == else if (expression.Contains(s_operatorSymbols[OperatorType.Inequality])) operatorType = CodeBinaryOperatorType.IdentityInequality; // != else if (expression.Contains(s_operatorSymbols[OperatorType.Inequality2])) operatorType = CodeBinaryOperatorType.IdentityInequality; // <> else if (expression.Contains(s_operatorSymbols[OperatorType.LessThanOrEqual])) operatorType = CodeBinaryOperatorType.LessThanOrEqual; // <= else if (expression.Contains(s_operatorSymbols[OperatorType.LessThan])) operatorType = CodeBinaryOperatorType.LessThan; // < else if (expression.Contains(s_operatorSymbols[OperatorType.GreaterThanOrEqual])) operatorType = CodeBinaryOperatorType.GreaterThanOrEqual; // >= else if (expression.Contains(s_operatorSymbols[OperatorType.GreaterThan])) operatorType = CodeBinaryOperatorType.GreaterThan; // > else operatorType = CodeBinaryOperatorType.IdentityEquality; // = return new CodeBinaryOperatorExpression(leftOperand, operatorType, rightOperand); } } expressionType = TypeCode.Object; return null; } // Parses expressions of the form "eval{expression}". Expressions cannot be nested. // Returns list of complete expressions (used as base replacement text) and evaluation results private List ParseEvaluations(string fieldReplacedTemplatedExpression) { List parsedEvaluations = new List(); #if DNF46 MatchCollection matches = m_evaluationParser.Matches(fieldReplacedTemplatedExpression); foreach (Match match in matches) { string source = match.Groups[0].Value; string result = new CompiledExpression(match.Groups[1].Value).Eval().ToString(); parsedEvaluations.Add(new ParsedEvaluation(source, result)); } #endif return parsedEvaluations; } #endregion #region [ Static ] // Static Fields // Operator symbol array - note that order is critical for proper split private static readonly string[] s_operatorSymbols = { "==", "!=", "<>", "<=", "<", ">=", ">", "=" }; #endregion } }