Get object instantiation correctly invoked when non-constructive StartMember is invoked.
[mono.git] / mcs / class / System.Xaml / System.Xaml / XamlObjectWriter.cs
1 //
2 // Copyright (C) 2010 Novell Inc. http://novell.com
3 //
4 // Permission is hereby granted, free of charge, to any person obtaining
5 // a copy of this software and associated documentation files (the
6 // "Software"), to deal in the Software without restriction, including
7 // without limitation the rights to use, copy, modify, merge, publish,
8 // distribute, sublicense, and/or sell copies of the Software, and to
9 // permit persons to whom the Software is furnished to do so, subject to
10 // the following conditions:
11 // 
12 // The above copyright notice and this permission notice shall be
13 // included in all copies or substantial portions of the Software.
14 // 
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 //
23 using System;
24 using System.Collections.Generic;
25 using System.Linq;
26 using System.Reflection;
27 using System.Windows.Markup;
28 using System.Xaml.Schema;
29
30 namespace System.Xaml
31 {
32         public class XamlObjectWriter : XamlWriter, IXamlLineInfoConsumer
33         {
34                 public XamlObjectWriter (XamlSchemaContext schemaContext)
35                         : this (schemaContext, null)
36                 {
37                 }
38
39                 public XamlObjectWriter (XamlSchemaContext schemaContext, XamlObjectWriterSettings settings)
40                 {
41                         if (schemaContext == null)
42                                 throw new ArgumentNullException ("schemaContext");
43                         this.sctx = schemaContext;
44                         this.settings = settings ?? new XamlObjectWriterSettings ();
45
46                         var p = new PrefixLookup (sctx);
47                         service_provider = new ValueSerializerContext (p, sctx);
48                         namespaces = p.Namespaces;
49                 }
50
51                 XamlSchemaContext sctx;
52                 XamlObjectWriterSettings settings;
53
54                 XamlWriterStateManager manager = new XamlWriterStateManager<XamlObjectWriterException, XamlObjectWriterException> (false);
55                 object result;
56                 int line = -1, column = -1;
57                 Stack<XamlMember> members = new Stack<XamlMember> ();
58
59                 List<NamespaceDeclaration> namespaces;
60                 IValueSerializerContext service_provider;
61                 Stack<ObjectState> object_states = new Stack<ObjectState> ();
62
63                 class ObjectState
64                 {
65                         public XamlType Type;
66                         public object Value;
67                         public object KeyValue;
68                         public List<object> Contents = new List<object> ();
69                         public List<XamlMember> WrittenProperties = new List<XamlMember> ();
70                         public bool IsInstantiated;
71                         public bool IsGetObject;
72
73                         public string FactoryMethod;
74                         public List<object> Arguments = new List<object> ();
75                 }
76
77                 public virtual object Result {
78                         get { return result; }
79                 }
80
81                 public INameScope RootNameScope {
82                         get { throw new NotImplementedException (); }
83                 }
84
85                 public override XamlSchemaContext SchemaContext {
86                         get { return sctx; }
87                 }
88
89                 public bool ShouldProvideLineInfo {
90                         get { return line > 0 && column > 0; }
91                 }
92                 
93                 public void Clear ()
94                 {
95                         throw new NotImplementedException ();
96                 }
97
98                 protected override void Dispose (bool disposing)
99                 {
100                         if (!disposing)
101                                 return;
102
103                         while (object_states.Count > 0) {
104                                 if (object_states.Count == members.Count)
105                                         WriteEndMember ();
106                                 WriteEndObject ();
107                         }
108                 }
109
110                 protected virtual void OnAfterBeginInit (object value)
111                 {
112                         throw new NotImplementedException ();
113                 }
114
115                 protected virtual void OnAfterEndInit (object value)
116                 {
117                         throw new NotImplementedException ();
118                 }
119
120                 protected virtual void OnAfterProperties (object value)
121                 {
122                         throw new NotImplementedException ();
123                 }
124
125                 protected virtual void OnBeforeProperties (object value)
126                 {
127                         throw new NotImplementedException ();
128                 }
129
130                 protected virtual bool OnSetValue (object eventSender, XamlMember member, object value)
131                 {
132                         if (settings.XamlSetValueHandler != null) {
133                                 settings.XamlSetValueHandler (eventSender, new XamlSetValueEventArgs (member, value));
134                                 return true;
135                         }
136                         return false;
137                 }
138
139                 void SetValue (XamlMember member, object value)
140                 {
141                         if (member.IsDirective)
142                                 return;
143                         if (!OnSetValue (this, member, value))
144                                 member.Invoker.SetValue (object_states.Peek ().Value, value);
145                 }
146
147                 public void SetLineInfo (int lineNumber, int linePosition)
148                 {
149                         line = lineNumber;
150                         column = linePosition;
151                 }
152
153                 static readonly BindingFlags static_flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
154
155                 [MonoTODO ("Dictionary needs implementation")]
156                 public override void WriteEndMember ()
157                 {
158                         manager.EndMember ();
159                         
160                         var xm = members.Pop ();
161                         var state = object_states.Peek ();
162                         var contents = state.Contents;
163
164                         if (xm == XamlLanguage.PositionalParameters) {
165                                 manager.AcceptMultipleValues = false;
166                                 //state.PositionalParameterIndex = -1;
167                                 var args = state.Type.GetSortedConstructorArguments ();
168                                 var argt = args != null ? (IList<XamlType>) (from arg in args select arg.Type).ToArray () : state.Type.GetPositionalParameters (state.Contents.Count);
169                                 var argv = new object [argt.Count];
170                                 for (int i = 0; i < argv.Length; i++)
171                                         argv [i] = GetCorrectlyTypedValue (argt [i], state.Contents [i]);
172                                 state.Value = state.Type.Invoker.CreateInstance (argv);
173                                 state.IsInstantiated = true;
174                         } else if (xm == XamlLanguage.FactoryMethod) {
175                                 if (contents.Count != 1 || !(contents [0] is string))
176                                         throw new XamlObjectWriterException (String.Format ("FactoryMethod must be non-empty string name. {0} value exists.", contents.Count > 0 ? contents [0] : "0"));
177                                 state.FactoryMethod = (string) contents [0];
178                         } else if (xm == XamlLanguage.Arguments) {
179                                 if (state.FactoryMethod != null) {
180                                         var mi = state.Type.UnderlyingType.GetMethods (static_flags).FirstOrDefault (mii => mii.Name == state.FactoryMethod && mii.GetParameters ().Length == contents.Count);
181                                         if (mi == null)
182                                                 throw new XamlObjectWriterException (String.Format ("Specified static factory method '{0}' for type '{1}' was not found", state.FactoryMethod, state.Type));
183                                         state.Value = mi.Invoke (null, contents.ToArray ());
184                                 }
185                                 else
186                                         throw new NotImplementedException ();
187                         } else if (xm == XamlLanguage.Initialization) {
188                                 // ... and no need to do anything. The object value to pop *is* the return value.
189                         } else if (xm == XamlLanguage.Items) {
190                                 var coll = state.Value;
191                                 foreach (var content in contents)
192                                         xm.Type.Invoker.AddToCollection (coll, content);
193                         } else if (xm.Type.IsDictionary) {
194                                 throw new NotImplementedException ();
195                         } else {
196                                 if (contents.Count > 1)
197                                         throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", xm, state.Type));
198                                 if (contents.Count == 1) {
199                                         var value = contents [0];
200                                         if (!xm.Type.IsCollection || !xm.IsReadOnly) // exclude read-only object.
201                                                 SetValue (xm, value);
202                                 }
203                         }
204
205                         contents.Clear ();
206
207                         if (object_states.Count > 0)
208                                 object_states.Peek ().WrittenProperties.Add (xm);
209                         //written_properties_stack.Peek ().Add (xm);
210                 }
211
212                 object GetCorrectlyTypedValue (XamlType xt, object value)
213                 {
214                         try {
215                                 return DoGetCorrectlyTypedValue (xt, value);
216                         } catch (Exception ex) {
217                                 // For + ex.Message, the runtime should print InnerException message like .NET does.
218                                 throw new XamlObjectWriterException (String.Format ("Could not convert object \'{0}' (of type {1}) to {2}: ", value, value != null ? (object) value.GetType () : "(null)", xt)  + ex.Message, ex);
219                         }
220                 }
221
222                 // It expects that it is not invoked when there is no value to 
223                 // assign.
224                 // When it is passed null, then it returns a default instance.
225                 // For example, passing null as Int32 results in 0.
226                 object DoGetCorrectlyTypedValue (XamlType xt, object value)
227                 {
228                         if (value == null)
229                                 return xt.Invoker.CreateInstance (new object [0]);
230
231                         // FIXME: this could be generalized by some means, but I cannot find any.
232                         if (xt.UnderlyingType == typeof (Type))
233                                 xt = XamlLanguage.Type;
234                         if (xt == XamlLanguage.Type && value is string)
235                                 value = new TypeExtension ((string) value);
236                         
237                         if (value is MarkupExtension)
238                                 value = ((MarkupExtension) value).ProvideValue (service_provider);
239
240                         if (IsAllowedType (xt, value))
241                                 return value;
242
243                         if (xt.TypeConverter != null && value != null) {
244                                 var tc = xt.TypeConverter.ConverterInstance;
245                                 if (tc != null && tc.CanConvertFrom (value.GetType ()))
246                                         value = tc.ConvertFrom (value);
247                                 if (IsAllowedType (xt, value))
248                                         return value;
249                         }
250
251                         throw new XamlObjectWriterException (String.Format ("Value '{1}' (of type {2}) is not of or convertible to type {0}", xt, value, value != null ? (object) value.GetType () : "(null)"));
252                 }
253
254                 bool IsAllowedType (XamlType xt, object value)
255                 {
256                         return  xt == null ||
257                                 xt.UnderlyingType == null ||
258                                 xt.UnderlyingType.IsInstanceOfType (value) ||
259                                 value == null && xt == XamlLanguage.Null ||
260                                 xt.IsMarkupExtension && IsAllowedType (xt.MarkupExtensionReturnType, value);
261                 }
262
263                 public override void WriteEndObject ()
264                 {
265                         manager.EndObject (object_states.Count > 0);
266
267                         InitializeObjectIfRequired (false); // this is required for such case that there was no StartMember call.
268
269                         var state = object_states.Pop ();
270                         var obj = GetCorrectlyTypedValue (state.Type, state.Value);
271                         if (members.Count > 0) {
272                                 var pstate = object_states.Peek ();
273                                 pstate.Contents.Add (obj);
274                                 pstate.WrittenProperties.Add (members.Peek ());
275                         }
276                         if (object_states.Count == 0)
277                                 result = obj;
278                 }
279
280                 public override void WriteGetObject ()
281                 {
282                         manager.GetObject ();
283
284                         var xm = members.Peek ();
285                         // see GetObjectOnNonNullString() test. Below is invalid.
286                         //if (!xm.Type.IsCollection)
287                         //      throw new XamlObjectWriterException (String.Format ("WriteGetObject method can be invoked only when current member '{0}' is of collection type", xm.Name));
288
289                         var instance = xm.Invoker.GetValue (object_states.Peek ().Value);
290                         if (instance == null)
291                                 throw new XamlObjectWriterException (String.Format ("The value  for '{0}' property is null", xm.Name));
292
293                         var state = new ObjectState () {Type = SchemaContext.GetXamlType (instance.GetType ()), Value = instance, IsInstantiated = true, IsGetObject = true};
294                         object_states.Push (state);
295                 }
296
297                 public override void WriteNamespace (NamespaceDeclaration namespaceDeclaration)
298                 {
299                         if (namespaceDeclaration == null)
300                                 throw new ArgumentNullException ("namespaceDeclaration");
301
302                         manager.Namespace ();
303
304                         namespaces.Add (namespaceDeclaration);
305                 }
306
307                 public override void WriteStartMember (XamlMember property)
308                 {
309                         if (property == null)
310                                 throw new ArgumentNullException ("property");
311
312                         manager.StartMember ();
313                         if (property == XamlLanguage.PositionalParameters)
314                                 manager.AcceptMultipleValues = true;
315
316                         // FIXME: this condition needs to be examined. What is known to be prevented are: PositionalParameters, Initialization and Base (the last one sort of indicates there's a lot more).
317                         if (!(property is XamlDirective))
318                                 InitializeObjectIfRequired (false);
319
320                         //var wpl = object_states.Peek ().WrittenProperties;
321                         // FIXME: enable this. Duplicate property check should
322                         // be differentiate from duplicate contents (both result
323                         // in XamlDuplicateMemberException though).
324                         // Now it is done at WriteStartObject/WriteValue, but
325                         // it is simply wrong.
326 //                      if (wpl.Contains (property))
327 //                              throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", property, object_states.Peek ().Type));
328 //                      wpl.Add (property);
329
330                         members.Push (property);
331                 }
332
333                 void InitializeObjectIfRequired (bool waitForParameters)
334                 {
335                         var state = object_states.Peek ();
336                         if (state.IsInstantiated)
337                                 return;
338
339                         if (waitForParameters && (state.Type.ConstructionRequiresArguments || state.Type.HasPositionalParameters (service_provider)))
340                                 return;
341
342                         // FIXME: "The default techniques in absence of a factory method are to attempt to find a default constructor, then attempt to find an identified type converter on type, member, or destination type."
343                         // http://msdn.microsoft.com/en-us/library/system.xaml.xamllanguage.factorymethod%28VS.100%29.aspx
344                         object obj;
345                         if (state.FactoryMethod != null) // FIXME: it must be implemented and verified with tests.
346                                 throw new NotImplementedException ();
347                         else
348                                 obj = state.Type.Invoker.CreateInstance (null);
349                         state.Value = obj;
350                         state.IsInstantiated = true;
351                 }
352
353                 public override void WriteStartObject (XamlType xamlType)
354                 {
355                         if (xamlType == null)
356                                 throw new ArgumentNullException ("xamlType");
357
358                         manager.StartObject ();
359
360                         var xm = members.Count > 0 ? members.Peek () : null;
361                         var pstate = xm != null ? object_states.Peek () : null;
362                         var wpl = xm != null && xm != XamlLanguage.Items ? pstate.WrittenProperties : null;
363                         if (wpl != null && wpl.Contains (xm))
364                                 throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", xm, pstate.Type));
365
366                         var cstate = new ObjectState () {Type = xamlType, IsInstantiated = false};
367                         object_states.Push (cstate);
368
369                         if (!xamlType.IsContentValue (service_provider))
370                                 InitializeObjectIfRequired (true);
371                         
372                         if (wpl != null) // note that this adds to the *owner* object's properties.
373                                 wpl.Add (xm);
374                 }
375
376                 public override void WriteValue (object value)
377                 {
378                         manager.Value ();
379
380                         var xm = members.Peek ();
381                         var state = object_states.Peek ();
382
383                         var wpl = xm != null && xm != XamlLanguage.Items ? state.WrittenProperties : null;
384                         if (wpl != null && wpl.Contains (xm))
385                                 throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", xm, state.Type));
386
387                         if (xm == XamlLanguage.Initialization) {
388                                 value = GetCorrectlyTypedValue (state.Type, value);
389                                 state.Value = value;
390                                 state.IsInstantiated = true;
391                         }
392                         else if (xm.Type.IsDictionary) {
393                                 if (xm == XamlLanguage.Key)
394                                         state.KeyValue = GetCorrectlyTypedValue (xm.Type.KeyType, value);
395                                 else
396                                         state.Contents.Add (GetCorrectlyTypedValue (xm.Type.ItemType, value));
397                         }
398                         else if (xm.Type.IsCollection)
399                                 state.Contents.Add (GetCorrectlyTypedValue (xm.Type.ItemType, value));
400                         else
401                                 state.Contents.Add (GetCorrectlyTypedValue (xm.Type, value));
402                         if (wpl != null)
403                                 wpl.Add (xm);
404                 }
405         }
406 }