From 6a5c10a456d939de1421783b71b054cad1b2338e Mon Sep 17 00:00:00 2001 From: Keivan Beigi Date: Mon, 15 Apr 2013 17:07:58 -0700 Subject: [PATCH] added Expansive --- .../Expansive/CircularReferenceException.cs | 12 + NzbDrone.Common/Expansive/Expansive.cs | 272 ++++++++++++++++++ NzbDrone.Common/Expansive/PatternStyle.cs | 12 + NzbDrone.Common/Expansive/Tree.cs | 11 + NzbDrone.Common/Expansive/TreeNode.cs | 85 ++++++ NzbDrone.Common/Expansive/TreeNodeList.cs | 32 +++ NzbDrone.Common/Expansive/license.txt | 2 + NzbDrone.Common/NzbDrone.Common.csproj | 9 + 8 files changed, 435 insertions(+) create mode 100644 NzbDrone.Common/Expansive/CircularReferenceException.cs create mode 100644 NzbDrone.Common/Expansive/Expansive.cs create mode 100644 NzbDrone.Common/Expansive/PatternStyle.cs create mode 100644 NzbDrone.Common/Expansive/Tree.cs create mode 100644 NzbDrone.Common/Expansive/TreeNode.cs create mode 100644 NzbDrone.Common/Expansive/TreeNodeList.cs create mode 100644 NzbDrone.Common/Expansive/license.txt diff --git a/NzbDrone.Common/Expansive/CircularReferenceException.cs b/NzbDrone.Common/Expansive/CircularReferenceException.cs new file mode 100644 index 000000000..93478ad00 --- /dev/null +++ b/NzbDrone.Common/Expansive/CircularReferenceException.cs @@ -0,0 +1,12 @@ +using System; + +namespace NzbDrone.Common.Expansive +{ + public class CircularReferenceException : Exception + { + public CircularReferenceException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Expansive/Expansive.cs b/NzbDrone.Common/Expansive/Expansive.cs new file mode 100644 index 000000000..3f611666a --- /dev/null +++ b/NzbDrone.Common/Expansive/Expansive.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Dynamic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace NzbDrone.Common.Expansive +{ + public static class Expansive + { + private static Dictionary _patternStyles; + + + public static bool RequireAllExpansions { get; set; } + + public static Func DefaultExpansionFactory { get; set; } + + public static TokenStyle DefaultTokenStyle { get; set; } + + static Expansive() + { + Initialize(); + } + + public static string Expand(this string source) + { + return source.Expand(DefaultExpansionFactory); + } + + public static string Expand(this string source, TokenStyle tokenStyle) + { + return source.ExpandInternal(DefaultExpansionFactory, tokenStyle); + } + + public static string Expand(this string source, params string[] args) + { + var output = source; + var tokens = new List(); + var patternStyle = _patternStyles[DefaultTokenStyle]; + var pattern = new Regex(patternStyle.TokenMatchPattern, RegexOptions.IgnoreCase); + var calls = new Stack(); + string callingToken = null; + + while (pattern.IsMatch(output)) + { + foreach (Match match in pattern.Matches(output)) + { + var token = patternStyle.TokenReplaceFilter(match.Value); + var tokenIndex = 0; + if (!tokens.Contains(token)) + { + tokens.Add(token); + tokenIndex = tokens.Count - 1; + } + else + { + tokenIndex = tokens.IndexOf(token); + } + output = Regex.Replace(output, patternStyle.OutputFilter(match.Value), "{" + tokenIndex + "}"); + } + } + var newArgs = new List(); + foreach (var arg in args) + { + var newArg = arg; + var tokenPattern = new Regex(patternStyle.TokenFilter(String.Join("|", tokens))); + while (tokenPattern.IsMatch(newArg)) + { + foreach (Match match in tokenPattern.Matches(newArg)) + { + var token = patternStyle.TokenReplaceFilter(match.Value); + if (calls.Contains(string.Format("{0}:{1}", callingToken, token))) throw new CircularReferenceException(string.Format("Circular Reference Detected for token '{0}'.", callingToken)); + calls.Push(string.Format("{0}:{1}", callingToken, token)); + callingToken = token; + newArg = Regex.Replace(newArg, patternStyle.OutputFilter(match.Value), args[tokens.IndexOf(token)]); + } + + } + newArgs.Add(newArg); + } + return string.Format(output, newArgs.ToArray()); + } + + public static string Expand(this string source, Func expansionFactory) + { + return source.ExpandInternal(expansionFactory, DefaultTokenStyle); + } + + public static string Expand(this string source, Func expansionFactory, TokenStyle tokenStyle) + { + return source.ExpandInternal(expansionFactory, tokenStyle); + } + + public static string Expand(this string source, object model) + { + return source.Expand(model, DefaultTokenStyle); + } + + public static string Expand(this string source, params object[] models) + { + var mergedModel = new ExpandoObject().ToDictionary(); + models.ToList().ForEach(m => + { + var md = m.ToDictionary(); + var keys = md.Keys; + keys.ToList().ForEach(k => + { + if (!mergedModel.ContainsKey(k)) + { + mergedModel.Add(k, md[k]); + } + }); + }); + return source.Expand(mergedModel as ExpandoObject); + } + + public static string Expand(this string source, object model, TokenStyle tokenStyle) + { + return source.ExpandInternal( + name => + { + IDictionary modelDict = model.ToDictionary(); + if (RequireAllExpansions && !modelDict.ContainsKey(name)) + { + return ""; + } + + if (modelDict[name] == null) + { + return ""; + } + + return modelDict[name].ToString(); + } + , tokenStyle); + } + + #region : Private Helper Methods : + + private static void Initialize() + { + DefaultTokenStyle = TokenStyle.MvcRoute; + _patternStyles = new Dictionary + { + { + TokenStyle.MvcRoute, new PatternStyle + { + TokenMatchPattern = @"\{[a-zA-Z]\w*\}", + TokenReplaceFilter = token => token.Replace("{", "").Replace("}", ""), + OutputFilter = output => (output.StartsWith("{") && output.EndsWith("}") ? output : @"\{" + output + @"\}"), + TokenFilter = tokens => "{(" + tokens + ")}" + } + } + , + { + TokenStyle.Razor, new PatternStyle + { + TokenMatchPattern = @"@([a-zA-Z]\w*|\([a-zA-Z]\w*\))", + TokenReplaceFilter = token => token.Replace("@", "").Replace("(", "").Replace(")", ""), + OutputFilter = output => (output.StartsWith("@") ? output.Replace("(", @"\(").Replace(")",@"\)") : "@" + output.Replace("(", @"\(").Replace(")",@"\)")), + TokenFilter = tokens => @"@(" + tokens + @"|\(" + tokens + @"\))" + } + } + , + { + TokenStyle.NAnt, new PatternStyle + { + TokenMatchPattern = @"\$\{[a-zA-Z]\w*\}", + TokenReplaceFilter = token => token.Replace("${", "").Replace("}", ""), + OutputFilter = output => (output.StartsWith("${") && output.EndsWith("}") ? output.Replace("$",@"\$").Replace("{",@"\{").Replace("}",@"\}") : @"\$\{" + output + @"\}"), + TokenFilter = tokens => @"\$\{(" + tokens + @")\}" + } + } + , + { + TokenStyle.MSBuild, new PatternStyle + { + TokenMatchPattern = @"\$\([a-zA-Z]\w*\)", + TokenReplaceFilter = token => token.Replace("$(", "").Replace(")", ""), + OutputFilter = output => (output.StartsWith("$(") && output.EndsWith(")") ? output.Replace("$",@"\$").Replace("(",@"\(").Replace(")",@"\)") : @"\$\(" + output + @"\)"), + TokenFilter = tokens => @"\$\((" + tokens + @")\)" + } + } + }; + } + + private static string ExpandInternal(this string source, Func expansionFactory, TokenStyle tokenStyle) + { + if (expansionFactory == null) throw new ApplicationException("ExpansionFactory not defined.\nDefine a DefaultExpansionFactory or call Expand(source, Func expansionFactory))"); + + var patternStyle = _patternStyles[tokenStyle]; + var pattern = new Regex(patternStyle.TokenMatchPattern, RegexOptions.IgnoreCase); + + var callTreeParent = new Tree("root").Root; + + return source.Explode(pattern, patternStyle, expansionFactory, callTreeParent); + } + + private static string Explode(this string source, Regex pattern, PatternStyle patternStyle, Func expansionFactory, TreeNode parent) + { + var output = source; + while (output.HasChildren(pattern)) + { + foreach (Match match in pattern.Matches(source)) + { + var child = match.Value; + var token = patternStyle.TokenReplaceFilter(match.Value); + + var thisNode = parent.Children.Add(token); + + // if we have already encountered this token in this call tree, we have a circular reference + if (thisNode.CallTree.Contains(token)) + throw new CircularReferenceException(string.Format("Circular Reference Detected for token '{0}'. Call Tree: {1}->{2}", + token, + String.Join("->", thisNode.CallTree.ToArray().Reverse()), token)); + + // expand this match + var expandedValue = expansionFactory(token); + + // Replace the match with the expanded value + child = Regex.Replace(child, patternStyle.OutputFilter(match.Value), expandedValue); + + // Recursively expand the child until we no longer encounter nested tokens (or hit a circular reference) + child = child.Explode(pattern, patternStyle, expansionFactory, thisNode); + + // finally, replace the match in the output with the fully-expanded value + output = Regex.Replace(output, patternStyle.OutputFilter(match.Value), child); + } + } + return output; + } + + private static bool HasChildren(this string token, Regex pattern) + { + return pattern.IsMatch(token); + } + + /// + /// Turns the object into an ExpandoObject + /// + private static dynamic ToExpando(this object o) + { + var result = new ExpandoObject(); + var d = result as IDictionary; //work with the Expando as a Dictionary + if (o is ExpandoObject) return o; //shouldn't have to... but just in case + if (o is NameValueCollection || o.GetType().IsSubclassOf(typeof(NameValueCollection))) + { + var nv = (NameValueCollection)o; + nv.Cast().Select(key => new KeyValuePair(key, nv[key])).ToList().ForEach(i => d.Add(i)); + } + else + { + var props = o.GetType().GetProperties(); + foreach (var item in props) + { + d.Add(item.Name, item.GetValue(o, null)); + } + } + return result; + } + /// + /// Turns the object into a Dictionary + /// + private static IDictionary ToDictionary(this object thingy) + { + return (IDictionary)thingy.ToExpando(); + } + + #endregion + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Expansive/PatternStyle.cs b/NzbDrone.Common/Expansive/PatternStyle.cs new file mode 100644 index 000000000..0e4f0fcc1 --- /dev/null +++ b/NzbDrone.Common/Expansive/PatternStyle.cs @@ -0,0 +1,12 @@ +using System; + +namespace NzbDrone.Common.Expansive +{ + internal class PatternStyle + { + public string TokenMatchPattern { get; set; } + public Func TokenFilter { get; set; } + public Func TokenReplaceFilter { get; set; } + public Func OutputFilter { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Expansive/Tree.cs b/NzbDrone.Common/Expansive/Tree.cs new file mode 100644 index 000000000..24be6b642 --- /dev/null +++ b/NzbDrone.Common/Expansive/Tree.cs @@ -0,0 +1,11 @@ +namespace NzbDrone.Common.Expansive +{ + internal class Tree : TreeNode + { + public Tree(T rootValue) + : base(rootValue) + { + Value = rootValue; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Expansive/TreeNode.cs b/NzbDrone.Common/Expansive/TreeNode.cs new file mode 100644 index 000000000..d3470072a --- /dev/null +++ b/NzbDrone.Common/Expansive/TreeNode.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; + +namespace NzbDrone.Common.Expansive +{ + internal class TreeNode + { + private List _CallTree; + private TreeNode _Parent; + + public TreeNode(T Value) + { + this.Value = Value; + Parent = null; + Children = new TreeNodeList(this); + _CallTree = new List(); + } + + public TreeNode(T Value, TreeNode Parent) + { + this.Value = Value; + this.Parent = Parent; + Children = new TreeNodeList(this); + _CallTree = new List(); + } + + public TreeNode Parent + { + get { return _Parent; } + set + { + if (value == _Parent) + { + return; + } + + if (_Parent != null) + { + _Parent.Children.Remove(this); + } + + if (value != null && !value.Children.Contains(this)) + { + value.Children.Add(this); + } + + _Parent = value; + } + } + + public TreeNode Root + { + get + { + //return (Parent == null) ? this : Parent.Root; + + TreeNode node = this; + while (node.Parent != null) + { + node = node.Parent; + } + return node; + } + } + + public TreeNodeList Children { get; private set; } + + public List CallTree + { + get + { + _CallTree = new List(); + TreeNode node = this; + while (node.Parent != null) + { + node = node.Parent; + _CallTree.Add(node.Value); + } + return _CallTree; + } + private set { _CallTree = value; } + } + + public T Value { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Expansive/TreeNodeList.cs b/NzbDrone.Common/Expansive/TreeNodeList.cs new file mode 100644 index 000000000..3c92944b2 --- /dev/null +++ b/NzbDrone.Common/Expansive/TreeNodeList.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace NzbDrone.Common.Expansive +{ + internal class TreeNodeList : List> + { + public TreeNode Parent; + + public TreeNodeList(TreeNode Parent) + { + this.Parent = Parent; + } + + public new TreeNode Add(TreeNode Node) + { + base.Add(Node); + Node.Parent = Parent; + return Node; + } + + public TreeNode Add(T Value) + { + return Add(new TreeNode(Value)); + } + + + public override string ToString() + { + return "Count=" + Count.ToString(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Expansive/license.txt b/NzbDrone.Common/Expansive/license.txt new file mode 100644 index 000000000..b48f7ea2f --- /dev/null +++ b/NzbDrone.Common/Expansive/license.txt @@ -0,0 +1,2 @@ +Source: https://github.com/anderly/Expansive +Microsoft Public License (MS-PL): http://opensource.org/licenses/MS-PL \ No newline at end of file diff --git a/NzbDrone.Common/NzbDrone.Common.csproj b/NzbDrone.Common/NzbDrone.Common.csproj index 4b00b4112..de7951f70 100644 --- a/NzbDrone.Common/NzbDrone.Common.csproj +++ b/NzbDrone.Common/NzbDrone.Common.csproj @@ -107,6 +107,12 @@ + + + + + + @@ -152,6 +158,9 @@ + + +