1 // Copyright 2004-2007 Castle Project - http://www.castleproject.org/
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 namespace Castle
.MonoRail
.ActiveRecordSupport
18 using System
.Collections
;
19 using System
.Reflection
;
20 using Castle
.ActiveRecord
;
21 using Castle
.ActiveRecord
.Framework
.Internal
;
22 using Castle
.Components
.Binder
;
23 using Iesi
.Collections
;
24 using NHibernate
.Property
;
25 using Iesi
.Collections
.Generic
;
26 using System
.Collections
.Generic
;
29 /// Extends <see cref="DataBinder"/> class with some
30 /// ActiveRecord specific functionality.
31 /// <seealso cref="AutoLoadBehavior"/>
32 /// <seealso cref="ARDataBindAttribute"/>
35 /// Autoload can be turned <i>on</i> on the parameter, see <see cref="AutoLoadBehavior"/>.
37 public class ARDataBinder
: DataBinder
39 protected internal static readonly object[] EmptyArg
= new object[0];
41 private AutoLoadBehavior autoLoad
;
42 private bool persistchanges
;
43 private string[] expectCollPropertiesList
;
44 private Stack
<ActiveRecordModel
> modelStack
= new Stack
<ActiveRecordModel
>();
47 /// Gets or sets a value indicating if the changes should be persisted.
49 /// <value><c>true</c> if the changes should be persisted; otherwise, <c>false</c>.</value>
50 public bool PersistChanges
52 get { return persistchanges; }
53 set { persistchanges = value; }
57 /// Gets or sets the <see cref="AutoLoadBehavior"/>.
59 /// <value>The auto load behavior.</value>
60 public AutoLoadBehavior AutoLoad
62 get { return autoLoad; }
63 set { autoLoad = value; }
66 protected override void PushInstance(object instance
, string prefix
)
68 ActiveRecordModel model
= ActiveRecordModel
.GetModel(instance
.GetType());
70 if (model
== null && modelStack
.Count
!= 0)
72 foreach(NestedModel nestedModel
in CurrentARModel
.Components
)
74 if (string.Compare(nestedModel
.Property
.Name
, prefix
, true) == 0)
76 model
= nestedModel
.Model
;
84 modelStack
.Push(model
);
87 base.PushInstance(instance
, prefix
);
90 protected override void PopInstance(object instance
, string prefix
)
92 ActiveRecordModel model
= ActiveRecordModel
.GetModel(instance
.GetType());
94 if (model
== null && CurrentARModel
!= null && CurrentARModel
.IsNestedType
)
101 ActiveRecordModel actualModel
= modelStack
.Pop();
103 if (actualModel
!= model
)
105 throw new BindingException("Unexpected ARModel on the stack: found {0}, expecting {1}",
106 actualModel
.ToString(), model
.ToString());
110 base.PopInstance(instance
, prefix
);
114 /// Gets the current AR model.
116 /// <value>The current AR model.</value>
117 protected ActiveRecordModel CurrentARModel
121 return modelStack
.Count
== 0 ? null : modelStack
.Peek();
125 public object BindObject(Type targetType
, string prefix
, string exclude
, string allow
, string expect
,
126 CompositeNode treeRoot
)
128 expectCollPropertiesList
= CreateNormalizedList(expect
);
130 return BindObject(targetType
, prefix
, exclude
, allow
, treeRoot
);
133 protected override object CreateInstance(Type instanceType
, String paramPrefix
, Node node
)
137 throw new BindingException(
138 "Nothing found for the given prefix. Are you sure the form fields are using the prefix " +
142 if (node
.NodeType
!= NodeType
.Composite
)
144 throw new BindingException("Unexpected node type. Expecting Composite, found " + node
.NodeType
);
147 CompositeNode cNode
= (CompositeNode
) node
;
151 bool shouldLoad
= autoLoad
!= AutoLoadBehavior
.Never
;
153 if (autoLoad
== AutoLoadBehavior
.OnlyNested
)
155 shouldLoad
= StackDepth
!= 0;
160 if (instanceType
.IsArray
)
162 throw new BindingException("ARDataBinder AutoLoad does not support arrays");
165 ActiveRecordModel model
= ActiveRecordModel
.GetModel(instanceType
);
167 PrimaryKeyModel pkModel
;
169 object id
= ObtainPrimaryKeyValue(model
, cNode
, paramPrefix
, out pkModel
);
173 instance
= FindByPrimaryKey(instanceType
, id
);
177 if (autoLoad
== AutoLoadBehavior
.NewInstanceIfInvalidKey
||
178 (autoLoad
== AutoLoadBehavior
.NewRootInstanceIfInvalidKey
&& StackDepth
== 0))
180 instance
= base.CreateInstance(instanceType
, paramPrefix
, node
);
182 else if (autoLoad
== AutoLoadBehavior
.NullIfInvalidKey
||
183 autoLoad
== AutoLoadBehavior
.OnlyNested
||
184 (autoLoad
== AutoLoadBehavior
.NewRootInstanceIfInvalidKey
&& StackDepth
!= 0))
190 throw new BindingException(string.Format(
191 "Could not find primary key '{0}' for '{1}'",
192 pkModel
.Property
.Name
, instanceType
.FullName
));
198 instance
= base.CreateInstance(instanceType
, paramPrefix
, node
);
205 /// for joined subclasses HasAndBelongsToMany properties doesn't include the ones of the parent class
206 /// so we need to check them recursively
208 protected bool FindPropertyInHasAndBelongsToMany(ActiveRecordModel model
, string propertyName
,
209 ref Type foundType
, ref ActiveRecordModel foundModel
)
211 foreach(HasAndBelongsToManyModel hasMany2ManyModel
in model
.HasAndBelongsToMany
)
213 // Inverse=true relations will be ignored
214 if (hasMany2ManyModel
.Property
.Name
== propertyName
&& !hasMany2ManyModel
.HasManyAtt
.Inverse
)
216 foundType
= hasMany2ManyModel
.HasManyAtt
.MapType
;
217 foundModel
= ActiveRecordModel
.GetModel(foundType
);
221 if (model
.IsJoinedSubClass
|| model
.IsDiscriminatorSubClass
)
223 return FindPropertyInHasAndBelongsToMany(model
.Parent
, propertyName
, ref foundType
, ref foundModel
);
229 /// for joined subclasses HasMany properties doesn't include the ones of the parent class
230 /// so we need to check them recursively
232 protected bool FindPropertyInHasMany(ActiveRecordModel model
, string propertyName
,
233 ref Type foundType
, ref ActiveRecordModel foundModel
)
235 foreach(HasManyModel hasManyModel
in model
.HasMany
)
237 // Inverse=true relations will be ignored
238 if (hasManyModel
.Property
.Name
== propertyName
&& !hasManyModel
.HasManyAtt
.Inverse
)
240 foundType
= hasManyModel
.HasManyAtt
.MapType
;
241 foundModel
= ActiveRecordModel
.GetModel(foundType
);
245 if (model
.IsJoinedSubClass
|| model
.IsDiscriminatorSubClass
)
247 return FindPropertyInHasMany(model
.Parent
, propertyName
, ref foundType
, ref foundModel
);
252 protected override object BindSpecialObjectInstance(Type instanceType
, string prefix
, Node node
,
257 ActiveRecordModel model
= CurrentARModel
;
264 object container
= CreateContainer(instanceType
);
267 Type targetType
= null;
268 ActiveRecordModel targetModel
= null;
270 found
= FindPropertyInHasAndBelongsToMany(model
, prefix
, ref targetType
, ref targetModel
);
274 found
= FindPropertyInHasMany(model
, prefix
, ref targetType
, ref targetModel
);
281 ClearContainer(container
);
283 if (node
.NodeType
== NodeType
.Indexed
)
285 IndexedNode indexNode
= (IndexedNode
) node
;
287 Array collArray
= Array
.CreateInstance(targetType
, indexNode
.ChildrenCount
);
289 collArray
= (Array
) InternalBindObject(collArray
.GetType(), prefix
, node
);
291 foreach(object item
in collArray
)
293 AddToContainer(container
, item
);
296 else if (node
.NodeType
== NodeType
.Leaf
)
298 PrimaryKeyModel pkModel
= targetModel
.PrimaryKey
;
299 Type pkType
= pkModel
.Property
.PropertyType
;
301 LeafNode leafNode
= (LeafNode
) node
;
305 if (leafNode
.IsArray
) // Multiples values found
307 foreach(object element
in (Array
) leafNode
.Value
)
309 object keyConverted
= Converter
.Convert(pkType
, leafNode
.ValueType
.GetElementType(),
310 element
, out convSucceeded
);
314 object item
= FindByPrimaryKey(targetType
, keyConverted
);
315 AddToContainer(container
, item
);
319 else // Single value found
321 object keyConverted
= Converter
.Convert(pkType
, leafNode
.ValueType
.GetElementType(),
322 leafNode
.Value
, out convSucceeded
);
326 object item
= FindByPrimaryKey(targetType
, keyConverted
);
327 AddToContainer(container
, item
);
336 protected virtual object FindByPrimaryKey(Type targetType
, object id
)
338 return FindByPrimaryKey(targetType
, id
, true);
341 protected virtual object FindByPrimaryKey(Type targetType
, object id
, bool throwOnNotFound
)
343 return ActiveRecordMediator
.FindByPrimaryKey(targetType
, id
, throwOnNotFound
);
346 protected override bool IsSpecialType(Type instanceType
)
348 return IsContainerType(instanceType
);
351 protected override void SetPropertyValue(object instance
, PropertyInfo prop
, object value)
353 object[] attributes
= prop
.GetCustomAttributes(typeof(WithAccessAttribute
), false);
355 if (attributes
.Length
== 0)
357 base.SetPropertyValue(instance
, prop
, value);
361 WithAccessAttribute accessAttribute
= (WithAccessAttribute
) attributes
[0];
362 IPropertyAccessor propertyAccessor
;
364 switch(accessAttribute
.Access
)
366 case PropertyAccess
.Property
:
367 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("property");
369 case PropertyAccess
.Field
:
370 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("field");
372 case PropertyAccess
.FieldCamelcase
:
373 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("field.camelcase");
375 case PropertyAccess
.FieldCamelcaseUnderscore
:
376 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("field.camelcase-underscore");
378 case PropertyAccess
.FieldPascalcaseMUnderscore
:
379 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("field.pascalcase-m-underscore");
381 case PropertyAccess
.FieldLowercaseUnderscore
:
382 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("field.lowercase-underscore");
384 case PropertyAccess
.NosetterCamelcase
:
385 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("nosetter.camelcase");
387 case PropertyAccess
.NosetterCamelcaseUnderscore
:
388 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("nosetter.camelcase-underscore");
390 case PropertyAccess
.NosetterPascalcaseMUndersc
:
391 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("nosetter.pascalcase-m-underscore");
393 case PropertyAccess
.NosetterLowercaseUnderscore
:
394 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("nosetter.lowercase-underscore");
396 case PropertyAccess
.NosetterLowercase
:
397 propertyAccessor
= PropertyAccessorFactory
.GetPropertyAccessor("nosetter.lowercase");
400 throw new ArgumentOutOfRangeException();
403 propertyAccessor
.GetSetter(instance
.GetType(), prop
.Name
).Set(instance
, value);
407 /// for joined subclasses BelongsTo properties doesn't include the ones of the parent class
408 /// so we need to check them recursively
410 protected bool IsBelongsToRef(ActiveRecordModel arModel
, string prefix
)
412 foreach(BelongsToModel model
in arModel
.BelongsTo
)
414 if (model
.Property
.Name
== prefix
)
419 if (arModel
.IsJoinedSubClass
|| arModel
.IsDiscriminatorSubClass
)
421 return IsBelongsToRef(arModel
.Parent
, prefix
);
426 protected override bool ShouldRecreateInstance(object value, Type type
, string prefix
, Node node
)
428 if (IsContainerType(type
))
433 if (node
!= null && CurrentARModel
!= null)
435 // If it's a belongsTo ref, we need to recreate it
436 // instead of overwrite its properties, otherwise NHibernate will complain
437 if (IsBelongsToRef(CurrentARModel
, prefix
))
443 return base.ShouldRecreateInstance(value, type
, prefix
, node
);
446 protected override void BeforeBindingProperty(object instance
, PropertyInfo prop
, string prefix
,
449 base.BeforeBindingProperty(instance
, prop
, prefix
, node
);
451 if (IsPropertyExpected(prop
, node
))
453 ClearExpectedCollectionProperties(instance
, prop
);
457 private bool IsPropertyExpected(PropertyInfo prop
, CompositeNode node
)
459 string propId
= string.Format("{0}.{1}", node
.FullName
, prop
.Name
);
461 if (expectCollPropertiesList
!= null)
463 return Array
.BinarySearch(expectCollPropertiesList
, propId
, CaseInsensitiveComparer
.Default
) >= 0;
469 private void ClearExpectedCollectionProperties(object instance
, PropertyInfo prop
)
471 object value = prop
.GetValue(instance
, null);
473 ClearContainer(value);
478 private object ObtainPrimaryKeyValue(ActiveRecordModel model
, CompositeNode node
, String prefix
,
479 out PrimaryKeyModel pkModel
)
481 pkModel
= ObtainPrimaryKey(model
);
483 String pkPropName
= pkModel
.Property
.Name
;
485 Node idNode
= node
.GetChildNode(pkPropName
);
487 if (idNode
!= null && idNode
.NodeType
!= NodeType
.Leaf
)
489 throw new BindingException("Expecting leaf node to contain id for ActiveRecord class. " +
490 "Prefix: {0} PK Property Name: {1}", prefix
, pkPropName
);
493 LeafNode lNode
= (LeafNode
) idNode
;
497 throw new BindingException("ARDataBinder autoload failed as element {0} " +
498 "doesn't have a primary key {1} value", prefix
, pkPropName
);
503 return Converter
.Convert(pkModel
.Property
.PropertyType
, lNode
.ValueType
, lNode
.Value
, out conversionSuc
);
506 private static PrimaryKeyModel
ObtainPrimaryKey(ActiveRecordModel model
)
508 if (model
.IsJoinedSubClass
|| model
.IsDiscriminatorSubClass
)
510 return ObtainPrimaryKey(model
.Parent
);
512 return model
.PrimaryKey
;
515 private bool IsValidKey(object id
)
519 if (id
.GetType() == typeof(String
))
521 return id
.ToString() != String
.Empty
;
523 else if (id
.GetType() == typeof(Guid
))
525 return Guid
.Empty
!= ((Guid
) id
);
529 return Convert
.ToInt64(id
) != 0;
536 private bool IsContainerType(Type type
)
538 bool isContainerType
= type
== typeof(IList
) || type
== typeof(ISet
);
540 if (!isContainerType
&& type
.IsGenericType
)
542 Type
[] genericArgs
= type
.GetGenericArguments();
544 Type genType
= typeof(ICollection
<>).MakeGenericType(genericArgs
);
546 isContainerType
= genType
.IsAssignableFrom(type
);
549 return isContainerType
;
552 private object CreateContainer(Type type
)
554 if (type
.IsGenericType
)
556 if (type
.GetGenericTypeDefinition() == typeof(ISet
<>))
558 Type
[] genericArgs
= type
.GetGenericArguments();
559 Type genericType
= typeof(HashedSet
<>).MakeGenericType(genericArgs
);
560 return Activator
.CreateInstance(genericType
);
562 else if (type
.GetGenericTypeDefinition() == typeof(IList
<>))
564 Type
[] genericArgs
= type
.GetGenericArguments();
565 Type genericType
= typeof(List
<>).MakeGenericType(genericArgs
);
566 return Activator
.CreateInstance(genericType
);
571 if (type
== typeof(IList
))
573 return new ArrayList();
575 else if (type
== typeof(ISet
))
577 return new HashedSet();
583 private void ClearContainer(object instance
)
585 if (instance
is IList
)
587 (instance
as IList
).Clear();
589 else if (instance
is ISet
)
591 (instance
as ISet
).Clear();
595 private void AddToContainer(object container
, object item
)
597 if (container
is IList
)
599 (container
as IList
).Add(item
);
601 else if (container
is ISet
)
603 (container
as ISet
).Add(item
);
605 else if (container
!= null)
607 Type itemType
= item
.GetType();
609 Type collectionType
= typeof(ICollection
<>).MakeGenericType(itemType
);
611 if (collectionType
.IsAssignableFrom(container
.GetType()))
613 MethodInfo addMethod
= container
.GetType().GetMethod("Add");
615 addMethod
.Invoke(container
, new object[] {item}
);