CAL and Tapestry 5, Part 2: Algebraic Types and Forms

In my previous post I described how to use a CAL function as part of the implementation of a Java class.

This post looks at interfacing CAL to Tapestry 5 using the ‘Java Bean’ conventions of getter and setter methods for the fields in an object.

Tapestry 5 provides a BeanEditForm component which simplifies providing CRUD operations for Beans. This is described in the second part of the Tapestry 5 tutorial.

By creating a Java class which provides a Bean with fields equivalent to the constructor parameters of a CAL algebraic data type we can use CAL to provide the data model for a web UI created with Tapestry. A Tapestry application doesn’t need to have a compile-time dependency on the fields in a Bean which will be edited in a BeanEditForm. This means that we can simply declare an abstract class, associate it with a CAL data constructor and have immediate web-based CRUD for that type.

On the Java side, we make the following changes to the code described in the tutorial.

The Address class becomes:

@CalBean(workspace="myworkspace.cws", module="TDavies.Address", constructorName = "Address")
public abstract class Address implements CalBeanBase {
}

The type referred to by the annotation looks like this:

data Address = Address
    honorific :: String
    firstName :: String
    lastName :: String
    street1 :: String
    street2 :: (Maybe String)
    city :: String
    state :: String
    zip :: (Maybe Int)
    email :: String
    phone :: (Maybe String)
    fax :: (Maybe String)
deriving Inputable, Outputable, Show;

The type must be an instance of Inputable and Outputable.

The CalBeanBase interface provides the information about the Bean needed for copying between an instance of Address (or any other Bean constructed from a CAL type) and an AlgebraicValue:

/**
 * This interface describes the structure of a Bean derived from a CAL type.
 * The information allows values to be transferred between bean instances and
 * CAL AlgebraicValue instances, using the BeanConverter class.
 */
public interface CalBeanBase {
    public static class FieldSpec {
        public String name;
        public BeanValueConverter converter;

        public FieldSpec(String name, BeanValueConverter converter) {
            this.name = name;
            this.converter = converter;
        }
    }
    public List<FieldSpec> getFieldSpecs();
    String getDataConstructorName();
    int getDataConstructorOrdinal();
}

BeanValueConverter implementations copy between a CAL value and a field. At present the only non-trivial implementation is the MAYBE converter, which maps Nothing to null.

The BeanConverter class has these methods:

/**
 * Copy values between CalBeanBase subclasses and CAL AlgebraicValue instances.
 */
public class BeanConverter {
    /**
     * Create a new AlgebraicValue which has been constructed using
     * the field values from bean.
     */
    public static AlgebraicValue toCal(CalBeanBase bean) {
        ...
    }

    /**
     * Set the field values in bean from the constructor parameters
     * in v.
     */
    public static void fromCal(CalBeanBase bean, AlgebraicValue v) {
        ...
    }
}

At runtime Javassist implements the CalBeanBase interface and adds the fields, getters and setters needed to make a bean. It does this by looking up the CAL type given in the annotation and introspecting it.

In addition, setters for fields which are not of type Maybe a are given a @Validate("required") annotation. @Order annotations are used to keep the fields in the same order as they appear in the CAL type. (The tutorial is out of date in this respect, and doesn’t mention the @Order annotation).

To exercise this technique we can make the CreateAddress class less trivial:

       // make _address persistent, so we can see the results of processing it on submit
  @Persist
  private Address _address;
       ... getter and setter unchanged ...
       // CAL function -- the body will be replaced with a call to TDavies.Address.sendAddress
  @Cal
  public AlgebraicValue sendAddress(Object address) {return null;}

  public void onSubmit()
  {
      BeanConverter.fromCal(_address, sendAddress(BeanConverter.toCal(_address)));
  }

When we submit the form, the bean’s field values are converted to an AlgebraicValue and passed to the CAL function sendAddress. The value returned by the function is used to set the fields of the bean, which is then displayed.

The sendAddress function just converts some of the data to upper case:

sendAddress :: Address -> Address;
public sendAddress o =
    let
        address = o;
    in
        Address
            address.Address.honorific
            (toUpperCase address.Address.firstName)
            (toUpperCase address.Address.lastName)
            (toUpperCase address.Address.street1)
            (case address.Address.street2 of Nothing -> Nothing; Just s -> Just $ toUpperCase s;)
            (toUpperCase address.Address.city)
            (toUpperCase address.Address.state)
             address.Address.zip
             address.Address.email
             address.Address.phone
             address.Address.fax;

There are a number of enhancements which could be made to this scheme:

  • Unlike the example, Java Enums are not supported as field values.
  • Only required validation can be specified, not regex or other types.
  • There is a problem allowing optional field values for non-string values — this may be a Tapestry limitation.
  • The BeanConverter methods should be on the CalBeanBase interface. Not only would the onSubmit implementation above be simpler, but the existing methods on the CalBeanBase interface could be removed, and implemented as private methods called by the toCal/fromCal implementations.

To really tell whether these techniques are worthwhile, I need to write a non-trivial web app using them!

One Trackback

  1. By My Diversions » GWT as a CAL client on April 12, 2008 at 9:00 pm

    [...] used a similar approach to marshalling Javabeans to CAL algebraic types as I used before, but this time I haven’t used any bytecode manipulation — as the Java classes are [...]

Post a Comment

Your email is never shared. Required fields are marked *

*
*