Atom Feed SITE FEED   ADD TO GOOGLE READER

Implementing Observers with Stateless Properties

I've got a simplified Property interface and a mixin that makes the property observable:
public interface Property<B,V> {
V get(B bean);
void set(B bean, V value);
}

public interface ObservableProperty<B,V> extends Property <B,V> {
void addPropertyChangeListener(B bean, PropertyChangeListener p);
void removePropertyChangeListener(B bean, PropertyChangeListener p);
}
For most use cases, the interface is implemented by a library. For example, this instance uses the getters and setters:
  ObservableProperty p = new BeanProperty(Customer.class, "address");
Today I'm interested in the implementation of this interface. Here's a very straightforward implementation that doesn't even need reflection - getting the Document property from a JTextField:
public class JTextFieldDocumentProperty 
implements ObservableProperty<JTextField, Document> {

public Document get(JTextField bean) {
return bean.getDocument();
}

public void set(JTextField bean, Document value) {
bean.setDocument(value);
}

public void addPropertyChangeListener(JTextField bean, PropertyChangeListener p) {
bean.addPropertyChangeListener("document", p);
}

public void removePropertyChangeListener(JTextField bean, PropertyChangeListener p) {
bean.removePropertyChangeListener("document", p);
}
}
This code is deceivingly simple because the listener types in the Property interface (PropertyChangeListener) exactly match the listener types in the bean. Things get a much more interesting if the listener types are different.

Using an adapter
The natural solution is to code the addPropertyChangeListener to use a simple adapter to convert events of one type to events of another type. This adapter converts between DocumentListener and PropertyChangeListener:
private class DocumentToPropertyChangeListener
implements DocumentListener {
private String currentText;
private final Document document;
private final PropertyChangeListener delegate;

public DocumentToPropertyChangeListener(Document bean, PropertyChangeListener p) {
this.document = bean;
this.delegate = p;
this.currentText = getDocumentText();
}

private String getDocumentText() {
try {
return document.getText(0, document.getLength());
} catch (BadLocationException e) {
throw new RuntimeException(e);
}
}

public void insertUpdate(DocumentEvent e) {
fireChanged();
}
public void removeUpdate(DocumentEvent e) {
fireChanged();
}
public void changedUpdate(DocumentEvent e) {
fireChanged();
}
public void fireChanged() {
String oldText = currentText;
currentText = getDocumentText();
delegate.propertyChange(
new PropertyChangeEvent(document, "text", oldText, currentText));
}
}
As you can see, the Document class is a bit cumbersome to use. But this interface correctly adapts the listener types. Unfortunately, we still have the trickiest problem ahead of us...

The problem with removePropertyChangeListener()
In my second example, I'll implement Property for the text of a Document. The implementation of addPropertyChangeListener is easy; we just create the adapter inline:
public class DocumentTextProperty 
implements StatelessProperty<Document, String> {
...
public void addPropertyChangeListener(Document bean, PropertyChangeListener p) {
bean.addDocumentListener(new DocumentToPropertyChangeListener(bean, p));
}
public void removePropertyChangeListener(Document bean, PropertyChangeListener p) {
...
}
}
But how does remove work? There's a big problem - we didn't save a reference to our adapter! We need to recover that reference somehow so we can remove the same adapter that was added.

The Document interface doesn't expose its listeners; ie. there's no getDocumentListeners() method. So we can't just iterate over the document's listeners to find the matching adapter.

If we store a reference to the adapter in the DocumentTextProperty object, then we'll almost certainly have a memory leak. Even if we stored just a WeakReference<DocumentTextProperty>, it would make our Property stateful. In the ideal implementation, any two instances of the same property should be interchangeable. For example, we shouldn't force the user to store a reference to the Property instance. This code should work fine but it won't work if the property is stateful:
  public void installListeners() {
new DocumentTextProperty().addPropertyChangeListener(
myDocument, myPropertyChangeListener);
}
...
public void shutdown() {
new DocumentTextProperty().removePropertyChangeListener(
myDocument, myPropertyChangeListener);
}

The next option is to store this state in a static global registry. This is the approach taken by the bean-properties Java.net project. I don't like this approach because it feels very heavyweight:
  • It needs WeakReferences.
  • The shared registry requires synchronization.
  • There's memory overhead to store each registered bean/listener pair.
  • The registry is complex because it requires each listener pair to be uniquely identified.
  • It prevents bean/listener pairs from being serialized.

    Fortunately, there is a clever solution to this problem - don't store a reference to the adapter. Instead, when we call removePropertyChangeListener, we pass in a different instance that equals the first:
    public class DocumentTextProperty 
    implements StatelessProperty<Document, String> {
    ...
    public void addPropertyChangeListener(Document bean, PropertyChangeListener p) {
    bean.addDocumentListener(new DocumentToPropertyChangeListener(bean, p));
    }
    public void removePropertyChangeListener(Document bean, PropertyChangeListener p) {
    bean.removeDocumentListener(new DocumentToPropertyChangeListener(bean, p));
    }
    }
    This requires us to implement equals and hashCode in our adapter:
    private class DocumentToPropertyChangeListener 
    implements DocumentListener {
    public boolean equals(Object obj) {
    return obj != null
    && obj.getClass() == this.getClass()
    && ((DocumentToPropertyChangeListener) obj).delegate.equals(delegate)
    && ((DocumentToPropertyChangeListener) obj).document.equals(document);
    }
    public int hashCode() {
    return delegate.hashCode() * 37 + document.hashCode();
    }
    }

    I love equals()
    We've taken advantage of one of Java's greatest features: a strong concept of equality. It makes our solution stateless, immutable, serializable, efficient and easy to use.
  • Jesse, there is another problem in your
    DocumentToPropertyChangeListener.
    A listener of a document event
    can't modify the document.
    (see Document overview section Notification).

    So you need to wrap your method fire
    in an invokeLater to avoid this.

    Cheers,
    Rémi
    Remi,

    Thanks for the feedback, although I disagree.

    I believe it would be wrong to wrap the notification in an invokeLater() from the adapter. Doing this introduces the possibility that when the PropertyChangeListener is notified, the Document has changed from the newValue reported in the PropertyChangeEvent.

    Changing the Document from its Listeners is a misuse of the DocumentListeners. The correct API for filtering and vetoing Document changes is DocumentFilter.