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