New test.
[mono.git] / mcs / class / System.Web.Mvc2 / System.Web.Mvc / Async / AsyncActionMethodSelector.cs
1 /* ****************************************************************************\r
2  *\r
3  * Copyright (c) Microsoft Corporation. All rights reserved.\r
4  *\r
5  * This software is subject to the Microsoft Public License (Ms-PL). \r
6  * A copy of the license can be found in the license.htm file included \r
7  * in this distribution.\r
8  *\r
9  * You must not remove this notice, or any other, from this software.\r
10  *\r
11  * ***************************************************************************/\r
12 \r
13 namespace System.Web.Mvc.Async {\r
14     using System;\r
15     using System.Collections.Generic;\r
16     using System.Globalization;\r
17     using System.Linq;\r
18     using System.Reflection;\r
19     using System.Text;\r
20     using System.Web.Mvc.Resources;\r
21 \r
22     internal sealed class AsyncActionMethodSelector {\r
23 \r
24         public AsyncActionMethodSelector(Type controllerType) {\r
25             ControllerType = controllerType;\r
26             PopulateLookupTables();\r
27         }\r
28 \r
29         public Type ControllerType {\r
30             get;\r
31             private set;\r
32         }\r
33 \r
34         public MethodInfo[] AliasedMethods {\r
35             get;\r
36             private set;\r
37         }\r
38 \r
39         public ILookup<string, MethodInfo> NonAliasedMethods {\r
40             get;\r
41             private set;\r
42         }\r
43 \r
44         private AmbiguousMatchException CreateAmbiguousActionMatchException(IEnumerable<MethodInfo> ambiguousMethods, string actionName) {\r
45             string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods);\r
46             string message = String.Format(CultureInfo.CurrentUICulture, MvcResources.ActionMethodSelector_AmbiguousMatch,\r
47                 actionName, ControllerType.Name, ambiguityList);\r
48             return new AmbiguousMatchException(message);\r
49         }\r
50 \r
51         private AmbiguousMatchException CreateAmbiguousMethodMatchException(IEnumerable<MethodInfo> ambiguousMethods, string methodName) {\r
52             string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods);\r
53             string message = String.Format(CultureInfo.CurrentUICulture, MvcResources.AsyncActionMethodSelector_AmbiguousMethodMatch,\r
54                 methodName, ControllerType.Name, ambiguityList);\r
55             return new AmbiguousMatchException(message);\r
56         }\r
57 \r
58         private static string CreateAmbiguousMatchList(IEnumerable<MethodInfo> ambiguousMethods) {\r
59             StringBuilder exceptionMessageBuilder = new StringBuilder();\r
60             foreach (MethodInfo methodInfo in ambiguousMethods) {\r
61                 exceptionMessageBuilder.AppendLine();\r
62                 exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentUICulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, methodInfo, methodInfo.DeclaringType.FullName);\r
63             }\r
64 \r
65             return exceptionMessageBuilder.ToString();\r
66         }\r
67 \r
68         public ActionDescriptorCreator FindAction(ControllerContext controllerContext, string actionName) {\r
69             List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName);\r
70             methodsMatchingName.AddRange(NonAliasedMethods[actionName]);\r
71             List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName);\r
72 \r
73             switch (finalMethods.Count) {\r
74                 case 0:\r
75                     return null;\r
76 \r
77                 case 1:\r
78                     MethodInfo entryMethod = finalMethods[0];\r
79                     return GetActionDescriptorDelegate(entryMethod);\r
80 \r
81                 default:\r
82                     throw CreateAmbiguousActionMatchException(finalMethods, actionName);\r
83             }\r
84         }\r
85 \r
86         private ActionDescriptorCreator GetActionDescriptorDelegate(MethodInfo entryMethod) {\r
87             // Is this the FooAsync() / FooCompleted() pattern?\r
88             if (IsAsyncSuffixedMethod(entryMethod)) {\r
89                 string completionMethodName = entryMethod.Name.Substring(0, entryMethod.Name.Length - "Async".Length) + "Completed";\r
90                 MethodInfo completionMethod = GetMethodByName(completionMethodName);\r
91                 if (completionMethod != null) {\r
92                     return (actionName, controllerDescriptor) => new ReflectedAsyncActionDescriptor(entryMethod, completionMethod, actionName, controllerDescriptor);\r
93                 }\r
94                 else {\r
95                     throw Error.AsyncActionMethodSelector_CouldNotFindMethod(completionMethodName, ControllerType);\r
96                 }\r
97             }\r
98 \r
99             // Fallback to synchronous method\r
100             return (actionName, controllerDescriptor) => new ReflectedActionDescriptor(entryMethod, actionName, controllerDescriptor);\r
101         }\r
102 \r
103         private static string GetCanonicalMethodName(MethodInfo methodInfo) {\r
104             string methodName = methodInfo.Name;\r
105             return (IsAsyncSuffixedMethod(methodInfo))\r
106                 ? methodName.Substring(0, methodName.Length - "Async".Length)\r
107                 : methodName;\r
108         }\r
109 \r
110         internal List<MethodInfo> GetMatchingAliasedMethods(ControllerContext controllerContext, string actionName) {\r
111             // find all aliased methods which are opting in to this request\r
112             // to opt in, all attributes defined on the method must return true\r
113 \r
114             var methods = from methodInfo in AliasedMethods\r
115                           let attrs = (ActionNameSelectorAttribute[])methodInfo.GetCustomAttributes(typeof(ActionNameSelectorAttribute), true /* inherit */)\r
116                           where attrs.All(attr => attr.IsValidName(controllerContext, actionName, methodInfo))\r
117                           select methodInfo;\r
118             return methods.ToList();\r
119         }\r
120 \r
121         private static bool IsAsyncSuffixedMethod(MethodInfo methodInfo) {\r
122             return methodInfo.Name.EndsWith("Async", StringComparison.OrdinalIgnoreCase);\r
123         }\r
124 \r
125         private static bool IsCompletedSuffixedMethod(MethodInfo methodInfo) {\r
126             return methodInfo.Name.EndsWith("Completed", StringComparison.OrdinalIgnoreCase);\r
127         }\r
128 \r
129         private static bool IsMethodDecoratedWithAliasingAttribute(MethodInfo methodInfo) {\r
130             return methodInfo.IsDefined(typeof(ActionNameSelectorAttribute), true /* inherit */);\r
131         }\r
132 \r
133         private MethodInfo GetMethodByName(string methodName) {\r
134             List<MethodInfo> methods = (from MethodInfo methodInfo in ControllerType.GetMember(methodName, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase)\r
135                                         where IsValidActionMethod(methodInfo, false /* stripInfrastructureMethods */)\r
136                                         select methodInfo).ToList();\r
137 \r
138             switch (methods.Count) {\r
139                 case 0:\r
140                     return null;\r
141 \r
142                 case 1:\r
143                     return methods[0];\r
144 \r
145                 default:\r
146                     throw CreateAmbiguousMethodMatchException(methods, methodName);\r
147             }\r
148         }\r
149 \r
150         private static bool IsValidActionMethod(MethodInfo methodInfo) {\r
151             return IsValidActionMethod(methodInfo, true /* stripInfrastructureMethods */);\r
152         }\r
153 \r
154         private static bool IsValidActionMethod(MethodInfo methodInfo, bool stripInfrastructureMethods) {\r
155             if (methodInfo.IsSpecialName) {\r
156                 // not a normal method, e.g. a constructor or an event\r
157                 return false;\r
158             }\r
159 \r
160             if (methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(typeof(AsyncController))) {\r
161                 // is a method on Object, ControllerBase, Controller, or AsyncController\r
162                 return false;\r
163             };\r
164 \r
165             if (stripInfrastructureMethods) {\r
166                 if (IsCompletedSuffixedMethod(methodInfo)) {\r
167                     // do not match FooCompleted() methods, as these are infrastructure methods\r
168                     return false;\r
169                 }\r
170             }\r
171 \r
172             return true;\r
173         }\r
174 \r
175         private void PopulateLookupTables() {\r
176             MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);\r
177             MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod);\r
178 \r
179             AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute);\r
180             NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(GetCanonicalMethodName, StringComparer.OrdinalIgnoreCase);\r
181         }\r
182 \r
183         private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos) {\r
184             // remove all methods which are opting out of this request\r
185             // to opt out, at least one attribute defined on the method must return false\r
186 \r
187             List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>();\r
188             List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>();\r
189 \r
190             foreach (MethodInfo methodInfo in methodInfos) {\r
191                 ActionMethodSelectorAttribute[] attrs = (ActionMethodSelectorAttribute[])methodInfo.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), true /* inherit */);\r
192                 if (attrs.Length == 0) {\r
193                     matchesWithoutSelectionAttributes.Add(methodInfo);\r
194                 }\r
195                 else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo))) {\r
196                     matchesWithSelectionAttributes.Add(methodInfo);\r
197                 }\r
198             }\r
199 \r
200             // if a matching action method had a selection attribute, consider it more specific than a matching action method\r
201             // without a selection attribute\r
202             return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;\r
203         }\r
204 \r
205     }\r
206 }\r