> I'm still going through the patch but as I understand it so far the
> key changes are (please correct me if I'm wrong):
No corrections.
> In a quick attempt at solving 1. and have used your idea and added
> support for annotations on the BeanModelProvider class declaration.
> i.e.
>
> @NestedBeans({Address.class, ContactDetails.class})
> public class PersonProvider extends BeanModelProvider<Person>
>
> The above usage however won't automatically handle the recursion
> problem in 2. (and I would need ensure the rebind generator gives you
> an meaningful/useful RecusiveBeanPathException if this case occurs).
My problem with that is that it can get really verbose. For example, if
I have 3 object A, B, C with link from A to B and B to C, I have to
annotate the BeanModelProvider of A with B and C, and the
BeanModelProvider of B with C. And if I add something to C, I then might
have to go back to all those providers and add the new class.
> To handle the recursion issues I'd had been considering an annotation
> as follows:
>
> @NestedBeanPaths({"address", "contactDetails", "some.other.path"})
> public class PersonProvider extends BeanModelProvider<Person>
>
> This wouldn't suffer from the recursion problem but uses type-unsafe
> strings for property names. It also might be a little confusing as
> while it lists "address" it's actually making paths like
> "address.postcode" etc available.
This one seems very hard to maintain. One has to explicitly declares all
properties path one will be using. If one is reusing the same
BeanModelProvider in more than one place, one has to declare all the
nested beans one will use.
> So in summary for me (at this stage):
> * I think I prefer annotated providers since it keeps the beans that
> the provider exposes with the provider declaration. i.e. if I'm using
> the factory I'd need to go and find what/where to annotate additional
> beans. The flip side is that you'll have to annotate every provider.
> There may be some inbetween cas.
Well, I do prefer the inverse :) I can have a single place where I
declare all the Beans that are usable with pectin, and I have to do only
once. I can still create a class by bean if I wish, but it is not
required anymore.
> * If optimisation isn't an issue, or if people find the multiple
> annotations too complex/confusing then your implementation is better
> since it's simpler to use and the recursion problem just goes away.
>
> * If optimisation is an issue, or if people want the @NestedBeanPath
> option then the current generator mechanism is probably worth
> pursuing.
>
> I'm interested in how people feel about the addition of both
> @NestedBean & @NestedBeanPaths. Is it harming the usability of the
> API (i.e. mutliply ways of doing the same thing, multiple annotations,
> annotations that don't work in certain situations)?
I'd really like to know how well optimization is working.
On the another hand (and as you proposed by mail), maybe one solution
would be to have a clear API for the reflection part of pectin, and
allows multiple implementations.
> And in summary summary... I think the recursion problem and
> optimisation are mutually exclusive, i.e. any optimisable solution
> that uses only types to declare nested beans will have the recursion
> problem. Happy to be proved wrong.
Well, I agree with your point, but I think it is not the only case where
optimization will not work. As soon as the user is not using constant
strings for the property (if it is constructing those in a not trivial
way), then the optimization will go away.
> > Moreover, I might be stupid, but I do not see what is the point of
> > having a parameter in BeanIntrospector#getBeanAccessorForPropertyPath.
> > This parameter is only used for an exception on your implementation of
> > the generator, and it is quite confusing.
>
> I presume you mean `getAccessorByType(String path, Class type)`? If
> so then yes the path is only used to generate context for the "you
> found a pectin bug" exception. I think it's even more confusing
> because it's the first parameter and it's poorly named. I'll change
> the signature to something like `getAccessorByType(Class type, String
> pathForDebugInfo)`.
Sorry, I did read the generated implementation of TestBeanProvider and I
get a better understanding of the parameter. But, from an API point of
view, it is quite weird. If I understand correctly, the only reason of
this parameter, is for handling nested properties. Now let's say I have a
BeanIntrospector for Object A, that as a b property to Object B, that has
a string property.
I will consider an object aObject of type A and the "b.string" nested property.
What I have to do, manually, with the BeanIntrospector as it is now, is:
# call getBeanAccessorForPropertyPath("b").readProperty(aObject, "b") and
stock it on bObject (note that I have more or less to give the same
parameter "b" on the first and second call)
# call getBeanAccessorForPropertyPath("b.string").readProperty(bObject,
"string") to get my String. Once again I have to have a strong
correlation between my first and second calls (mainly the "string")
I'll be a lot more confortable using something like
getBeanAccessorForPropertyPath().readProperty("b.string", aObject)
It will allows
# to have getBeanAccessorForPropertyPath returns a cached object, because
it does not depends anymore on the usage afterwards
# to prevent all parsing of the properties outside of the
BeanIntrospector, and if you use your idea of declaring all nested
properties that will be used, it allows not to use any parsing inside
the method as well, so it will help optimization (if indeed GWT is able
to optimize this case, which I do not know)
--
Benjamin Lerman
Here a new version of the patch that add another functionality:
polymorphism
http://www.ambre.net/misc/gwt-pectin-nested-20100618.patch
This won't work yet with a BeanModelProvider, ony with the
BeanPropertyAccessor, but I'm pretty certain I can make it work.
There is 2 problems to have this work with a BeanModelProvider
1. BeanModelProvider#getValueModel do a type check using
getPropertyType, and getPropertyType does not work yet for properties
that only exist on a sub-class. One solution for that is to remove this
check (but that's quite inelegant, and won't solve the next problem), or
to have getPropertyType take into account the hierarchy of the beans and
returns a type as soon as all descendant of a bean agrees on it.
2. BeanModelProvider do send event with the old value of all declared
ValueModel as soon as one set the value, and this will fail the first
time, because the old value is null and one cannot find the real
implementation of a polymorphic attribute in that case. One again it can
be resolved by the same solution.
I'll try to implements a BeanIntrospector that know about the type
hierarchy to resolve thoss problems.
--
Benjamin Lerman
That one I did not think about, and you are completely right.
> Perhaps a solution that allows developer to limits the code created
> while allowing annotations to be defined in one place would be to use
> an annotated factory?
That would be fine with me. I'll need to have the getBeanIntrospector
methods also, but this one is cheap to add.
> To me this has a couple of advantages, for one the bean declarations
> are always on the factory so there's only one place to look for them
> (easier for developer consistency), developers can control the scope
> of what introspectors will be created for a given entry point in a
> single project and it also works nicely with GIN while allowing those
> who prefer a static getters to use some thing like the snippet above.
Fine with me.
> I think extracting an AbstractBeanModelProvider or some such that
> works for both of us would be well worth it. For what it's worth
> FormModel only relies on the ValueModelProvider and ListModelProvider
> interface types so you can use what ever you want, however
> implementing something like BeanModelProvider from scratch is somewhat
> of an onerous task.
Agreed. My patch relied almost entirely on your code.
> That's a usecase I've never come across. Do you have an example? I'd
> still be very nervous of ditching any/all optimisation because it may
> not work in some cases.
I don't have a real use case just now, but you can look at the test I
wrote for inheritance. It did construct the path dynamically. I can
imagine other case when one has a Bean A with 2 properties b1 and b2 to
a bean B and one wants to factorize the code by using a method that
takes a String parameter than can be either b1 or b2.
> Yep, it's sort of one level out and is a bit confusing. I wasn't
> really expecting anyone to be using this code so I haven't tried too
> hard to polish it.
Sorry :)
> The getAccessorForPropertyPath(String path) is designed to take a full
> path such as 'a.b.c' and return an introspector for the object of type
> 'b'. From here the provider creates a BeanPropertyValueModel for
> `a.b.c` using the ValueModel for the path 'a.b' and a
> BeanPropertyAccessor for type b. Using unprocessed strings gives the
> change for optimisation (but I introduced the PropertyKey bug during
> the changes to support nested properties).
Well, you'll always need to process string with this approach, because
you have to compute the c from 'a.b.c'. You need to call something like:
getBeanAccessorForPropertyPath('a.b.c').readProperty(o, 'c')
the second 'c' is always computed in your case.
What you could do, is removing the parameter of
getBeanAccessorForPropertyPath and have:
getBeanAccessorForPropertyPath().readProperty(o, 'a.b.c.')
Then you won't have any computation, but the method readProperty can be
quite long. You have to consider all the possible paths (and forget
about recursion, but I think we agreed that recursion won't be possible
without computation). Now, if the optimization do cut all unused path
and inline all calls, you have something very good, if it doesn't, then
you have a monstrous if else if else... that is quite inefficient.
> I'm going to make some changes to PropertyKey to fix the optimisation
> issues, and change getAccessorForType(String,Class) etc and check
> those in. I'll let you know when I've done that.
Ok.
> There are also a couple of basic principles I'm trying to follow as
> well which are probably worth outlining when you're generating ideas
> and patches:
> * What I can't catch at compile time I try as much as possbile to
> catch at construction. I.e. provider.getValueModel("a.b.c",
> String.class) must fail if the property doesn't exist, is the wrong
> type, wrong element type etc.
That's a good principle in general, but I have a problem here. Maybe
you can tell me what would be a good solution for it.
In my second path, I write the test classes AbtractRootBean, SonBean1
and SonBean2 with SonBean1 and SonBean2 extending AbtractRootBean. I
wanted to be able to use pectin in the case of polymorphism. My problem
is that I cannot check the property without having a real bean, because
when I have a property of type AbtractRootBean, it can in fact be a
SonBean1 or a SonBean2. Would you consider acceptable to lose a little
bit of check at construction to allow those case to work?
My idea was to enhance the BeanIntrospector to take inheritance into
account. That means that if I have a hierarchy, and that a property "a"
exist for some element in the hierarchy (but always with a compatible
type), I will consider that "a" is a property of all the parents. I'm
not sure to be clear so there is an example:
class A {
}
class A1 extends A {
String a;
String b;
}
class A2 extends A {
int b;
}
class A21 extends A2 {
String a;
}
class A22 extends A2 {
}
Than, I will have the BeanIntrospector for A, A1, A2 and A21 accept "a"
as a property for all methods that do not take an actual object. But
A won't accept "b", because the type is not coherent in the hierarchy.
That will break you principle, because there would be way to fail later
than construction, but that would allow to use polymorphism in some
case.
> * Exceptions need to be meaningful and helpful. I.e.
> BeanModelProviders know are given the full path of the property they
> use purely for meaningful exception messages. That way when they
> throw a ReadOnlyPropertyException the can say "a.b.c.e.date" was read
> only. Saying only the property "date" was readonly is the type of
> exception that causes pain.
On this ,I agree 100%, and I have no problem with the extra parameter
in getAccessorByType (it is a private, generated, method so not part of
any API). My question was there because I had not understood how you
were using getBeanAccessorForPropertyPath.
--
Benjamin
> Anyway, I hope the new implementation is easier to understand (with
> the parent path stuff, check out ReflectionBeanModelProvider for a
> runtime implementation example).
>
> It's not quite finished yet but I'm much happier with the use of the
> AbstractBeanModelProvider/PropertyDescriptor combination and I don't
> expect that part of the API to change (althought I'm not overly happy
> with the PropertyDescriptor name).
>
> If you have any hassles with it let me know.
I redid my patch, you can find it there:
http://www.ambre.net/misc/gwt-pectin-nested-20100707.patch
What I'd like to add is:
* Follow your suggestion, and be able to generate a Factory from a given
class that will be annotated, instead of looking for all existing
annotations to handle the case of multiple entry points.
* Rework on the generator to be able to handle polymorphism.
Unfortunately, I might not be able to do that before at least a full
week.
Now, I have a question. Having a BeanModelProvider, a bean and a
nested property. I have to write the following method to be able to read/write
the property:
<code>
public <T> Object readProperty(BeanModelProvider<T> provider, T bean, String path) {
PropertyDescriptor propertyDescriptor = provider.getPropertyDescriptor(path);
if(propertyDescriptor.getParentPath() == null) {
return propertyDescriptor.readProperty(bean);
} else {
Object subBean = readProperty(provider, bean, propertyDescriptor.getParentPath());
return propertyDescriptor.readProperty(subBean);
}
}
public <T> void writeProperty(BeanModelProvider<T> provider, T bean, String path, Object value) {
PropertyDescriptor propertyDescriptor = provider.getPropertyDescriptor(path);
if(propertyDescriptor.getParentPath() == null) {
propertyDescriptor.writeProperty(bean, value);
} else {
Object subBean = readProperty(provider, bean, propertyDescriptor.getParentPath());
propertyDescriptor.writeProperty(subBean, value);
}
}
</code>
Would'nt it be simple to be able to do:
<code>
provider.getPropertyDescriptor(path).readProperty(bean)
</code>
even with nested properties. I'm pretty certain that your generator
needs only a very few modification to be able to work like that. So is
there a reason why you want to keep using parentPath and recursion,
instead of having all the logic in the generated PropertyDescriptor
Cheers,
--
Benjamin
> That's fine. Were you still hoping to get it accepted into trunk? If
> so it would help me if you moved your implementation to a distinct
> package, perhaps under `com...client.bean.dynamic` perhaps? It will
> also need to be a server side reflection implementation along with
> some wiki documentation, but hold off on that till I'm closer to
> making a decision on what I want to do.
>
> I also need to extract the test classes (i.e. the test beans etc) out
> into a new source tree/sub-project so a common data structure can be
> used by both the genreated and reflection based providers (till now
> it's been an evil cut & paste). This is especially important if I add
> your implementation to trunk. This will take a while too and I'm not
> sure when I'll get onto it.
Yes, I still hope to have it accept. If not, I probably try to have a
dedicated project with it, but I'll go this path only if you do not want
it in trunk.
Sorry, I wasn't clear.
I know you do not use the code I posted, it is just an excerpt of what
one must do with PropertyDescriptor to read/write a nested property.
In pectin you do not do it directly like that because you have a tree
of ValueSource that are created to get the list of PropertyDescriptor,
and then each one call the other to do the recursion I showed.
What I do not understand is, why is this necessary. You could modify
the PropertyDescritor so that, for whichever path that it can use, the
PropertyDescritor for this path is sufficient to read/write the value.
Is there a reason for you to have written your API this way.
If you need, I think I can write a patch that show you what I mean. It
should simplify your code, because you won't have to consider if a path
is nested or not anywhere but in the generator.
--
Benjamin
> Cool, I think a separate project would be preferable for me at this
> point. That way people won't send me bug fixes and feature requests
> and our release cycles can remain independant (once we sort out the
> current api issues I expect things to be pretty stable for you to code
> against). I'll add link to you implementation on the wiki with a list
> of features you want to highlight. I'm still very open to including
> it in the future once it's stablised and especially if others find it
> useful.
If I create a separate project, I'll need an API to call on your
generators.
Specifically, I added a method
public String createBeanDescriptor(JClassType beanType)
in BeanModelProviderCreator that calls your generateDescriptorVars and
generateGetPropertyDescriptorMethod methods.
Adding the createBeanDescriptor in your generator class does not seem
to be the right move :). I don't know if you can make the 2 generate
methods public, or if we need to find a better way to extract an API
from this.
> I'm still not sure what you mean, so I'll try and outline what I'm
> doing if you think your approach is better then you can send me a
> patch.
Thanks for the explanation. I did find out while writing my patch why
you need the parent path (mainly to be able to change the mutable state
of your properties if an intermediary object is null) and so my idea
won't work at all.
On another subject, I'm still trying to find out if it is possible to do
any polymorphism. Did you already think about it?
--
Benjamin