using System;
using System.CodeDom;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Web.Compilation;
using System.Web.Hosting;
using System.Xml;
namespace CSharpVitamins.Compilation
{
///
/// Generates a static class for urls within an ASP.NET project
///
public class HrefBuildProvider : BuildProvider
{
#region Fields
///
/// Path to the web root for determining urls within the site
///
string _basePath;
///
/// Whether to convert urls to lowercase - based on your preference
///
bool _useLowerCaseUrls;
///
/// Whether to include the extension of the file in the name
/// of the member
///
bool _includeExtension;
///
/// Whether to create the root class as 'partial', allowing
/// additional members to be added to the resultant class.
///
bool _makePartial;
///
/// Maximum recurring depth for the project - 1 for top level,
/// 2 for first level of subfolders etc...
///
int _maxDepth = 100;
///
/// Namespace the generated class will be placed within.
/// Leave empty to add to the global namespace e.g. global::Href
///
string _namespace;
///
/// The name of the main class e.g. Href
///
string _className;
///
/// Current depth of recursion
///
int _depth = 0;
///
/// Predicate to determine if a file should be included
///
Predicate isValidFile;
///
/// Predicate to determine if a folder should be included
///
Predicate isValidFolder;
#endregion
#region GenerateCode
///
/// Initialises settings from the BuildProvider file, and
/// generates the appropriate code.
///
///
public override void GenerateCode(AssemblyBuilder assemblyBuilder)
{
// init settings from xml source file
init();
// create root class e.g. global::Href
CodeTypeDeclaration root = createStaticClass(_className, _makePartial);
addSummary(root, "Provides access to urls within the project.");
build(new DirectoryInfo(_basePath), root);
CodeNamespace ns = new CodeNamespace();
ns.Types.Add(root);
// leave blank for global::namespace
if (!string.IsNullOrEmpty(_namespace))
ns.Name = _namespace;
CodeCompileUnit unit = new CodeCompileUnit();
unit.Namespaces.Add(ns);
assemblyBuilder.AddCodeCompileUnit(this, unit);
}
#endregion
#region build
///
/// Iterates over the given directory, adding the files as members to
/// the parent type, then recurse down the folder tree until the max
/// depth is reached.
///
/// The directory to process
/// Parent class to add members too.
void build(DirectoryInfo dir, CodeTypeDeclaration parent)
{
if (_depth >= _maxDepth)
return;
++_depth;
/// 'members' keeps a record of the number of times a
/// member name is repeated within the parent type/class.
/// Pass this to 'ensureUniqueMemberName' to get the name
/// with an index number appended to the end e.g. the second
/// occurrence of "MyProperty" becomes "MyProperty1"
Dictionary members = new Dictionary();
///
/// process files:
/// iterate over files and add the member
/// public const string MyfileName = "~/myfileName.aspx";
///
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
if (isValidFile(file))
{
CodeMemberField field = new CodeMemberField();
field.Name = getName(file);
field.Type = new CodeTypeReference(typeof(string));
field.Attributes = MemberAttributes.Public | MemberAttributes.Const;
field.InitExpression = getInitExpression(getUrl(file));
addSummary(field, getUrl(file, false));
ensureUniqueMemberName(members, field);
parent.Members.Add(field);
}
}
///
/// process subfolders:
/// iterate over folders and add a nested class
///
DirectoryInfo[] subfolders = dir.GetDirectories();
foreach (DirectoryInfo folder in subfolders)
{
if (isValidFolder(folder))
{
CodeTypeDeclaration nested = createStaticClass(getName(folder), false);
addSummary(nested, "Provides access to urls under: {0}", getUrl(folder, false).TrimStart('~'));
ensureUniqueMemberName(members, nested);
build(folder, nested);
/// .ctor will have already been added to members
/// so only add to parent if there are additional
/// members present
if (nested.Members.Count > 1)
parent.Members.Add(nested);
}
}
--_depth;
}
#endregion
#region init
///
/// Initialises settings from the config file.
///
///
///
/// Sample configuration - all nodes and elements are optional, but the file
/// needs a root node to load as an xml document.
///
///
///
///
///
///
///
///
void init()
{
_basePath = HostingEnvironment.ApplicationPhysicalPath;
XmlDocument xml = new XmlDocument();
using (Stream stream = VirtualPathProvider.OpenFile(base.VirtualPath))
xml.Load(stream);
/// code below uses syntax,
/// xml["elementName"] ?? xml.CreateElement( "elementName" );
/// as a lazy initialisation technique, rather than checking for
/// the node and performing alternate initialisation - this keeps
/// it within the same reading context.
///
/// general settings
///
XmlElement settings = xml["settings"] ?? xml.CreateElement("settings");
_maxDepth = int.Parse(getAttributeValue(settings, "maxDepth", "100"));
_useLowerCaseUrls = bool.Parse(getAttributeValue(settings, "lowercaseUrls", "false"));
_includeExtension = bool.Parse(getAttributeValue(settings, "includeExtension", "false"));
_makePartial = bool.Parse(getAttributeValue(settings, "makePartial", "false"));
_namespace = getAttributeValue(settings, "namespace");
_className = getAttributeValue(settings, "className", "Href");
///
/// files settings
///
XmlElement files = settings["files"] ?? xml.CreateElement("files");
isValidFile = createFilter(
getAttributeValue(files, "include", @"\.aspx$"),
getAttributeValue(files, "exclude")
);
///
/// folders settings
///
XmlElement folders = settings["folders"] ?? xml.CreateElement("folders");
isValidFolder = createFilter(
getAttributeValue(folders, "include"),
getAttributeValue(folders, "exclude", "App_|Bin")
);
}
#endregion
#region getAttributeValue
///
///
///
///
///
///
///
static string getAttributeValue(XmlElement parent, string name, string @default)
{
XmlAttribute attribute = parent.Attributes[name];
return null == attribute ? @default : attribute.Value.Trim();
}
///
///
///
///
///
///
static string getAttributeValue(XmlElement parent, string name)
{
return getAttributeValue(parent, name, null);
}
#endregion
#region createFilter
///
/// Creates a Predicate for filtering based on the given regex patterns
///
///
///
///
static Predicate createFilter(string includePattern, string excludePattern)
{
Regex include = null, exclude = null;
if (!string.IsNullOrEmpty(includePattern))
include = new Regex(includePattern,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
if (!string.IsNullOrEmpty(excludePattern))
exclude = new Regex(excludePattern,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
return createFilter(include, exclude);
}
///
/// Creates a Predicate for filtering based on the given regex
///
///
///
///
static Predicate createFilter(Regex include, Regex exclude)
{
if (null != include && null != exclude)
return delegate(FileSystemInfo info) { return include.IsMatch(info.FullName) && !exclude.IsMatch(info.FullName); };
else if (null != include)
return delegate(FileSystemInfo info) { return include.IsMatch(info.FullName); };
else if (null != exclude)
return delegate(FileSystemInfo info) { return !exclude.IsMatch(info.FullName); };
else
return delegate(FileSystemInfo info) { return true; };
}
#endregion
#region getInitExpression
///
/// Returns the given value surrounded with quotes.
///
///
///
static CodeSnippetExpression getInitExpression(string value)
{
return new CodeSnippetExpression(string.Concat("\"", value, "\""));
}
#endregion
#region getName
///
/// Gets the normalised/escaped member name for the given file
///
///
///
string getName(FileSystemInfo file)
{
string name = Path.GetFileNameWithoutExtension(file.Name);
if (_includeExtension)
name += file.Extension.Replace('.', '_');
name = Regex.Replace(name, @"[^a-z0-9_]*", string.Empty,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
// Ensure pascal casing of the name - not really required
//name = TextHelper.PascalCase( name );
// Escape names not starting with a letter
if (name.Length > 0 && !char.IsLetter(name[0]))
name = "_" + name;
// Escape C# keywords
// included in separate library, omitted here for clarity
//name = CodeHelper.CSharp.EscapeWord(name);
return name;
}
#endregion
#region getUrl
///
/// Gets the url of the file, relative to the app root
///
///
///
string getUrl(FileSystemInfo file)
{
return getUrl(file, true);
}
///
/// Gets the url of the file, relative to the app root
///
///
/// When true, allows lowercasing of the
/// url and stripping of default.aspx
///
string getUrl(FileSystemInfo file, bool allowNormalisation)
{
string url = file.FullName.Substring(_basePath.Length);
if (allowNormalisation)
{
if (_useLowerCaseUrls)
url = url.ToLower();
if (url.EndsWith("default.aspx", StringComparison.OrdinalIgnoreCase))
url = url.Substring(0, url.Length - 12);
}
return string.Concat("~/", url.Replace("\\", "/"));
}
#endregion
#region addSummary
///
/// Adds a summary doc comment to the type's comment collection
///
///
///
///
void addSummary(CodeTypeMember type, string format, params object[] args)
{
addSummary(type.Comments, format, args);
}
///
/// Adds a summary doc comment to the collection
///
///
///
///
void addSummary(CodeCommentStatementCollection comments, string format, params object[] args)
{
comments.Add(new CodeCommentStatement("", true));
comments.Add(new CodeCommentStatement(string.Format(format, args), true));
comments.Add(new CodeCommentStatement("", true));
}
#endregion
#region createStaticClass
///
/// Creates a static class type with the given name
///
///
///
///
static CodeTypeDeclaration createStaticClass(string name, bool partial)
{
CodeTypeDeclaration type = new CodeTypeDeclaration(name);
type.TypeAttributes |= System.Reflection.TypeAttributes.Sealed;
type.Attributes = MemberAttributes.Public | MemberAttributes.Static;
type.IsClass = true;
type.IsPartial = partial;
CodeConstructor ctor = new CodeConstructor();
ctor.Attributes = MemberAttributes.Private;
type.Members.Add(ctor);
return type;
}
#endregion
#region ensureUniqueMemberName
///
/// Ensures a unique name within the dictionary to get the name
/// with an index number appended to the end e.g. the second
/// occurrence of "MyProperty" becomes "MyProperty1"
///
/// A caveat of this method is where you may have two members
/// like MyPage.aspx, MyPage.html as well as MyPage1.aspx. A conflict
/// will occur giving duplicate members i.e. in the same order MyPage,
/// MyPage1 and MyPage1 a second time. The likelihood of such conflicts
/// is possible, with one possible solution to recurse using
/// ensureUniqueMemberName with the newly generated name, or enumerate
/// the parent type's Members to see whether there will be a conflict.
///
/// Dictionary containing the name of the members
/// in the current namespace as the key + the number of appearances as
/// the value.
/// The next member to add to the current namespace.
static void ensureUniqueMemberName(Dictionary members, CodeTypeMember type)
{
string key = type.Name;
int count;
if (members.TryGetValue(key, out count))
{
type.Name = string.Concat(key, ++count);
members[key] = count;
}
else
{
members[key] = 0;
}
}
#endregion
}
}