2009-10-08 Atsushi Enomoto <atsushi@ximian.com>
[mono.git] / mcs / class / System.ServiceModel.Web / System.ServiceModel.Dispatcher / WebMessageFormatter.cs
1 //
2 // WebMessageFormatter.cs
3 //
4 // Author:
5 //      Atsushi Enomoto  <atsushi@ximian.com>
6 //
7 // Copyright (C) 2008,2009 Novell, Inc (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28 using System;
29 using System.Collections.Specialized;
30 using System.Reflection;
31 using System.Runtime.Serialization;
32 using System.Runtime.Serialization.Json;
33 using System.ServiceModel;
34 using System.ServiceModel.Channels;
35 using System.ServiceModel.Dispatcher;
36 using System.ServiceModel.Web;
37 using System.Text;
38 using System.Xml;
39
40 namespace System.ServiceModel.Description
41 {
42         internal abstract class WebMessageFormatter
43         {
44                 OperationDescription operation;
45                 ServiceEndpoint endpoint;
46                 QueryStringConverter converter;
47                 WebHttpBehavior behavior;
48                 UriTemplate template;
49                 WebAttributeInfo info = null;
50
51                 public WebMessageFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
52                 {
53                         this.operation = operation;
54                         this.endpoint = endpoint;
55                         this.converter = converter;
56                         this.behavior = behavior;
57                         ApplyWebAttribute ();
58                         // This is a hack for WebScriptEnablingBehavior
59                         var jqc = converter as JsonQueryStringConverter;
60                         if (jqc != null)
61                                 BodyName = jqc.CustomWrapperName;
62                 }
63
64                 void ApplyWebAttribute ()
65                 {
66                         MethodInfo mi = operation.SyncMethod ?? operation.BeginMethod;
67
68                         object [] atts = mi.GetCustomAttributes (typeof (WebGetAttribute), false);
69                         if (atts.Length > 0)
70                                 info = ((WebGetAttribute) atts [0]).Info;
71                         atts = mi.GetCustomAttributes (typeof (WebInvokeAttribute), false);
72                         if (atts.Length > 0)
73                                 info = ((WebInvokeAttribute) atts [0]).Info;
74                         if (info == null)
75                                 info = new WebAttributeInfo ();
76
77                         template = info.BuildUriTemplate (Operation, GetMessageDescription (MessageDirection.Input));
78                 }
79
80                 public string BodyName { get; set; }
81
82                 public WebHttpBehavior Behavior {
83                         get { return behavior; }
84                 }
85
86                 public WebAttributeInfo Info {
87                         get { return info; }
88                 }
89
90                 public WebMessageBodyStyle BodyStyle {
91                         get { return info.IsBodyStyleSetExplicitly ? info.BodyStyle : behavior.DefaultBodyStyle; }
92                 }
93
94                 public bool IsResponseBodyWrapped {
95                         get {
96                                 switch (BodyStyle) {
97                                 case WebMessageBodyStyle.Wrapped:
98                                 case WebMessageBodyStyle.WrappedResponse:
99                                         return true;
100                                 }
101                                 return BodyName != null;
102                         }
103                 }
104
105                 public OperationDescription Operation {
106                         get { return operation; }
107                 }
108
109                 public QueryStringConverter Converter {
110                         get { return converter; }
111                 }
112
113                 public ServiceEndpoint Endpoint {
114                         get { return endpoint; }
115                 }
116
117                 public UriTemplate UriTemplate {
118                         get { return template; }
119                 }
120
121                 protected WebContentFormat ToContentFormat (WebMessageFormat src)
122                 {
123                         switch (src) {
124                         case WebMessageFormat.Xml:
125                                 return WebContentFormat.Xml;
126                         case WebMessageFormat.Json:
127                                 return WebContentFormat.Json;
128                         }
129                         throw new SystemException ("INTERNAL ERROR: should not happen");
130                 }
131
132                 protected void CheckMessageVersion (MessageVersion messageVersion)
133                 {
134                         if (messageVersion == null)
135                                 throw new ArgumentNullException ("messageVersion");
136
137                         if (!MessageVersion.None.Equals (messageVersion))
138                                 throw new ArgumentException ("Only MessageVersion.None is supported");
139                 }
140
141                 protected MessageDescription GetMessageDescription (MessageDirection dir)
142                 {
143                         foreach (MessageDescription md in operation.Messages)
144                                 if (md.Direction == dir)
145                                         return md;
146                         throw new SystemException ("INTERNAL ERROR: no corresponding message description for the specified direction: " + dir);
147                 }
148
149                 protected XmlObjectSerializer GetSerializer (WebContentFormat msgfmt)
150                 {
151                         switch (msgfmt) {
152                         case WebContentFormat.Xml:
153                                 if (IsResponseBodyWrapped)
154                                         return GetSerializer (ref xml_serializer, p => new DataContractSerializer (p.Type, p.Name, p.Namespace));
155                                 else
156                                         return GetSerializer (ref xml_serializer, p => new DataContractSerializer (p.Type));
157                                 break;
158                         case WebContentFormat.Json:
159                                 if (IsResponseBodyWrapped)
160                                         return GetSerializer (ref json_serializer, p => new DataContractJsonSerializer (p.Type, BodyName ?? p.Name));
161                                 else
162                                         return GetSerializer (ref json_serializer, p => new DataContractJsonSerializer (p.Type));
163                                 break;
164                         default:
165                                 throw new NotImplementedException ();
166                         }
167                 }
168
169                 XmlObjectSerializer xml_serializer, json_serializer;
170
171                 XmlObjectSerializer GetSerializer (ref XmlObjectSerializer serializer, Func<MessagePartDescription,XmlObjectSerializer> f)
172                 {
173                         if (serializer == null) {
174                                 MessageDescription md = GetMessageDescription (MessageDirection.Output);
175                                 serializer = f (md.Body.ReturnValue);
176                         }
177                         return serializer;
178                 }
179
180                 internal class RequestClientFormatter : WebClientMessageFormatter
181                 {
182                         public RequestClientFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
183                                 : base (operation, endpoint, converter, behavior)
184                         {
185                         }
186
187                         public override object DeserializeReply (Message message, object [] parameters)
188                         {
189                                 throw new NotSupportedException ();
190                         }
191                 }
192
193                 internal class ReplyClientFormatter : WebClientMessageFormatter
194                 {
195                         public ReplyClientFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
196                                 : base (operation, endpoint, converter, behavior)
197                         {
198                         }
199
200                         public override Message SerializeRequest (MessageVersion messageVersion, object [] parameters)
201                         {
202                                 throw new NotSupportedException ();
203                         }
204                 }
205
206                 internal class RequestDispatchFormatter : WebDispatchMessageFormatter
207                 {
208                         public RequestDispatchFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
209                                 : base (operation, endpoint, converter, behavior)
210                         {
211                         }
212
213                         public override Message SerializeReply (MessageVersion messageVersion, object [] parameters, object result)
214                         {
215                                 throw new NotSupportedException ();
216                         }
217                 }
218
219                 internal class ReplyDispatchFormatter : WebDispatchMessageFormatter
220                 {
221                         public ReplyDispatchFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
222                                 : base (operation, endpoint, converter, behavior)
223                         {
224                         }
225
226                         public override void DeserializeRequest (Message message, object [] parameters)
227                         {
228                                 throw new NotSupportedException ();
229                         }
230                 }
231
232                 internal abstract class WebClientMessageFormatter : WebMessageFormatter, IClientMessageFormatter
233                 {
234                         protected WebClientMessageFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
235                                 : base (operation, endpoint, converter, behavior)
236                         {
237                         }
238
239                         public virtual Message SerializeRequest (MessageVersion messageVersion, object [] parameters)
240                         {
241                                 if (parameters == null)
242                                         throw new ArgumentNullException ("parameters");
243                                 CheckMessageVersion (messageVersion);
244
245                                 var c = new NameValueCollection ();
246
247                                 MessageDescription md = GetMessageDescription (MessageDirection.Input);
248
249                                 if (parameters.Length != md.Body.Parts.Count)
250                                         throw new ArgumentException ("Parameter array length does not match the number of message body parts");
251
252                                 for (int i = 0; i < parameters.Length; i++) {
253                                         var p = md.Body.Parts [i];
254                                         string name = p.Name.ToUpperInvariant ();
255                                         if (UriTemplate.PathSegmentVariableNames.Contains (name) ||
256                                             UriTemplate.QueryValueVariableNames.Contains (name))
257                                                 c.Add (name, parameters [i] != null ? Converter.ConvertValueToString (parameters [i], parameters [i].GetType ()) : null);
258                                         else
259                                                 // FIXME: bind as a message part
260                                                 throw new NotImplementedException (String.Format ("parameter {0} is not contained in the URI template {1} {2} {3}", p.Name, UriTemplate, UriTemplate.PathSegmentVariableNames.Count, UriTemplate.QueryValueVariableNames.Count));
261                                 }
262
263                                 Uri to = UriTemplate.BindByName (Endpoint.Address.Uri, c);
264
265                                 Message ret = Message.CreateMessage (messageVersion, (string) null);
266                                 ret.Headers.To = to;
267
268                                 var hp = new HttpRequestMessageProperty ();
269                                 hp.Method = Info.Method;
270
271                                 // FIXME: isn't it always null?
272                                 if (WebOperationContext.Current != null)
273                                         WebOperationContext.Current.OutgoingRequest.Apply (hp);
274                                 // FIXME: set hp.SuppressEntityBody for some cases.
275                                 ret.Properties.Add (HttpRequestMessageProperty.Name, hp);
276
277                                 var wp = new WebBodyFormatMessageProperty (ToContentFormat (Info.IsRequestFormatSetExplicitly ? Info.RequestFormat : Behavior.DefaultOutgoingRequestFormat));
278                                 ret.Properties.Add (WebBodyFormatMessageProperty.Name, wp);
279
280                                 return ret;
281                         }
282
283                         public virtual object DeserializeReply (Message message, object [] parameters)
284                         {
285                                 if (parameters == null)
286                                         throw new ArgumentNullException ("parameters");
287                                 CheckMessageVersion (message.Version);
288
289                                 string pname = WebBodyFormatMessageProperty.Name;
290                                 if (!message.Properties.ContainsKey (pname))
291                                         throw new SystemException ("INTERNAL ERROR: it expects WebBodyFormatMessageProperty existence");
292                                 var wp = (WebBodyFormatMessageProperty) message.Properties [pname];
293
294                                 var serializer = GetSerializer (wp.Format);
295
296                                 // FIXME: handle ref/out parameters
297
298                                 var md = GetMessageDescription (MessageDirection.Output);
299
300                                 var reader = message.GetReaderAtBodyContents ();
301
302                                 if (IsResponseBodyWrapped) {
303                                         if (wp.Format == WebContentFormat.Json)
304                                                 reader.ReadStartElement ("root", String.Empty); // note that the wrapper name is passed to the serializer.
305                                         else
306                                                 reader.ReadStartElement (md.Body.WrapperName, md.Body.WrapperNamespace);
307                                 }
308
309                                 var ret = serializer.ReadObject (reader, true);
310
311                                 if (IsResponseBodyWrapped)
312                                         reader.ReadEndElement ();
313
314                                 return ret;
315                         }
316                 }
317
318                 internal class WrappedBodyWriter : BodyWriter
319                 {
320                         public WrappedBodyWriter (object value, XmlObjectSerializer serializer, string name, string ns, bool json)
321                                 : base (true)
322                         {
323                                 this.name = name;
324                                 this.ns = ns;
325                                 this.value = value;
326                                 this.serializer = serializer;
327                                 this.is_json = json;
328                         }
329
330                         bool is_json;
331                         string name, ns;
332                         object value;
333                         XmlObjectSerializer serializer;
334
335                         protected override BodyWriter OnCreateBufferedCopy (int maxBufferSize)
336                         {
337                                 return new WrappedBodyWriter (value, serializer, name, ns, is_json);
338                         }
339
340                         protected override void OnWriteBodyContents (XmlDictionaryWriter writer)
341                         {
342                                 if (is_json)
343                                         WriteJsonBodyContents (writer);
344                                 else
345                                         WriteXmlBodyContents (writer);
346                         }
347                         
348                         void WriteJsonBodyContents (XmlDictionaryWriter writer)
349                         {
350                                 if (name != null) {
351                                         writer.WriteStartElement ("root");
352                                         writer.WriteAttributeString ("type", "object");
353                                 }
354                                 serializer.WriteObject (writer, value);
355                                 if (name != null)
356                                         writer.WriteEndElement ();
357                         }
358
359                         void WriteXmlBodyContents (XmlDictionaryWriter writer)
360                         {
361                                 if (name != null)
362                                         writer.WriteStartElement (name, ns);
363                                 serializer.WriteObject (writer, value);
364                                 if (name != null)
365                                         writer.WriteEndElement ();
366                         }
367                 }
368
369                 internal abstract class WebDispatchMessageFormatter : WebMessageFormatter, IDispatchMessageFormatter
370                 {
371                         protected WebDispatchMessageFormatter (OperationDescription operation, ServiceEndpoint endpoint, QueryStringConverter converter, WebHttpBehavior behavior)
372                                 : base (operation, endpoint, converter, behavior)
373                         {
374                         }
375
376                         public virtual Message SerializeReply (MessageVersion messageVersion, object [] parameters, object result)
377                         {
378                                 try {
379                                         return SerializeReplyCore (messageVersion, parameters, result);
380                                 } finally {
381                                         if (WebOperationContext.Current != null)
382                                                 OperationContext.Current.Extensions.Remove (WebOperationContext.Current);
383                                 }
384                         }
385
386                         Message SerializeReplyCore (MessageVersion messageVersion, object [] parameters, object result)
387                         {
388                                 if (parameters == null)
389                                         throw new ArgumentNullException ("parameters");
390                                 CheckMessageVersion (messageVersion);
391
392                                 MessageDescription md = GetMessageDescription (MessageDirection.Output);
393
394                                 // FIXME: use them.
395                                 // var dcob = Operation.Behaviors.Find<DataContractSerializerOperationBehavior> ();
396                                 // XmlObjectSerializer xos = dcob.CreateSerializer (result.GetType (), md.Body.WrapperName, md.Body.WrapperNamespace, null);
397                                 // var xsob = Operation.Behaviors.Find<XmlSerializerOperationBehavior> ();
398                                 // XmlSerializer [] serializers = XmlSerializer.FromMappings (xsob.GetXmlMappings ().ToArray ());
399
400                                 WebMessageFormat msgfmt = Info.IsResponseFormatSetExplicitly ? Info.ResponseFormat : Behavior.DefaultOutgoingResponseFormat;
401
402                                 string mediaType = null;
403                                 XmlObjectSerializer serializer = null;
404
405                                 // FIXME: serialize ref/out parameters as well.
406
407                                 string name = null, ns = null;
408
409                                 switch (msgfmt) {
410                                 case WebMessageFormat.Xml:
411                                         serializer = GetSerializer (WebContentFormat.Xml);
412                                         mediaType = "application/xml";
413                                         name = IsResponseBodyWrapped ? md.Body.WrapperName : null;
414                                         ns = IsResponseBodyWrapped ? md.Body.WrapperNamespace : null;
415                                         break;
416                                 case WebMessageFormat.Json:
417                                         serializer = GetSerializer (WebContentFormat.Json);
418                                         mediaType = "application/json";
419                                         name = IsResponseBodyWrapped ? (BodyName ?? md.Body.ReturnValue.Name) : null;
420                                         ns = String.Empty;
421                                         break;
422                                 }
423
424                                 bool json = msgfmt == WebMessageFormat.Json;
425                                 Message ret = Message.CreateMessage (MessageVersion.None, null, new WrappedBodyWriter (result, serializer, name, ns, json));
426
427                                 // Message properties
428
429                                 var hp = new HttpResponseMessageProperty ();
430                                 // FIXME: get encoding from somewhere
431                                 hp.Headers ["Content-Type"] = mediaType + "; charset=utf-8";
432
433                                 // apply user-customized HTTP results via WebOperationContext.
434                                 WebOperationContext.Current.OutgoingResponse.Apply (hp);
435
436                                 // FIXME: fill some properties if required.
437                                 ret.Properties.Add (HttpResponseMessageProperty.Name, hp);
438
439                                 var wp = new WebBodyFormatMessageProperty (ToContentFormat (msgfmt));
440                                 ret.Properties.Add (WebBodyFormatMessageProperty.Name, wp);
441
442                                 return ret;
443                         }
444
445                         public virtual void DeserializeRequest (Message message, object [] parameters)
446                         {
447                                 if (parameters == null)
448                                         throw new ArgumentNullException ("parameters");
449                                 CheckMessageVersion (message.Version);
450
451                                 OperationContext.Current.Extensions.Add (new WebOperationContext (OperationContext.Current));
452
453                                 IncomingWebRequestContext iwc = WebOperationContext.Current.IncomingRequest;
454
455                                 Uri to = message.Headers.To;
456                                 UriTemplateMatch match = UriTemplate.Match (Endpoint.Address.Uri, to);
457                                 if (match == null)
458                                         // not sure if it could happen
459                                         throw new SystemException (String.Format ("INTERNAL ERROR: UriTemplate does not match with the request: {0} / {1}", UriTemplate, to));
460                                 iwc.UriTemplateMatch = match;
461
462                                 MessageDescription md = GetMessageDescription (MessageDirection.Input);
463
464                                 for (int i = 0; i < parameters.Length; i++) {
465                                         var p = md.Body.Parts [i];
466                                         string name = p.Name.ToUpperInvariant ();
467                                         var str = match.BoundVariables [name];
468                                         parameters [i] = Converter.ConvertStringToValue (str, p.Type);
469                                 }
470                         }
471                 }
472         }
473 }