PROPOSAL: Lightweight Properties

David Goodenough david.goodenough at linkchoose.co.uk
Tue Mar 3 06:59:02 PST 2009


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