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