2013-04-16 00:07:58 +00:00
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
{
2013-04-16 00:08:06 +00:00
private static PatternStyle _patternStyle ;
2013-04-16 00:07:58 +00:00
public static bool RequireAllExpansions { get ; set ; }
public static Func < string , string > DefaultExpansionFactory { get ; set ; }
static Expansive ( )
{
Initialize ( ) ;
}
public static string Expand ( this string source )
{
return source . Expand ( DefaultExpansionFactory ) ;
}
public static string Expand ( this string source , params string [ ] args )
{
var output = source ;
var tokens = new List < string > ( ) ;
2013-04-16 00:08:06 +00:00
var pattern = new Regex ( _patternStyle . TokenMatchPattern , RegexOptions . IgnoreCase ) ;
2013-04-16 00:07:58 +00:00
var calls = new Stack < string > ( ) ;
string callingToken = null ;
while ( pattern . IsMatch ( output ) )
{
foreach ( Match match in pattern . Matches ( output ) )
{
2013-04-16 00:08:06 +00:00
var token = _patternStyle . TokenReplaceFilter ( match . Value ) ;
2013-04-16 00:07:58 +00:00
var tokenIndex = 0 ;
if ( ! tokens . Contains ( token ) )
{
tokens . Add ( token ) ;
tokenIndex = tokens . Count - 1 ;
}
else
{
tokenIndex = tokens . IndexOf ( token ) ;
}
2013-04-16 00:08:06 +00:00
output = Regex . Replace ( output , _patternStyle . OutputFilter ( match . Value ) , "{" + tokenIndex + "}" ) ;
2013-04-16 00:07:58 +00:00
}
}
var newArgs = new List < string > ( ) ;
foreach ( var arg in args )
{
var newArg = arg ;
2013-04-16 00:08:06 +00:00
var tokenPattern = new Regex ( _patternStyle . TokenFilter ( String . Join ( "|" , tokens ) ) ) ;
2013-04-16 00:07:58 +00:00
while ( tokenPattern . IsMatch ( newArg ) )
{
foreach ( Match match in tokenPattern . Matches ( newArg ) )
{
2013-04-16 00:08:06 +00:00
var token = _patternStyle . TokenReplaceFilter ( match . Value ) ;
2013-04-16 00:07:58 +00:00
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 ;
2013-04-16 00:08:06 +00:00
newArg = Regex . Replace ( newArg , _patternStyle . OutputFilter ( match . Value ) , args [ tokens . IndexOf ( token ) ] ) ;
2013-04-16 00:07:58 +00:00
}
}
newArgs . Add ( newArg ) ;
}
return string . Format ( output , newArgs . ToArray ( ) ) ;
}
public static string Expand ( this string source , Func < string , string > expansionFactory )
{
2013-04-16 00:08:06 +00:00
return source . ExpandInternal ( expansionFactory ) ;
2013-04-16 00:07:58 +00:00
}
2013-04-16 00:08:06 +00:00
public static string Expand ( this string source , object model )
2013-04-16 00:07:58 +00:00
{
return source . ExpandInternal (
name = >
{
IDictionary < string , object > modelDict = model . ToDictionary ( ) ;
if ( RequireAllExpansions & & ! modelDict . ContainsKey ( name ) )
{
return "" ;
}
if ( modelDict [ name ] = = null )
{
return "" ;
}
return modelDict [ name ] . ToString ( ) ;
2013-04-16 00:08:06 +00:00
} ) ;
2013-04-16 00:07:58 +00:00
}
private static void Initialize ( )
{
2013-04-16 00:08:06 +00:00
_patternStyle = new PatternStyle
2013-04-16 00:07:58 +00:00
{
2013-04-16 00:08:06 +00:00
TokenMatchPattern = @"\{[a-zA-Z]\w*\}" ,
TokenReplaceFilter = token = > token . Replace ( "{" , "" ) . Replace ( "}" , "" ) ,
OutputFilter = output = > ( output . StartsWith ( "{" ) & & output . EndsWith ( "}" ) ? output : @"\{" + output + @"\}" ) ,
TokenFilter = tokens = > "{(" + tokens + ")}"
2013-04-16 00:07:58 +00:00
} ;
}
2013-04-16 00:08:06 +00:00
private static string ExpandInternal ( this string source , Func < string , string > expansionFactory )
2013-04-16 00:07:58 +00:00
{
if ( expansionFactory = = null ) throw new ApplicationException ( "ExpansionFactory not defined.\nDefine a DefaultExpansionFactory or call Expand(source, Func<string, string> expansionFactory))" ) ;
2013-04-16 00:08:06 +00:00
var pattern = new Regex ( _patternStyle . TokenMatchPattern , RegexOptions . IgnoreCase ) ;
2013-04-16 00:07:58 +00:00
var callTreeParent = new Tree < string > ( "root" ) . Root ;
2013-04-16 00:08:06 +00:00
return source . Explode ( pattern , _patternStyle , expansionFactory , callTreeParent ) ;
2013-04-16 00:07:58 +00:00
}
private static string Explode ( this string source , Regex pattern , PatternStyle patternStyle , Func < string , string > expansionFactory , TreeNode < string > 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 ) ;
}
/// <summary>
/// Turns the object into an ExpandoObject
/// </summary>
private static dynamic ToExpando ( this object o )
{
var result = new ExpandoObject ( ) ;
var d = result as IDictionary < string , object > ; //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 < string > ( ) . Select ( key = > new KeyValuePair < string , object > ( 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 ;
}
/// <summary>
/// Turns the object into a Dictionary
/// </summary>
private static IDictionary < string , object > ToDictionary ( this object thingy )
{
return ( IDictionary < string , object > ) thingy . ToExpando ( ) ;
}
}
}