Merge branch 'xml-fixes' of https://github.com/myeisha/mono into myeisha-xml-fixes
[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                                 FillConstructedObject (true);
168                         } else if (xm == XamlLanguage.FactoryMethod) {
169                                 if (contents.Count != 1 || !(contents [0] is string))
170                                         throw new XamlObjectWriterException (String.Format ("FactoryMethod must be non-empty string name. {0} value exists.", contents.Count > 0 ? contents [0] : "0"));
171                                 state.FactoryMethod = (string) contents [0];
172                         } else if (xm == XamlLanguage.Arguments) {
173                                 if (state.FactoryMethod != null) {
174                                         var mi = state.Type.UnderlyingType.GetMethods (static_flags).FirstOrDefault (mii => mii.Name == state.FactoryMethod && mii.GetParameters ().Length == contents.Count);
175                                         if (mi == null)
176                                                 throw new XamlObjectWriterException (String.Format ("Specified static factory method '{0}' for type '{1}' was not found", state.FactoryMethod, state.Type));
177                                         state.Value = mi.Invoke (null, contents.ToArray ());
178                                 }
179                                 else
180                                         FillConstructedObject (false);
181                         } else if (xm == XamlLanguage.Initialization) {
182                                 // ... and no need to do anything. The object value to pop *is* the return value.
183                         } else if (xm == XamlLanguage.Items) {
184                                 var coll = state.Value;
185                                 foreach (var content in contents)
186                                         xm.Type.Invoker.AddToCollection (coll, content);
187                         } else if (xm.Type.IsDictionary) {
188                                 throw new NotImplementedException ();
189                         } else {
190                                 if (contents.Count > 1)
191                                         throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", xm, state.Type));
192                                 if (contents.Count == 1) {
193                                         var value = contents [0];
194                                         if (!xm.Type.IsCollection || !xm.IsReadOnly) // exclude read-only object.
195                                                 SetValue (xm, value);
196                                 }
197                         }
198
199                         contents.Clear ();
200
201                         if (object_states.Count > 0)
202                                 object_states.Peek ().WrittenProperties.Add (xm);
203                         //written_properties_stack.Peek ().Add (xm);
204                 }
205
206                 void FillConstructedObject (bool considerPositionalParameters)
207                 {
208                         var state = object_states.Peek ();
209
210                         var args = state.Type.GetSortedConstructorArguments ();
211                         var argt = args != null ? (IList<XamlType>) (from arg in args select arg.Type).ToArray () : considerPositionalParameters ? state.Type.GetPositionalParameters (state.Contents.Count) : null;
212
213                         var argv = new object [argt.Count];
214                         for (int i = 0; i < argv.Length; i++)
215                                 argv [i] = GetCorrectlyTypedValue (argt [i], state.Contents [i]);
216                         state.Value = state.Type.Invoker.CreateInstance (argv);
217                         state.IsInstantiated = true;
218                 }
219
220                 object GetCorrectlyTypedValue (XamlType xt, object value)
221                 {
222                         try {
223                                 return DoGetCorrectlyTypedValue (xt, value);
224                         } catch (Exception ex) {
225                                 // For + ex.Message, the runtime should print InnerException message like .NET does.
226                                 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);
227                         }
228                 }
229
230                 // It expects that it is not invoked when there is no value to 
231                 // assign.
232                 // When it is passed null, then it returns a default instance.
233                 // For example, passing null as Int32 results in 0.
234                 object DoGetCorrectlyTypedValue (XamlType xt, object value)
235                 {
236                         if (value == null)
237                                 return xt.Invoker.CreateInstance (new object [0]);
238
239                         // FIXME: this could be generalized by some means, but I cannot find any.
240                         if (xt.UnderlyingType == typeof (Type))
241                                 xt = XamlLanguage.Type;
242                         if (xt == XamlLanguage.Type && value is string)
243                                 value = new TypeExtension ((string) value);
244
245                         // FIXME: this could be generalized by some means, but I cannot find any.
246                         if (xt.UnderlyingType == typeof (XamlType) && value is string) {
247                                 var nsr = (IXamlNamespaceResolver) service_provider.GetService (typeof (IXamlNamespaceResolver));
248                                 value = sctx.GetXamlType (XamlTypeName.Parse ((string) value, nsr));
249                         }
250                         
251                         if (value is MarkupExtension)
252                                 value = ((MarkupExtension) value).ProvideValue (service_provider);
253
254                         if (IsAllowedType (xt, value))
255                                 return value;
256
257                         if (xt.TypeConverter != null && value != null) {
258                                 var tc = xt.TypeConverter.ConverterInstance;
259                                 if (tc != null && tc.CanConvertFrom (value.GetType ()))
260                                         value = tc.ConvertFrom (value);
261                                 return value;
262                         }
263
264                         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)"));
265                 }
266
267                 bool IsAllowedType (XamlType xt, object value)
268                 {
269                         return  xt == null ||
270                                 xt.UnderlyingType == null ||
271                                 xt.UnderlyingType.IsInstanceOfType (value) ||
272                                 value == null && xt == XamlLanguage.Null ||
273                                 xt.IsMarkupExtension && IsAllowedType (xt.MarkupExtensionReturnType, value);
274                 }
275
276                 public override void WriteEndObject ()
277                 {
278                         manager.EndObject (object_states.Count > 0);
279
280                         InitializeObjectIfRequired (false); // this is required for such case that there was no StartMember call.
281
282                         var state = object_states.Pop ();
283                         var obj = GetCorrectlyTypedValue (state.Type, state.Value);
284                         if (members.Count > 0) {
285                                 var pstate = object_states.Peek ();
286                                 pstate.Contents.Add (obj);
287                                 pstate.WrittenProperties.Add (members.Peek ());
288                         }
289                         if (object_states.Count == 0)
290                                 result = obj;
291                 }
292
293                 public override void WriteGetObject ()
294                 {
295                         manager.GetObject ();
296
297                         var xm = members.Peek ();
298                         // see GetObjectOnNonNullString() test. Below is invalid.
299                         //if (!xm.Type.IsCollection)
300                         //      throw new XamlObjectWriterException (String.Format ("WriteGetObject method can be invoked only when current member '{0}' is of collection type", xm.Name));
301
302                         var instance = xm.Invoker.GetValue (object_states.Peek ().Value);
303                         if (instance == null)
304                                 throw new XamlObjectWriterException (String.Format ("The value  for '{0}' property is null", xm.Name));
305
306                         var state = new ObjectState () {Type = SchemaContext.GetXamlType (instance.GetType ()), Value = instance, IsInstantiated = true, IsGetObject = true};
307                         object_states.Push (state);
308                 }
309
310                 public override void WriteNamespace (NamespaceDeclaration namespaceDeclaration)
311                 {
312                         if (namespaceDeclaration == null)
313                                 throw new ArgumentNullException ("namespaceDeclaration");
314
315                         manager.Namespace ();
316
317                         namespaces.Add (namespaceDeclaration);
318                 }
319
320                 public override void WriteStartMember (XamlMember property)
321                 {
322                         if (property == null)
323                                 throw new ArgumentNullException ("property");
324
325                         manager.StartMember ();
326                         if (property == XamlLanguage.PositionalParameters)
327                                 manager.AcceptMultipleValues = true;
328
329                         // 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).
330                         if (!(property is XamlDirective))
331                                 InitializeObjectIfRequired (false);
332
333                         //var wpl = object_states.Peek ().WrittenProperties;
334                         // FIXME: enable this. Duplicate property check should
335                         // be differentiate from duplicate contents (both result
336                         // in XamlDuplicateMemberException though).
337                         // Now it is done at WriteStartObject/WriteValue, but
338                         // it is simply wrong.
339 //                      if (wpl.Contains (property))
340 //                              throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", property, object_states.Peek ().Type));
341 //                      wpl.Add (property);
342
343                         members.Push (property);
344                 }
345
346                 void InitializeObjectIfRequired (bool waitForParameters)
347                 {
348                         var state = object_states.Peek ();
349                         if (state.IsInstantiated)
350                                 return;
351
352                         if (waitForParameters && (state.Type.ConstructionRequiresArguments || state.Type.HasPositionalParameters (service_provider)))
353                                 return;
354
355                         // 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."
356                         // http://msdn.microsoft.com/en-us/library/system.xaml.xamllanguage.factorymethod%28VS.100%29.aspx
357                         object obj;
358                         if (state.FactoryMethod != null) // FIXME: it must be implemented and verified with tests.
359                                 throw new NotImplementedException ();
360                         else
361                                 obj = state.Type.Invoker.CreateInstance (null);
362                         state.Value = obj;
363                         state.IsInstantiated = true;
364                 }
365
366                 public override void WriteStartObject (XamlType xamlType)
367                 {
368                         if (xamlType == null)
369                                 throw new ArgumentNullException ("xamlType");
370
371                         manager.StartObject ();
372
373                         var xm = members.Count > 0 ? members.Peek () : null;
374                         var pstate = xm != null ? object_states.Peek () : null;
375                         var wpl = xm == null || xm.Type.IsCollection || xm.Type.IsDictionary ? null : pstate.WrittenProperties;
376                         if (wpl != null && wpl.Contains (xm))
377                                 throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", xm, pstate.Type));
378
379                         var cstate = new ObjectState () {Type = xamlType, IsInstantiated = false};
380                         object_states.Push (cstate);
381
382                         if (!xamlType.IsContentValue (service_provider))
383                                 InitializeObjectIfRequired (true);
384                         
385                         if (wpl != null) // note that this adds to the *owner* object's properties.
386                                 wpl.Add (xm);
387                 }
388
389                 public override void WriteValue (object value)
390                 {
391                         manager.Value ();
392
393                         var xm = members.Peek ();
394                         var state = object_states.Peek ();
395
396                         var wpl = xm != null && xm != XamlLanguage.Items ? state.WrittenProperties : null;
397                         if (wpl != null && wpl.Contains (xm))
398                                 throw new XamlDuplicateMemberException (String.Format ("Property '{0}' is already set to this '{1}' object", xm, state.Type));
399
400                         if (xm == XamlLanguage.Initialization) {
401                                 value = GetCorrectlyTypedValue (state.Type, value);
402                                 state.Value = value;
403                                 state.IsInstantiated = true;
404                         }
405                         else if (xm.Type.IsDictionary) {
406                                 if (xm == XamlLanguage.Key)
407                                         state.KeyValue = GetCorrectlyTypedValue (xm.Type.KeyType, value);
408                                 else
409                                         state.Contents.Add (GetCorrectlyTypedValue (xm.Type.ItemType, value));
410                         }
411                         else if (xm.Type.IsCollection)
412                                 state.Contents.Add (GetCorrectlyTypedValue (xm.Type.ItemType, value));
413                         else
414                                 state.Contents.Add (GetCorrectlyTypedValue (xm.Type, value));
415                         if (wpl != null)
416                                 wpl.Add (xm);
417                 }
418         }
419 }