Last active
December 12, 2016 12:21
-
-
Save hidegh/4802fd2c4a2f0b250e853a976146fac8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
namespace MyProject.Reporting.ExpressionResolver | |
{ | |
/// <summary> | |
/// Examples to expressions: | |
/// ------------------------ | |
/// Assuming that DataSources containd a data-set the key (name) "d": | |
/// | |
/// %(d.Uid) | |
/// %(d.Subjekts.Client.SSN) | |
/// %(d.Subjekts.Client.DateOfBirth) | |
/// %(d.Subjekts.Client.DateOfBirth|yyyy-MM-dd HH:mm) | |
/// %(d.Subjekts.Client.DateOfBirth|yyyy) | |
/// %(d.Output.AnnualFee) | |
/// %(d.Output.AnnualFee|N6) | |
/// %(d.Output.AnnualFee|C) | |
/// %(d.Output.PaymentSchedules{<CR><LF>}MM-dd) | |
/// %(d.Output.Param["A"]) | |
/// %(d.Output.PaymentSchedules["2"]MM-dd) | |
/// %(d.Output.Param["1"]) | |
/// | |
/// formatting sequence must use )) to escape ) | |
/// indexer must use ]] to escape ] | |
/// joiner must use }} to escape } | |
/// furthermore joiner may use following special strings: <CR><LF><TAB> | |
/// </summary> | |
/// <typeparam name="T">The type of the concrete implementation</typeparam> | |
public abstract class ExpressionResolver<T> | |
where T : ExpressionResolver<T> | |
{ | |
/// <summary> | |
/// Data-sets. | |
/// </summary> | |
public Dictionary<string, object> DataSources { get; protected set; } | |
/// <summary> | |
/// FALSE to avoid exception throwing and displaying default value when dataset referenced by the expression is NULL. | |
/// Default: TRUE; | |
/// </summary> | |
public bool ThrowExceptionOnNullDataSet { get; set; } | |
/// <summary> | |
/// When no exception is thrown on referencing a non-defined dataset, hen this value should be displayed. | |
/// A single parameter {0} can be used to display the current NULL dataset name. | |
/// </summary> | |
public string DisplayValueForNullDataSet { get; set; } | |
/// <summary> | |
/// FALSE to avoid exception throwing and displaying default value when object inside expression is NULL. | |
/// Default: FALSE; | |
/// </summary> | |
public bool ThrowExceptionOnNullObject { get; set; } | |
/// <summary> | |
/// When no exception is thrown on null object inside expression, then this value should be displayed. | |
/// A single parameter {0} can be used to display the expression to the current NULL object. | |
/// </summary> | |
public string DisplayValueForNullObject { get; set; } | |
/// <summary> | |
/// FALSE to avoid exception throwing and displaying default value. | |
/// Default: TRUE; | |
/// </summary> | |
public bool ThrowExceptionOnPropertyNotFound { get; set; } | |
/// <summary> | |
/// When no exception is thrown on property not found, this value is displayed. | |
/// Parameter {0} can be used to refers to the property name, parameter {1} to refer to the object where property should be available. | |
/// </summary> | |
public string DisplayValueForPropertyNotFound { get; set; } | |
/// <summary> | |
/// Display string for NULL properties. | |
/// </summary> | |
public string DisplayValueForNullProperty { get; set; } | |
/// <summary> | |
/// ctor. | |
/// </summary> | |
public ExpressionResolver() | |
{ | |
// set initial values | |
ThrowExceptionOnNullDataSet = true; | |
DisplayValueForNullDataSet = "ERROR: dataset {0} not found!"; | |
ThrowExceptionOnNullObject = false; | |
DisplayValueForNullObject = ""; | |
ThrowExceptionOnPropertyNotFound = true; | |
DisplayValueForPropertyNotFound = "ERROR: property {0} on object {1} not found!"; | |
DisplayValueForNullProperty = ""; | |
DataSources = new Dictionary<string, object>(); | |
} | |
/// <summary> | |
/// Ads a single data-set to the report. | |
/// Previously set data-sets will be cleared. | |
/// </summary> | |
/// <param name="key">The name which is used to reference the dataset</param> | |
/// <param name="data">The data object</param> | |
/// <returns></returns> | |
public T SetSingleDataSource(string key, object data) | |
{ | |
DataSources.Clear(); | |
DataSources.Add(key, data); | |
return (T)this; | |
} | |
/// <summary> | |
/// Sets multiple data-sets to the report. | |
/// Previously set data-sets will be cleared. | |
/// </summary> | |
/// <param name="dataSources"></param> | |
/// <returns></returns> | |
public T SetDataSources(IDictionary<string, object> dataSources) | |
{ | |
DataSources.Clear(); | |
foreach (var ds in dataSources) | |
{ | |
DataSources.Add(ds.Key, ds.Value); | |
} | |
return (T)this; | |
} | |
public abstract T ProcessCustomExpressions(); | |
/// <summary> | |
/// REGEX match described: | |
/// _ (one or more time) and any alpha-numeric without underscore (At least once) then any alpha-numeric w. underscore may occure any time | |
/// or | |
/// alpha (one time) then any times any alpha-numeric w. underscore may occure | |
/// | |
/// EXPLANATION: | |
/// \w - all alphanumeric and _ | |
/// \W is [^\w] | |
/// [^\W_] - will match all alphanumeric, except _ (NOT (not alphanumeric or _)) | |
/// [^\W0-9_] - will match all alphanumeric, except 0-9 and _ (NOT (not alphanumeric or 0-9 or _)) | |
/// </summary> | |
private const string identifierRegex = @"(?:_+[^\W_]\w*|[^\W0-9_]+\w*)"; | |
private const string propertyExpressionRegex = | |
@"(?<propertyExpression>" + | |
identifierRegex + | |
@"(?:\." + identifierRegex + @")*" + | |
@")"; | |
private const string formatExpressionRegexWithPercentEscaping = @" | |
(?<formatter> | |
(?: | |
\)\) # match the ESCAPED % | |
| # OR | |
[^\)] # match anything, except ending character | |
)* | |
)"; | |
private const string formatExpressionRegexOptionalWithPercentEscaping = formatExpressionRegexWithPercentEscaping + @"?"; | |
private const string indexerExpressionRegexWithEscaping = @" | |
(?:\[) # match starting sequence | |
(?<indexer> | |
(?: | |
]] # match the ESCAPED char | |
| # OR | |
[^\]] # match anything, except ending character | |
)* | |
) | |
(?:]) # match ending sequence"; | |
private const string joinExpressionRegexWithEscaping = @" | |
(?:{) # match starting sequence | |
(?<joiner> | |
(?: | |
}} # match the ESCAPED char | |
| # OR | |
[^}] # match anything, except ending character | |
)* | |
) | |
(?:}) # match ending sequence"; | |
/// <summary> | |
/// OUR REGEX SYNTAX: | |
/// ----------------- | |
/// =%propertyNamesWithDelimiter | |
/// ends with : | |
/// % | |
/// or |formatExpression% | |
/// or [indexer]formatExpression% | |
/// or {joiner}formatExpression% | |
/// </summary> | |
private const string rdlcExpressionRegex = @" | |
(?:%\( # match starting sequence" + "\r\n" + | |
propertyExpressionRegex + "\r\n" + | |
@"( # ends with" + "\r\n" + | |
@"(?:\|" + formatExpressionRegexWithPercentEscaping + @")" + | |
@"|" + | |
@"(" + indexerExpressionRegexWithEscaping + formatExpressionRegexOptionalWithPercentEscaping + ")" + | |
@"|" + | |
@"(" + joinExpressionRegexWithEscaping + formatExpressionRegexOptionalWithPercentEscaping + ")" + | |
@")? # ends with must be present one or zero time" + "\r\n" + | |
@"\)) # match end-sequence"; | |
private static Regex rdlcRegex = new Regex(rdlcExpressionRegex, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); | |
private object GetPropertyValue(string propertyExpression) | |
{ | |
// initialize | |
var propertyNames = propertyExpression.Split('.'); | |
var processedExpression = new StringBuilder(); | |
// process 1st part of expression (the data-set) | |
var propertyNameForDataset = propertyNames.Length > 0 | |
? propertyNames[0] | |
: propertyExpression; | |
if (!DataSources.ContainsKey(propertyNameForDataset)) | |
throw new NullReferenceException(string.Format("Dataset: {0} was not found inside the collection!", propertyNameForDataset)); | |
var ds = DataSources[propertyNameForDataset]; | |
if (ds == null) | |
{ | |
if (ThrowExceptionOnNullDataSet) | |
throw new NullReferenceException(string.Format("Dataset: {0} is NULL!", propertyNameForDataset)); | |
return DisplayValueForNullDataSet | |
.Replace("{0}", propertyNameForDataset); | |
} | |
processedExpression.Append(propertyNameForDataset); | |
// now process rest of the expression (object hierarchy)... | |
PropertyInfo property; | |
var currentObject = ds; | |
for (int index = 1; index < propertyNames.Length; index++) | |
{ | |
var currentPropertyName = propertyNames[index]; | |
// NOTE: first true result may come only after the 1st iteration | |
if (currentObject == null) | |
{ | |
if (ThrowExceptionOnNullObject) | |
throw new NullReferenceException(string.Format("Null value for object {0}", processedExpression)); | |
return DisplayValueForNullObject | |
.Replace("{0}", processedExpression.ToString()); | |
} | |
// get the current object's property with the desired (currentPropertyName) name | |
property = currentObject.GetType().GetProperty(currentPropertyName); | |
if (property == null) | |
{ | |
if (ThrowExceptionOnPropertyNotFound) | |
throw new Exception( | |
string.Format("Property: {0} not found for object o.{1}", currentPropertyName, processedExpression) | |
); | |
return DisplayValueForPropertyNotFound | |
.Replace("{0}", currentPropertyName) | |
.Replace("{1}", processedExpression.ToString()); | |
} | |
// get value for the current property | |
// and use that value as the new current object | |
currentObject = property.GetValue(currentObject, null); | |
// also extend processed expression | |
processedExpression | |
.Append(".") | |
.Append(currentPropertyName); | |
} | |
return currentObject; | |
} | |
private string FormatValue(object v, string formatter) | |
{ | |
if (string.IsNullOrWhiteSpace(formatter)) | |
{ | |
// no formatter | |
if (v == null) | |
return DisplayValueForNullProperty; | |
return v.ToString(); | |
} | |
else | |
{ | |
// with formatter | |
return string.Format("{0:" + formatter + "}", v); | |
} | |
} | |
/// <summary> | |
/// Evaluates expression, replaces expression text with the evaluated value. | |
/// </summary> | |
/// <param name="text"></param> | |
/// <returns></returns> | |
protected string EvaluateText(string text) | |
{ | |
// Store original value | |
var originalValue = text; | |
// If nothing to check | |
if (string.IsNullOrWhiteSpace(originalValue)) | |
return originalValue; | |
// Only process items, where the value is FULLY matched! | |
var matches = rdlcRegex.Matches(originalValue); | |
var nextUnprocessedIndex = 0; | |
var newValue = new StringBuilder(); | |
foreach (Match match in matches) | |
{ // ### start foreach ### | |
// paste text between matches | |
if (nextUnprocessedIndex < match.Index) | |
{ | |
var length = match.Index - nextUnprocessedIndex; | |
newValue.Append(originalValue.Substring(nextUnprocessedIndex, length)); | |
} | |
// update unprocessed index | |
nextUnprocessedIndex = match.Index + match.Length; | |
// get expression details | |
var propExpr = match.Groups["propertyExpression"].Value; | |
var formatter = match.Groups["formatter"].Value; | |
var indexer = match.Groups["indexer"].Value; | |
var joiner = match.Groups["joiner"].Value; | |
// since formatter is used with string.Format, we must escape the characters { and } | |
formatter = formatter | |
.Replace("{", "{{") | |
.Replace("}", "}}"); | |
// get property details | |
var o = GetPropertyValue(propExpr); | |
// handle indexers as 1st | |
if (!string.IsNullOrWhiteSpace(indexer)) | |
{ | |
// NOTE: - because .net report viewer uses [] as a placeholder, we have to use [""] | |
indexer = indexer.Trim('"'); | |
// For IDictionary, Hashtable, IList, IDictionary<T, K>, Hashtable<T, K>, Ilist<T> there is an indexer property called Item! | |
// Problem may occure, if there are more indexers with only differences in the indexing type (list has an indexer for int, hashtable for string, IDictionary<T,K> for T)! | |
// NOTE: Assume there's only a single indexer - otherise we will get an AmbiguousMatchException! | |
var oType = o.GetType(); | |
var isList = o is IList; | |
var isIDictionary = o is IDictionary; | |
var isHashtable = typeof(Hashtable).IsAssignableFrom(oType); // NOTE: o is Hashtable is always FALSE | |
var propIndexer = o.GetType().GetProperty("Item"); | |
var hasIndexer = propIndexer != null && propIndexer.GetIndexParameters().Length > 0; | |
var isIListT = false; | |
var isIDictionaryTK = false; | |
if (oType.IsGenericType) | |
{ | |
if (typeof(IList<>).IsAssignableFrom(oType.GetGenericTypeDefinition())) isIListT = true; | |
if (typeof(IDictionary<,>).IsAssignableFrom(oType.GetGenericTypeDefinition())) isIDictionaryTK = true; | |
} | |
var isIndexedCollection = hasIndexer || isList || isIDictionary || isHashtable || isIListT || isIDictionaryTK; | |
// check object type against expression | |
if (!isIndexedCollection) | |
throw new Exception(string.Format("Indexer expression can be used only with IList/IList<T>/IDictionsry/IDictionary<T,K>/Hashtable/or objects implementing an indexer property! Exception at evaluating: {0}", propExpr)); | |
// now use indexer to fetch our value (note: indexing is supported only for string/int/long) | |
var indexParams = propIndexer.GetIndexParameters(); | |
if (indexParams.Length != 1) | |
throw new Exception(string.Format("Null or multiple index parameter found for indexer! Exception at evaluating: {0}", propExpr)); | |
var indexParam0Type = indexParams[0].ParameterType; | |
object v = null; | |
if (indexParam0Type == typeof(string)) | |
{ | |
v = propIndexer.GetValue(o, new object[] { indexer }); | |
} | |
else if (indexParam0Type == typeof(int)) | |
{ | |
var intIndexer = 0; | |
var intSuccess = int.TryParse(indexer, out intIndexer); | |
if (intSuccess) | |
v = propIndexer.GetValue(o, new object[] { intIndexer }); | |
else | |
throw new Exception(string.Format("Indexer for expression: {0} must be type of: {1}", propExpr, indexParam0Type)); | |
} | |
else if (indexParam0Type == typeof(long)) | |
{ | |
var longIndexer = 0; | |
var longSuccess = int.TryParse(indexer, out longIndexer); | |
if (longSuccess) | |
v = propIndexer.GetValue(o, new object[] { longIndexer }); | |
else | |
throw new Exception(string.Format("Indexer for expression: {0} must be type of: {1}", propExpr, indexParam0Type)); | |
} | |
else | |
{ | |
throw new NotSupportedException(string.Format("Only string/int/long indexers are supported. Exception at evaluating: {0}", propExpr)); | |
} | |
// set new value | |
newValue.Append(FormatValue(v, formatter)); | |
// process next match | |
continue; | |
} | |
// handle ienumerables as 2nd (most indexers are ienumerable) | |
if (!string.IsNullOrWhiteSpace(joiner)) | |
{ | |
// Fortunatelly IDictionary<T,K>, IList<T>, IList, IEnumerable<T> are all descendants of IEnumerable. | |
var isEnumerable = o is IEnumerable; | |
if (!isEnumerable) | |
throw new Exception(string.Format("Joiner expression can be used only with IEnumerable objects! Exception at evaluating: {0}", propExpr)); | |
// handle special sub-strings | |
joiner = joiner | |
.Replace("<CR>", "\r") | |
.Replace("<LF>", "\n") | |
.Replace("<TAB>", "\t"); | |
// convert to IEnumerable and process | |
var enumerableObject = (IEnumerable)o; | |
var firstItem = true; | |
var sb = new StringBuilder(); | |
foreach (var v in enumerableObject) | |
{ | |
if (firstItem) | |
{ | |
// before first item there's no delimiter | |
firstItem = false; | |
} | |
else | |
{ | |
// from the 2nd item on we will add delimiter | |
sb.Append(joiner); | |
} | |
// add formatted value | |
sb.Append(FormatValue(v, formatter)); | |
} | |
// set new value for node | |
newValue.Append(sb.ToString()); | |
// process next match | |
continue; | |
} | |
// simple object | |
newValue.Append(FormatValue(o, formatter)); | |
// process next match | |
continue; | |
} // ### end foreach ### | |
// after processing matches - append rest of the original string after last match (suffix) | |
if (originalValue.Length >= nextUnprocessedIndex) | |
{ | |
var suffixLength = originalValue.Length - nextUnprocessedIndex; | |
newValue.Append(originalValue.Substring(nextUnprocessedIndex, suffixLength)); | |
} | |
// FINALLY: return new value | |
return newValue.ToString(); | |
} | |
} | |
// | |
// | |
// | |
public class ExpressionResolverForXml : ExpressionResolver<ExpressionResolverForXml> | |
{ | |
private XmlDocument OriginalXmlDoc { get; set; } | |
/// <summary> | |
/// The resulting XML document. | |
/// Until processing, it's instantiated with the original XmlDocument contents. | |
/// </summary> | |
public XmlDocument ResultXmlDoc { get; set; } | |
/// <summary> | |
/// ctor. | |
/// </summary> | |
/// <param name="inputRdlcStream"></param> | |
public ExpressionResolverForXml(Stream inputRdlcStream) | |
: base() | |
{ | |
// load original RDLC xml, keep original as a copy for multiple processing (do not modify it) | |
OriginalXmlDoc = new XmlDocument(); | |
OriginalXmlDoc.Load(inputRdlcStream); | |
// set original as processed... | |
ResultXmlDoc = (XmlDocument)OriginalXmlDoc.Clone(); | |
} | |
/// <summary> | |
/// Processes custom expressions in the supported stream (original xml document) and replaces them with values from the supported data-sources. | |
/// Without calling this method, the original document will be returned. | |
/// </summary> | |
/// <returns></returns> | |
public override ExpressionResolverForXml ProcessCustomExpressions() | |
{ | |
// copy source to processed | |
ResultXmlDoc = (XmlDocument)OriginalXmlDoc.Clone(); | |
// parse and modify processed RDLC xml | |
if (ResultXmlDoc.HasChildNodes) | |
ProcessNodes(ResultXmlDoc.ChildNodes); | |
else | |
ProcessNode(ResultXmlDoc); | |
// fluent IF | |
return this; | |
} | |
/// <summary> | |
/// Saves the processed XML to a stream. | |
/// </summary> | |
/// <returns></returns> | |
public void Save(Stream output) | |
{ | |
ResultXmlDoc.Save(output); | |
} | |
/// <summary> | |
/// Returns the processed XML as a byte array. | |
/// </summary> | |
/// <returns></returns> | |
public byte[] ToBytes() | |
{ | |
using (var stream = new MemoryStream()) | |
{ | |
Save(stream); | |
return stream.ToArray(); | |
} | |
} | |
/// <summary> | |
/// Returns the processed XML as string. | |
/// </summary> | |
/// <returns></returns> | |
public override string ToString() | |
{ | |
return Encoding.UTF8.GetString(ToBytes()); | |
} | |
private void ProcessNodes(XmlNodeList xmlNodeList) | |
{ | |
foreach (XmlNode xmlNode in xmlNodeList) | |
{ | |
ProcessNode(xmlNode); | |
} | |
} | |
private void ProcessNode(XmlNode xmlNode) | |
{ | |
// process current node | |
OnNodeFound(xmlNode); | |
// process node attributes | |
if (xmlNode.Attributes != null) | |
{ | |
var xmlNodeAttributeCollection = xmlNode.Attributes; | |
for (int i = 0; i < xmlNodeAttributeCollection.Count; i++) | |
{ | |
var xmlAttr = xmlNodeAttributeCollection[i]; | |
OnNodeFound(xmlAttr); | |
} | |
} | |
// recurse other nodes | |
if (xmlNode.HasChildNodes) | |
ProcessNodes(xmlNode.ChildNodes); | |
} | |
private void OnNodeFound(XmlNode node) | |
{ | |
if (!string.IsNullOrWhiteSpace(node.Value)) | |
{ | |
var value = EvaluateText(node.Value); | |
node.Value = value; | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Notation:
Escaping:
Beside that such complex expressions had to be parsed, we had to escape ) with )) inside the whole expression, ] with ]] inside the indexing expression, and } with }} inside the separator expression.
Input:
To the instance of ExpressionResolverForXml DataSources has to be added (f.e. with key "d" as data). Then by calling ProcessCustomExpressions() the expressions inside the XML will be parsed and replaced by real values.