Sun GlassFish Mobility Platform 1.1 Developer's Guide for Enterprise Connectors

Extending the BusinessObject Class

The BusinessObject class holds the data you need to synchronize. In addition to the required properties name and extension, you specify properties and define getter and setter methods for this data.

For details on this class, see The BusinessObject Class.

The name property is the most important BusinessObject property. This property defines the identity of the business object. The name you specify must be unique within your database or EIS/EAI system.

For the MusicDB example, the class that extends BusinessObject is MusicAlbum. The source file MusicAlbum.java begins by importing Java SE packages and the required ECBO API class that it extends:

package com.sun.mep.connector.jdbc.album;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.sql.Connection;
import java.util.Calendar;
import java.sql.Timestamp;

import com.sun.mep.connector.api.BusinessObject;

The class code itself begins by declaring a string value to be used by the serialize method:

public class MusicAlbum extends BusinessObject<MusicAlbumProvider> {

    private static final String DEFAULT_VALUE = "$$default$$";

This example then declares its data properties (there are only four in addition to the name property):

    /**
     * Album's artist.
     */ 
    String artist;

    /**
     * Date when the album was published.
     */
    Calendar datePublished;
    
    /**
     * Album's rating from 1 to 5.
     */
    int rating;

    /**
     * Last modified timestamp.
     */
    long lastModified;

The lastModified property is unique to the Enterprise Connector version of the business object; it is not used in the mobile client application. This property allows the Gateway to use timestamps to query the back-end system to determine whether a business object has been updated since the last synchronization. If the Enterprise Connector does not use timestamps, the Gateway computes a digest over the serialized business object, using a hash function, and compares it to the previous digest. Using timestamps is more efficient than using digests.

The code then declares a StringBuilder and defines the one-argument constructor, which takes the MusicAlbumProvider as its argument:

    /**
     * String builder used to return SQL commands.
     */ 
    StringBuilder stringBuilder = new StringBuilder();
    
    public MusicAlbum(MusicAlbumProvider provider) {
        super(provider);
    }

Now the class implements its getter and setter methods for the artist, rating, datePublished, and lastModified properties:

    public String getArtist() {
        return artist;
    }
    
    public void setArtist(String artist) {
        this.artist = artist;
    }

    public int getRating() {
        return rating;
    }
    
    public void setRating(int rating) {
        this.rating = rating;
    }

    /**
     * Returns the date published as a string in the format
     * 'YYYYMMDD'.
     */
    public String getDatePublished() {
         stringBuilder.setLength(0);
         stringBuilder.append(datePublished.get(Calendar.YEAR));
         int month = datePublished.get(Calendar.MONTH) + 1;
         if (month < 10) {
             stringBuilder.append('0');
         }
         stringBuilder.append(month);
         int day = datePublished.get(Calendar.DAY_OF_MONTH);
         if (day < 10) {
             stringBuilder.append('0');
         }
         stringBuilder.append(day);
         return stringBuilder.toString();
    }
    
    /**
     * Set the date published in the format 'YYYYMMDD'.
     */ 
    public void setDatePublished(String date) {
        datePublished = Calendar.getInstance();
        datePublished.set(Calendar.YEAR, 
                Integer.parseInt(date.substring(0, 4)));
        datePublished.set(Calendar.MONTH, 
                Integer.parseInt(date.substring(4, 6)) - 1);
        datePublished.set(Calendar.DAY_OF_MONTH, 
                Integer.parseInt(date.substring(6, 8)));
    }

The code next implements the getLastModified and setLastModified methods:

    /**
     * Returns last modified timestamp.
     */
    @Override
    public long getLastModified() {
        if (getBusinessObjectProvider().useTimestamps()) {
            return lastModified;
        }
        else {
            throw new UnsupportedOperationException(getClass().getName() +
                    " does not support lastModified timestamp");
        }
    }

    /**
     * Sets last modified timestamp.
     */
    @Override
    public void setLastModified(long millis) {
        if (getBusinessObjectProvider().useTimestamps()) {
            lastModified = millis;
        }
        else {
            throw new UnsupportedOperationException(getClass().getName() +
                    " does not support lastModified timestamp");
        }
    }

If the Enterprise Connector does not use timestamps, the methods throw exceptions that cause the Gateway to use digests.

The class implements the getExtension method by specifying .alb as the file extension for MusicAlbum objects. This extension must match the extension used by the client.

    @Override
    public String getExtension() {
        return ".alb";
    }

The class does not implement its own versions of the getName and setName methods; instead, it uses the versions defined in the BusinessObject class.

The class uses Java Serialization to implement the serialize and deserialize methods as follows. Note the calls to the BusinessObject versions of getName and setName in addition to the getter and setter methods defined by MusicAlbum. The format used in the serialize and deserialize methods is part of the contract between the client and the Enterprise Connector.

    public byte[] serialize() throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutputStream dOut = new DataOutputStream(out);

        dOut.writeUTF(getName());
        dOut.writeUTF(getArtist() != null ? getArtist() : DEFAULT_VALUE);
        dOut.writeUTF(getDatePublished() != null ? getDatePublished() : DEFAULT_VALUE);
        dOut.writeUTF(Integer.toString(getRating()));
        dOut.flush();
        return out.toByteArray();
    }

    public void deserialize(byte[] array) throws IOException {
        ByteArrayInputStream in = new ByteArrayInputStream(array);
        DataInputStream dIn = new DataInputStream(in);
        
        setName(dIn.readUTF());
        artist = dIn.readUTF();
        if (artist.equals(DEFAULT_VALUE)) {
            artist = null;
        }
        String date = dIn.readUTF();
        if (date.equals(DEFAULT_VALUE)) {
            datePublished = null;
        }
        else {
            setDatePublished(date);
        }
        rating = Integer.parseInt(dIn.readUTF());
    }

The class implements the getInsertCommand, getUpdateCommand, and getDeleteCommand methods using the constructors specific to this business object:

    /**
     * {@inheritDoc}
     */
    @Override
    public MusicAlbumInsertCommand getInsertCommand() {
        return new MusicAlbumInsertCommand(this, getSQLConnection(),
                getInsertString());
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public MusicAlbumUpdateCommand getUpdateCommand() {
        return new MusicAlbumUpdateCommand(this, getSQLConnection(),
                getUpdateString());
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public MusicAlbumDeleteCommand getDeleteCommand() {
        return new MusicAlbumDeleteCommand(this, getSQLConnection(),
                getDeleteString());
    }

One of the constructor arguments for each command is the value returned by a helper method (getInsertString, getUpdateString, getDeleteString) that generates an SQL statement string. These methods are implemented as follows:

    /**
     * Returns an SQL insert statement to add this instance
     * to the database.
     */
    public String getInsertString() {
        stringBuilder.setLength(0);
        stringBuilder.append(hasLastModified() ? 
            "INSERT INTO album" :
            "INSERT INTO album (name, artist, date_published, rating, username)")
            .append(" VALUES ('")
            .append(getName()).append("', '")
            .append(artist).append("', DATE '")
            .append(datePublished.get(Calendar.YEAR)).append("-")
            .append(datePublished.get(Calendar.MONTH) + 1).append("-")
            .append(datePublished.get(Calendar.DAY_OF_MONTH)).append("', ")
            .append(Integer.toString(rating)).append(", '")
            .append(getBusinessObjectProvider().getUsername())
            .append("'");
        
        if (hasLastModified()) {
            Timestamp timestamp = new Timestamp(getLastModified());
            stringBuilder.append(", TIMESTAMP '")
                    .append(timestamp.toString())
                    .append("'");
        } 
        stringBuilder.append(')');
        
        return stringBuilder.toString();
    }

    /**
     * Returns an SQL update statement to modify this instance
     * in the database.
     */
    public String getUpdateString() {
        stringBuilder.setLength(0);
        stringBuilder.append("UPDATE album SET artist='")
             .append(artist).append("', date_published=DATE '")
             .append(datePublished.get(Calendar.YEAR)).append("-")
             .append(datePublished.get(Calendar.MONTH) + 1).append("-")
             .append(datePublished.get(Calendar.DAY_OF_MONTH)).append("', rating=")
             .append(Integer.toString(rating));
        
        if (hasLastModified()) {
            Timestamp timestamp = new Timestamp(getLastModified()); 
            stringBuilder.append(", last_modified = TIMESTAMP '")
                .append(timestamp.toString()).append("'");
        }
        
        stringBuilder.append(" WHERE name = '").append(getName())
             .append("' AND username = '")
             .append(getBusinessObjectProvider().getUsername())
             .append("'");       
        return stringBuilder.toString();
    }

    /**
     * Returns an SQL delete statement to remove this instance
     * from the database.
     */
    public String getDeleteString() {
        stringBuilder.setLength(0);
        stringBuilder.append("DELETE FROM album WHERE name = '")
                     .append(getName())
                     .append("' AND username = '" 
                            + getBusinessObjectProvider().getUsername() + "'");
        return stringBuilder.toString();
    }

You may notice that the SQL statements show an additional column, username, in the database's album table in addition to columns for the MusicAlbum class's properties (name, artist, datePublished, and rating). The username column and the name column provide a composite primary key for the album table. The username column identifies the owner of an album and allows the MusicAlbumProvider.getBusinessObjects method to return only the albums for a particular user, so that multiple users can share the album table.

The getInsertString and getUpdateString methods also set the timestamp of the new or modified business object if the business object uses timestamps.

An additional method, getSelectString, is provided for testing purposes.

Another constructor argument for the commands is the value returned by another helper method, getSQLConnection, which returns a JDBC Connection object created by the MusicAlbumProvider class.

    /** 
     * Returns a connection object that can be used to 
     * execute SQL commands.
     */
    public Connection getSQLConnection() {
        return getBusinessObjectProvider().getSQLConnection();
    }