Update Reference Sources to .NET Framework 4.6.1
[mono.git] / mcs / class / referencesource / System.Web / Routing / RouteCollection.cs
1 namespace System.Web.Routing {
2     using System.Collections.Generic;
3     using System.Collections.ObjectModel;
4     using System.Diagnostics.CodeAnalysis;
5     using System.Globalization;
6     using System.Runtime.CompilerServices;
7     using System.Threading;
8     using System.Web.Hosting;
9
10     [TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")]
11     public class RouteCollection : Collection<RouteBase> {
12         private Dictionary<string, RouteBase> _namedMap = new Dictionary<string, RouteBase>(StringComparer.OrdinalIgnoreCase);
13         private VirtualPathProvider _vpp;
14
15         private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
16
17         public RouteCollection() {
18         }
19
20         public RouteCollection(VirtualPathProvider virtualPathProvider) {
21             VPP = virtualPathProvider;
22         }
23
24         public bool AppendTrailingSlash {
25             get;
26             set;
27         }
28
29         public bool LowercaseUrls {
30             get;
31             set;
32         }
33
34         public bool RouteExistingFiles {
35             get;
36             set;
37         }
38
39         private VirtualPathProvider VPP {
40             get {
41                 if (_vpp == null) {
42                     return HostingEnvironment.VirtualPathProvider;
43                 }
44                 return _vpp;
45             }
46             set {
47                 _vpp = value;
48             }
49         }
50
51         public RouteBase this[string name] {
52             get {
53                 if (String.IsNullOrEmpty(name)) {
54                     return null;
55                 }
56                 RouteBase route;
57                 if (_namedMap.TryGetValue(name, out route)) {
58                     return route;
59                 }
60                 return null;
61             }
62         }
63
64         public void Add(string name, RouteBase item) {
65             if (item == null) {
66                 throw new ArgumentNullException("item");
67             }
68
69             if (!String.IsNullOrEmpty(name)) {
70                 if (_namedMap.ContainsKey(name)) {
71                     throw new ArgumentException(
72                         String.Format(
73                             CultureInfo.CurrentUICulture,
74                             SR.GetString(SR.RouteCollection_DuplicateName),
75                             name),
76                         "name");
77                 }
78             }
79
80             Add(item);
81             if (!String.IsNullOrEmpty(name)) {
82                 _namedMap[name] = item;
83             }
84         }
85
86         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
87             Justification = "Warning was suppressed for consistency with existing similar routing API")]
88         public Route MapPageRoute(string routeName, string routeUrl, string physicalFile) {
89             return MapPageRoute(routeName, routeUrl, physicalFile, true /* checkPhysicalUrlAccess */, null, null, null);
90         }
91
92         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
93             Justification = "Warning was suppressed for consistency with existing similar routing API")]
94         public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess) {
95             return MapPageRoute(routeName, routeUrl, physicalFile, checkPhysicalUrlAccess, null, null, null);
96         }
97
98         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
99             Justification = "Warning was suppressed for consistency with existing similar routing API")]
100         public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults) {
101             return MapPageRoute(routeName, routeUrl, physicalFile, checkPhysicalUrlAccess, defaults, null, null);
102         }
103
104         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
105             Justification = "Warning was suppressed for consistency with existing similar routing API")]
106         public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults, RouteValueDictionary constraints) {
107             return MapPageRoute(routeName, routeUrl, physicalFile, checkPhysicalUrlAccess, defaults, constraints, null);
108         }
109
110         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
111             Justification = "Warning was suppressed for consistency with existing similar routing API")]
112         public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens) {
113             if (routeUrl == null) {
114                 throw new ArgumentNullException("routeUrl");
115             }
116             Route route = new Route(routeUrl, defaults, constraints, dataTokens, new PageRouteHandler(physicalFile, checkPhysicalUrlAccess));
117             Add(routeName, route);
118             return route;
119         }
120
121         [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
122         protected override void ClearItems() {
123             _namedMap.Clear();
124             base.ClearItems();
125         }
126
127         [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not worth a breaking change.")]
128         public IDisposable GetReadLock() {
129             _rwLock.EnterReadLock();
130             return new ReadLockDisposable(_rwLock);
131         }
132
133         private RequestContext GetRequestContext(RequestContext requestContext) {
134             if (requestContext != null) {
135                 return requestContext;
136             }
137             HttpContext httpContext = HttpContext.Current;
138             if (httpContext == null) {
139                 throw new InvalidOperationException(SR.GetString(SR.RouteCollection_RequiresContext));
140             }
141             return new RequestContext(new HttpContextWrapper(httpContext), new RouteData());
142         }
143
144         // Returns true if this is a request to an existing file
145         private bool IsRouteToExistingFile(HttpContextBase httpContext) {
146             string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
147             return ((requestPath != "~/") &&
148                 (VPP != null) &&
149                 (VPP.FileExists(requestPath) ||
150                 VPP.DirectoryExists(requestPath)));
151         }
152
153         public RouteData GetRouteData(HttpContextBase httpContext) {
154             if (httpContext == null) {
155                 throw new ArgumentNullException("httpContext");
156             }
157             if (httpContext.Request == null) {
158                 throw new ArgumentException(SR.GetString(SR.RouteTable_ContextMissingRequest), "httpContext");
159             }
160
161             // Optimize performance when the route collection is empty.  The main improvement is that we avoid taking
162             // a read lock when the collection is empty.  Without this check, the UrlRoutingModule causes a 25%-50%
163             // regression in HelloWorld RPS due to lock contention.  The UrlRoutingModule is now in the root web.config,
164             // so we need to ensure the module is performant, especially when you are not using routing.
165             // This check does introduce a slight bug, in that if a writer clears the collection as part of a write
166             // transaction, a reader may see the collection when it's empty, which the read lock is supposed to prevent.
167             // We will investigate a better fix in Dev10 Beta2.  The Beta1 bug is Dev10 652986.
168             if (Count == 0) {
169                 return null;
170             }
171
172             bool isRouteToExistingFile = false;
173             bool doneRouteCheck = false; // We only want to do the route check once
174             if (!RouteExistingFiles) {
175                 isRouteToExistingFile = IsRouteToExistingFile(httpContext);
176                 doneRouteCheck = true;
177                 if (isRouteToExistingFile) {
178                     // If we're not routing existing files and the file exists, we stop processing routes
179                     return null;
180                 }
181             }
182
183             // Go through all the configured routes and find the first one that returns a match
184             using (GetReadLock()) {
185                 foreach (RouteBase route in this) {
186                     RouteData routeData = route.GetRouteData(httpContext);
187                     if (routeData != null) {
188                         // If we're not routing existing files on this route and the file exists, we also stop processing routes
189                         if (!route.RouteExistingFiles) {
190                             if (!doneRouteCheck) {
191                                 isRouteToExistingFile = IsRouteToExistingFile(httpContext);
192                                 doneRouteCheck = true;
193                             }
194                             if (isRouteToExistingFile) {
195                                 return null;
196                             }
197                         }
198                         return routeData;
199                     }
200                 }
201             }
202
203             return null;
204         }
205
206
207         [SuppressMessage("Microsoft.Globalization", "CA1307:SpecifyStringComparison", MessageId = "System.String.EndsWith(System.String)", Justification = @"okay")]
208         private string NormalizeVirtualPath(RequestContext requestContext, string virtualPath) {
209             string url = System.Web.UI.Util.GetUrlWithApplicationPath(requestContext.HttpContext, virtualPath);
210
211             if (LowercaseUrls || AppendTrailingSlash) {
212                 int iqs = url.IndexOfAny(new char[] { '?', '#' });
213                 string urlWithoutQs;
214                 string qs;
215                 if (iqs >= 0) {
216                     urlWithoutQs = url.Substring(0, iqs);
217                     qs = url.Substring(iqs);
218                 }
219                 else {
220                     urlWithoutQs = url;
221                     qs = "";
222                 }
223
224                 // Don't lowercase the query string
225                 if (LowercaseUrls) {
226                     urlWithoutQs = urlWithoutQs.ToLowerInvariant();
227                 }
228
229                 if (AppendTrailingSlash && !urlWithoutQs.EndsWith("/")) {
230                     urlWithoutQs += "/";
231                 }
232
233                 url = urlWithoutQs + qs;
234             }
235
236             return url;
237         }
238
239         public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
240             requestContext = GetRequestContext(requestContext);
241
242             // Go through all the configured routes and find the first one that returns a match
243             using (GetReadLock()) {
244                 foreach (RouteBase route in this) {
245                     VirtualPathData vpd = route.GetVirtualPath(requestContext, values);
246                     if (vpd != null) {
247                         vpd.VirtualPath = NormalizeVirtualPath(requestContext, vpd.VirtualPath);
248                         return vpd;
249                     }
250                 }
251             }
252
253             return null;
254         }
255
256         public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values) {
257             requestContext = GetRequestContext(requestContext);
258
259             if (!String.IsNullOrEmpty(name)) {
260                 RouteBase namedRoute;
261                 bool routeFound;
262                 using (GetReadLock()) {
263                     routeFound = _namedMap.TryGetValue(name, out namedRoute);
264                 }
265                 if (routeFound) {
266                     VirtualPathData vpd = namedRoute.GetVirtualPath(requestContext, values);
267                     if (vpd != null) {
268                         vpd.VirtualPath = NormalizeVirtualPath(requestContext, vpd.VirtualPath);
269                         return vpd;
270                     }
271                     return null;
272                 }
273                 else {
274                     throw new ArgumentException(
275                         String.Format(
276                             CultureInfo.CurrentUICulture,
277                             SR.GetString(SR.RouteCollection_NameNotFound),
278                             name),
279                         "name");
280                 }
281             }
282             else {
283                 return GetVirtualPath(requestContext, values);
284             }
285         }
286
287         [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not worth a breaking change.")]
288         public IDisposable GetWriteLock() {
289             _rwLock.EnterWriteLock();
290             return new WriteLockDisposable(_rwLock);
291         }
292
293         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", 
294             Justification = "This is not a regular URL as it may contain special routing characters.")]
295         public void Ignore(string url) {
296             Ignore(url, null /* constraints */);
297         }
298
299         [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", 
300             Justification = "This is not a regular URL as it may contain special routing characters.")]
301         public void Ignore(string url, object constraints) {
302             if (url == null) {
303                 throw new ArgumentNullException("url");
304             }
305
306             IgnoreRouteInternal route = new IgnoreRouteInternal(url) {
307                 Constraints = new RouteValueDictionary(constraints)
308             };
309
310             Add(route);
311         }
312
313         [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
314         protected override void InsertItem(int index, RouteBase item) {
315             if (item == null) {
316                 throw new ArgumentNullException("item");
317             }
318
319             if (Contains(item)) {
320                 throw new ArgumentException(
321                     String.Format(
322                         CultureInfo.CurrentCulture,
323                         SR.GetString(SR.RouteCollection_DuplicateEntry)),
324                     "item");
325             }
326             base.InsertItem(index, item);
327         }
328
329         [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
330         protected override void RemoveItem(int index) {
331             RemoveRouteName(index);
332             base.RemoveItem(index);
333         }
334
335         private void RemoveRouteName(int index) {
336             // Search for the specified route and clear out its name if we have one
337             RouteBase route = this[index];
338             foreach (KeyValuePair<string, RouteBase> namedRoute in _namedMap) {
339                 if (namedRoute.Value == route) {
340                     _namedMap.Remove(namedRoute.Key);
341                     break;
342                 }
343             }
344         }
345
346         [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
347         protected override void SetItem(int index, RouteBase item) {
348             if (item == null) {
349                 throw new ArgumentNullException("item");
350             }
351
352             if (Contains(item)) {
353                 throw new ArgumentException(
354                     String.Format(
355                         CultureInfo.CurrentCulture,
356                         SR.GetString(SR.RouteCollection_DuplicateEntry)),
357                     "item");
358             }
359             RemoveRouteName(index);
360             base.SetItem(index, item);
361         }
362
363         private class ReadLockDisposable : IDisposable {
364
365             private ReaderWriterLockSlim _rwLock;
366
367             public ReadLockDisposable(ReaderWriterLockSlim rwLock) {
368                 _rwLock = rwLock;
369             }
370
371             [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly",
372                 Justification = "Type does not have a finalizer.")]
373             void IDisposable.Dispose() {
374                 _rwLock.ExitReadLock();
375             }
376         }
377
378         private class WriteLockDisposable : IDisposable {
379
380             private ReaderWriterLockSlim _rwLock;
381
382             public WriteLockDisposable(ReaderWriterLockSlim rwLock) {
383                 _rwLock = rwLock;
384             }
385
386             [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly",
387                 Justification = "Type does not have a finalizer.")]
388             void IDisposable.Dispose() {
389                 _rwLock.ExitWriteLock();
390             }
391         }
392
393         private sealed class IgnoreRouteInternal : Route {
394             public IgnoreRouteInternal(string url)
395                 : base(url, new StopRoutingHandler()) {
396             }
397
398             public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) {
399                 // Never match during route generation. This avoids the scenario where an IgnoreRoute with
400                 // fairly relaxed constraints ends up eagerly matching all generated URLs.
401                 return null;
402             }
403         }
404     }
405 }