2 // System.Web.Compilation.AppResourceFilesCollection
5 // Marek Habersack (grendello@gmail.com)
7 // (C) 2006 Marek Habersack
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System.CodeDom.Compiler;
34 using System.Collections;
35 using System.Collections.Generic;
36 using System.Globalization;
38 using System.Reflection;
39 using System.Resources;
41 using System.Web.Caching;
42 using System.Web.Configuration;
43 using System.Web.Util;
45 namespace System.Web.Compilation
47 internal class AppResourcesCompiler
49 const string cachePrefix = "@@LocalResourcesAssemblies";
50 public const string DefaultCultureKey = ".:!DefaultCulture!:.";
53 AppResourceFilesCollection files;
56 Dictionary <string, List <string>> cultureFiles;
58 string TempDirectory {
60 if (tempDirectory != null)
62 return (tempDirectory = AppDomain.CurrentDomain.SetupInformation.DynamicBase);
66 public Dictionary <string, List <string>> CultureFiles {
67 get { return cultureFiles; }
70 public AppResourcesCompiler (HttpContext context)
73 this.files = new AppResourceFilesCollection (context);
74 this.cultureFiles = new Dictionary <string, List <string>> ();
77 public AppResourcesCompiler (string virtualPath)
80 this.virtualPath = virtualPath;
81 this.isGlobal = false;
82 this.files = new AppResourceFilesCollection (HttpContext.Current.Request.MapPath (virtualPath));
83 this.cultureFiles = new Dictionary <string, List <string>> ();
86 public Assembly Compile ()
92 return CompileGlobal ();
94 return CompileLocal ();
97 Assembly CompileGlobal ()
99 string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
100 "App_GlobalResources",
102 OnCreateRandomFile) as string;
104 if (assemblyPath == null)
105 throw new ApplicationException ("Failed to create global resources assembly");
107 List <string>[] fileGroups = GroupGlobalFiles ();
108 if (fileGroups == null || fileGroups.Length == 0)
111 CodeCompileUnit unit = new CodeCompileUnit ();
112 CodeNamespace ns = new CodeNamespace (null);
113 ns.Imports.Add (new CodeNamespaceImport ("System"));
114 ns.Imports.Add (new CodeNamespaceImport ("System.Globalization"));
115 ns.Imports.Add (new CodeNamespaceImport ("System.Reflection"));
116 ns.Imports.Add (new CodeNamespaceImport ("System.Resources"));
117 unit.Namespaces.Add (ns);
119 AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_GlobalResources", assemblyPath,
121 CodeDomProvider provider = builder.Provider;
123 Dictionary <string,bool> assemblies = new Dictionary<string,bool> ();
124 foreach (List<string> ls in fileGroups)
125 DomFromResource (ls [0], unit, assemblies, provider);
127 foreach (KeyValuePair<string,bool> de in assemblies)
128 unit.ReferencedAssemblies.Add (de.Key);
130 builder.Build (unit);
131 HttpContext.AppGlobalResourcesAssembly = builder.MainAssembly;
133 return builder.MainAssembly;
136 Assembly CompileLocal ()
138 if (String.IsNullOrEmpty (virtualPath))
141 Assembly cached = GetCachedLocalResourcesAssembly (virtualPath);
146 if (virtualPath == "/")
147 prefix = "App_LocalResources.root";
149 prefix = "App_LocalResources" + virtualPath.Replace ('/', '.');
151 string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
154 OnCreateRandomFile) as string;
155 if (assemblyPath == null)
156 throw new ApplicationException ("Failed to create local resources assembly");
158 List<AppResourceFileInfo> files = this.files.Files;
159 foreach (AppResourceFileInfo arfi in files)
160 GetResourceFile (arfi, true);
162 AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_LocalResources", assemblyPath,
165 Assembly ret = builder.MainAssembly;
168 AddAssemblyToCache (virtualPath, ret);
173 internal static Assembly GetCachedLocalResourcesAssembly (string path)
175 Dictionary <string, Assembly> cache;
177 cache = HttpRuntime.InternalCache[cachePrefix] as Dictionary <string, Assembly>;
178 if (cache == null || !cache.ContainsKey (path))
183 void AddAssemblyToCache (string path, Assembly asm)
185 Cache runtimeCache = HttpRuntime.InternalCache;
186 Dictionary <string, Assembly> cache;
188 cache = runtimeCache[cachePrefix] as Dictionary <string, Assembly>;
190 cache = new Dictionary <string, Assembly> ();
192 runtimeCache.Insert (cachePrefix, cache);
195 uint CountChars (char c, string s)
198 foreach (char ch in s) {
205 string IsFileCultureValid (string fileName)
207 string tmp = Path.GetFileNameWithoutExtension (fileName);
208 tmp = Path.GetExtension (tmp);
209 if (tmp != null && tmp.Length > 0) {
210 tmp = tmp.Substring (1);
212 CultureInfo.GetCultureInfo (tmp);
221 string GetResourceFile (AppResourceFileInfo arfi, bool local)
224 if (arfi.Kind == AppResourceFileKind.ResX)
225 resfile = CompileResource (arfi, local);
227 resfile = arfi.Info.FullName;
228 if (!String.IsNullOrEmpty (resfile)) {
229 string culture = IsFileCultureValid (resfile);
231 culture = DefaultCultureKey;
233 List <string> cfiles;
234 if (cultureFiles.ContainsKey (culture))
235 cfiles = cultureFiles [culture];
237 cfiles = new List <string> (1);
238 cultureFiles [culture] = cfiles;
240 cfiles.Add (resfile);
246 List <string>[] GroupGlobalFiles ()
248 List<AppResourceFileInfo> files = this.files.Files;
249 List<List<string>> groups = new List<List<string>> ();
250 AppResourcesLengthComparer<List<string>> lcList = new AppResourcesLengthComparer<List<string>> ();
252 string tmp, s, basename;
253 uint basedots, filedots;
254 AppResourceFileInfo defaultFile;
256 foreach (AppResourceFileInfo arfi in files) {
257 if (arfi.Kind != AppResourceFileKind.ResX && arfi.Kind != AppResourceFileKind.Resource)
260 s = arfi.Info.FullName;
261 basename = Path.GetFileNameWithoutExtension (s);
262 basedots = CountChars ('.', basename);
265 // If there are any files that start with this baseName, we have a default file
266 foreach (AppResourceFileInfo fi in files) {
270 string s2 = fi.Info.FullName;
271 if (s2 == null || s == s2)
273 tmp = Path.GetFileNameWithoutExtension (s2);
274 filedots = CountChars ('.', tmp);
276 if (filedots == basedots + 1 && tmp.StartsWith (basename)) {
277 if (IsFileCultureValid (s2) != null) {
278 // A valid translated file for this name
282 // This file shares the base name, but the culture is invalid - we must
283 // ignore it since the name of the generated strongly typed class for this
284 // resource will clash with the one generated from the default file with
285 // the given basename.
290 if (defaultFile != null) {
291 List<string> al = new List<string> ();
292 al.Add (GetResourceFile (arfi, false));
298 groups.Sort (lcList);
301 // Now find their translated counterparts
302 foreach (List<string> al in groups) {
304 tmp = Path.GetFileNameWithoutExtension (s);
305 if (tmp.StartsWith ("Resources."))
306 tmp = tmp.Substring (10);
307 foreach (AppResourceFileInfo arfi in files) {
310 s = arfi.Info.FullName;
313 tmp2 = arfi.Info.Name;
314 if (tmp2.StartsWith (tmp)) {
315 al.Add (GetResourceFile (arfi, false));
321 // Anything that's left here might be orphans or lone default files.
322 // For those files we check the part following the last dot
323 // before the .resx/.resource extensions and test whether it's a registered
324 // culture or not. If it is not a culture, then we have a
325 // default file that doesn't have any translations. Otherwise,
326 // the file is ignored (it's the same thing MS.NET does)
327 foreach (AppResourceFileInfo arfi in files) {
331 if (IsFileCultureValid (arfi.Info.FullName) != null)
332 continue; // Culture found, we reject the file
334 // A single default file, create a group
335 List<string> al = new List<string> ();
336 al.Add (GetResourceFile (arfi, false));
339 groups.Sort (lcList);
340 return groups.ToArray ();
343 // CodeDOM generation
344 void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary <string,bool> assemblies,
345 CodeDomProvider provider)
347 if (String.IsNullOrEmpty (resfile))
350 string fname, nsname, classname;
352 fname = Path.GetFileNameWithoutExtension (resfile);
353 nsname = Path.GetFileNameWithoutExtension (fname);
354 classname = Path.GetExtension (fname);
355 if (classname == null || classname.Length == 0) {
357 nsname = "Resources";
359 if (!nsname.StartsWith ("Resources", StringComparison.InvariantCulture))
360 nsname = String.Concat ("Resources.", nsname);
361 classname = classname.Substring(1);
364 if (!String.IsNullOrEmpty (classname))
365 classname = classname.Replace ('.', '_');
366 if (!String.IsNullOrEmpty (nsname))
367 nsname = nsname.Replace ('.', '_');
369 if (!provider.IsValidIdentifier (nsname) || !provider.IsValidIdentifier (classname))
370 throw new ApplicationException ("Invalid resource file name.");
374 res = new ResourceReader (resfile);
375 } catch (ArgumentException) {
376 // invalid stream, probably empty - ignore silently and abort
380 CodeNamespace ns = new CodeNamespace (nsname);
381 CodeTypeDeclaration cls = new CodeTypeDeclaration (classname);
383 cls.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
385 CodeMemberField cmf = new CodeMemberField (typeof(CultureInfo), "_culture");
386 cmf.InitExpression = new CodePrimitiveExpression (null);
387 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
388 cls.Members.Add (cmf);
390 cmf = new CodeMemberField (typeof(ResourceManager), "_resourceManager");
391 cmf.InitExpression = new CodePrimitiveExpression (null);
392 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
393 cls.Members.Add (cmf);
395 // Property: ResourceManager
396 CodeMemberProperty cmp = new CodeMemberProperty ();
397 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
398 cmp.Name = "ResourceManager";
400 cmp.Type = new CodeTypeReference (typeof(ResourceManager));
401 CodePropertyResourceManagerGet (cmp.GetStatements, resfile, classname);
402 cls.Members.Add (cmp);
405 cmp = new CodeMemberProperty ();
406 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
407 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
408 cmp.Name = "Culture";
411 cmp.Type = new CodeTypeReference (typeof(CultureInfo));
412 CodePropertyGenericGet (cmp.GetStatements, "_culture", classname);
413 CodePropertyGenericSet (cmp.SetStatements, "_culture", classname);
414 cls.Members.Add (cmp);
416 // Add the resource properties
417 Dictionary<string,bool> imports = new Dictionary<string,bool> ();
419 foreach (DictionaryEntry de in res) {
420 Type type = de.Value.GetType ();
422 if (!imports.ContainsKey (type.Namespace))
423 imports [type.Namespace] = true;
425 string asname = new AssemblyName (type.Assembly.FullName).Name;
426 if (!assemblies.ContainsKey (asname))
427 assemblies [asname] = true;
429 cmp = new CodeMemberProperty ();
430 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
431 cmp.Name = SanitizeResourceName ((string)de.Key);
433 CodePropertyResourceGet (cmp.GetStatements, (string)de.Key, type, classname);
434 cmp.Type = new CodeTypeReference (type);
435 cls.Members.Add (cmp);
437 } catch (Exception ex) {
438 throw new ApplicationException ("Failed to compile global resources.", ex);
440 foreach (KeyValuePair<string,bool> de in imports)
441 ns.Imports.Add (new CodeNamespaceImport(de.Key));
444 unit.Namespaces.Add (ns);
447 string SanitizeResourceName (string name)
449 return name.Replace (' ', '_').Replace ('-', '_').Replace ('.', '_');
452 CodeObjectCreateExpression NewResourceManager (string name, string typename)
454 CodeExpression resname = new CodePrimitiveExpression (name);
455 CodePropertyReferenceExpression asm = new CodePropertyReferenceExpression (
456 new CodeTypeOfExpression (new CodeTypeReference (typename)),
459 return new CodeObjectCreateExpression ("System.Resources.ResourceManager",
460 new CodeExpression [] {resname, asm});
463 void CodePropertyResourceManagerGet (CodeStatementCollection csc, string resfile, string typename)
465 string name = Path.GetFileNameWithoutExtension (resfile);
469 exp = new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), "_resourceManager");
470 st = new CodeConditionStatement (
471 new CodeBinaryOperatorExpression (
473 CodeBinaryOperatorType.IdentityInequality,
474 new CodePrimitiveExpression (null)),
475 new CodeStatement [] { new CodeMethodReturnStatement (exp) });
478 st = new CodeAssignStatement (exp, NewResourceManager (name, typename));
480 csc.Add (new CodeMethodReturnStatement (exp));
483 void CodePropertyResourceGet (CodeStatementCollection csc, string resname, Type restype, string typename)
485 CodeStatement st = new CodeVariableDeclarationStatement (
486 typeof (ResourceManager),
488 new CodePropertyReferenceExpression (
489 new CodeTypeReferenceExpression (typename), "ResourceManager"));
492 st = new CodeConditionStatement (
493 new CodeBinaryOperatorExpression (
494 new CodeVariableReferenceExpression ("rm"),
495 CodeBinaryOperatorType.IdentityEquality,
496 new CodePrimitiveExpression (null)),
497 new CodeStatement [] { new CodeMethodReturnStatement (new CodePrimitiveExpression (null)) });
500 bool gotstr = (restype == typeof (string));
501 CodeExpression exp = new CodeMethodInvokeExpression (
502 new CodeVariableReferenceExpression ("rm"),
503 gotstr ? "GetString" : "GetObject",
504 new CodeExpression [] { new CodePrimitiveExpression (resname),
505 new CodeFieldReferenceExpression (
506 new CodeTypeReferenceExpression (typename), "_culture") });
507 st = new CodeVariableDeclarationStatement (
510 gotstr ? exp : new CodeCastExpression (restype, exp));
512 csc.Add (new CodeMethodReturnStatement (new CodeVariableReferenceExpression ("obj")));
515 void CodePropertyGenericGet (CodeStatementCollection csc, string field, string typename)
517 csc.Add(new CodeMethodReturnStatement (
518 new CodeFieldReferenceExpression (
519 new CodeTypeReferenceExpression (typename), field)));
522 void CodePropertyGenericSet (CodeStatementCollection csc, string field, string typename)
524 csc.Add(new CodeAssignStatement (
525 new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), field),
526 new CodeVariableReferenceExpression ("value")));
529 string CompileResource (AppResourceFileInfo arfi, bool local)
531 string path = arfi.Info.FullName;
532 string rname = Path.GetFileNameWithoutExtension (path) + ".resources";
534 rname = "Resources." + rname;
536 string resource = Path.Combine (TempDirectory, rname);
537 FileStream source = null, destination = null;
538 IResourceReader reader = null;
539 ResourceWriter writer = null;
542 source = new FileStream (path, FileMode.Open, FileAccess.Read);
543 destination = new FileStream (resource, FileMode.Create, FileAccess.Write);
544 reader = GetReaderForKind (arfi.Kind, source);
545 writer = new ResourceWriter (destination);
546 foreach (DictionaryEntry de in reader) {
547 object val = de.Value;
549 writer.AddResource ((string)de.Key, (string)val);
551 writer.AddResource ((string)de.Key, val);
553 } catch (Exception ex) {
554 throw new HttpException ("Failed to compile resource file", ex);
558 else if (source != null)
562 else if (destination != null)
563 destination.Close ();
569 IResourceReader GetReaderForKind (AppResourceFileKind kind, Stream stream)
572 case AppResourceFileKind.ResX:
573 return new ResXResourceReader (stream);
575 case AppResourceFileKind.Resource:
576 return new ResourceReader (stream);
584 object OnCreateRandomFile (string path)
586 FileStream f = new FileStream (path, FileMode.CreateNew);