PROPOSAL: Lightweight Properties
David Goodenough
david.goodenough at linkchoose.co.uk
Tue Mar 3 13:04:58 PST 2009
Yes I do. What you propose is much more invasive. Look at it
again and maybe you will see the Light(ness).
David
On Tuesday 03 March 2009, Reinier Zwitserloot wrote:
> You call that lightweight?
>
> How about following the beans spec more to the letter and just
> generate the addPropertyChangeListener, removePropertyChangeListener,
> setX(), and get/isX() method in response to seeing a keyword or
> annotation on a given field. You'll have to work out the details, but
> that sounds far, far simpler to understand.
>
> You'll need to flesh this out, but it would look something like:
>
> public class Foo {
> private property int x;
> }
>
> Which would generate the addPropertyChangeListener,
> removePropertyChangeListener, setX, getX methods, all public, along
> with the required infrastructure to make it tick. If you don't like
> the generation, for example because you want the setter to be package
> private, you just add the setter in the source file; the keyword will
> only generate the missing stuff. It doesn't cover every use case, but
> there's always the alternative of doing whatever people do now with
> beans. Something you didn't mention in your proposal, by the way.
>
> I think there's also a fully fleshed out property proposal (including
> a 'property' keyword) out there somewhere.
>
> Possibly make a way to opt out of generating the property change
> listener support, and just the getters/setters.
> --Reinier Zwitserloot
>
> On Mar 3, 2009, at 15:59, David Goodenough wrote:
> > Below is my proposal for Lightweight Properties. I know that the
> > syntax
> > change is an abbomination to some people, but I have tried to reduce
> > this to its absolute minimum, while still getting a significant
> > benefit.
> >
> > PROJECT COIN SMALL LANGUAGE CHANGE PROPOSAL FORM v1.0
> >
> > AUTHOR(S):
> >
> > David Goodenough, long time Java user. I can be reached at
> > david.goodenough at linkchoose.co.uk.
> >
> > OVERVIEW
> >
> > FEATURE SUMMARY:
> >
> > Lightweight Property support
> >
> > MAJOR ADVANTAGE:
> >
> > Both BeansBinding (whether JSR-295 or others such an JFace or the
> > JGoodies
> > binding) and the JPA Criteria API currently require field names (as
> > Strings)
> > as arguments, which an IDE/compiler can not check. With this
> > proposal the
> > strings would be abandoned, and the IDE/compiler will be able to
> > check the
> > correctness of the code.
> >
> > MAJOR BENEFIT:
> >
> > Manual checking no longer required. This proposal introduces a
> > simple well
> > defined IDE/compiler checkable solution.
> >
> > MAJOR DISADVANTAGE:
> >
> > It is a language change, and this seems to upset some people.
> >
> > ALTERNATIVES:
> >
> > None really, apart from using another language or continuing to use
> > String
> > names. The existing solutions all require String names which are
> > uncheckable.
> >
> > EXAMPLES
> >
> > Lets assume we have a POJO called foo, of type Foo with a field bar
> > of type
> > Bar, which itself has a field of type Jim called jim.
> >
> > There are two forms of lightweight properties:-
> >
> > 1) foo#bar would be translated by the compiler into:-
> >
> > new Property<Foo,Bar>(foo,"bar");
> >
> > while foo#bar#jim would be translated into:-
> >
> > new Property<Foo,Jim>(foo,"bar","jim");
> >
> > 2) Foo#bar would be translated into:-
> >
> > new Property<Foo,Bar>(Foo.class,"bar");
> >
> > while Foo#bar#jim would be translated into:-
> >
> > new Property<Foo,Jim>(Foo.class,"bar","jim");
> >
> > These two forms create (1) a bound Property, or (2) an unbound one.
> > Bound
> > Properties are explicitly bound to a particular instance of a class
> > (in this
> > case foo), while unbound Properties are templates which can be
> > applied to any
> > instance of class Foo. Actually bound properties can also be used as
> > unbound
> > properties, but that is a harmless and useful side effect not a
> > primary
> > intent.
> >
> > The Property class would need to be added (it is appended below),
> > and should
> > be added either to the java.beans package or to the
> > java.lang.reflect package
> > (with which is probably has more in common).
> >
> > Syntactically a "#" can be placed wherever a "." can be placed
> > (except inside
> > a number), and the same checks need to be made (that each field to
> > the right
> > of a # is a field in the left hand side) as would be made for a ".".
> > The only
> > difference is in field visibility - For the "#" any field is
> > visible, which
> > follows the model currently available in the Field class with
> > getDeclaredFields(). It also follows the model that while a field
> > might be
> > private and therefore not directly accessible from outside, getters
> > and
> > setters can provide access.
> >
> > The Property object provides type safe access to the field in the
> > form of
> > getters and setters. These come in pairs, one for bound and the
> > other for
> > unbound access. So for bound access no object is required to fetch
> > the value,
> > for an unbound object the parent object needs to be specified. So if
> > we
> > have:-
> >
> > Property<Foo,Bar>prop = foo#bar;
> >
> > we can later say:-
> >
> > Bar b = prop.get();
> >
> > or for an unbound one from a second Foo object foo2:-
> >
> > Bar b = prop.get(foo2);
> >
> > The getters and setters in the Property object will defer to
> > explicitly coded
> > getters and setters if present, otherwise they will use the Field
> > getter and
> > setter.
> >
> > If a setter is not explicitly coded, the implicit setter will look
> > for a
> > PropertyChangeSupport object in the parent object of the rightmost
> > field and
> > fire a PropertyChangeEvent to that object.
> >
> > There are also two Annotations provided by the Property class,
> > ReadOnly and
> > WriteOnly. These stop implicit getters and setters from trying to
> > read/write
> > the property.
> >
> > Talking of Annotations, this notation can also be used to get at the
> > Annotations for a field. So to test for the presence of an
> > Annotation Ann on
> > Foo.bar we would use:-
> >
> > if(Foo#bar.getFields()[0].isAnnotationPresent(Ann.class)) ...
> >
> > SIMPLE EXAMPLE:
> >
> > To take an example from BeansBinding (taken from Shannon Hickey's
> > blog):-
> >
> > // create a BeanProperty representing a bean's firstName
> > Property firstP = BeanProperty.create("firstName");
> > // Bind Duke's first name to the text property of a Swing JTextField
> > BeanProperty textP = BeanProperty.create("text");
> > Binding binding = Bindings.createAutoBinding(READ_WRITE, duke,
> > firstP, textfield, textP);
> > binding.bind();
> >
> >
> > would instead be written:-
> >
> > Binding binding = Bindings.createAutoBinding(READ_WRITE,
> > duke#firstName, textfield#text);
> > binding.bind();
> >
> > which of course can be checked by the IDE/compiler, and will not
> > wait until
> > run time (not even instantiation time) to show up the error.
> >
> > ADVANCED EXAMPLE:
> >
> > For a JComboBox (or JList or JTable or JTree) there is a need to map
> > a list of
> > objects to the value strings (or column contents). For this we need
> > to have
> > an unbound Property which can be applied to each element of the list.
> >
> > Duke duke;
> > List<Duke>dukes;
> > BoundComboBox combo = new
> > BoundComboBox(dukes,Duke#fullname,this#duke);
> >
> > and now the combo box will be populated from the list dukes, and the
> > display
> > values in the list will be taken from the fullname field of each Duke
> > element, and the initial value will be set from the local class
> > field duke
> > and any changes to the combo box selected element will be reflected
> > back to
> > the duke field.
> >
> > DETAILS
> >
> > SPECIFICATION:
> >
> > This proposal adds a new syntactic element, "#", which can be used
> > in the same
> > way that "." can be used to qualify fields within a Java object.
> >
> > COMPILATION:
> >
> > This proposal requires no change to the class files, and is
> > implemented by a
> > simple generation of the required instance using the relevant Property
> > constructor. Obviously the compiler would have to make sure that the
> > use that
> > the property object was being put to (in the examples above the left
> > hand
> > side of the assignment) had the correct Generic attributes.
> >
> > TESTING:
> >
> > How can the feature be tested?
> >
> > LIBRARY SUPPORT:
> >
> > The new Property class is required (see below).
> >
> > REFLECTIVE APIS:
> >
> > No changes are required to the reflective APIs although it makes
> > extensive use
> > of those APIs.
> >
> > OTHER CHANGES:
> >
> > No other changes are requires.
> >
> > MIGRATION:
> >
> > Fortunately there is no code that is formally part of J2SE 6 which
> > uses such
> > Properties. There are however two proposals which will need it
> > (BeansBinding
> > and JPA Criteria API), but neither of these seem to be destined to
> > be part of
> > J2SE 7 (BeansBinding seems to have died the death and the Criteria
> > API would
> > be part of the next J2EE which will follow J2SE 7), so this will
> > provide a
> > base for them to use and no existing code need to be updated.
> >
> > There are other extant Beans-Binding libraries, which could be
> > modified to use
> > this proposal, but as none of the existing features have been
> > changed there
> > is no need to change them (other than for type safety and compiler/IDE
> > checkability).
> >
> > COMPATIBILITY
> >
> > BREAKING CHANGES:
> >
> > None. This change should not make any existing correct code fail to
> > compile
> > or run or change the way in which it compiles/runs.
> >
> > EXISTING PROGRAMS:
> >
> > No change required to any existing programs
> >
> > REFERENCES
> >
> > EXISTING BUGS:
> >
> > None
> >
> > URL FOR PROTOTYPE (optional):
> >
> > I do not have the knowledge to make changes to the compiler, and the
> > only
> > documentation making such changes concentrated on adding operators not
> > changes at this level. So there is no prototype of the compiler
> > part, but the
> > Property class follows:-
> >
> > package java.lang.reflect;
> >
> > import java.beans.BeanInfo;
> > import java.beans.Introspector;
> > import java.beans.PropertyChangeSupport;
> > import java.beans.PropertyDescriptor;
> > import java.lang.reflect.Field;
> > import java.lang.reflect.Method;
> >
> > /**
> > * Property class
> > * This is the support class for use with the # notation to provide
> > lightweight
> > * Property support for Java.
> > *
> > * @copyright Copyright(C) 2009 David Goodenough Linkchoose Ltd
> > * @licence LPGL V2 : details of which can be found at http://fsf.org.
> > * @author david.goodenough at linkchoose.co.uk
> > *
> > * @param <C> The Parent class for this field
> > * @param <F> The Type of this field
> > */
> > public class Property<C,F> {
> > private C parent;
> > private Class<?> parentClass;
> > private Field[] fields;
> > private PropertyDescriptor[] pd = null;
> > /**
> > * Constructor used to create Property objects. The Parent object
> > may be
> > * null, but should normally be specified as it can be overridden
> > anyway.
> > * @param parent C object that contains the field
> > * @param field Field describing this field
> > */
> > public Property(C parent, String ... fieldNames) {
> > this.parent = parent;
> > this(parent.getClass(), fieldNames);
> > }
> > /**
> > * Constructor for unbound Properties, but also used internally
> > after
> > setting
> > * the parent object by the bound Property objects.
> > * @param parentClass Class of the parent object
> > * @param fieldNames String[] of field names
> > */
> > public Property(Class<?>parentClass, String .. fieldNames) {
> > this.parentClass = parentClass;
> > fields = new Field[fieldNames.length];
> > pd = new PropertyDescriptor[fieldNames.length];
> > outer: for(int index = 0; index < fields.length; index++) {
> > Field[]dclFields = parentClass.getDeclaredFields();
> > for(Field field:dclFields) {
> > if(field.getName().equals(fieldNames[index])) {
> > fields[index] = field;
> > field.setAccessible(true);
> > try {
> > BeanInfo beanInfo =
> > Introspector.getBeanInfo(parent.getClass());
> > PropertyDescriptor[]props =
> > beanInfo.getPropertyDescriptors();
> > for(PropertyDescriptor prop : props) {
> > if(prop.getName().equals(field.getName())) {
> > pd[index] = prop;
> > break;
> > }
> > }
> > } catch(Exception e) { /* assume can not find getter/
> > setter
> > */ }
> > parentClass = field.getType();
> > continue outer;
> > }
> > }
> > throw new IllegalArgumentException("Field " + fieldNames[index] +
> > " not found in class " +
> > parentClass.getCanonicalName());
> > }
> > }
> > /**
> > * Getter from the field in the parent specified when this
> > Property was
> > created.
> > * @see Property.get(C otherParent)
> > * @return F the value of this field
> > */
> > public F get() {
> > return get(parent);
> > }
> > /**
> > * Getter with explicit parent.
> > * This code will check see if this field is WriteOnly, and
> > complain if it
> > is.
> > * It will then see if the use has provided am explicit getter,
> > and call
> > that
> > * if present, otherwise it will just fetch the value through the
> > Field
> > provided
> > * method.
> > * @param otherParent C parent object
> > * @return F value of the field
> > */
> > @SuppressWarnings("unchecked") // This should actually not be
> > needed,
> > // but the Field.get method is not
> > typed
> > public F get(C otherParent) {
> > Object result = otherParent;
> > try {
> > for(int index = 0; index < fields.length; index++) {
> >
> > if(fields[index].getType().isAnnotationPresent(WriteOnly.class))
> > throw new IllegalAccessException(
> > "Can not get from a WriteOnly field - " +
> > fields[index].getName());
> > Method getter = pd[index] == null ? null :
> > pd[index].getReadMethod();
> > if(getter == null) result = fields[index].get(result);
> > else result = getter.invoke(result);
> > }
> > } catch(Exception e) {
> > throw new RuntimeException("Should not occur exception", e);
> > }
> > return (F)result;
> > }
> > /**
> > * Setter to set the value of the field in the parent object
> > declared with
> > the
> > * Property object
> > * @param newValue F new value of this field
> > */
> > public void set(F newValue) {
> > set(parent,newValue);
> > }
> > /**
> > * Setter to set the value of the field to an explicit parent
> > object.
> > * If there is a ReadOnly annotation, then we object. If there is
> > an
> > explicit
> > * setter then we use that, otherwise we set the field using the
> > Field
> > provided
> > * set method and if there is a PropertyChangeSupport field, fire a
> > property
> > * change event to it.
> > * We walk our way down the field chain, until we have the last
> > object and
> > its
> > * field, and then we do the set.
> > * @param parent C explicit parent object
> > * @param newValue F new value for field in parent
> > */
> > public void set(C parent,F newValue) {
> > try {
> > Object last = parent;
> > int index;
> > for(index = 0; index < fields.length - 1; index++) {
> >
> > if(fields[index].getType().isAnnotationPresent(WriteOnly.class))
> > throw new IllegalAccessException(
> > "Can not get from a WriteOnly field - " +
> > fields[index].getName());
> > Method getter = pd[index] == null ? null :
> > pd[index].getReadMethod();
> > if(getter == null) last = fields[index].get(last);
> > else last = getter.invoke(last);
> > }
> >
> > if(fields[index].getType().isAnnotationPresent(ReadOnly.class))
> > throw new IllegalAccessException(
> > "Can not get from a WriteOnly field - " +
> > fields[index].getName());
> > Method setter = pd[index] == null ? null :
> > pd[index].getWriteMethod();
> > if(setter == null) {
> > PropertyChangeSupport pcs = findPcs(last.getClass());
> > fields[index].set(last,newValue);
> > if(pcs != null)
> > pcs.firePropertyChange(fields[index].getName(),
> > newValue,
> >
> > fields[index].get(last));
> > } else setter.invoke(last,newValue);
> > } catch(Exception e) {
> > throw new RuntimeException("Should not occur
> > exception", e);
> > }
> > }
> > /**
> > * This is used so that the caller can view the Field name
> > * @return String field name
> > */
> > public String[] getFieldName() {
> > String[]names = new String[fields.length];
> > for(int index = 0; index < fields.length; index++) {
> > names[index] = fields[index].getName();
> > }
> > return names;
> > }
> > /**
> > * This method is used to fetch the Field array, which is useful
> > if you
> > need to
> > * access the Annotations of a field.
> > * @return Field[] the array of Fields describing this Property.
> > */
> > public Field[] getFields() {
> > return fields;
> > }
> > /**
> > * This private method looks for a PropertyChangeSupport object in
> > the
> > class and
> > * if one is found it will return it. It looks right the way up
> > the class
> > tree
> > * by recurring up the superClasses.
> > * @param parent Class to check for PropertyChangeSupport fields
> > * @return PropertyChangeSupport first found object, or null if
> > not found
> > */
> > private PropertyChangeSupport findPcs(Class<?> parent) {
> > Field fields[] = parent.getDeclaredFields();
> > for(Field field:fields) {
> > field.setAccessible(true);
> > try {
> > if(field.getType() == PropertyChangeSupport.class)
> > return (PropertyChangeSupport)field.get(parent);
> > } catch(Exception e) { }
> > }
> > // If we did not find it then try the superclass
> > Class<?>superClass = parent.getSuperclass();
> > if(superClass == null) return null;
> > return findPcs(parent.getClass().getSuperclass());
> > }
> > /**
> > * This annotation is used to mark a field as WriteOnly, i.e. it
> > can not
> > be read.
> > * This stops the automatic getter operation.
> > */
> > public @interface WriteOnly {
> > }
> > /**
> > * This annotation is used to mark a field as ReadOnly, i.e. it
> > can not be
> > written.
> > * This stops the automatic setter operation.
> > */
> > public @interface ReadOnly {
> > }
> > }
More information about the coin-dev
mailing list