2008-11-18 Marek Habersack <mhabersack@novell.com>
[mono.git] / mcs / class / System.Web / System.Web.Compilation / AppResourcesCompiler.cs
1 //
2 // System.Web.Compilation.AppResourceFilesCollection
3 //
4 // Authors:
5 //   Marek Habersack (grendello@gmail.com)
6 //
7 // (C) 2006 Marek Habersack
8 //
9
10 //
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:
18 // 
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
21 // 
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.
29 //
30 #if NET_2_0
31 using System;
32 using System.CodeDom;
33 using System.CodeDom.Compiler;
34 using System.Collections;
35 using System.Collections.Generic;
36 using System.Globalization;
37 using System.IO;
38 using System.Reflection;
39 using System.Resources;
40 using System.Web;
41 using System.Web.Caching;
42 using System.Web.Configuration;
43 using System.Web.Util;
44
45 namespace System.Web.Compilation 
46 {
47         internal class AppResourcesCompiler
48         {
49                 const string cachePrefix = "@@LocalResourcesAssemblies";
50                 public const string DefaultCultureKey = ".:!DefaultCulture!:.";
51                 
52                 bool isGlobal;
53                 AppResourceFilesCollection files;
54                 string tempDirectory;
55                 string virtualPath;
56                 Dictionary <string, List <string>> cultureFiles;
57                 
58                 string TempDirectory {
59                         get {
60                                 if (tempDirectory != null)
61                                         return tempDirectory;
62                                 return (tempDirectory = AppDomain.CurrentDomain.SetupInformation.DynamicBase);
63                         }
64                 }
65
66                 public Dictionary <string, List <string>> CultureFiles {
67                         get { return cultureFiles; }
68                 }
69                 
70                 public AppResourcesCompiler (HttpContext context)
71                 {
72                         this.isGlobal = true;
73                         this.files = new AppResourceFilesCollection (context);
74                         this.cultureFiles = new Dictionary <string, List <string>> ();
75                 }
76
77                 public AppResourcesCompiler (string virtualPath)
78                 {
79
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>> ();
84                 }
85                 
86                 public Assembly Compile ()
87                 {
88                         files.Collect ();
89                         if (!files.HasFiles)
90                                 return null;
91                         if (isGlobal)
92                                 return CompileGlobal ();
93                         else
94                                 return CompileLocal ();
95                 }
96
97                 Assembly CompileGlobal ()
98                 {
99                         string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
100                                                                              "App_GlobalResources",
101                                                                              "dll",
102                                                                              OnCreateRandomFile) as string;
103
104                         if (assemblyPath == null)
105                                 throw new ApplicationException ("Failed to create global resources assembly");
106                         
107                         List <string>[] fileGroups = GroupGlobalFiles ();
108                         if (fileGroups == null || fileGroups.Length == 0)
109                                 return null;
110                         
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);
118
119                         AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_GlobalResources", assemblyPath,
120                                                                                                this);
121                         CodeDomProvider provider = builder.Provider;
122                         
123                         Dictionary <string,bool> assemblies = new Dictionary<string,bool> ();
124                         foreach (List<string> ls in fileGroups)
125                                 DomFromResource (ls [0], unit, assemblies, provider);
126                         
127                         foreach (KeyValuePair<string,bool> de in assemblies)
128                                 unit.ReferencedAssemblies.Add (de.Key);
129                         
130                         builder.Build (unit);
131                         HttpContext.AppGlobalResourcesAssembly = builder.MainAssembly;
132                         
133                         return builder.MainAssembly;
134                 }
135
136                 Assembly CompileLocal ()
137                 {
138                         if (String.IsNullOrEmpty (virtualPath))
139                                 return null;
140                         
141                         Assembly cached = GetCachedLocalResourcesAssembly (virtualPath);
142                         if (cached != null)
143                                 return cached;
144                         
145                         string prefix;
146                         if (virtualPath == "/")
147                                 prefix = "App_LocalResources.root";
148                         else
149                                 prefix = "App_LocalResources" + virtualPath.Replace ('/', '.');
150                         
151                         string assemblyPath = FileUtils.CreateTemporaryFile (TempDirectory,
152                                                                              prefix,
153                                                                              "dll",
154                                                                              OnCreateRandomFile) as string;
155                         if (assemblyPath == null)
156                                 throw new ApplicationException ("Failed to create local resources assembly");
157
158                         List<AppResourceFileInfo> files = this.files.Files;
159                         foreach (AppResourceFileInfo arfi in files)
160                                 GetResourceFile (arfi, true);
161
162                         AppResourcesAssemblyBuilder builder = new AppResourcesAssemblyBuilder ("App_LocalResources", assemblyPath,
163                                                                                                this);
164                         builder.Build ();
165                         Assembly ret = builder.MainAssembly;
166                         
167                         if (ret != null)
168                                 AddAssemblyToCache (virtualPath, ret);
169
170                         return ret;
171                 }
172                 
173                 internal static Assembly GetCachedLocalResourcesAssembly (string path)
174                 {
175                         Dictionary <string, Assembly> cache;
176
177                         cache = HttpRuntime.InternalCache[cachePrefix] as Dictionary <string, Assembly>;
178                         if (cache == null || !cache.ContainsKey (path))
179                                 return null;
180                         return cache [path];
181                 }
182                 
183                 void AddAssemblyToCache (string path, Assembly asm)
184                 {
185                         Cache runtimeCache = HttpRuntime.InternalCache;
186                         Dictionary <string, Assembly> cache;
187                         
188                         cache = runtimeCache[cachePrefix] as Dictionary <string, Assembly>;
189                         if (cache == null)
190                                 cache = new Dictionary <string, Assembly> ();
191                         cache [path] = asm;
192                         runtimeCache.Insert (cachePrefix, cache);
193                 }
194                 
195                 uint CountChars (char c, string s)
196                 {
197                         uint ret = 0;
198                         foreach (char ch in s) {
199                                 if (ch == c)
200                                         ret++;
201                         }
202                         return ret;
203                 }
204
205                 string IsFileCultureValid (string fileName)
206                 {
207                     string tmp = Path.GetFileNameWithoutExtension (fileName);
208                     tmp = Path.GetExtension (tmp);
209                     if (tmp != null && tmp.Length > 0) {
210                               tmp = tmp.Substring (1);
211                             try {
212                                 CultureInfo.GetCultureInfo (tmp);
213                                 return tmp;
214                             } catch {
215                                 return null;
216                             }
217                     } 
218                     return null;
219                 }
220                 
221                 string GetResourceFile (AppResourceFileInfo arfi, bool local)
222                 {
223                         string resfile;
224                         if (arfi.Kind == AppResourceFileKind.ResX)
225                                 resfile = CompileResource (arfi, local);
226                         else
227                                 resfile = arfi.Info.FullName;
228                         if (!String.IsNullOrEmpty (resfile)) {
229                                 string culture = IsFileCultureValid (resfile);
230                                 if (culture == null)
231                                         culture = DefaultCultureKey;
232                                 
233                                 List <string> cfiles;
234                                 if (cultureFiles.ContainsKey (culture))
235                                         cfiles = cultureFiles [culture];
236                                 else {
237                                         cfiles = new List <string> (1);
238                                         cultureFiles [culture] = cfiles;
239                                 }
240                                 cfiles.Add (resfile);
241                         }
242                                 
243                         return resfile;
244                 }
245                 
246                 List <string>[] GroupGlobalFiles ()
247                 {
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>> ();
251                         
252                         string tmp, s, basename;
253                         uint basedots, filedots;
254                         AppResourceFileInfo defaultFile;
255                         
256                         foreach (AppResourceFileInfo arfi in files) {
257                                 if (arfi.Kind != AppResourceFileKind.ResX && arfi.Kind != AppResourceFileKind.Resource)
258                                         continue;
259
260                                 s = arfi.Info.FullName;
261                                 basename = Path.GetFileNameWithoutExtension (s);
262                                 basedots = CountChars ('.', basename);
263                                 defaultFile = null;
264                                 
265                                 // If there are any files that start with this baseName, we have a default file
266                                 foreach (AppResourceFileInfo fi in files) {
267                                         if (fi.Seen)
268                                                 continue;
269                                         
270                                         string s2 = fi.Info.FullName;
271                                         if (s2 == null || s == s2)
272                                                 continue;
273                                         tmp = Path.GetFileNameWithoutExtension (s2);
274                                         filedots = CountChars ('.', tmp);
275
276                                         if (filedots == basedots + 1 && tmp.StartsWith (basename)) {
277                                                 if (IsFileCultureValid (s2) != null) {
278                                                         // A valid translated file for this name
279                                                         defaultFile = arfi;
280                                                         break;
281                                                 } else {
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.
286                                                         fi.Seen = true;
287                                                 }
288                                         }
289                                 }
290                                 if (defaultFile != null) {
291                                         List<string> al = new List<string> ();
292                                         al.Add (GetResourceFile (arfi, false));
293                                         arfi.Seen = true;
294                                         groups.Add (al);
295                                         
296                                 }
297                         }
298                         groups.Sort (lcList);
299
300                         string tmp2;
301                         // Now find their translated counterparts
302                         foreach (List<string> al in groups) {
303                                 s = al [0];
304                                 tmp = Path.GetFileNameWithoutExtension (s);
305                                 if (tmp.StartsWith ("Resources."))
306                                         tmp = tmp.Substring (10);
307                                 foreach (AppResourceFileInfo arfi in files) {
308                                         if (arfi.Seen)
309                                                 continue;
310                                         s = arfi.Info.FullName;
311                                         if (s == null)
312                                                 continue;
313                                         tmp2 = arfi.Info.Name;
314                                         if (tmp2.StartsWith (tmp)) {
315                                                 al.Add (GetResourceFile (arfi, false));
316                                                 arfi.Seen = true;
317                                         }
318                                 }
319                         }
320
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) {
328                                 if (arfi.Seen)
329                                         continue;
330
331                                 if (IsFileCultureValid (arfi.Info.FullName) != null)
332                                         continue; // Culture found, we reject the file
333
334                                 // A single default file, create a group
335                                 List<string> al = new List<string> ();
336                                 al.Add (GetResourceFile (arfi, false));
337                                 groups.Add (al);
338                         }
339                         groups.Sort (lcList);
340                         return groups.ToArray ();
341                 }
342
343                 // CodeDOM generation
344                 void DomFromResource (string resfile, CodeCompileUnit unit, Dictionary <string,bool> assemblies,
345                                       CodeDomProvider provider)
346                 {
347                         if (String.IsNullOrEmpty (resfile))
348                                 return;
349
350                         string fname, nsname, classname;
351
352                         fname = Path.GetFileNameWithoutExtension (resfile);
353                         nsname = Path.GetFileNameWithoutExtension (fname);
354                         classname = Path.GetExtension (fname);
355                         if (classname == null || classname.Length == 0) {
356                                 classname = nsname;
357                                 nsname = "Resources";
358                         } else {
359                                 if (!nsname.StartsWith ("Resources", StringComparison.InvariantCulture))
360                                         nsname = String.Concat ("Resources.", nsname);
361                                 classname = classname.Substring(1);
362                         }
363
364                         if (!String.IsNullOrEmpty (classname))
365                                 classname = classname.Replace ('.', '_');
366                         if (!String.IsNullOrEmpty (nsname))
367                                 nsname = nsname.Replace ('.', '_');
368                         
369                         if (!provider.IsValidIdentifier (nsname) || !provider.IsValidIdentifier (classname))
370                                 throw new ApplicationException ("Invalid resource file name.");
371
372                         ResourceReader res;
373                         try {
374                                 res = new ResourceReader (resfile);
375                         } catch (ArgumentException) {
376                                 // invalid stream, probably empty - ignore silently and abort
377                                 return;
378                         }
379                         
380                         CodeNamespace ns = new CodeNamespace (nsname);
381                         CodeTypeDeclaration cls = new CodeTypeDeclaration (classname);
382                         cls.IsClass = true;
383                         cls.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
384
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);
389
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);
394                         
395                         // Property: ResourceManager
396                         CodeMemberProperty cmp = new CodeMemberProperty ();
397                         cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
398                         cmp.Name = "ResourceManager";
399                         cmp.HasGet = true;
400                         cmp.Type = new CodeTypeReference (typeof(ResourceManager));
401                         CodePropertyResourceManagerGet (cmp.GetStatements, resfile, classname);
402                         cls.Members.Add (cmp);
403
404                         // Property: Culture
405                         cmp = new CodeMemberProperty ();
406                         cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
407                         cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
408                         cmp.Name = "Culture";
409                         cmp.HasGet = true;
410                         cmp.HasSet = true;
411                         cmp.Type = new CodeTypeReference (typeof(CultureInfo));
412                         CodePropertyGenericGet (cmp.GetStatements, "_culture", classname);
413                         CodePropertyGenericSet (cmp.SetStatements, "_culture", classname);
414                         cls.Members.Add (cmp);
415
416                         // Add the resource properties
417                         Dictionary<string,bool> imports = new Dictionary<string,bool> ();
418                         try {
419                                 foreach (DictionaryEntry de in res) {
420                                         Type type = de.Value.GetType ();
421
422                                         if (!imports.ContainsKey (type.Namespace))
423                                                 imports [type.Namespace] = true;
424
425                                         string asname = new AssemblyName (type.Assembly.FullName).Name;
426                                         if (!assemblies.ContainsKey (asname))
427                                                 assemblies [asname] = true;
428                                         
429                                         cmp = new CodeMemberProperty ();
430                                         cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
431                                         cmp.Name = SanitizeResourceName ((string)de.Key);
432                                         cmp.HasGet = true;
433                                         CodePropertyResourceGet (cmp.GetStatements, (string)de.Key, type, classname);
434                                         cmp.Type = new CodeTypeReference (type);
435                                         cls.Members.Add (cmp);
436                                 }
437                         } catch (Exception ex) {
438                                 throw new ApplicationException ("Failed to compile global resources.", ex);
439                         }
440                         foreach (KeyValuePair<string,bool> de in imports)
441                                 ns.Imports.Add (new CodeNamespaceImport(de.Key));
442                         
443                         ns.Types.Add (cls);
444                         unit.Namespaces.Add (ns);
445                 }
446
447                 string SanitizeResourceName (string name)
448                 {
449                         return name.Replace (' ', '_').Replace ('-', '_').Replace ('.', '_');
450                 }
451                 
452                 CodeObjectCreateExpression NewResourceManager (string name, string typename)
453                 {
454                         CodeExpression resname = new CodePrimitiveExpression (name);
455                         CodePropertyReferenceExpression asm = new CodePropertyReferenceExpression (
456                                 new CodeTypeOfExpression (new CodeTypeReference (typename)),
457                                 "Assembly");
458                         
459                         return new CodeObjectCreateExpression ("System.Resources.ResourceManager",
460                                                                new CodeExpression [] {resname, asm});
461                 }
462                 
463                 void CodePropertyResourceManagerGet (CodeStatementCollection csc, string resfile, string typename)
464                 {
465                         string name = Path.GetFileNameWithoutExtension (resfile);
466                         CodeStatement st;
467                         CodeExpression exp;
468
469                         exp = new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), "_resourceManager");
470                         st = new CodeConditionStatement (
471                                 new CodeBinaryOperatorExpression (
472                                         exp,
473                                         CodeBinaryOperatorType.IdentityInequality,
474                                         new CodePrimitiveExpression (null)),
475                                 new CodeStatement [] { new CodeMethodReturnStatement (exp) });
476                         csc.Add (st);
477
478                         st = new CodeAssignStatement (exp, NewResourceManager (name, typename));
479                         csc.Add (st);
480                         csc.Add (new CodeMethodReturnStatement (exp));
481                 }
482
483                 void CodePropertyResourceGet (CodeStatementCollection csc, string resname, Type restype, string typename)
484                 {
485                         CodeStatement st = new CodeVariableDeclarationStatement (
486                                 typeof (ResourceManager),
487                                 "rm",
488                                 new CodePropertyReferenceExpression (
489                                         new CodeTypeReferenceExpression (typename), "ResourceManager"));
490                         csc.Add (st);
491
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)) });
498                         csc.Add (st);
499
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 (
508                                 restype,
509                                 "obj",
510                                 gotstr ? exp : new CodeCastExpression (restype, exp));
511                         csc.Add (st);
512                         csc.Add (new CodeMethodReturnStatement (new CodeVariableReferenceExpression ("obj")));
513                 }
514                 
515                 void CodePropertyGenericGet (CodeStatementCollection csc, string field, string typename)
516                 {
517                         csc.Add(new CodeMethodReturnStatement (
518                                         new CodeFieldReferenceExpression (
519                                                 new CodeTypeReferenceExpression (typename), field)));
520                 }
521
522                 void CodePropertyGenericSet (CodeStatementCollection csc, string field, string typename)
523                 {
524                         csc.Add(new CodeAssignStatement (
525                                         new CodeFieldReferenceExpression (new CodeTypeReferenceExpression (typename), field),
526                                         new CodeVariableReferenceExpression ("value")));
527                 }
528                 
529                 string CompileResource (AppResourceFileInfo arfi, bool local)
530                 {
531                         string path = arfi.Info.FullName;
532                         string rname = Path.GetFileNameWithoutExtension (path) + ".resources";
533                         if (!local)
534                                 rname = "Resources." + rname;
535                         
536                         string resource = Path.Combine (TempDirectory, rname);
537                         FileStream source = null, destination = null;
538                         IResourceReader reader = null;
539                         ResourceWriter writer = null;
540
541                         try {
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;
548                                         if (val is string)
549                                                 writer.AddResource ((string)de.Key, (string)val);
550                                         else
551                                                 writer.AddResource ((string)de.Key, val);
552                                 }
553                         } catch (Exception ex) {
554                                 throw new HttpException ("Failed to compile resource file", ex);
555                         } finally {
556                                 if (reader != null)
557                                         reader.Close ();
558                                 else if (source != null)
559                                         source.Close ();
560                                 if (writer != null)
561                                         writer.Close ();
562                                 else if (destination != null)
563                                         destination.Close ();
564                         }
565                         
566                         return resource;
567                 }
568
569                 IResourceReader GetReaderForKind (AppResourceFileKind kind, Stream stream)
570                 {
571                         switch (kind) {
572                                 case AppResourceFileKind.ResX:
573                                         return new ResXResourceReader (stream);
574
575                                 case AppResourceFileKind.Resource:
576                                         return new ResourceReader (stream);
577
578                                 default:
579                                         return null;
580                         }
581                 }
582                 
583                                                                
584                 object OnCreateRandomFile (string path)
585                 {
586                         FileStream f = new FileStream (path, FileMode.CreateNew);
587                         f.Close ();
588                         return path;
589                 }
590         };
591 };
592 #endif