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.Errors.Count == 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);
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.Errors.Count == 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)
269 if (arfi.Kind == AppResourceFileKind.ResX)
270 resfile = CompileResource (arfi);
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));
330 groups.Sort (lcList);
333 // Now find their translated counterparts
334 foreach (List<string> al in groups) {
336 tmp = Path.GetFileNameWithoutExtension (s);
337 foreach (AppResourceFileInfo arfi in files) {
341 s = arfi.Info.FullName;
344 tmp2 = arfi.Info.Name;
345 if (tmp2.StartsWith (tmp)) {
346 al.Add (GetResourceFile (arfi, cp));
352 // Anything that's left here might be orphans or lone default files.
353 // For those files we check the part following the last dot
354 // before the .resx/.resource extensions and test whether it's a registered
355 // culture or not. If it is not a culture, then we have a
356 // default file that doesn't have any translations. Otherwise,
357 // the file is ignored (it's the same thing MS.NET does)
358 foreach (AppResourceFileInfo arfi in files) {
362 if (IsFileCultureValid (arfi.Info.FullName))
363 continue; // Culture found, we reject the file
365 // A single default file, create a group
366 List<string> al = new List<string> ();
367 al.Add (GetResourceFile (arfi, cp));
370 groups.Sort (lcList);
371 return groups.ToArray ();
374 // CodeDOM generation
375 void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary <string,bool> assemblies,
376 CodeDomProvider provider)
378 if (String.IsNullOrEmpty (resfile))
381 string fname, nsname, classname;
383 fname = Path.GetFileNameWithoutExtension (resfile);
384 nsname = Path.GetFileNameWithoutExtension (fname);
385 classname = Path.GetExtension (fname);
386 if (classname == null || classname.Length == 0) {
388 nsname = "Resources";
390 if (!nsname.StartsWith ("Resources", StringComparison.InvariantCulture))
391 nsname = String.Format ("Resources.{0}", nsname);
392 classname = classname.Substring(1);
395 if (!provider.IsValidIdentifier (nsname) || !provider.IsValidIdentifier (classname))
396 throw new ApplicationException ("Invalid resource file name.");
400 res = new ResourceReader (resfile);
401 } catch (ArgumentException) {
402 // invalid stream, probably empty - ignore silently and abort
406 CodeNamespace ns = new CodeNamespace (nsname);
407 CodeTypeDeclaration cls = new CodeTypeDeclaration (classname);
409 cls.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
411 CodeMemberField cmf = new CodeMemberField (typeof(CultureInfo), "culture");
412 cmf.InitExpression = new CodePrimitiveExpression (null);
413 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
414 cls.Members.Add (cmf);
416 cmf = new CodeMemberField (typeof(ResourceManager), "resourceManager");
417 cmf.InitExpression = new CodePrimitiveExpression (null);
418 cmf.Attributes = MemberAttributes.Private | MemberAttributes.Final | MemberAttributes.Static;
419 cls.Members.Add (cmf);
421 // Property: ResourceManager
422 CodeMemberProperty cmp = new CodeMemberProperty ();
423 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
424 cmp.Name = "ResourceManager";
426 cmp.Type = new CodeTypeReference (typeof(ResourceManager));
427 CodePropertyResourceManagerGet (cmp.GetStatements, resfile, classname);
428 cls.Members.Add (cmp);
431 cmp = new CodeMemberProperty ();
432 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
433 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
434 cmp.Name = "Culture";
437 cmp.Type = new CodeTypeReference (typeof(CultureInfo));
438 CodePropertyGenericGet (cmp.GetStatements, "culture", classname);
439 CodePropertyGenericSet (cmp.SetStatements, "culture", classname);
440 cls.Members.Add (cmp);
442 // Add the resource properties
443 Dictionary<string,bool> imports = new Dictionary<string,bool> ();
445 foreach (DictionaryEntry de in res) {
446 Type type = de.Value.GetType ();
448 if (!imports.ContainsKey (type.Namespace))
449 imports [type.Namespace] = true;
451 string asname = new AssemblyName (type.Assembly.FullName).Name;
452 if (!assemblies.ContainsKey (asname))
453 assemblies [asname] = true;
455 cmp = new CodeMemberProperty ();
456 cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
457 cmp.Name = SanitizeResourceName ((string)de.Key);
459 CodePropertyResourceGet (cmp.GetStatements, (string)de.Key, type, classname);
460 cmp.Type = new CodeTypeReference (type);
461 cls.Members.Add (cmp);
463 } catch (Exception ex) {
464 throw new ApplicationException ("Failed to compile global resources.", ex);
466 foreach (KeyValuePair<string,bool> de in imports)
467 ns.Imports.Add (new CodeNamespaceImport(de.Key));
470 unit.Namespaces.Add (ns);
473 string SanitizeResourceName (string name)
475 return name.Replace (' ', '_').Replace ('-', '_').Replace ('.', '_');
478 CodeObjectCreateExpression NewResourceManager (string name, string typename)
480 CodeExpression resname = new CodePrimitiveExpression (name);
481 CodePropertyReferenceExpression asm = new CodePropertyReferenceExpression (
482 new CodeTypeOfExpression (new CodeTypeReference (typename)),
485 return new CodeObjectCreateExpression ("System.Resources.ResourceManager",
486 new CodeExpression [] {resname, asm});
489 void CodePropertyResourceManagerGet (CodeStatementCollection csc, string resfile, string typename)
491 string name = Path.GetFileNameWithoutExtension (resfile);
495 exp = new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), "resourceManager");
496 st = new CodeConditionStatement (
497 new CodeBinaryOperatorExpression (
499 CodeBinaryOperatorType.IdentityInequality,
500 new CodePrimitiveExpression (null)),
501 new CodeStatement [] { new CodeMethodReturnStatement (exp) });
504 st = new CodeAssignStatement (exp, NewResourceManager (name, typename));
506 csc.Add (new CodeMethodReturnStatement (exp));
509 void CodePropertyResourceGet (CodeStatementCollection csc, string resname, Type restype, string typename)
511 CodeStatement st = new CodeVariableDeclarationStatement (
512 typeof (ResourceManager),
514 new CodePropertyReferenceExpression (
515 new CodeTypeReferenceExpression (typename), "ResourceManager"));
518 st = new CodeConditionStatement (
519 new CodeBinaryOperatorExpression (
520 new CodeVariableReferenceExpression ("rm"),
521 CodeBinaryOperatorType.IdentityEquality,
522 new CodePrimitiveExpression (null)),
523 new CodeStatement [] { new CodeMethodReturnStatement (new CodePrimitiveExpression (null)) });
526 bool gotstr = (restype == typeof (string));
527 CodeExpression exp = new CodeMethodInvokeExpression (
528 new CodeVariableReferenceExpression ("rm"),
529 gotstr ? "GetString" : "GetObject",
530 new CodeExpression [] { new CodePrimitiveExpression (resname),
531 new CodeFieldReferenceExpression (
532 new CodeTypeReferenceExpression (typename), "culture") });
533 st = new CodeVariableDeclarationStatement (
536 gotstr ? exp : new CodeCastExpression (restype, exp));
538 csc.Add (new CodeMethodReturnStatement (new CodeVariableReferenceExpression ("obj")));
541 void CodePropertyGenericGet (CodeStatementCollection csc, string field, string typename)
543 csc.Add(new CodeMethodReturnStatement (
544 new CodeFieldReferenceExpression (
545 new CodeTypeReferenceExpression (typename), field)));
548 void CodePropertyGenericSet (CodeStatementCollection csc, string field, string typename)
550 csc.Add(new CodeAssignStatement (
551 new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), field),
552 new CodeVariableReferenceExpression ("value")));
555 string CompileResource (AppResourceFileInfo arfi)
557 string path = arfi.Info.FullName;
558 string resource = Path.Combine (TempDirectory,
559 "Resources." + Path.GetFileNameWithoutExtension (path) + ".resources");
560 FileStream source = null, destination = null;
561 IResourceReader reader = null;
562 ResourceWriter writer = null;
565 source = new FileStream (path, FileMode.Open, FileAccess.Read);
566 destination = new FileStream (resource, FileMode.Create, FileAccess.Write);
567 reader = GetReaderForKind (arfi.Kind, source);
568 writer = new ResourceWriter (destination);
569 foreach (DictionaryEntry de in reader) {
570 object val = de.Value;
572 writer.AddResource ((string)de.Key, (string)val);
574 writer.AddResource ((string)de.Key, val);
576 } catch (Exception ex) {
577 throw new HttpException ("Failed to compile resource file", ex);
581 else if (source != null)
585 else if (destination != null)
586 destination.Close ();
592 IResourceReader GetReaderForKind (AppResourceFileKind kind, Stream stream)
595 case AppResourceFileKind.ResX:
596 return new ResXResourceReader (stream);
598 case AppResourceFileKind.Resource:
599 return new ResourceReader (stream);
607 object OnCreateRandomFile (string path)
609 FileStream f = new FileStream (path, FileMode.CreateNew);