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!:.";
54 AppResourceFilesCollection files;
57 Dictionary <string, List <string>> cultureFiles;
59 string TempDirectory {
61 if (tempDirectory != null)
63 return (tempDirectory = AppDomain.CurrentDomain.SetupInformation.DynamicBase);
67 public Dictionary <string, List <string>> CultureFiles {
68 get { return cultureFiles; }
71 public AppResourcesCompiler (HttpContext context)
73 this.context = context;
75 this.files = new AppResourceFilesCollection (context);
76 this.cultureFiles = new Dictionary <string, List <string>> ();
79 public AppResourcesCompiler (string virtualPath)
82 this.virtualPath = virtualPath;
83 this.isGlobal = false;
84 this.files = new AppResourceFilesCollection (HttpContext.Current.Request.MapPath (virtualPath));
85 this.cultureFiles = new Dictionary <string, List <string>> ();
88 public Assembly Compile ()
94 return CompileGlobal ();
96 return CompileLocal ();
99 Assembly CompileGlobal ()
101 string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
102 "App_GlobalResources",
104 OnCreateRandomFile) as string;
106 if (assemblyPath == null)
107 throw new ApplicationException ("Failed to create global resources assembly");
109 List <string>[] fileGroups = GroupGlobalFiles ();
110 if (fileGroups == null || fileGroups.Length == 0)
113 CodeCompileUnit unit = new CodeCompileUnit ();
114 CodeNamespace ns = new CodeNamespace (null);
115 ns.Imports.Add (new CodeNamespaceImport ("System"));
116 ns.Imports.Add (new CodeNamespaceImport ("System.Globalization"));
117 ns.Imports.Add (new CodeNamespaceImport ("System.Reflection"));
118 ns.Imports.Add (new CodeNamespaceImport ("System.Resources"));
119 unit.Namespaces.Add (ns);
121 AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_GlobalResources", assemblyPath,
123 CodeDomProvider provider = builder.Provider;
125 Dictionary <string,bool> assemblies = new Dictionary<string,bool> ();
126 foreach (List<string> ls in fileGroups)
127 DomFromResource (ls [0], unit, assemblies, provider);
129 foreach (KeyValuePair<string,bool> de in assemblies)
130 unit.ReferencedAssemblies.Add (de.Key);
132 builder.Build (unit);
133 HttpContext.AppGlobalResourcesAssembly = builder.MainAssembly;
135 return builder.MainAssembly;
138 Assembly CompileLocal ()
140 if (String.IsNullOrEmpty (virtualPath))
143 Assembly cached = GetCachedLocalResourcesAssembly (virtualPath);
148 if (virtualPath == "/")
149 prefix = "App_LocalResources.root";
151 prefix = "App_LocalResources" + virtualPath.Replace ('/', '.');
153 string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
156 OnCreateRandomFile) as string;
157 if (assemblyPath == null)
158 throw new ApplicationException ("Failed to create local resources assembly");
160 List<AppResourceFileInfo> files = this.files.Files;
161 foreach (AppResourceFileInfo arfi in files)
162 GetResourceFile (arfi, true);
164 AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_LocalResources", assemblyPath,
167 Assembly ret = builder.MainAssembly;
170 AddAssemblyToCache (virtualPath, ret);
175 internal static Assembly GetCachedLocalResourcesAssembly (string path)
177 Dictionary <string, Assembly> cache;
179 cache = HttpRuntime.InternalCache[cachePrefix] as Dictionary <string, Assembly>;
180 if (cache == null || !cache.ContainsKey (path))
185 void AddAssemblyToCache (string path, Assembly asm)
187 Cache runtimeCache = HttpRuntime.InternalCache;
188 Dictionary <string, Assembly> cache;
190 cache = runtimeCache[cachePrefix] as Dictionary <string, Assembly>;
192 cache = new Dictionary <string, Assembly> ();
194 runtimeCache.Insert (cachePrefix, cache);
197 uint CountChars (char c, string s)
200 foreach (char ch in s) {
207 string IsFileCultureValid (string fileName)
209 string tmp = Path.GetFileNameWithoutExtension (fileName);
210 tmp = Path.GetExtension (tmp);
211 if (tmp != null && tmp.Length > 0) {
212 tmp = tmp.Substring (1);
214 CultureInfo.GetCultureInfo (tmp);
223 string GetResourceFile (AppResourceFileInfo arfi, bool local)
226 if (arfi.Kind == AppResourceFileKind.ResX)
227 resfile = CompileResource (arfi, local);
229 resfile = arfi.Info.FullName;
230 if (!String.IsNullOrEmpty (resfile)) {
231 string culture = IsFileCultureValid (resfile);
233 culture = DefaultCultureKey;
235 List <string> cfiles;
236 if (cultureFiles.ContainsKey (culture))
237 cfiles = cultureFiles [culture];
239 cfiles = new List <string> (1);
240 cultureFiles [culture] = cfiles;
242 cfiles.Add (resfile);
248 List <string>[] GroupGlobalFiles ()
250 List<AppResourceFileInfo> files = this.files.Files;
251 List<List<string>> groups = new List<List<string>> ();
252 AppResourcesLengthComparer<List<string>> lcList = new AppResourcesLengthComparer<List<string>> ();
254 string tmp, s, basename;
255 uint basedots, filedots;
256 AppResourceFileInfo defaultFile;
258 foreach (AppResourceFileInfo arfi in files) {
259 if (arfi.Kind != AppResourceFileKind.ResX && arfi.Kind != AppResourceFileKind.Resource)
262 s = arfi.Info.FullName;
263 basename = Path.GetFileNameWithoutExtension (s);
264 basedots = CountChars ('.', basename);
267 // If there are any files that start with this baseName, we have a default file
268 foreach (AppResourceFileInfo fi in files) {
272 string s2 = fi.Info.FullName;
273 if (s2 == null || s == s2)
275 tmp = Path.GetFileNameWithoutExtension (s2);
276 filedots = CountChars ('.', tmp);
278 if (filedots == basedots + 1 && tmp.StartsWith (basename)) {
279 if (IsFileCultureValid (s2) != null) {
280 // A valid translated file for this name
284 // This file shares the base name, but the culture is invalid - we must
285 // ignore it since the name of the generated strongly typed class for this
286 // resource will clash with the one generated from the default file with
287 // the given basename.
292 if (defaultFile != null) {
293 List<string> al = new List<string> ();
294 al.Add (GetResourceFile (arfi, false));
300 groups.Sort (lcList);
303 // Now find their translated counterparts
304 foreach (List<string> al in groups) {
306 tmp = Path.GetFileNameWithoutExtension (s);
307 if (tmp.StartsWith ("Resources."))
308 tmp = tmp.Substring (10);
309 foreach (AppResourceFileInfo arfi in files) {
312 s = arfi.Info.FullName;
315 tmp2 = arfi.Info.Name;
316 if (tmp2.StartsWith (tmp)) {
317 al.Add (GetResourceFile (arfi, false));
323 // Anything that's left here might be orphans or lone default files.
324 // For those files we check the part following the last dot
325 // before the .resx/.resource extensions and test whether it's a registered
326 // culture or not. If it is not a culture, then we have a
327 // default file that doesn't have any translations. Otherwise,
328 // the file is ignored (it's the same thing MS.NET does)
329 foreach (AppResourceFileInfo arfi in files) {
333 if (IsFileCultureValid (arfi.Info.FullName) != null)
334 continue; // Culture found, we reject the file
336 // A single default file, create a group
337 List<string> al = new List<string> ();
338 al.Add (GetResourceFile (arfi, false));
341 groups.Sort (lcList);
342 return groups.ToArray ();
345 // CodeDOM generation
346 void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary <string,bool> assemblies,
347 CodeDomProvider provider)
349 if (String.IsNullOrEmpty (resfile))
352 string fname, nsname, classname;
354 fname = Path.GetFileNameWithoutExtension (resfile);
355 nsname = Path.GetFileNameWithoutExtension (fname);
356 classname = Path.GetExtension (fname);
357 if (classname == null || classname.Length == 0) {
359 nsname = "Resources";
361 if (!nsname.StartsWith ("Resources", StringComparison.InvariantCulture))
362 nsname = String.Concat ("Resources.", nsname);
363 classname = classname.Substring(1);
366 if (!String.IsNullOrEmpty (classname))
367 classname = classname.Replace ('.', '_');
368 if (!String.IsNullOrEmpty (nsname))
369 nsname = nsname.Replace ('.', '_');
371 if (!provider.IsValidIdentifier (nsname) || !provider.IsValidIdentifier (classname))
372 throw new ApplicationException ("Invalid resource file name.");
376 res = new ResourceReader (resfile);
377 } catch (ArgumentException) {
378 // invalid stream, probably empty - ignore silently and abort
382 CodeNamespace ns = new CodeNamespace (nsname);
383 CodeTypeDeclaration cls = new CodeTypeDeclaration (classname);
385 cls.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
387 CodeMemberField cmf = new CodeMemberField (typeof(CultureInfo), "_culture");
388 cmf.InitExpression = new CodePrimitiveExpression (null);
389 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
390 cls.Members.Add (cmf);
392 cmf = new CodeMemberField (typeof(ResourceManager), "_resourceManager");
393 cmf.InitExpression = new CodePrimitiveExpression (null);
394 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
395 cls.Members.Add (cmf);
397 // Property: ResourceManager
398 CodeMemberProperty cmp = new CodeMemberProperty ();
399 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
400 cmp.Name = "ResourceManager";
402 cmp.Type = new CodeTypeReference (typeof(ResourceManager));
403 CodePropertyResourceManagerGet (cmp.GetStatements, resfile, classname);
404 cls.Members.Add (cmp);
407 cmp = new CodeMemberProperty ();
408 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
409 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
410 cmp.Name = "Culture";
413 cmp.Type = new CodeTypeReference (typeof(CultureInfo));
414 CodePropertyGenericGet (cmp.GetStatements, "_culture", classname);
415 CodePropertyGenericSet (cmp.SetStatements, "_culture", classname);
416 cls.Members.Add (cmp);
418 // Add the resource properties
419 Dictionary<string,bool> imports = new Dictionary<string,bool> ();
421 foreach (DictionaryEntry de in res) {
422 Type type = de.Value.GetType ();
424 if (!imports.ContainsKey (type.Namespace))
425 imports [type.Namespace] = true;
427 string asname = new AssemblyName (type.Assembly.FullName).Name;
428 if (!assemblies.ContainsKey (asname))
429 assemblies [asname] = true;
431 cmp = new CodeMemberProperty ();
432 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
433 cmp.Name = SanitizeResourceName ((string)de.Key);
435 CodePropertyResourceGet (cmp.GetStatements, (string)de.Key, type, classname);
436 cmp.Type = new CodeTypeReference (type);
437 cls.Members.Add (cmp);
439 } catch (Exception ex) {
440 throw new ApplicationException ("Failed to compile global resources.", ex);
442 foreach (KeyValuePair<string,bool> de in imports)
443 ns.Imports.Add (new CodeNamespaceImport(de.Key));
446 unit.Namespaces.Add (ns);
449 string SanitizeResourceName (string name)
451 return name.Replace (' ', '_').Replace ('-', '_').Replace ('.', '_');
454 CodeObjectCreateExpression NewResourceManager (string name, string typename)
456 CodeExpression resname = new CodePrimitiveExpression (name);
457 CodePropertyReferenceExpression asm = new CodePropertyReferenceExpression (
458 new CodeTypeOfExpression (new CodeTypeReference (typename)),
461 return new CodeObjectCreateExpression ("System.Resources.ResourceManager",
462 new CodeExpression [] {resname, asm});
465 void CodePropertyResourceManagerGet (CodeStatementCollection csc, string resfile, string typename)
467 string name = Path.GetFileNameWithoutExtension (resfile);
471 exp = new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), "_resourceManager");
472 st = new CodeConditionStatement (
473 new CodeBinaryOperatorExpression (
475 CodeBinaryOperatorType.IdentityInequality,
476 new CodePrimitiveExpression (null)),
477 new CodeStatement [] { new CodeMethodReturnStatement (exp) });
480 st = new CodeAssignStatement (exp, NewResourceManager (name, typename));
482 csc.Add (new CodeMethodReturnStatement (exp));
485 void CodePropertyResourceGet (CodeStatementCollection csc, string resname, Type restype, string typename)
487 CodeStatement st = new CodeVariableDeclarationStatement (
488 typeof (ResourceManager),
490 new CodePropertyReferenceExpression (
491 new CodeTypeReferenceExpression (typename), "ResourceManager"));
494 st = new CodeConditionStatement (
495 new CodeBinaryOperatorExpression (
496 new CodeVariableReferenceExpression ("rm"),
497 CodeBinaryOperatorType.IdentityEquality,
498 new CodePrimitiveExpression (null)),
499 new CodeStatement [] { new CodeMethodReturnStatement (new CodePrimitiveExpression (null)) });
502 bool gotstr = (restype == typeof (string));
503 CodeExpression exp = new CodeMethodInvokeExpression (
504 new CodeVariableReferenceExpression ("rm"),
505 gotstr ? "GetString" : "GetObject",
506 new CodeExpression [] { new CodePrimitiveExpression (resname),
507 new CodeFieldReferenceExpression (
508 new CodeTypeReferenceExpression (typename), "_culture") });
509 st = new CodeVariableDeclarationStatement (
512 gotstr ? exp : new CodeCastExpression (restype, exp));
514 csc.Add (new CodeMethodReturnStatement (new CodeVariableReferenceExpression ("obj")));
517 void CodePropertyGenericGet (CodeStatementCollection csc, string field, string typename)
519 csc.Add(new CodeMethodReturnStatement (
520 new CodeFieldReferenceExpression (
521 new CodeTypeReferenceExpression (typename), field)));
524 void CodePropertyGenericSet (CodeStatementCollection csc, string field, string typename)
526 csc.Add(new CodeAssignStatement (
527 new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), field),
528 new CodeVariableReferenceExpression ("value")));
531 string CompileResource (AppResourceFileInfo arfi, bool local)
533 string path = arfi.Info.FullName;
534 string rname = Path.GetFileNameWithoutExtension (path) + ".resources";
536 rname = "Resources." + rname;
538 string resource = Path.Combine (TempDirectory, rname);
539 FileStream source = null, destination = null;
540 IResourceReader reader = null;
541 ResourceWriter writer = null;
544 source = new FileStream (path, FileMode.Open, FileAccess.Read);
545 destination = new FileStream (resource, FileMode.Create, FileAccess.Write);
546 reader = GetReaderForKind (arfi.Kind, source);
547 writer = new ResourceWriter (destination);
548 foreach (DictionaryEntry de in reader) {
549 object val = de.Value;
551 writer.AddResource ((string)de.Key, (string)val);
553 writer.AddResource ((string)de.Key, val);
555 } catch (Exception ex) {
556 throw new HttpException ("Failed to compile resource file", ex);
560 else if (source != null)
564 else if (destination != null)
565 destination.Close ();
571 IResourceReader GetReaderForKind (AppResourceFileKind kind, Stream stream)
574 case AppResourceFileKind.ResX:
575 return new ResXResourceReader (stream);
577 case AppResourceFileKind.Resource:
578 return new ResourceReader (stream);
586 object OnCreateRandomFile (string path)
588 FileStream f = new FileStream (path, FileMode.CreateNew);