//
-// System.Web.Compilation.AppResourcesCompiler: Support for compilation of .resx files into a satellite assembly
+// System.Web.Compilation.AppResourceFilesCollection
//
// Authors:
// Marek Habersack (grendello@gmail.com)
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
+using System.Collections;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Reflection;
+using System.Resources;
using System.Web;
+using System.Web.Caching;
+using System.Web.Configuration;
+using System.Web.Util;
-namespace System.Web.Compilation
+namespace System.Web.Compilation
{
- internal abstract class AppResourcesCompiler: AppResourceFilesCompiler
+ internal class AppResourcesCompiler
{
- protected string resourceDirName = null;
-
- public AppResourcesCompiler ()
- : base ()
- {}
-
- public AppResourcesCompiler (string [] filePaths)
- : base (filePaths)
- {}
-
- public AppResourcesCompiler (string [] filePaths, FcCodeGenerator fg)
- : base (filePaths, fg)
- {}
-
- public string DynamicDirectory {
- get { return AppDomain.CurrentDomain.DynamicDirectory; }
- }
-
- public string ResourceDirectory {
+ const string cachePrefix = "@@LocalResourcesAssemblies";
+ public const string DefaultCultureKey = ".:!DefaultCulture!:.";
+
+ bool isGlobal;
+ HttpContext context;
+ AppResourceFilesCollection files;
+ string tempDirectory;
+ string virtualPath;
+ Dictionary <string, List <string>> cultureFiles;
+
+ string TempDirectory {
get {
- if (resourceDirName == null)
- return null;
- string dir = Path.Combine (TopPath, resourceDirName);
- if (!Directory.Exists (dir))
- return null;
- return dir;
+ if (tempDirectory != null)
+ return tempDirectory;
+ return (tempDirectory = AppDomain.CurrentDomain.SetupInformation.DynamicBase);
}
}
-
- public bool CompilationPossible {
- get { return (ResourceDirectory != null); }
+
+ public Dictionary <string, List <string>> CultureFiles {
+ get { return cultureFiles; }
}
+
+ public AppResourcesCompiler (HttpContext context)
+ {
+ this.context = context;
+ this.isGlobal = true;
+ this.files = new AppResourceFilesCollection (context);
+ this.cultureFiles = new Dictionary <string, List <string>> ();
+ }
+
+ public AppResourcesCompiler (string virtualPath)
+ {
- public abstract string TopPath {
- get;
+ this.virtualPath = virtualPath;
+ this.isGlobal = false;
+ this.files = new AppResourceFilesCollection (HttpContext.Current.Request.MapPath (virtualPath));
+ this.cultureFiles = new Dictionary <string, List <string>> ();
}
-
- virtual public CompilerResults Compile ()
+
+ public Assembly Compile ()
{
- string dir = ResourceDirectory;
-
- if (dir == null)
+ files.Collect ();
+ if (!files.HasFiles)
return null;
+ if (isGlobal)
+ return CompileGlobal ();
+ else
+ return CompileLocal ();
+ }
+
+ Assembly CompileGlobal ()
+ {
+ string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
+ "App_GlobalResources",
+ "dll",
+ OnCreateRandomFile) as string;
- DirectoryInfo di = new DirectoryInfo (dir);
- if (di == null)
+ if (assemblyPath == null)
+ throw new ApplicationException ("Failed to create global resources assembly");
+
+ List <string>[] fileGroups = GroupGlobalFiles ();
+ if (fileGroups == null || fileGroups.Length == 0)
return null;
+
+ CodeCompileUnit unit = new CodeCompileUnit ();
+ CodeNamespace ns = new CodeNamespace (null);
+ ns.Imports.Add (new CodeNamespaceImport ("System"));
+ ns.Imports.Add (new CodeNamespaceImport ("System.Globalization"));
+ ns.Imports.Add (new CodeNamespaceImport ("System.Reflection"));
+ ns.Imports.Add (new CodeNamespaceImport ("System.Resources"));
+ unit.Namespaces.Add (ns);
- List<string> al = new List<string> ();
- foreach (FileInfo fi in di.GetFiles ("*.resx"))
- al.Add (fi.FullName);
- filePaths = al.ToArray ();
- CodeCompileUnit unit = FilesToDom ();
- if (unit == null || resourceFiles == null)
+ AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_GlobalResources", assemblyPath,
+ this);
+ CodeDomProvider provider = builder.Provider;
+
+ Dictionary <string,bool> assemblies = new Dictionary<string,bool> ();
+ foreach (List<string> ls in fileGroups)
+ DomFromResource (ls [0], unit, assemblies, provider);
+
+ foreach (KeyValuePair<string,bool> de in assemblies)
+ unit.ReferencedAssemblies.Add (de.Key);
+
+ builder.Build (unit);
+ HttpContext.AppGlobalResourcesAssembly = builder.MainAssembly;
+
+ return builder.MainAssembly;
+ }
+
+ Assembly CompileLocal ()
+ {
+ if (String.IsNullOrEmpty (virtualPath))
return null;
+
+ Assembly cached = GetCachedLocalResourcesAssembly (virtualPath);
+ if (cached != null)
+ return cached;
+
+ string prefix;
+ if (virtualPath == "/")
+ prefix = "App_LocalResources.root";
+ else
+ prefix = "App_LocalResources" + virtualPath.Replace ('/', '.');
+
+ string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
+ prefix,
+ "dll",
+ OnCreateRandomFile) as string;
+ if (assemblyPath == null)
+ throw new ApplicationException ("Failed to create local resources assembly");
+
+ List<AppResourceFileInfo> files = this.files.Files;
+ foreach (AppResourceFileInfo arfi in files)
+ GetResourceFile (arfi, true);
+
+ AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_LocalResources", assemblyPath,
+ this);
+ builder.Build ();
+ Assembly ret = builder.MainAssembly;
+
+ if (ret != null)
+ AddAssemblyToCache (virtualPath, ret);
- CompilerParameters cp = new CompilerParameters ();
- foreach (string rf in resourceFiles)
- cp.EmbeddedResources.Add (rf);
- cp.IncludeDebugInformation = false;
- cp.GenerateExecutable = false;
- cp.TreatWarningsAsErrors = false;
- cp.OutputAssembly = GenRandomFileName (TempDir, "dll");
-
- CodeDomProvider provider = GetCodeProvider ();
- StringWriter sw = new StringWriter ();
- provider.GenerateCodeFromCompileUnit (unit, sw, new CodeGeneratorOptions ());
- Console.WriteLine ("Generated code:\n{0}", sw.ToString ());
- CompilerResults ret = provider.CompileAssemblyFromDom (cp, unit);
- Console.WriteLine ("Assembly generated");
-
- if (ret.CompiledAssembly == null) {
- Console.WriteLine ("Failed to compile {0}/*.resx. Errors:", ResourceDirectory);
- foreach (CompilerError ce in ret.Errors)
- Console.WriteLine("{5} {0} ({1} {2}:{3}): {4}", ce.ErrorNumber,
- ce.FileName, ce.Line, ce.Column, ce.ErrorText,
- ce.IsWarning ? "warning" : "error");
+ return ret;
+ }
+
+ internal static Assembly GetCachedLocalResourcesAssembly (string path)
+ {
+ Dictionary <string, Assembly> cache;
+
+ cache = HttpRuntime.InternalCache[cachePrefix] as Dictionary <string, Assembly>;
+ if (cache == null || !cache.ContainsKey (path))
+ return null;
+ return cache [path];
+ }
+
+ void AddAssemblyToCache (string path, Assembly asm)
+ {
+ Cache runtimeCache = HttpRuntime.InternalCache;
+ Dictionary <string, Assembly> cache;
+
+ cache = runtimeCache[cachePrefix] as Dictionary <string, Assembly>;
+ if (cache == null)
+ cache = new Dictionary <string, Assembly> ();
+ cache [path] = asm;
+ runtimeCache.Insert (cachePrefix, cache);
+ }
+
+ uint CountChars (char c, string s)
+ {
+ uint ret = 0;
+ foreach (char ch in s) {
+ if (ch == c)
+ ret++;
}
return ret;
}
- }
- internal sealed class AppGlobalResourcesCompiler: AppResourcesCompiler
- {
- public AppGlobalResourcesCompiler ()
- : base ()
+ string IsFileCultureValid (string fileName)
+ {
+ string tmp = Path.GetFileNameWithoutExtension (fileName);
+ tmp = Path.GetExtension (tmp);
+ if (tmp != null && tmp.Length > 0) {
+ tmp = tmp.Substring (1);
+ try {
+ CultureInfo.GetCultureInfo (tmp);
+ return tmp;
+ } catch {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ string GetResourceFile (AppResourceFileInfo arfi, bool local)
{
- this.resourceDirName = "App_GlobalResources";
+ string resfile;
+ if (arfi.Kind == AppResourceFileKind.ResX)
+ resfile = CompileResource (arfi, local);
+ else
+ resfile = arfi.Info.FullName;
+ if (!String.IsNullOrEmpty (resfile)) {
+ string culture = IsFileCultureValid (resfile);
+ if (culture == null)
+ culture = DefaultCultureKey;
+
+ List <string> cfiles;
+ if (cultureFiles.ContainsKey (culture))
+ cfiles = cultureFiles [culture];
+ else {
+ cfiles = new List <string> (1);
+ cultureFiles [culture] = cfiles;
+ }
+ cfiles.Add (resfile);
+ }
+
+ return resfile;
}
-
- public override string TopPath {
- get { return HttpRuntime.AppDomainAppPath; }
+
+ List <string>[] GroupGlobalFiles ()
+ {
+ List<AppResourceFileInfo> files = this.files.Files;
+ List<List<string>> groups = new List<List<string>> ();
+ AppResourcesLengthComparer<List<string>> lcList = new AppResourcesLengthComparer<List<string>> ();
+
+ string tmp, s, basename;
+ uint basedots, filedots;
+ AppResourceFileInfo defaultFile;
+
+ foreach (AppResourceFileInfo arfi in files) {
+ if (arfi.Kind != AppResourceFileKind.ResX && arfi.Kind != AppResourceFileKind.Resource)
+ continue;
+
+ s = arfi.Info.FullName;
+ basename = Path.GetFileNameWithoutExtension (s);
+ basedots = CountChars ('.', basename);
+ defaultFile = null;
+
+ // If there are any files that start with this baseName, we have a default file
+ foreach (AppResourceFileInfo fi in files) {
+ if (fi.Seen)
+ continue;
+
+ string s2 = fi.Info.FullName;
+ if (s2 == null || s == s2)
+ continue;
+ tmp = Path.GetFileNameWithoutExtension (s2);
+ filedots = CountChars ('.', tmp);
+
+ if (filedots == basedots + 1 && tmp.StartsWith (basename)) {
+ if (IsFileCultureValid (s2) != null) {
+ // A valid translated file for this name
+ defaultFile = arfi;
+ break;
+ } else {
+ // This file shares the base name, but the culture is invalid - we must
+ // ignore it since the name of the generated strongly typed class for this
+ // resource will clash with the one generated from the default file with
+ // the given basename.
+ fi.Seen = true;
+ }
+ }
+ }
+ if (defaultFile != null) {
+ List<string> al = new List<string> ();
+ al.Add (GetResourceFile (arfi, false));
+ arfi.Seen = true;
+ groups.Add (al);
+
+ }
+ }
+ groups.Sort (lcList);
+
+ string tmp2;
+ // Now find their translated counterparts
+ foreach (List<string> al in groups) {
+ s = al [0];
+ tmp = Path.GetFileNameWithoutExtension (s);
+ if (tmp.StartsWith ("Resources."))
+ tmp = tmp.Substring (10);
+ foreach (AppResourceFileInfo arfi in files) {
+ if (arfi.Seen)
+ continue;
+ s = arfi.Info.FullName;
+ if (s == null)
+ continue;
+ tmp2 = arfi.Info.Name;
+ if (tmp2.StartsWith (tmp)) {
+ al.Add (GetResourceFile (arfi, false));
+ arfi.Seen = true;
+ }
+ }
+ }
+
+ // Anything that's left here might be orphans or lone default files.
+ // For those files we check the part following the last dot
+ // before the .resx/.resource extensions and test whether it's a registered
+ // culture or not. If it is not a culture, then we have a
+ // default file that doesn't have any translations. Otherwise,
+ // the file is ignored (it's the same thing MS.NET does)
+ foreach (AppResourceFileInfo arfi in files) {
+ if (arfi.Seen)
+ continue;
+
+ if (IsFileCultureValid (arfi.Info.FullName) != null)
+ continue; // Culture found, we reject the file
+
+ // A single default file, create a group
+ List<string> al = new List<string> ();
+ al.Add (GetResourceFile (arfi, false));
+ groups.Add (al);
+ }
+ groups.Sort (lcList);
+ return groups.ToArray ();
}
- }
- //FIXME: it seems that local resources are NOT strongly typed
- // see http://msdn2.microsoft.com/en-us/library/ms227427.aspx
- //
- // Local resources should probably be only put in a satellite assembly and referenced
- // from there.
- internal sealed class AppLocalResourcesCompiler: AppResourcesCompiler
- {
- public AppLocalResourcesCompiler ()
- : base ()
+ // CodeDOM generation
+ void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary <string,bool> assemblies,
+ CodeDomProvider provider)
{
- this.resourceDirName = "App_LocalResources";
+ if (String.IsNullOrEmpty (resfile))
+ return;
+
+ string fname, nsname, classname;
+
+ fname = Path.GetFileNameWithoutExtension (resfile);
+ nsname = Path.GetFileNameWithoutExtension (fname);
+ classname = Path.GetExtension (fname);
+ if (classname == null || classname.Length == 0) {
+ classname = nsname;
+ nsname = "Resources";
+ } else {
+ if (!nsname.StartsWith ("Resources", StringComparison.InvariantCulture))
+ nsname = String.Concat ("Resources.", nsname);
+ classname = classname.Substring(1);
+ }
+
+ if (!String.IsNullOrEmpty (classname))
+ classname = classname.Replace ('.', '_');
+ if (!String.IsNullOrEmpty (nsname))
+ nsname = nsname.Replace ('.', '_');
+
+ if (!provider.IsValidIdentifier (nsname) || !provider.IsValidIdentifier (classname))
+ throw new ApplicationException ("Invalid resource file name.");
+
+ ResourceReader res;
+ try {
+ res = new ResourceReader (resfile);
+ } catch (ArgumentException) {
+ // invalid stream, probably empty - ignore silently and abort
+ return;
+ }
+
+ CodeNamespace ns = new CodeNamespace (nsname);
+ CodeTypeDeclaration cls = new CodeTypeDeclaration (classname);
+ cls.IsClass = true;
+ cls.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
+
+ CodeMemberField cmf = new CodeMemberField (typeof(CultureInfo), "_culture");
+ cmf.InitExpression = new CodePrimitiveExpression (null);
+ cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
+ cls.Members.Add (cmf);
+
+ cmf = new CodeMemberField (typeof(ResourceManager), "_resourceManager");
+ cmf.InitExpression = new CodePrimitiveExpression (null);
+ cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
+ cls.Members.Add (cmf);
+
+ // Property: ResourceManager
+ CodeMemberProperty cmp = new CodeMemberProperty ();
+ cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
+ cmp.Name = "ResourceManager";
+ cmp.HasGet = true;
+ cmp.Type = new CodeTypeReference (typeof(ResourceManager));
+ CodePropertyResourceManagerGet (cmp.GetStatements, resfile, classname);
+ cls.Members.Add (cmp);
+
+ // Property: Culture
+ cmp = new CodeMemberProperty ();
+ cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
+ cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
+ cmp.Name = "Culture";
+ cmp.HasGet = true;
+ cmp.HasSet = true;
+ cmp.Type = new CodeTypeReference (typeof(CultureInfo));
+ CodePropertyGenericGet (cmp.GetStatements, "_culture", classname);
+ CodePropertyGenericSet (cmp.SetStatements, "_culture", classname);
+ cls.Members.Add (cmp);
+
+ // Add the resource properties
+ Dictionary<string,bool> imports = new Dictionary<string,bool> ();
+ try {
+ foreach (DictionaryEntry de in res) {
+ Type type = de.Value.GetType ();
+
+ if (!imports.ContainsKey (type.Namespace))
+ imports [type.Namespace] = true;
+
+ string asname = new AssemblyName (type.Assembly.FullName).Name;
+ if (!assemblies.ContainsKey (asname))
+ assemblies [asname] = true;
+
+ cmp = new CodeMemberProperty ();
+ cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
+ cmp.Name = SanitizeResourceName ((string)de.Key);
+ cmp.HasGet = true;
+ CodePropertyResourceGet (cmp.GetStatements, (string)de.Key, type, classname);
+ cmp.Type = new CodeTypeReference (type);
+ cls.Members.Add (cmp);
+ }
+ } catch (Exception ex) {
+ throw new ApplicationException ("Failed to compile global resources.", ex);
+ }
+ foreach (KeyValuePair<string,bool> de in imports)
+ ns.Imports.Add (new CodeNamespaceImport(de.Key));
+
+ ns.Types.Add (cls);
+ unit.Namespaces.Add (ns);
}
- public override string TopPath {
- get {
- return Path.GetDirectoryName (
- HttpContext.Current.Request.MapPath (
- HttpContext.Current.Request.CurrentExecutionFilePath));
+ string SanitizeResourceName (string name)
+ {
+ return name.Replace (' ', '_').Replace ('-', '_').Replace ('.', '_');
+ }
+
+ CodeObjectCreateExpression NewResourceManager (string name, string typename)
+ {
+ CodeExpression resname = new CodePrimitiveExpression (name);
+ CodePropertyReferenceExpression asm = new CodePropertyReferenceExpression (
+ new CodeTypeOfExpression (new CodeTypeReference (typename)),
+ "Assembly");
+
+ return new CodeObjectCreateExpression ("System.Resources.ResourceManager",
+ new CodeExpression [] {resname, asm});
+ }
+
+ void CodePropertyResourceManagerGet (CodeStatementCollection csc, string resfile, string typename)
+ {
+ string name = Path.GetFileNameWithoutExtension (resfile);
+ CodeStatement st;
+ CodeExpression exp;
+
+ exp = new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), "_resourceManager");
+ st = new CodeConditionStatement (
+ new CodeBinaryOperatorExpression (
+ exp,
+ CodeBinaryOperatorType.IdentityInequality,
+ new CodePrimitiveExpression (null)),
+ new CodeStatement [] { new CodeMethodReturnStatement (exp) });
+ csc.Add (st);
+
+ st = new CodeAssignStatement (exp, NewResourceManager (name, typename));
+ csc.Add (st);
+ csc.Add (new CodeMethodReturnStatement (exp));
+ }
+
+ void CodePropertyResourceGet (CodeStatementCollection csc, string resname, Type restype, string typename)
+ {
+ CodeStatement st = new CodeVariableDeclarationStatement (
+ typeof (ResourceManager),
+ "rm",
+ new CodePropertyReferenceExpression (
+ new CodeTypeReferenceExpression (typename), "ResourceManager"));
+ csc.Add (st);
+
+ st = new CodeConditionStatement (
+ new CodeBinaryOperatorExpression (
+ new CodeVariableReferenceExpression ("rm"),
+ CodeBinaryOperatorType.IdentityEquality,
+ new CodePrimitiveExpression (null)),
+ new CodeStatement [] { new CodeMethodReturnStatement (new CodePrimitiveExpression (null)) });
+ csc.Add (st);
+
+ bool gotstr = (restype == typeof (string));
+ CodeExpression exp = new CodeMethodInvokeExpression (
+ new CodeVariableReferenceExpression ("rm"),
+ gotstr ? "GetString" : "GetObject",
+ new CodeExpression [] { new CodePrimitiveExpression (resname),
+ new CodeFieldReferenceExpression (
+ new CodeTypeReferenceExpression (typename), "_culture") });
+ st = new CodeVariableDeclarationStatement (
+ restype,
+ "obj",
+ gotstr ? exp : new CodeCastExpression (restype, exp));
+ csc.Add (st);
+ csc.Add (new CodeMethodReturnStatement (new CodeVariableReferenceExpression ("obj")));
+ }
+
+ void CodePropertyGenericGet (CodeStatementCollection csc, string field, string typename)
+ {
+ csc.Add(new CodeMethodReturnStatement (
+ new CodeFieldReferenceExpression (
+ new CodeTypeReferenceExpression (typename), field)));
+ }
+
+ void CodePropertyGenericSet (CodeStatementCollection csc, string field, string typename)
+ {
+ csc.Add(new CodeAssignStatement (
+ new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), field),
+ new CodeVariableReferenceExpression ("value")));
+ }
+
+ string CompileResource (AppResourceFileInfo arfi, bool local)
+ {
+ string path = arfi.Info.FullName;
+ string rname = Path.GetFileNameWithoutExtension (path) + ".resources";
+ if (!local)
+ rname = "Resources." + rname;
+
+ string resource = Path.Combine (TempDirectory, rname);
+ FileStream source = null, destination = null;
+ IResourceReader reader = null;
+ ResourceWriter writer = null;
+
+ try {
+ source = new FileStream (path, FileMode.Open, FileAccess.Read);
+ destination = new FileStream (resource, FileMode.Create, FileAccess.Write);
+ reader = GetReaderForKind (arfi.Kind, source);
+ writer = new ResourceWriter (destination);
+ foreach (DictionaryEntry de in reader) {
+ object val = de.Value;
+ if (val is string)
+ writer.AddResource ((string)de.Key, (string)val);
+ else
+ writer.AddResource ((string)de.Key, val);
+ }
+ } catch (Exception ex) {
+ throw new HttpException ("Failed to compile resource file", ex);
+ } finally {
+ if (reader != null)
+ reader.Close ();
+ else if (source != null)
+ source.Close ();
+ if (writer != null)
+ writer.Close ();
+ else if (destination != null)
+ destination.Close ();
+ }
+
+ return resource;
+ }
+
+ IResourceReader GetReaderForKind (AppResourceFileKind kind, Stream stream)
+ {
+ switch (kind) {
+ case AppResourceFileKind.ResX:
+ return new ResXResourceReader (stream);
+
+ case AppResourceFileKind.Resource:
+ return new ResourceReader (stream);
+
+ default:
+ return null;
}
}
- }
-}
+
+
+ object OnCreateRandomFile (string path)
+ {
+ FileStream f = new FileStream (path, FileMode.CreateNew);
+ f.Close ();
+ return path;
+ }
+ };
+};
#endif