Saturday, October 23, 2010

JavaBean Convention: Relating Public Getters/Setters To Private Fields

The upcoming v1.05 release of Metawidget, the pragmatic User Interface generator, includes support for annotating private fields. Take the following example:

public class Person {

   @Id
   private int mId;

   @Column( nullable = false )
   private String mName;

   private int mAge;

   public String getName() {
      return mName;
   }

   public void setName( String name ) {
      mName = name;
   }

   @UiComesAfter( "name" )
   public String getAge() {
      return mAge;
   }

   public void setAge( int age ) {
      mAge = age;
   }
}

Here we have some JPA annotations (@Id, @Column) on our private fields and some UI annotations (@UiComesAfter) on our public getters/setters. This is quite a common situation, but it's very hard to support because the JavaBean specification doesn't define a relationship between public getters/setters and which private field they relate to. This is pretty obvious when you consider that some getters/setters don't have any private field. For example, a getAge method might calculate its value based on getDateOfBirth rather than have an mAge field per se.

So how do JPA, and other frameworks like Hibernate Validator, support this? Well, they cheat: using reflection to set the fields directly. But this doesn't work for most UI technologies, such as Swing's BeansBinding, or JSF, or Spring. Most UI technologies expect publically accessible getters/setters.

So we need to key off public getters/setters, but we want to annotate private fields. Implementations like GroovyPropertyStyle and ScalaPropertyStyle support this nicely, because those environments do define a mapping between getter/setter and private field. But JavaBeans do not. So how can JavaBeanPropertyStyle support it?

Well, let's be pragmatic: although not enforced, most developers adopt some kind of convention for how their getters/setters are named versus how their private fields are named. There are variations, but we can make this configurable. For v1.05 I've adopted a simple approach based on MessageFormat. So:

  • {0} (eg. dateOfBirth, surname)

  • 'm'{1} (eg. mDateOfBirth, mSurname)

  • 'm_'{0} (eg. m_dateOfBirth, m_surname)
Here's a complete example:

package com.myapp;

import java.text.MessageFormat;
import java.util.List;

import javax.swing.JFrame;

import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.impl.propertystyle.javabean.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.SwingMetawidget;
import org.metawidget.util.CollectionUtils;

public class Main {

   public static void main( String[] args ) {

      // Model

      Person person = new Person();

      // Metawidget

      SwingMetawidget metawidget = new SwingMetawidget();
      JavaBeanPropertyStyleConfig propertyStyleConfig = new JavaBeanPropertyStyleConfig();
      propertyStyleConfig.setPrivateFieldConvention( new MessageFormat( "'m'{1}" ) );

      BaseObjectInspectorConfig inspectorConfig = new BaseObjectInspectorConfig();
      inspectorConfig.setPropertyStyle( new JavaBeanPropertyStyle( propertyStyleConfig ) );
      metawidget.setInspector( new CompositeInspector(
            new CompositeInspectorConfig().setInspectors(
                  new PropertyTypeInspector( inspectorConfig ),
                  new MetawidgetAnnotationInspector( inspectorConfig ) )));
      metawidget.setToInspect( person );

      // Frame

      JFrame frame = new JFrame( "Example" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   /**
    * Model
    */

   public static class Person {

      private String         mName;

      @UiComesAfter( "name" )
      private int            mAge;

      @UiComesAfter( "age" )
      private boolean         mRetired;

      @UiComesAfter( "retired" )
      private List<Address>   mAddresses   = CollectionUtils.newArrayList();

      @UiLarge
      @UiComesAfter( "addresses" )
      private String         mNotes;

      public String getName() {

         return mName;
      }

      public void setName( String name ) {

         mName = name;
      }

      public int getAge() {

         return mAge;
      }

      public void setAge( int age ) {

         mAge = age;
      }

      public boolean isRetired() {

         return mRetired;
      }

      public void setRetired( boolean retired ) {

         mRetired = retired;
      }

      public List<Address> getAddresses() {

         return mAddresses;
      }

      public void setAddresses( List<Address> addresses ) {

         mAddresses = addresses;
      }

      public String getNotes() {

         return mNotes;
      }

      public void setNotes( String notes ) {

         mNotes = notes;
      }
   }

   public static class Address {

      private String   mStreet;

      @UiComesAfter( "street" )
      private String   mCity;

      @UiComesAfter( "city" )
      private String   mState;

      public Address( String street, String city, String state ) {

         mStreet = street;
         mCity = city;
         mState = state;
      }

      public String getStreet() {

         return mStreet;
      }

      public void setStreet( String street ) {

         mStreet = street;
      }

      public String getCity() {

         return mCity;
      }

      public void setCity( String city ) {

         mCity = city;
      }

      public String getState() {

         return mState;
      }

      public void setState( String state ) {

         mState = state;
      }
   }
}

Hopefully this will work for most use cases. For those needing more control, consider extending JavaBeanPropertyStyle and overriding getPrivateField.

Feedback welcome!

1 comments:

Simone said...

Very interesting feature!

I've used a similar approach in order to call some setter methods (all starting with a "preInit" prefix name) before calling the init() method in an injector toolbar framework developed with my collegues some times ago.

I'll try it as soon as I can!

Bye,
Simone