auto generate script types for method params
[mono.git] / mcs / class / System.Web.Extensions / System.Web.Script.Services / LogicalTypeInfo.cs
1 //
2 // ScriptHandlerFactory.cs
3 //
4 // Author:
5 //   Konstantin Triger <kostat@mainsoft.com>
6 //
7 // (C) 2007 Mainsoft, Inc.  http://www.mainsoft.com
8 //
9 //
10 // Permission is hereby granted, free of charge, to any person obtaining
11 // a copy of this software and associated documentation files (the
12 // "Software"), to deal in the Software without restriction, including
13 // without limitation the rights to use, copy, modify, merge, publish,
14 // distribute, sublicense, and/or sell copies of the Software, and to
15 // permit persons to whom the Software is furnished to do so, subject to
16 // the following conditions:
17 // 
18 // The above copyright notice and this permission notice shall be
19 // included in all copies or substantial portions of the Software.
20 // 
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 //
29
30 using System;
31 using System.Collections.Generic;
32 using System.Text;
33 using System.Web.Services;
34 using System.Reflection;
35 using System.Collections;
36 using System.Web.Script.Serialization;
37 using System.IO;
38 using System.Xml.Serialization;
39 using System.Xml;
40
41 namespace System.Web.Script.Services
42 {
43         internal sealed class LogicalTypeInfo
44         {
45                 #region LogicalMethodInfo
46
47                 public sealed class LogicalMethodInfo
48                 {
49                         readonly LogicalTypeInfo _typeInfo;
50                         readonly MethodInfo _methodInfo;
51
52                         readonly WebMethodAttribute _wma;
53
54                         readonly ScriptMethodAttribute _sma;
55
56                         readonly ParameterInfo [] _params;
57                         readonly Dictionary<string, int> _paramMap;
58                         readonly XmlSerializer _xmlSer;
59
60                         public LogicalMethodInfo (LogicalTypeInfo typeInfo, MethodInfo method) {
61                                 _typeInfo = typeInfo;
62                                 _methodInfo = method;
63
64                                 _wma = (WebMethodAttribute) Attribute.GetCustomAttribute (method, typeof (WebMethodAttribute));
65
66                                 _sma = (ScriptMethodAttribute) Attribute.GetCustomAttribute (method, typeof (ScriptMethodAttribute));
67                                 if (_sma == null)
68                                         _sma = ScriptMethodAttribute.Default;
69
70                                 _params = MethodInfo.GetParameters ();
71
72                                 if (HasParameters) {
73                                         _paramMap = new Dictionary<string, int> (_params.Length, StringComparer.Ordinal);
74                                         for (int i = 0; i < _params.Length; i++)
75                                                 _paramMap.Add(_params[i].Name, i);
76                                 }
77
78                                 if (ScriptMethod.ResponseFormat == ResponseFormat.Xml
79                                         && MethodInfo.ReturnType != typeof(void))
80                                         _xmlSer = new XmlSerializer (MethodInfo.ReturnType);
81                         }
82
83                         public void Invoke (IDictionary<string, object> @params, TextWriter writer) {
84                                 object [] pp = null;
85                                 if (HasParameters) {
86                                         pp = new object [_params.Length];
87
88                                         foreach (KeyValuePair<string, object> pair in @params) {
89                                                 int i = _paramMap [pair.Key];
90                                                 pp [i] = LogicalTypeInfo.JSSerializer.ConvertToType (_params [i].ParameterType, pair.Value);
91                                         }
92                                 }
93
94                                 object target = MethodInfo.IsStatic ? null : Activator.CreateInstance (_typeInfo._type);
95                                 object result = MethodInfo.Invoke (target, pp);
96                                 if (_xmlSer != null) {
97                                         XmlTextWriter xwriter = new XmlTextWriter (writer);
98                                         xwriter.Formatting = Formatting.None;
99                                         _xmlSer.Serialize (xwriter, result);
100                                 }
101                                 else
102                                         LogicalTypeInfo.JSSerializer.Serialize (result, writer);
103                         }
104
105                         bool HasParameters { get { return _params != null && _params.Length > 0; } }
106                         public string MethodName { get { return String.IsNullOrEmpty (WebMethod.MessageName) ? MethodInfo.Name : WebMethod.MessageName; } }
107
108                         public ScriptMethodAttribute ScriptMethod { get { return _sma; } }
109                         public MethodInfo MethodInfo { get { return _methodInfo; } }
110                         public WebMethodAttribute WebMethod { get { return _wma; } }
111                         public IEnumerable<Type> GetParameterTypes () {
112                                 if (HasParameters)
113                                         for (int i = 0; i < _params.Length; i++)
114                                                 yield return _params [i].ParameterType;
115
116                                 yield return MethodInfo.ReturnType;
117                         }
118
119                         public void GenerateMethod (StringBuilder proxy, bool isPrototype, bool isPage) {
120                                 string service = isPage ? "PageMethods" : MethodInfo.DeclaringType.FullName;
121
122                                 string useHttpGet = ScriptMethod.UseHttpGet ? "true" : "false";
123                                 string paramMap = GenerateParameters (true);
124                                 string paramList = GenerateParameters (false);
125
126                                 if (isPrototype){
127                                         proxy.AppendFormat (
128 @"
129 {1}:function({4}succeededCallback, failedCallback, userContext) {{
130 return this._invoke({0}.get_path(), '{1}',{2},{{{3}}},succeededCallback,failedCallback,userContext); }}",
131                                         service, MethodName, useHttpGet, paramMap, paramList);
132                                 }
133                                 else
134                                         proxy.AppendFormat (
135 @"
136 {0}.{1}= function({2}onSuccess,onFailed,userContext) {{{0}._staticInstance.{1}({2}onSuccess,onFailed,userContext); }}",
137                                         service, MethodName, paramList);
138                         }
139
140                         string GenerateParameters (bool isMap) {
141                                 if (!HasParameters)
142                                         return null;
143
144                                 StringBuilder builder = new StringBuilder ();
145
146                                 for (int i = 0; i < _params.Length; i++) {
147                                         builder.AppendFormat (isMap ? "{0}:{0}" : "{0}", _params [i].Name);
148                                         builder.Append (',');
149                                 }
150
151                                 if (isMap)
152                                         builder.Length--;
153
154                                 return builder.ToString ();
155                         }
156                 }
157
158                 #endregion
159
160 #if !TARGET_J2EE
161                 static Hashtable _type_to_logical_type = Hashtable.Synchronized (new Hashtable ());
162 #else
163                 const string type_to_logical_type_key = "System.Web.Script.Services.LogicalTypeInfo";
164                 static Hashtable _type_to_logical_type {
165                         get {
166                                 Hashtable hash = (Hashtable) AppDomain.CurrentDomain.GetData (type_to_logical_type_key);
167
168                                 if (hash != null)
169                                         return hash;
170
171                                 AppDomain.CurrentDomain.SetData (type_to_logical_type_key, Hashtable.Synchronized (new Hashtable ()));
172
173
174                                 return (Hashtable) AppDomain.CurrentDomain.GetData (type_to_logical_type_key);
175                         }
176                 }
177 #endif
178
179                 //readonly LogicalMethodInfo [] _logicalMethods;
180                 readonly Hashtable _methodMap;
181                 readonly Type _type;
182                 readonly string _proxy;
183                 static readonly JavaScriptSerializer JSSerializer = new JavaScriptSerializer ();
184
185                 private LogicalTypeInfo (Type t, string filePath) {
186                         _type = t;
187                         bool isPage = _type.IsSubclassOf (typeof (System.Web.UI.Page));
188                         BindingFlags bindingAttr = isPage ? (BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public) : (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
189                         MethodInfo [] all_type_methods = _type.GetMethods (bindingAttr);
190                         List<LogicalMethodInfo> logicalMethods = new List<LogicalMethodInfo> (all_type_methods.Length);
191                         foreach (MethodInfo mi in all_type_methods) {
192                                 if (mi.IsPublic && 
193                                         mi.GetCustomAttributes (typeof (WebMethodAttribute), false).Length > 0)
194                                         logicalMethods.Add (new LogicalMethodInfo (this, mi));
195                                 else {
196                                         foreach (Type ifaceType in _type.GetInterfaces ()) {
197                                                 if (ifaceType.GetCustomAttributes (typeof (WebServiceBindingAttribute), false).Length > 0) {
198                                                         MethodInfo found = FindInInterface (ifaceType, mi);
199                                                         if (found != null) {
200                                                                 if (found.GetCustomAttributes (typeof (WebMethodAttribute), false).Length > 0)
201                                                                         logicalMethods.Add (new LogicalMethodInfo (this, found));
202
203                                                                 break;
204                                                         }
205                                                 }
206                                         }
207                                 }
208                         }
209
210                         //_logicalMethods = (LogicalMethodInfo []) list.ToArray (typeof (LogicalMethodInfo));
211
212                         _methodMap = new Hashtable (logicalMethods.Count);
213                         for (int i = 0; i < logicalMethods.Count; i++)
214                                 _methodMap.Add (logicalMethods [i].MethodName, logicalMethods [i]);
215
216                         string ns = isPage ? String.Empty : t.Namespace;
217                         string service = isPage ? "PageMethods" : t.FullName;
218                         
219                         StringBuilder proxy = new StringBuilder ();
220                         if (String.IsNullOrEmpty (ns))
221                                 proxy.AppendFormat (
222 @"var {0}",
223         service);
224                         else
225                                 proxy.AppendFormat (
226 @"Type.registerNamespace('{0}');
227 {1}",
228         ns, service);
229                         proxy.AppendFormat (
230 @"=function() {{
231 {0}.initializeBase(this);
232 this._timeout = 0;
233 this._userContext = null;
234 this._succeeded = null;
235 this._failed = null;
236 }}
237 {0}.prototype={{",
238                         service);
239
240                         for (int i = 0; i < logicalMethods.Count; i++) {
241                                 if (i > 0)
242                                         proxy.Append (',');
243                                 logicalMethods [i].GenerateMethod (proxy, true, isPage);
244                         }
245
246                         proxy.AppendFormat (
247 #if NET_3_5
248 @"}}
249 {0}.registerClass('{0}',Sys.Net.WebServiceProxy);
250 {0}._staticInstance = new {0}();
251 {0}.set_path = function(value) {{ {0}._staticInstance.set_path(value); }}
252 {0}.get_path = function() {{ return {0}._staticInstance.get_path(); }}
253 {0}.set_timeout = function(value) {{ {0}._staticInstance.set_timeout(value); }}
254 {0}.get_timeout = function() {{ return {0}._staticInstance.get_timeout(); }}
255 {0}.set_defaultUserContext = function(value) {{ {0}._staticInstance.set_userContext(value); }}
256 {0}.get_defaultUserContext = function() {{ return {0}._staticInstance.get_userContext(); }}
257 {0}.set_defaultSucceededCallback = function(value) {{ {0}._staticInstance.set_succeeded(value); }}
258 {0}.get_defaultSucceededCallback = function() {{ return {0}._staticInstance.get_succeeded(); }}
259 {0}.set_defaultFailedCallback = function(value) {{ {0}._staticInstance.set_failed(value); }}
260 {0}.get_defaultFailedCallback = function() {{ return {0}._staticInstance.get_failed(); }}
261 {0}.set_path(""{1}"");",
262 #else
263 @"}}
264 {0}.registerClass('{0}',Sys.Net.WebServiceProxy);
265 {0}._staticInstance = new {0}();
266 {0}.set_path = function(value) {{ {0}._staticInstance._path = value; }}
267 {0}.get_path = function() {{ return {0}._staticInstance._path; }}
268 {0}.set_timeout = function(value) {{ {0}._staticInstance._timeout = value; }}
269 {0}.get_timeout = function() {{ return {0}._staticInstance._timeout; }}
270 {0}.set_defaultUserContext = function(value) {{ {0}._staticInstance._userContext = value; }}
271 {0}.get_defaultUserContext = function() {{ return {0}._staticInstance._userContext; }}
272 {0}.set_defaultSucceededCallback = function(value) {{ {0}._staticInstance._succeeded = value; }}
273 {0}.get_defaultSucceededCallback = function() {{ return {0}._staticInstance._succeeded; }}
274 {0}.set_defaultFailedCallback = function(value) {{ {0}._staticInstance._failed = value; }}
275 {0}.get_defaultFailedCallback = function() {{ return {0}._staticInstance._failed; }}
276 {0}.set_path(""{1}"");",
277 #endif
278                         service, filePath);
279
280                         for (int i = 0; i < logicalMethods.Count; i++)
281                                 logicalMethods [i].GenerateMethod (proxy, false, isPage);
282
283                         bool gtc = false;
284
285                         foreach (GenerateScriptTypeAttribute gsta in GetGenerateScriptTypeAttributes ()) {
286                                 if (!gtc && !gsta.Type.IsEnum) {
287                                         proxy.Append (
288 @"
289 var gtc = Sys.Net.WebServiceProxy._generateTypedConstructor;");
290                                         gtc = true;
291                                 }
292                                 GenerateScript (proxy, gsta);
293                         }
294
295                         proxy.AppendLine ();
296                         _proxy = proxy.ToString ();
297                 }
298
299                 IEnumerable<MemberInfo> GetGenerateScriptTypes () {
300                         foreach (LogicalMethodInfo lmi in _methodMap.Values)
301                                 yield return lmi.MethodInfo;
302
303                         yield return _type;
304                 }
305
306                 IEnumerable<GenerateScriptTypeAttribute> GetGenerateScriptTypeAttributes () {
307                         Hashtable generatedTypes = new Hashtable ();
308
309                         foreach (MemberInfo mi in GetGenerateScriptTypes ()) {
310                                 GenerateScriptTypeAttribute [] gstas = (GenerateScriptTypeAttribute []) mi.GetCustomAttributes (typeof (GenerateScriptTypeAttribute), true);
311                                 if (gstas == null || gstas.Length == 0)
312                                         continue;
313
314                                 for (int i = 0; i < gstas.Length; i++) {
315                                         if (!generatedTypes.Contains (gstas [i].Type)) {
316                                                 if (ShouldGenerateScript (gstas [i].Type, true)) {
317                                                         generatedTypes [gstas [i].Type] = gstas [i].Type;
318                                                         yield return gstas [i];
319                                                 }
320                                         }
321                                 }
322                         }
323
324                         foreach (LogicalMethodInfo lmi in _methodMap.Values) {
325                                 foreach (Type param in lmi.GetParameterTypes ()) {
326                                         if (!generatedTypes.Contains (param)) {
327                                                 if (ShouldGenerateScript (param, false)) {
328                                                         generatedTypes [param] = param;
329                                                         yield return new GenerateScriptTypeAttribute (param);
330                                                 }
331                                         }
332                                 }
333                         }
334                 }
335
336                 static readonly Type typeOfIEnumerable = typeof (IEnumerable);
337                 static readonly Type typeOfIDictionary = typeof (IDictionary);
338
339                 static bool ShouldGenerateScript (Type type, bool throwIfNot) {
340                         if (type.IsEnum)
341                                 return true;
342
343                         if (Type.GetTypeCode (type) != TypeCode.Object)
344                                 return false;
345
346                         // LAMESPEC: MS never create proxies for GenericTypes
347                         //&& type.GetGenericTypeDefinition ().GetGenericArguments ().Length > 1
348                         if (type.IsGenericType)
349                                 return false;
350
351                         if (typeOfIEnumerable.IsAssignableFrom (type) ||
352                                 typeOfIDictionary.IsAssignableFrom (type) ||
353                                 type.IsAbstract || type.IsInterface) {
354                                 if (throwIfNot)
355                                         ThrowOnIncorrectGenerateScriptAttribute ();
356                                 return false;
357                         }
358
359                         ConstructorInfo ci = type.GetConstructor (Type.EmptyTypes);
360                         if (!ci.IsPublic) {
361                                 if (throwIfNot)
362                                         ThrowOnIncorrectGenerateScriptAttribute ();
363                                 return false;
364                         }
365
366                         return true;
367                 }
368
369                 static void ThrowOnIncorrectGenerateScriptAttribute () {
370                         throw new InvalidOperationException (
371                                 "Using the GenerateScriptTypes attribute is not supported for types in the following categories: primitive types; DateTime; generic types taking more than one parameter; types implementing IEnumerable or IDictionary; interfaces; Abstract classes; classes without a public default constructor.");
372                 }
373
374                 static void GenerateScript (StringBuilder proxy, GenerateScriptTypeAttribute gsta) {
375                         string className = gsta.Type.FullName.Replace ('+', '_');
376                         proxy.AppendFormat (
377 @"
378 if (typeof({0}) === 'undefined') {{", className);
379                         if (gsta.Type.IsEnum) {
380                                 proxy.AppendFormat (
381 @"
382 {0} = function() {{ throw Error.invalidOperation(); }}
383 {0}.prototype = {1}
384 {0}.registerEnum('{0}', {2});",
385                                 className,
386                                 JSSerializer.Serialize(new EnumPrototypeSerializer(gsta.Type)),
387                                 Attribute.GetCustomAttribute (gsta.Type, typeof (FlagsAttribute)) != null ? "true" : "false");
388                                 
389                         }
390                         else {
391                                 string typeId = String.IsNullOrEmpty (gsta.ScriptTypeId) ? gsta.Type.FullName : gsta.ScriptTypeId;
392                                 proxy.AppendFormat (
393 @"
394 {0}=gtc(""{1}"");
395 {0}.registerClass('{0}');",
396                                 className, typeId);
397                         }
398                         proxy.Append ('}');
399                 }
400
401                 sealed class EnumPrototypeSerializer : JavaScriptSerializer.LazyDictionary
402                 {
403                         readonly Type _type;
404                         public EnumPrototypeSerializer (Type type) {
405                                 _type = type;
406                         }
407                         protected override IEnumerator<KeyValuePair<string, object>> GetEnumerator () {
408                                 String [] names = Enum.GetNames (_type);
409                                 Array values = Enum.GetValues (_type);
410                                 for (int i = 0; i < names.Length; i++)
411                                         yield return new KeyValuePair<string, object> (names [i], values.GetValue (i));
412                         }
413                 }
414
415                 static MethodInfo FindInInterface (Type ifaceType, MethodInfo method) {
416                         int nameStartIndex = 0;
417                         if (method.IsPrivate) {
418                                 nameStartIndex = method.Name.LastIndexOf ('.');
419                                 if (nameStartIndex < 0)
420                                         nameStartIndex = 0;
421                                 else {
422                                         if (String.CompareOrdinal (
423                                                 ifaceType.FullName.Replace ('+', '.'), 0, method.Name, 0, nameStartIndex) != 0)
424                                                 return null;
425
426                                         nameStartIndex++;
427                                 }
428                         }
429                         foreach (MethodInfo mi in ifaceType.GetMembers ()) {
430                                 if (method.ReturnType == mi.ReturnType &&
431                                         String.CompareOrdinal (method.Name, nameStartIndex, mi.Name, 0, mi.Name.Length) == 0) {
432                                         ParameterInfo [] rpi = method.GetParameters ();
433                                         ParameterInfo [] lpi = mi.GetParameters ();
434                                         if (rpi.Length == lpi.Length) {
435                                                 bool match = true;
436                                                 for (int i = 0; i < rpi.Length; i++) {
437                                                         if (rpi [i].ParameterType != lpi [i].ParameterType) {
438                                                                 match = false;
439                                                                 break;
440                                                         }
441                                                 }
442
443                                                 if (match)
444                                                         return mi;
445                                         }
446                                 }
447                         }
448
449                         return null;
450                 }
451
452                 public string Proxy { get { return _proxy; } }
453
454                 public LogicalMethodInfo this [string method] {
455                         get { return (LogicalMethodInfo) _methodMap [method]; }
456                 }
457
458                 static internal LogicalTypeInfo GetLogicalTypeInfo (Type t, string filePath) {
459                         Hashtable type_to_manager = _type_to_logical_type;
460                         LogicalTypeInfo tm = (LogicalTypeInfo) type_to_manager [t];
461
462                         if (tm != null)
463                                 return tm;
464
465                         tm = new LogicalTypeInfo (t, filePath);
466                         type_to_manager [t] = tm;
467
468                         return tm;
469                 }
470         }
471 }