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

Extending the BusinessObjectProvider Class

The BusinessObjectProvider class serves several purposes:

For details on this class, see The BusinessObjectProvider Class.

For the MusicDB example, the class that extends BusinessObjectProvider is MusicAlbumProvider. Like the file for the MusicAlbum class, the MusicAlbumProvider.java source file begins by importing Java SE packages and the required ECBO API classes. It then begins by setting up a logger and declaring some string constants, a JDBC connection object, its implementation of the TransactionManager class, and a user name object:

public class MusicAlbumProvider extends BusinessObjectProvider<MusicAlbum> {

    static final Logger logger = BusinessObjectProvider.getLogger();
    
    public static final String REPOSITORY_NAME = "MusicDbRepository";    
    public static final String MUSICDB_JNDI_DATASOURCE = "jdbc/musicdb";
    public static final String DB_USER_NAME = "musicdbuser";
    public static final String DB_USER_PASS = "musicdbpass";
    Connection sqlConnection = null;
    
    MusicAlbumTransactionManager transactionManager;

    String username;

The REPOSITORY_NAME value is identical to the repository name specified in the resource file for the Enterprise Connector. In the MusicDB sample, the resource file is named MusicDbRepository.xml and defines a repository named MusicDbRepository. The MUSICDB_JNDI_DATASOURCE value defines the name of the JDBC Datasource used to connect with the database.

The code implements two forms of the business object constructor: the no-argument constructor specified by the API and a one-argument form that takes a user name as argument for testing purposes.

Next, the code implements the two lifecycle methods for the BusinessObjectProvider class, initialize and terminate, which coincide with the start and end of a synchronization session.

The initialize method allocates resources required for a synchronization session or for database authentication. In this case, the code does the following:

    /**
     * Creates a connection to the {@link #MUSICDB_JNDI_DATASOURCE}
     * database.
     */
    @Override
    public void initialize() {
        logger.fine("Initializing provider " + this);
       
        try {
            Context jndiContext = new InitialContext();
            DataSource ds = null;

            // If unable to get JNDI datasource, use local one for testing
            try {
                ds = (DataSource) jndiContext.lookup(MUSICDB_JNDI_DATASOURCE);
            } catch (NoInitialContextException e) {
                ds = new MusicDbDataSource();       // testing only!
            }

            // Get database credentials from provider's context
            SessionContext sessionContext = getSessionContext();
            username = sessionContext.getUsername();
            if (username == null || !username.startsWith(DB_USER_NAME)) {
                throw new RuntimeException("A MusicDB user name must start " 
                        + "with the '"+ DB_USER_NAME + "' prefix");
            }
            
            // Get connection using default credentials
            sqlConnection = ds.getConnection(DB_USER_NAME, DB_USER_PASS);
            
            // Init transaction manager
            transactionManager = new MusicAlbumTransactionManager();
        }
        catch (SQLException ex) {
            throw new RuntimeException(ex);
        } 
        catch (NamingException ex) {
            throw new RuntimeException(ex);
        } 
    }

The implementation of the terminate method releases any resources allocated by the initialize method. In this case, it closes the JDBC connection.

    /**
     * Closes a connection to the {@link #MUSICDB_JNDI_DATASOURCE}
     * database.
     */
    @Override
    public void terminate() {
        logger.debug("Terminating provider " + this);   
       
        try {
            if (sqlConnection != null) {
                sqlConnection.close();
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

The implementation of the getRepositoryName method specifies the string value declared at the beginning of the class. The repository in question is the repository that is used for communication between the Gateway Engine and the Enterprise Connector and that is specified by the resource file.

    /**
     * {@inheritDoc}
     */
    @Override
    public String getRepositoryName() {
        return REPOSITORY_NAME;
    }

The implementation of the getBusinessObjects method uses a JDBC query to retrieve all the albums for the user username from the database, instantiates a MusicAlbum object for each retrieved album, and adds it to an ArrayList of albums.

    /**
     * {@inheritDoc}
     */
    @Override
    public List<MusicAlbum> getBusinessObjects() {
        logger.fine("Getting objects from provider " + this);   
       
        Statement stmt = null;
        List<MusicAlbum> albums = null;
        
        try {
            stmt = sqlConnection.createStatement();
            
            // Read all music albums and store them in array
            albums = new ArrayList<MusicAlbum>();
            ResultSet rs = stmt.executeQuery(
                    "SELECT * FROM album WHERE username = '" + username + "'");
            while (rs.next()) {
                MusicAlbum album = new MusicAlbum(this);
                album.setName(rs.getString(1));
                album.setArtist(rs.getString(2));
                album.setDatePublished(rs.getString(3).replace("-", ""));
                album.setRating(rs.getInt(4));
                if (album.hasLastModified()) {
                    album.setLastModified(rs.getTimestamp(6).getTime());
                }
                albums.add(album);
            }
            rs.close();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            if (stmt != null) {
                try { stmt.close(); } catch (Exception e) { /* ignore !*/ }
            }
        }        
        return albums;
    }

The implementation of the newBusinessObject method is much simpler: it calls the one-argument constructor for MusicAlbum.

    /**
     * {@inheritDoc}
     */
    @Override
    public MusicAlbum newBusinessObject() {
        return new MusicAlbum(this);
    }

The provider class also implements the helper method getSQLConnection, which returns the JDBC connection that was instantiated by the initialize method. This method is called by the MusicAlbum class.

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

The provider class also implements a getUsername method that is called by the MusicAlbum class's utility methods:

    /**
     * Returns the user name logged into the session that
     * created this provider.
     */
    public String getUsername() {
        return username;
    }

The MusicDB provider also implements a useTimestamps method, which returns true if the provider is using timestamps to determine when an object was last modified. This method is not part of the ECBO API but is specific to this Enterprise Connector. Another connector may use another mechanism to implement the lastModified property.

    /**
     * Returns the value of parameter 'useTimestamps'. This 
     * parameter controls the use of timestamps over digests
     * by the gateway. Default value is false.
     */
    public boolean useTimestamps() {
        SessionContext sessionContext = getSessionContext();
        String v = sessionContext.getParameters().get("useTimestamps");
        return (v == null) ? false : v.equalsIgnoreCase("true");
    }

An administrator can set the value of the useTimestamps property when defining the Enterprise Connector in the Sun GlassFish Mobility Platform Administration Console. If the property is not set, the useTimestamps method sets the value to false.

The provider implements the mergeBusinessObjects method to resolve conflicts between the client and server:

    /**
     * If timestamps are available then pick the object that
     * has been updated last. Otherwise, since we don't know
     * which object was updated last, we just compute the
     * average of the ratings and assume that all the other
     * fields have not been updated.
     */
    @Override
    public void mergeBusinessObjects(MusicAlbum serverObject, 
            MusicAlbum clientObject) 
    {
        if (clientObject.hasLastModified() && serverObject.hasLastModified()) {
            if (clientObject.getLastModified() > serverObject.getLastModified()) {
                serverObject.setArtist(clientObject.getArtist());
                serverObject.setDatePublished(clientObject.getDatePublished());
                serverObject.setRating(clientObject.getRating());
            }
        }
        else {
            serverObject.setRating(
                    (clientObject.getRating() + serverObject.getRating()) / 2);
        }
    }

The getTransactionManager method retrieves the MusicAlbumTransactionManager that is declared at the beginning of the file, instantiated in the initialize method, and implemented within the MusicAlbumProvider.java file.

    /**
     * Returns a transaction manager that uses JDBC to start,
     * stop and abort transactions.
     */
    @Override
    public MusicAlbumTransactionManager getTransactionManager() {
        return transactionManager;
    }