2010-05-05 Marek Habersack <mhabersack@novell.com>
[mono.git] / mcs / class / System.Web / System.Web / XmlSiteMapProvider.cs
1 //
2 // System.Web.XmlSiteMapProvider
3 //
4 // Authors:
5 //      Ben Maurer (bmaurer@users.sourceforge.net)
6 //      Lluis Sanchez Gual (lluis@novell.com)
7 //      Marek Habersack <mhabersack@novell.com>
8 //
9 // (C) 2003 Ben Maurer
10 // (C) 2005-2009 Novell, Inc (http://www.novell.com)
11 //
12
13 //
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 // 
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 // 
25 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33
34 using System.Collections;
35 using System.Collections.Generic;
36 using System.Collections.Specialized;
37 using System.Configuration;
38 using System.Configuration.Provider;
39 using System.Globalization;
40 using System.Text;
41 using System.Xml;
42 using System.Web.Hosting;
43 using System.Web.Util;
44 using System.IO;
45
46 namespace System.Web
47 {
48         public class XmlSiteMapProvider : StaticSiteMapProvider, IDisposable
49         {
50                 static readonly char [] seperators = { ';', ',' };
51                 
52                 bool initialized;
53                 string fileVirtualPath;
54                 SiteMapNode root = null;
55                 List <FileSystemWatcher> watchers;
56                 Dictionary <string, bool> _childProvidersPresent;
57                 List <SiteMapProvider> _childProviders;
58                 
59                 Dictionary <string, bool> ChildProvidersPresent {
60                         get {
61                                 if (_childProvidersPresent == null)
62                                         _childProvidersPresent = new Dictionary <string, bool> ();
63
64                                 return _childProvidersPresent;
65                         }
66                 }
67
68                 List <SiteMapProvider> ChildProviders {
69                         get {
70                                 if (_childProviders == null)
71                                         _childProviders = new List <SiteMapProvider> ();
72
73                                 return _childProviders;
74                         }
75                 }
76                 
77                 protected internal override void AddNode (SiteMapNode node, SiteMapNode parentNode)
78                 {
79                         if (node == null)
80                                 throw new ArgumentNullException ("node");
81
82                         if (parentNode == null)
83                                 throw new ArgumentNullException ("parentNode");
84
85                         SiteMapProvider nodeProvider = node.Provider;
86                         if (nodeProvider != this)
87                                 throw new ArgumentException ("SiteMapNode '" + node + "' cannot be found in current provider, only nodes in the same provider can be added.",
88                                                              "node");
89
90                         SiteMapProvider parentNodeProvider = parentNode.Provider;
91                         if (nodeProvider != parentNodeProvider)
92                                 throw new ArgumentException ("SiteMapNode '" + parentNode + "' cannot be found in current provider, only nodes in the same provider can be added.",
93                                                              "parentNode");
94
95                         AddNodeNoCheck (node, parentNode);
96                 }
97
98                 void AddNodeNoCheck (SiteMapNode node, SiteMapNode parentNode)
99                 {
100                         base.AddNode (node, parentNode);
101                         SiteMapProvider nodeProvider = node.Provider;
102                         if (nodeProvider != this)
103                                 RegisterChildProvider (nodeProvider.Name, nodeProvider);
104                 }
105                 
106                 protected virtual void AddProvider (string providerName, SiteMapNode parentNode)
107                 {
108                         if (parentNode == null)
109                                 throw new ArgumentNullException ("parentNode");
110
111                         if (parentNode.Provider != this)
112                                 throw new ArgumentException ("The Provider property of the parentNode does not reference the current provider.", "parentNode");
113
114                         SiteMapProvider smp = SiteMap.Providers [providerName];
115                         if (smp == null)
116                                 throw new ProviderException ("Provider with name [" + providerName + "] was not found.");
117
118                         AddNode (smp.GetRootNodeCore ());
119                         RegisterChildProvider (providerName, smp);
120                 }
121
122                 void RegisterChildProvider (string name, SiteMapProvider smp)
123                 {
124                         Dictionary <string, bool> childProvidersPresent = ChildProvidersPresent;
125                         
126                         if (childProvidersPresent.ContainsKey (name))
127                                 return;
128
129                         childProvidersPresent.Add (name, true);
130                         ChildProviders.Add (smp);
131                 }
132                 
133                 XmlNode FindStartingNode (string virtualPath, out bool enableLocalization)
134                 {
135                         XmlDocument d = GetConfigDocument (virtualPath);
136                         XmlElement docElement = d.DocumentElement;
137
138                         if (String.Compare ("siteMap", docElement.Name, StringComparison.Ordinal) != 0)
139                                 throw new ConfigurationErrorsException ("Top element must be 'siteMap'");
140                         
141                         XmlNode enloc = docElement.Attributes ["enableLocalization"];
142                         if (enloc != null && !String.IsNullOrEmpty (enloc.Value))
143                                 enableLocalization = (bool) Convert.ChangeType (enloc.Value, typeof (bool));
144                         else
145                                 enableLocalization = false;
146
147                         XmlNodeList childNodes = docElement.ChildNodes;
148                         XmlNode node = null;
149                         
150                         foreach (XmlNode child in childNodes) {
151                                 if (String.Compare ("siteMapNode", child.Name, StringComparison.Ordinal) != 0)
152                                         // Only <siteMapNode> is allowed at the top
153                                         throw new ConfigurationErrorsException ("Only <siteMapNode> elements are allowed at the document top level.");
154                                 
155                                 if (node != null)
156                                         // Only one <siteMapNode> is allowed at the top
157                                         throw new ConfigurationErrorsException ("Only one <siteMapNode> element is allowed at the document top level.");
158                                 
159                                 node = child;
160                         }
161                         
162                         if (node == null)
163                                 throw new ConfigurationErrorsException ("Missing <siteMapNode> element at the document top level.");
164                         
165                         return node;
166                 }
167
168                 XmlDocument GetConfigDocument (string virtualPath)
169                 {
170                         if (String.IsNullOrEmpty (virtualPath))
171                                 throw new ArgumentException ("The siteMapFile attribute must be specified on the XmlSiteMapProvider");
172                         
173                         string file = HostingEnvironment.MapPath (virtualPath);
174                         if (file == null)
175                                 throw new HttpException ("Virtual path '" + virtualPath + "' cannot be mapped to physical path.");
176                         
177                         if (String.Compare (Path.GetExtension (file), ".sitemap", RuntimeHelpers.StringComparison) != 0)
178                                 throw new InvalidOperationException (String.Format ("The file {0} has an invalid extension, only .sitemap files are allowed in XmlSiteMapProvider.",
179                                                                                     String.IsNullOrEmpty (virtualPath) ? Path.GetFileName (file) : virtualPath));
180                         
181                         if (!File.Exists (file))
182                                 throw new InvalidOperationException (String.Format ("The file '{0}' required by XmlSiteMapProvider does not exist.",
183                                                                                     String.IsNullOrEmpty (virtualPath) ? Path.GetFileName (file) : virtualPath));
184
185                         ResourceKey = Path.GetFileName (file);
186                         CreateWatcher (file);
187                         
188                         XmlDocument d = new XmlDocument ();
189                         d.Load (file);
190
191                         return d;
192                 }
193
194                 public override SiteMapNode BuildSiteMap ()
195                 {
196                         if (root != null)
197                                 return root;
198                         
199                         // Whenever you call AddNode, it tries to find dups, and will call this method
200                         // Is this a bug in MS??
201                         lock (this_lock) {
202                                 if (root != null)
203                                         return root;
204
205                                 Clear ();
206                                 bool enableLocalization;
207                                 XmlNode node = FindStartingNode (fileVirtualPath, out enableLocalization);
208                                 EnableLocalization = enableLocalization;
209                                 BuildSiteMapRecursive (node, null);
210
211                                 // if (builtRoot != root) {
212                                 //      root = builtRoot;
213                                 //      AddNode (root);
214                                 // }
215
216                                 return root;
217                         }
218                 }
219
220                 SiteMapNode ConvertToSiteMapNode (XmlNode xmlNode)
221                 {
222                         bool localize = EnableLocalization;
223                         string url = GetOptionalAttribute (xmlNode, "url");
224                         string title = GetOptionalAttribute (xmlNode, "title");
225                         string description = GetOptionalAttribute (xmlNode, "description");
226                         string roles = GetOptionalAttribute (xmlNode, "roles");
227                         string implicitResourceKey = GetOptionalAttribute (xmlNode, "resourceKey");
228                                 
229                         // var keywordsList = new List <string> ();
230                         // if (keywords != null && keywords.Length > 0) {
231                         //      foreach (string s in keywords.Split (seperators)) {
232                         //              string ss = s.Trim ();
233                         //              if (ss.Length > 0)
234                         //                      keywordsList.Add (ss);
235                         //      }
236                         // }
237                                 
238                         var rolesList = new List <string> ();
239                         if (roles != null && roles.Length > 0) {
240                                 foreach (string s in roles.Split (seperators)) {
241                                         string ss = s.Trim ();
242                                         if (ss.Length > 0)
243                                                 rolesList.Add (ss);
244                                 }
245                         }
246
247                         url = base.MapUrl (url);
248
249                         NameValueCollection attributes = null;
250                         NameValueCollection explicitResourceKeys = null;
251                         if (localize)
252                                 CollectLocalizationInfo (xmlNode, ref title, ref description, ref attributes, ref explicitResourceKeys);
253                         else
254                                 foreach (XmlNode att in xmlNode.Attributes)
255                                         PutInCollection (att.Name, att.Value, ref attributes);
256
257                         string key = Guid.NewGuid ().ToString ();
258                         return new SiteMapNode (this, key, url, title, description, rolesList.AsReadOnly (),
259                                                 attributes, explicitResourceKeys, implicitResourceKey);         
260                 }
261
262                 void BuildSiteMapRecursive (XmlNode xmlNode, SiteMapNode parent)
263                 {
264                         if (xmlNode.Name != "siteMapNode")
265                                 throw new ConfigurationException ("incorrect element name", xmlNode);
266                         
267                         string attrValue = GetNonEmptyOptionalAttribute (xmlNode, "provider");
268                         if (attrValue != null) {
269                                 SiteMapProvider provider = SiteMap.Providers [attrValue];
270                                 if (provider == null)
271                                         throw new ProviderException ("Provider with name [" + attrValue + "] was not found.");
272
273                                 provider.ParentProvider = this;
274                                 SiteMapNode providerRoot = provider.GetRootNodeCore();
275
276                                 if (parent == null)
277                                         root = providerRoot;
278                                 else
279                                         AddNodeNoCheck (providerRoot, parent);
280                                 return;
281                         }
282
283                         attrValue = GetNonEmptyOptionalAttribute (xmlNode, "siteMapFile");
284                         if (attrValue != null) {
285                                 var nvc = new NameValueCollection ();
286                                 nvc.Add ("siteMapFile", attrValue);
287
288                                 string description = GetOptionalAttribute (xmlNode, "description");
289                                 if (!String.IsNullOrEmpty (description))
290                                         nvc.Add ("description", description);
291
292                                 string name = MapUrl (attrValue);                               
293                                 var provider = new XmlSiteMapProvider ();
294                                 provider.Initialize (name, nvc);
295                                 
296                                 SiteMapNode providerRoot = provider.GetRootNodeCore ();
297                                 if (parent == null)
298                                         root = providerRoot;
299                                 else
300                                         AddNodeNoCheck (providerRoot, parent);
301                                 return;
302                         }
303
304                         SiteMapNode curNode = ConvertToSiteMapNode (xmlNode);
305                         if (parent == null)
306                                 root = curNode;
307                         else
308                                 AddNodeNoCheck (curNode, parent);
309                         
310                         XmlNodeList childNodes = xmlNode.ChildNodes;
311                         if (childNodes == null || childNodes.Count < 1)
312                                 return;
313                         
314                         foreach (XmlNode child in childNodes) {
315                                 if (child.NodeType != XmlNodeType.Element)
316                                         continue;
317
318                                 BuildSiteMapRecursive (child, curNode);
319                         }
320                 }
321
322                 string GetNonEmptyOptionalAttribute (XmlNode n, string name)
323                 {
324                         return System.Web.Configuration.HandlersUtil.ExtractAttributeValue (name, n, true);
325                 }
326                 
327                 string GetOptionalAttribute (XmlNode n, string name)
328                 {
329                         return System.Web.Configuration.HandlersUtil.ExtractAttributeValue (name, n, true, true);
330                 }
331
332                 void PutInCollection (string name, string value, ref NameValueCollection coll)
333                 {
334                         PutInCollection (name, null, value, ref coll);
335                 }
336                 
337                 void PutInCollection (string name, string classKey, string value, ref NameValueCollection coll)
338                 {
339                         if (coll == null)
340                                 coll = new NameValueCollection ();
341                         if (!String.IsNullOrEmpty (classKey))
342                                 coll.Add (name, classKey);
343                         coll.Add (name, value);
344                 }
345
346                 bool GetAttributeLocalization (string value, out string resClass, out string resKey, out string resDefault)
347                 {
348                         resClass = null;
349                         resKey = null;
350                         resDefault = null;
351
352                         if (String.IsNullOrEmpty (value))
353                                 return false;
354                         string val = value.TrimStart (new char[] {' ', '\t'});
355                         if (val.Length < 11 ||
356                                 String.Compare (val, 0, "$resources:", 0, 11, StringComparison.InvariantCultureIgnoreCase) != 0)
357                                 return false;
358
359                         val = val.Substring (11);
360                         if (val.Length == 0)
361                                 return false;
362                         string[] parts = val.Split (',');
363                         if (parts.Length < 2)
364                                 return false;
365                         resClass = parts [0].Trim ();
366                         resKey = parts [1].Trim ();
367                         if (parts.Length == 3)
368                                 resDefault = parts [2];
369                         else if (parts.Length > 3)
370                                 resDefault = String.Join (",", parts, 2, parts.Length - 2);
371
372                         return true;
373                 }
374                 
375                 void CollectLocalizationInfo (XmlNode xmlNode, ref string title, ref string description,
376                                               ref NameValueCollection attributes,
377                                               ref NameValueCollection explicitResourceKeys)
378                 {
379                         string resClass;
380                         string resKey;
381                         string resDefault;
382
383                         if (GetAttributeLocalization (title, out resClass, out resKey, out resDefault)) {
384                                 PutInCollection ("title", resClass, resKey, ref explicitResourceKeys);
385                                 title = resDefault;
386                         }
387                         
388                         if (GetAttributeLocalization (description, out resClass, out resKey, out resDefault)) {
389                                 PutInCollection ("description", resClass, resKey, ref explicitResourceKeys);
390                                 description = resDefault;
391                         }
392
393                         string value;
394                         foreach (XmlNode att in xmlNode.Attributes) {
395                                 if (GetAttributeLocalization (att.Value, out resClass, out resKey, out resDefault)) {
396                                         PutInCollection (att.Name, resClass, resKey, ref explicitResourceKeys);
397                                         value = resDefault;
398                                 } else
399                                         value = att.Value;
400                                 PutInCollection (att.Name, value, ref attributes);
401                         }
402                 }
403
404                 protected override void Clear ()
405                 {
406                         base.Clear ();
407                         root = null;
408                         ChildProviders.Clear ();
409                         ChildProvidersPresent.Clear ();
410                 }
411
412                 protected virtual void Dispose (bool disposing)
413                 {
414                         if (disposing) {
415                                 foreach (FileSystemWatcher watcher in watchers)
416                                         watcher.Dispose ();
417                                 watchers = null;
418                         }
419                 }
420
421                 public void Dispose ()
422                 {
423                         Dispose (true);
424                 }
425                 
426                 public override SiteMapNode FindSiteMapNode (string rawUrl)
427                 {
428                         string url = base.MapUrl (rawUrl);
429                         SiteMapNode node = base.FindSiteMapNode (url);
430                         if (node != null)
431                                 return node;
432
433                         foreach (SiteMapProvider smp in ChildProviders) {
434                                 node = smp.FindSiteMapNode (url);
435                                 if (node != null)
436                                         return node;
437                         }
438
439                         return null;
440                 }
441
442                 public override SiteMapNode FindSiteMapNodeFromKey (string key)
443                 {
444                         SiteMapNode node = base.FindSiteMapNodeFromKey (key);
445                         if (node != null)
446                                 return node;
447
448                         foreach (SiteMapProvider smp in ChildProviders) {
449                                 node = smp.FindSiteMapNodeFromKey (key);
450                                 if (node != null)
451                                         return node;
452                         }
453
454                         return null;
455                 }
456
457                 public override void Initialize (string name, NameValueCollection attributes)
458                 {
459                         if (initialized)
460                                 throw new InvalidOperationException ("XmlSiteMapProvider cannot be initialized twice.");
461
462                         initialized = true;
463                         if (attributes != null) {
464                                 foreach (string key in attributes.AllKeys) {
465                                         switch (key) {
466                                                 case "siteMapFile":
467                                                         fileVirtualPath = base.MapUrl (attributes ["siteMapFile"]);
468                                                         break;
469
470                                                 case "description":
471                                                 case "securityTrimmingEnabled":
472                                                         break;
473                                                         
474                                                 default:
475                                                         throw new ConfigurationErrorsException ("The attribute '" + key + "' is unexpected in the configuration of the '" + name + "' provider.");
476                                         }
477                                 }
478                         }
479                         
480                         base.Initialize (name, attributes != null ? attributes : new NameValueCollection ());
481                 }
482
483                 void CreateWatcher (string file)
484                 {
485                         var watcher = new FileSystemWatcher ();
486                         watcher.NotifyFilter |= NotifyFilters.Size;
487                         watcher.Path = Path.GetFullPath (Path.GetDirectoryName (file));
488                         watcher.Filter = Path.GetFileName (file);
489                         watcher.Changed += new FileSystemEventHandler (OnFileChanged);
490                         watcher.EnableRaisingEvents = true;
491
492                         if (watchers == null)
493                                 watchers = new List <FileSystemWatcher> ();
494                         
495                         watchers.Add (watcher);
496                 }
497                 
498                 protected override void RemoveNode (SiteMapNode node)
499                 {
500                         base.RemoveNode (node);
501                 }
502
503                 [MonoTODO ("Not implemented")]
504                 protected virtual void RemoveProvider (string providerName)
505                 {
506                         throw new NotImplementedException ();
507                 }
508
509                 void OnFileChanged (object sender, FileSystemEventArgs args)
510                 {
511                         Clear ();
512                 }
513
514                 public override SiteMapNode RootNode {
515                         get {
516                                 BuildSiteMap ();
517                                 return root;
518                         }
519                 }
520                 
521                 protected internal override SiteMapNode GetRootNodeCore ()
522                 {
523                         return BuildSiteMap ();
524                 }
525         }
526
527 }
528