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