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";
53 AppResourceFilesCollection files;
57 string TempDirectory {
59 if (tempDirectory != null)
61 return (tempDirectory = AppDomain.CurrentDomain.SetupInformation.DynamicBase);
65 public AppResourcesCompiler (HttpContext context)
67 this.context = context;
69 this.files = new AppResourceFilesCollection (context);
72 public AppResourcesCompiler (string virtualPath)
75 this.virtualPath = virtualPath;
76 this.isGlobal = false;
77 this.files = new AppResourceFilesCollection (HttpContext.Current.Request.MapPath (virtualPath));
80 public Assembly Compile ()
86 return CompileGlobal ();
88 return CompileLocal ();
91 Assembly CompileGlobal ()
93 string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
94 "App_GlobalResources",
96 OnCreateRandomFile) as string;
98 if (assemblyPath == null)
99 throw new ApplicationException ("Failed to create global resources assembly");
101 CompilationSection config = WebConfigurationManager.GetSection ("system.web/compilation") as CompilationSection;
102 if (config == null || !CodeDomProvider.IsDefinedLanguage (config.DefaultLanguage))
103 throw new ApplicationException ("Could not get the default compiler.");
104 CompilerInfo ci = CodeDomProvider.GetCompilerInfo (config.DefaultLanguage);
105 if (ci == null || !ci.IsCodeDomProviderTypeValid)
106 throw new ApplicationException ("Failed to obtain the default compiler information.");
108 CompilerParameters cp = ci.CreateDefaultCompilerParameters ();
109 cp.OutputAssembly = assemblyPath;
110 cp.GenerateExecutable = false;
111 cp.TreatWarningsAsErrors = true;
112 cp.IncludeDebugInformation = config.Debug;
114 List <string>[] fileGroups = GroupGlobalFiles (cp);
115 if (fileGroups == null || fileGroups.Length == 0)
118 CodeCompileUnit unit = new CodeCompileUnit ();
119 CodeNamespace ns = new CodeNamespace (null);
120 ns.Imports.Add (new CodeNamespaceImport ("System"));
121 ns.Imports.Add (new CodeNamespaceImport ("System.Globalization"));
122 ns.Imports.Add (new CodeNamespaceImport ("System.Reflection"));
123 ns.Imports.Add (new CodeNamespaceImport ("System.Resources"));
124 unit.Namespaces.Add (ns);
126 CodeDomProvider provider;
127 provider = ci.CreateProvider ();
128 if (provider == null)
129 throw new ApplicationException ("Failed to instantiate the default compiler.");
131 Dictionary <string,bool> assemblies = new Dictionary<string,bool> ();
132 foreach (List<string> ls in fileGroups)
133 DomFromResource (ls [0], unit, assemblies, provider);
134 foreach (KeyValuePair<string,bool> de in assemblies)
135 unit.ReferencedAssemblies.Add (de.Key);
137 AssemblyBuilder abuilder = new AssemblyBuilder (provider);
138 abuilder.AddCodeCompileUnit (unit);
140 CompilerResults results = abuilder.BuildAssembly (cp);
143 if (results.NativeCompilerReturnValue == 0) {
144 ret = results.CompiledAssembly;
145 BuildManager.TopLevelAssemblies.Add (ret);
146 HttpContext.AppGlobalResourcesAssembly = ret;
148 if (context.IsCustomErrorEnabled)
149 throw new ApplicationException ("An error occurred while compiling global resources.");
150 throw new CompilationException (null, results.Errors, null);
152 HttpRuntime.WritePreservationFile (ret, "App_GlobalResources");
153 HttpRuntime.EnableAssemblyMapping (true);
158 Assembly CompileLocal ()
160 if (String.IsNullOrEmpty (virtualPath))
163 Assembly cached = GetCachedLocalResourcesAssembly (virtualPath);
168 if (virtualPath == "/")
169 prefix = "App_LocalResources.root";
171 prefix = "App_LocalResources" + virtualPath.Replace ('/', '.');
173 string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
176 OnCreateRandomFile) as string;
177 if (assemblyPath == null)
178 throw new ApplicationException ("Failed to create global resources assembly");
180 CompilationSection config = WebConfigurationManager.GetSection ("system.web/compilation") as CompilationSection;
181 if (config == null || !CodeDomProvider.IsDefinedLanguage (config.DefaultLanguage))
182 throw new ApplicationException ("Could not get the default compiler.");
183 CompilerInfo ci = CodeDomProvider.GetCompilerInfo (config.DefaultLanguage);
184 if (ci == null || !ci.IsCodeDomProviderTypeValid)
185 throw new ApplicationException ("Failed to obtain the default compiler information.");
187 CompilerParameters cp = ci.CreateDefaultCompilerParameters ();
188 cp.OutputAssembly = assemblyPath;
189 cp.GenerateExecutable = false;
190 cp.TreatWarningsAsErrors = true;
191 cp.IncludeDebugInformation = config.Debug;
193 List<AppResourceFileInfo> files = this.files.Files;
194 foreach (AppResourceFileInfo arfi in files)
195 GetResourceFile (arfi, cp, true);
197 CodeDomProvider provider;
198 provider = ci.CreateProvider ();
199 if (provider == null)
200 throw new ApplicationException ("Failed to instantiate the default compiler.");
202 AssemblyBuilder abuilder = new AssemblyBuilder (provider);
203 CompilerResults results = abuilder.BuildAssembly (cp);
206 if (results.NativeCompilerReturnValue == 0) {
207 ret = results.CompiledAssembly;
208 AddAssemblyToCache (virtualPath, ret);
210 if (context.IsCustomErrorEnabled)
211 throw new ApplicationException ("An error occurred while compiling global resources.");
212 throw new CompilationException (null, results.Errors, null);
218 internal static Assembly GetCachedLocalResourcesAssembly (string path)
220 Dictionary <string, Assembly> cache;
222 cache = HttpRuntime.Cache[cachePrefix] as Dictionary <string, Assembly>;
223 if (cache == null || !cache.ContainsKey (path))
228 void AddAssemblyToCache (string path, Assembly asm)
230 Cache runtimeCache = HttpRuntime.Cache;
231 Dictionary <string, Assembly> cache;
233 cache = runtimeCache[cachePrefix] as Dictionary <string, Assembly>;
235 cache = new Dictionary <string, Assembly> ();
237 runtimeCache.Insert (cachePrefix, cache);
240 uint CountChars (char c, string s)
243 foreach (char ch in s) {
250 bool IsFileCultureValid (string fileName)
252 string tmp = Path.GetFileNameWithoutExtension (fileName);
253 tmp = Path.GetExtension (tmp);
254 if (tmp != null && tmp.Length > 0) {
255 tmp = tmp.Substring (1);
257 CultureInfo.GetCultureInfo (tmp);
266 string GetResourceFile (AppResourceFileInfo arfi, CompilerParameters cp, bool local)
269 if (arfi.Kind == AppResourceFileKind.ResX)
270 resfile = CompileResource (arfi, local);
272 resfile = arfi.Info.FullName;
273 if (!String.IsNullOrEmpty (resfile))
274 cp.EmbeddedResources.Add (resfile);
278 List <string>[] GroupGlobalFiles (CompilerParameters cp)
280 List<AppResourceFileInfo> files = this.files.Files;
281 List<List<string>> groups = new List<List<string>> ();
282 AppResourcesLengthComparer<List<string>> lcList = new AppResourcesLengthComparer<List<string>> ();
284 string tmp, s, basename;
285 uint basedots, filedots;
286 AppResourceFileInfo defaultFile;
288 foreach (AppResourceFileInfo arfi in files) {
289 if (arfi.Kind != AppResourceFileKind.ResX && arfi.Kind != AppResourceFileKind.Resource)
292 s = arfi.Info.FullName;
293 basename = Path.GetFileNameWithoutExtension (s);
294 basedots = CountChars ('.', basename);
297 // If there are any files that start with this baseName, we have a default file
298 foreach (AppResourceFileInfo fi in files) {
302 string s2 = fi.Info.FullName;
303 if (s2 == null || s == s2)
305 tmp = Path.GetFileNameWithoutExtension (s2);
306 filedots = CountChars ('.', tmp);
308 if (filedots == basedots + 1 && tmp.StartsWith (basename)) {
309 if (IsFileCultureValid (s2)) {
310 // A valid translated file for this name
314 // This file shares the base name, but the culture is invalid - we must
315 // ignore it since the name of the generated strongly typed class for this
316 // resource will clash with the one generated from the default file with
317 // the given basename.
322 if (defaultFile != null) {
323 List<string> al = new List<string> ();
324 al.Add (GetResourceFile (arfi, cp, false));
330 groups.Sort (lcList);
333 // Now find their translated counterparts
334 foreach (List<string> al in groups) {
336 tmp = Path.GetFileNameWithoutExtension (s);
337 if (tmp.StartsWith ("Resources."))
338 tmp = tmp.Substring (10);
339 foreach (AppResourceFileInfo arfi in files) {
342 s = arfi.Info.FullName;
345 tmp2 = arfi.Info.Name;
346 if (tmp2.StartsWith (tmp)) {
347 al.Add (GetResourceFile (arfi, cp, false));
353 // Anything that's left here might be orphans or lone default files.
354 // For those files we check the part following the last dot
355 // before the .resx/.resource extensions and test whether it's a registered
356 // culture or not. If it is not a culture, then we have a
357 // default file that doesn't have any translations. Otherwise,
358 // the file is ignored (it's the same thing MS.NET does)
359 foreach (AppResourceFileInfo arfi in files) {
363 if (IsFileCultureValid (arfi.Info.FullName))
364 continue; // Culture found, we reject the file
366 // A single default file, create a group
367 List<string> al = new List<string> ();
368 al.Add (GetResourceFile (arfi, cp, false));
371 groups.Sort (lcList);
372 return groups.ToArray ();
375 // CodeDOM generation
376 void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary <string,bool> assemblies,
377 CodeDomProvider provider)
379 if (String.IsNullOrEmpty (resfile))
382 string fname, nsname, classname;
384 fname = Path.GetFileNameWithoutExtension (resfile);
385 nsname = Path.GetFileNameWithoutExtension (fname);
386 classname = Path.GetExtension (fname);
387 if (classname == null || classname.Length == 0) {
389 nsname = "Resources";
391 if (!nsname.StartsWith ("Resources", StringComparison.InvariantCulture))
392 nsname = String.Format ("Resources.{0}", nsname);
393 classname = classname.Substring(1);
396 if (!String.IsNullOrEmpty (classname))
397 classname = classname.Replace ('.', '_');
398 if (!String.IsNullOrEmpty (nsname))
399 nsname = nsname.Replace ('.', '_');
401 if (!provider.IsValidIdentifier (nsname) || !provider.IsValidIdentifier (classname))
402 throw new ApplicationException ("Invalid resource file name.");
406 res = new ResourceReader (resfile);
407 } catch (ArgumentException) {
408 // invalid stream, probably empty - ignore silently and abort
412 CodeNamespace ns = new CodeNamespace (nsname);
413 CodeTypeDeclaration cls = new CodeTypeDeclaration (classname);
415 cls.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
417 CodeMemberField cmf = new CodeMemberField (typeof(CultureInfo), "culture");
418 cmf.InitExpression = new CodePrimitiveExpression (null);
419 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
420 cls.Members.Add (cmf);
422 cmf = new CodeMemberField (typeof(ResourceManager), "resourceManager");
423 cmf.InitExpression = new CodePrimitiveExpression (null);
424 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
425 cls.Members.Add (cmf);
427 // Property: ResourceManager
428 CodeMemberProperty cmp = new CodeMemberProperty ();
429 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
430 cmp.Name = "ResourceManager";
432 cmp.Type = new CodeTypeReference (typeof(ResourceManager));
433 CodePropertyResourceManagerGet (cmp.GetStatements, resfile, classname);
434 cls.Members.Add (cmp);
437 cmp = new CodeMemberProperty ();
438 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
439 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
440 cmp.Name = "Culture";
443 cmp.Type = new CodeTypeReference (typeof(CultureInfo));
444 CodePropertyGenericGet (cmp.GetStatements, "culture", classname);
445 CodePropertyGenericSet (cmp.SetStatements, "culture", classname);
446 cls.Members.Add (cmp);
448 // Add the resource properties
449 Dictionary<string,bool> imports = new Dictionary<string,bool> ();
451 foreach (DictionaryEntry de in res) {
452 Type type = de.Value.GetType ();
454 if (!imports.ContainsKey (type.Namespace))
455 imports [type.Namespace] = true;
457 string asname = new AssemblyName (type.Assembly.FullName).Name;
458 if (!assemblies.ContainsKey (asname))
459 assemblies [asname] = true;
461 cmp = new CodeMemberProperty ();
462 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
463 cmp.Name = SanitizeResourceName ((string)de.Key);
465 CodePropertyResourceGet (cmp.GetStatements, (string)de.Key, type, classname);
466 cmp.Type = new CodeTypeReference (type);
467 cls.Members.Add (cmp);
469 } catch (Exception ex) {
470 throw new ApplicationException ("Failed to compile global resources.", ex);
472 foreach (KeyValuePair<string,bool> de in imports)
473 ns.Imports.Add (new CodeNamespaceImport(de.Key));
476 unit.Namespaces.Add (ns);
479 string SanitizeResourceName (string name)
481 return name.Replace (' ', '_').Replace ('-', '_').Replace ('.', '_');
484 CodeObjectCreateExpression NewResourceManager (string name, string typename)
486 CodeExpression resname = new CodePrimitiveExpression (name);
487 CodePropertyReferenceExpression asm = new CodePropertyReferenceExpression (
488 new CodeTypeOfExpression (new CodeTypeReference (typename)),
491 return new CodeObjectCreateExpression ("System.Resources.ResourceManager",
492 new CodeExpression [] {resname, asm});
495 void CodePropertyResourceManagerGet (CodeStatementCollection csc, string resfile, string typename)
497 string name = Path.GetFileNameWithoutExtension (resfile);
501 exp = new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), "resourceManager");
502 st = new CodeConditionStatement (
503 new CodeBinaryOperatorExpression (
505 CodeBinaryOperatorType.IdentityInequality,
506 new CodePrimitiveExpression (null)),
507 new CodeStatement [] { new CodeMethodReturnStatement (exp) });
510 st = new CodeAssignStatement (exp, NewResourceManager (name, typename));
512 csc.Add (new CodeMethodReturnStatement (exp));
515 void CodePropertyResourceGet (CodeStatementCollection csc, string resname, Type restype, string typename)
517 CodeStatement st = new CodeVariableDeclarationStatement (
518 typeof (ResourceManager),
520 new CodePropertyReferenceExpression (
521 new CodeTypeReferenceExpression (typename), "ResourceManager"));
524 st = new CodeConditionStatement (
525 new CodeBinaryOperatorExpression (
526 new CodeVariableReferenceExpression ("rm"),
527 CodeBinaryOperatorType.IdentityEquality,
528 new CodePrimitiveExpression (null)),
529 new CodeStatement [] { new CodeMethodReturnStatement (new CodePrimitiveExpression (null)) });
532 bool gotstr = (restype == typeof (string));
533 CodeExpression exp = new CodeMethodInvokeExpression (
534 new CodeVariableReferenceExpression ("rm"),
535 gotstr ? "GetString" : "GetObject",
536 new CodeExpression [] { new CodePrimitiveExpression (resname),
537 new CodeFieldReferenceExpression (
538 new CodeTypeReferenceExpression (typename), "culture") });
539 st = new CodeVariableDeclarationStatement (
542 gotstr ? exp : new CodeCastExpression (restype, exp));
544 csc.Add (new CodeMethodReturnStatement (new CodeVariableReferenceExpression ("obj")));
547 void CodePropertyGenericGet (CodeStatementCollection csc, string field, string typename)
549 csc.Add(new CodeMethodReturnStatement (
550 new CodeFieldReferenceExpression (
551 new CodeTypeReferenceExpression (typename), field)));
554 void CodePropertyGenericSet (CodeStatementCollection csc, string field, string typename)
556 csc.Add(new CodeAssignStatement (
557 new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), field),
558 new CodeVariableReferenceExpression ("value")));
561 string CompileResource (AppResourceFileInfo arfi, bool local)
563 string path = arfi.Info.FullName;
564 string rname = Path.GetFileNameWithoutExtension (path) + ".resources";
566 rname = "Resources." + rname;
568 string resource = Path.Combine (TempDirectory, rname);
569 FileStream source = null, destination = null;
570 IResourceReader reader = null;
571 ResourceWriter writer = null;
574 source = new FileStream (path, FileMode.Open, FileAccess.Read);
575 destination = new FileStream (resource, FileMode.Create, FileAccess.Write);
576 reader = GetReaderForKind (arfi.Kind, source);
577 writer = new ResourceWriter (destination);
578 foreach (DictionaryEntry de in reader) {
579 object val = de.Value;
581 writer.AddResource ((string)de.Key, (string)val);
583 writer.AddResource ((string)de.Key, val);
585 } catch (Exception ex) {
586 throw new HttpException ("Failed to compile resource file", ex);
590 else if (source != null)
594 else if (destination != null)
595 destination.Close ();
601 IResourceReader GetReaderForKind (AppResourceFileKind kind, Stream stream)
604 case AppResourceFileKind.ResX:
605 return new ResXResourceReader (stream);
607 case AppResourceFileKind.Resource:
608 return new ResourceReader (stream);
616 object OnCreateRandomFile (string path)
618 FileStream f = new FileStream (path, FileMode.CreateNew);