// // System.Web.Compilation.AppResourceFilesCollection // // Authors: // Marek Habersack (grendello@gmail.com) // // (C) 2006 Marek Habersack // // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if NET_2_0 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 { internal class AppResourcesCompiler { const string cachePrefix = "@@LocalResourcesAssemblies"; public const string DefaultCultureKey = ".:!DefaultCulture!:."; bool isGlobal; HttpContext context; AppResourceFilesCollection files; string tempDirectory; string virtualPath; Dictionary > cultureFiles; string TempDirectory { get { if (tempDirectory != null) return tempDirectory; return (tempDirectory = AppDomain.CurrentDomain.SetupInformation.DynamicBase); } } public Dictionary > CultureFiles { get { return cultureFiles; } } public AppResourcesCompiler (HttpContext context) { this.context = context; this.isGlobal = true; this.files = new AppResourceFilesCollection (context); this.cultureFiles = new Dictionary > (); } public AppResourcesCompiler (string virtualPath) { this.virtualPath = virtualPath; this.isGlobal = false; this.files = new AppResourceFilesCollection (HttpContext.Current.Request.MapPath (virtualPath)); this.cultureFiles = new Dictionary > (); } public Assembly Compile () { 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; if (assemblyPath == null) throw new ApplicationException ("Failed to create global resources assembly"); List [] 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); AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_GlobalResources", assemblyPath, this); CodeDomProvider provider = builder.Provider; Dictionary assemblies = new Dictionary (); foreach (List ls in fileGroups) DomFromResource (ls [0], unit, assemblies, provider); foreach (KeyValuePair 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 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); return ret; } internal static Assembly GetCachedLocalResourcesAssembly (string path) { Dictionary cache; cache = HttpRuntime.InternalCache[cachePrefix] as Dictionary ; if (cache == null || !cache.ContainsKey (path)) return null; return cache [path]; } void AddAssemblyToCache (string path, Assembly asm) { Cache runtimeCache = HttpRuntime.InternalCache; Dictionary cache; cache = runtimeCache[cachePrefix] as Dictionary ; if (cache == null) cache = new Dictionary (); 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; } 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) { 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 cfiles; if (cultureFiles.ContainsKey (culture)) cfiles = cultureFiles [culture]; else { cfiles = new List (1); cultureFiles [culture] = cfiles; } cfiles.Add (resfile); } return resfile; } List [] GroupGlobalFiles () { List files = this.files.Files; List> groups = new List> (); AppResourcesLengthComparer> lcList = new AppResourcesLengthComparer> (); 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 al = new List (); al.Add (GetResourceFile (arfi, false)); arfi.Seen = true; groups.Add (al); } } groups.Sort (lcList); string tmp2; // Now find their translated counterparts foreach (List 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 al = new List (); al.Add (GetResourceFile (arfi, false)); groups.Add (al); } groups.Sort (lcList); return groups.ToArray (); } // CodeDOM generation void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary assemblies, CodeDomProvider provider) { 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.Format ("Resources.{0}", 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 imports = new Dictionary (); 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 de in imports) ns.Imports.Add (new CodeNamespaceImport(de.Key)); ns.Types.Add (cls); unit.Namespaces.Add (ns); } 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