Sun Java System Mobile Enterprise Platform 1.0 Developer's Guide for Enterprise Connectors

Chapter 2 Creating an Enterprise Connector

A Mobile Enterprise Platform (MEP) Enterprise Connector typically extends five classes in the Enterprise Connector Business Object (ECBO) API, along with an XML file that defines parameters used by the underlying Java Content Repository (JCR) implementation.

For details on the ECBO API classes and methods, see Chapter 3, Classes and Methods in the Enterprise Connector Business Object API Package. The API documentation is also included in the MEP client bundle. In the directory where you unzipped the client bundle (see the Sun Java System Mobile Enterprise Platform 1.0 Installation Guide for details), it is in the subdirectory sjsmep-client-1_0_02-fcs/doc/ecbo/api.

This chapter uses the MusicDB sample application provided with MEP to demonstrate how to use the ECBO API. The Enterprise Connector in this application acts as the intermediary between a client on a mobile device and a database. For this simple demo application, the database is not a full-fledged EIS/EAI system but an ordinary database that is accessed using the Java Database Connectivity (JDBC) API.

The source code for the MusicDB Enterprise Connector is included in the MEP client bundle. In the directory where you unzipped the client bundle, it is in the subdirectory sjsmep-client-1_0_02-fcs/samples/ecbo/.

This chapter covers the following topics:

Packages in the Enterprise Connector Business Object API

The Enterprise Connector Business Object (ECBO) API contains the following classes:

The MusicDB example implements its own versions of all of these classes except for SessionContext. It uses the default implementation of SessionContext.

See Chapter 3, Classes and Methods in the Enterprise Connector Business Object API Package, for summaries of the classes and methods in the ECBO API packages.

To synchronize data with an EIS/EAI system such as Siebel or SAP, your BusinessObjectProvider implementation and the three command implementations will need to call methods that access the Sun JCA Adapter for that system. These methods are provided by the Sun Java Composite Application Platform Suite (Java CAPS). See Accessing a Sun JCA Adapter for an EIS/EAI System for details.

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, along with com.synchronica logging packages and the required ECBO API classes:

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 com.synchronica.logging.Loggers;
import com.synchronica.logging.Logger;
import com.sun.mep.connector.api.BusinessObject;
import com.sun.mep.connector.api.InsertCommand;
import com.sun.mep.connector.api.UpdateCommand;
import com.sun.mep.connector.api.DeleteCommand;

The class code itself begins by declaring a string value and setting up a logger:

public class MusicAlbum extends BusinessObject<MusicAlbumProvider> {

    private static final String DEFAULT_VALUE = "$$default$$";
    
    static Logger logger = Loggers.getLogger(MusicAlbum.class);

This example then declares its data properties (there are only three 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;

The file 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, and datePublished 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 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("INSERT INTO album 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("')");
        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))
             .append(" WHERE name = '").append(getName())
             .append("' AND username = '" + getBusinessObjectProvider().getUsername() +
                 "'");
        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.

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();
    }

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, along with com.synchronica logging 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 Logger logger = Loggers.getLogger(MusicAlbumProvider.class);
    
    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 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.debug("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();
            
            // Get connection using default credentials
            sqlConnection = ds.getConnection(DB_USER_NAME, DB_USER_PASS);
            
            // Init transaction manager
            transactionManager = new MusicAlbumTransactionManager();
        }
        catch (Exception 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 JCR 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.debug("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));
                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 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;
    }

Extending the TransactionManager Class

The MusicAlbumProvider.java file includes the implementation of the TransactionManager class. The TransactionManager class can be implemented in a separate file, but the relationships between the two classes mean that it is simpler to keep them together. It is also possible to use the default implementation of the TransactionManager class instead of implementing it yourself.

For details on this class, see The TransactionManager Class.

The MusicDB implementation of this class is called MusicAlbumTransactionManager. This class manages the database transactions for MusicAlbum objects using the JDBC API. It turns the database's auto-commit feature off if it has one and starts, stops, and aborts database transactions. The class definition begins with the constructor, which takes no arguments.

    public class MusicAlbumTransactionManager extends
            TransactionManager<MusicAlbumProvider> {

        public MusicAlbumTransactionManager() {
            super(MusicAlbumProvider.this);
            
            assert (sqlConnection != null);
            try {
                sqlConnection.setAutoCommit(false);
            }
            catch (SQLException e) {
                // Ignore if not supported by DB
            }
        }

The abortTransaction method calls the JDBC Connection.rollback method:

        @Override
        public void abortTransaction() {
            logger.debug("Aborting transaction on SQL connection " 
                    + sqlConnection);
            try {
                sqlConnection.rollback();
            }
            catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

The beginTransaction and endTransaction methods are closely linked. The endTransaction method commits the current transaction, an action that automatically starts the next transaction. The beginTransaction method simply calls endTransaction.

        @Override
        public void beginTransaction() {
            endTransaction();   // starts a new one
        }
        @Override
        public void endTransaction() {
            logger.debug("Starting/Committing transaction on SQL connection " 
                    + sqlConnection);
            try {
                sqlConnection.commit();
            }
            catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

Extending the InsertCommand, UpdateCommand, and DeleteCommand Classes

The InsertCommand, UpdateCommand, and DeleteCommand classes all extend the Command class.

For details on these classes, see The InsertCommand Class, The UpdateCommand Class, The DeleteCommand Class, and The Command Class.

For the MusicDB example, the implementations of the three classes are almost identical. The source files for the implementations are MusicAlbumInsertCommand.java, MusicAlbumUpdateCommand.java, and MusicAlbumDeleteCommand.java.

Each source file begins by importing Java SE packages, along with com.synchronica logging packages and the required ECBO API classes. It then begins by setting up a logger and declaring two objects that are used by the constructor method and the execute method. For example, MusicAlbumInsertCommand.java begins as follows:

public class MusicAlbumInsertCommand extends InsertCommand<MusicAlbum> {

    static Logger logger = Loggers.getLogger(MusicAlbumInsertCommand.class);
    
    private String sqlStatement;
    
    private Connection sqlConnection;

The code then extends the class constructor. While the constructor for the base class takes one argument, the constructor for each of the implementation classes takes three arguments: the MusicAlbum, a string that represents a SQL statement, and a JDBC connection. For example, the constructor for MusicAlbumUpdateCommand looks like this:

    public MusicAlbumUpdateCommand(MusicAlbum album,
            Connection sqlConnection, String sqlStatement)
    {
        super(album);
        this.sqlConnection = sqlConnection;
        this.sqlStatement = sqlStatement;
        logger.debug("Creating instance " + this + ": " + sqlStatement);
    }

Finally, the code for each command implements the execute method in exactly the same way. First, it uses the instantiated connection to create a JDBC Statement object using the instantiated string. Then it executes the statement.

    public void execute() {
        int result = 0;
        Statement stmt = null;
        
        try {
            logger.debug("Executing instance " + this + ": " + sqlStatement);
            stmt = sqlConnection.createStatement();
            result = stmt.executeUpdate(sqlStatement);
        }
        catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        finally {
            if (stmt != null) {
                try { stmt.close(); } catch (Exception e) { /* ignore !*/ }
            }
        }
    }

Creating the Resource File for an Enterprise Connector

The resource file that you need to package with an Enterprise Connector (as described in About the Enterprise Connector Business Object (ECBO) API) is an XML file for Jeceira, the implementation of JCR used by MEP. The name of the file typically refers to the database or EIS/EAI system. For example, the resource file for MusicDB is named MusicDbRepository.xml. The file begins and ends as follows:

<jeceira xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    xmlns:jcr="http://www.jcp.org/jcr/1.0"
    xmlns:sync="http://www.synchronica.com/jcr/types"
    xmlns:aprzv="http://www.aparzev.com/jrc/aprzv"
    xmlns:udc="http://www.synchronica.com/udc/types/1.0">
    <repositories>
        <repository name="MusicDbRepository">

            <workspaces>
                <workspace name="MusicDbWorkspace" />
            </workspaces>
            ....
        </repository>
    </repositories>
</jeceira>

All resource files are identical except for two values:

To create your own resource file, you can copy the resource file from the sample Enterprise Connector source directory, rename it, and modify these two values. In the unzipped client bundle, you can find the file in sjsmep-client-1_0_02-fcs/samples/ecbo/src/MusicDbRepository.xml.

You use these values when you configure the Enterprise Connector in the MEP Admin Console. See Using the Connectors Tab in Sun Java System Mobile Enterprise Platform 1.0 Administration Guide for details.

Accessing a Sun JCA Adapter for an EIS/EAI System

If you are designing your Enterprise Connector to access an EIS/EAI system instead of a database, the connector must access the Sun JCA Adapter for that system instead of making JDBC calls.

You must first create an Object Type Definition (OTD) that maps your business object properties to data on the EIS/EAI system. To create the OTD, you need to use the NetBeans IDE with plugins that are provided with MEP.

After you create the OTD, use the NetBeans code completion feature to call methods on the classes generated by the OTD wizard.

This section covers the following topics:

Creating an Object Type Definition (OTD)

For information on working with Sun JCA Adapters, see the Designing section of the Java CAPS documentation. Specific sections you will need to look at include the following:

To obtain the NetBeans plugins needed to create an OTD, follow the instructions in Installation of Netbeans Modules. The NetBeansModules referred to in these instructions are part of your MEP installation. In the location where you unzipped the installation bundle sjsmep-1_0-fcs-operating-system.zip, you will find them in the directory sjsmep-1_0-fcs/NetBeansModules. (The Java CAPS documentation states that they are in the directory AdapterPack/NetBeansModules/CommonLib, but for MEP they are in the directory NetBeansModules/commonlib.)

To develop an OTD for your application, follow the instructions appropriate to your EIS/EAI system in Developing OTDs for Application Adapters.

The instructions for adapters supported by MEP are in the following sections:

An Enterprise Connector is a Sun JCA Adapter client application that is not an Enterprise JavaBeans (EJB) component. You may find that you need to create the OTD and develop the Enterprise Connector inside an EJB project. However, you should then remove the Enterprise Connector and OTD from the EJB JAR file and place them in an ordinary JAR file before you place the JAR file in the domains/mep/lib directory for the Application Server. The OTD is generated in a separate JAR file, so it is easy to copy it to another project.

Writing Code to Access a Sun JCA Adapter

To access a Sun JCA Adapter, your code needs to use Java CAPS APIs, which are documented at http://developers.sun.com/docs/javacaps/reference/javadocs/index.jsp.

This section describes how to extend the ECBO classes to access a Sun JCA Adapter.

The default implementation of the TransactionManager class may be sufficient for your application.

Extending the BusinessObjectProvider Class to Access a Sun JCA Adapter

To allow your Enterprise Connector to work with a Sun JCA Adapter, your BusinessObjectProvider implementation needs to create a connection to the Adapter in its initialize method, close that connection in its terminate method, and retrieve objects through the Adapter in its getBusinessObject method.

For a SAP BAPI application, for example, you import the following packages:

import com.stc.connector.appconn.common.ApplicationConnectionFactory;
import com.stc.connector.appconn.common.ApplicationConnection;
import com.stc.connector.sapbapiadapter.appconn.SAPApplicationConnection;
import com.stc.util.OtdObjectFactory;

When you create a provider for a Customer business object, you declare objects like the following. The customer.Customer class is generated by the OTD wizard.

public class CustomerProvider extends BusinessObjectProvider<Customer> {
    ...
    public static final String SAP_JNDI_DATASOURCE = "jcaps/sap";
    public static final String REPOSITORY_NAME = "SAPRepository";
    
    private ApplicationConnectionFactory mJCAsapadapter = null;
    private ApplicationConnection mJCAsapadapterConnection = null;
    private customer.Customer mJCAsapcustomerCommObj = null;

The provider's initialize method then allocates these resources. It obtains an ApplicationConnectionFactory object by means of a JNDI lookup, then uses the factory to create the ApplicationConnection object. These method calls are the same no matter which EIS/EAI system you are using:

    @Override
    public void initialize() {
        logger.debug("Initializing provider " + this);

        try {
            InitialContext ic = new InitialContext();
            // First get ApplicationConnectionFactory through JNDI lookup
            mJCAsapadapter = 
                (ApplicationConnectionFactory) ic.lookup(SAP_JNDI_DATASOURCE);

            /* Then create ApplicationConnection. One AppConn can be dynamically
             * allocated to a physical connection defined in connection pool;
             * this results in connection reuse according to JCA and Appserver
             * contract
             */
            mJCAsapadapterConnection = mJCAsapadapter.getConnection();

The initialize method then uses the OtdObjectFactory to create an instance of a SAP customer communication object. Methods called on this object are specific to the SAP OTD. The code casts the generic ApplicationConnection object mJCAsapadapterConnection to another application connection specific to SAP:

            /* Create Customer communication object
             */
            mJCAsapcustomerCommObj = 
                (customer.Customer) OtdObjectFactory.createInstance(null,
                    "customer.Customer");

            /* Set ApplicationConnection on Customer communication object
             */
            mJCAsapcustomerCommObj.setAppConn(
                (SAPApplicationConnection) mJCAsapadapterConnection);

The initialize method next uses the ECBO API SessionContext object to retrieve the user name and password. It then uses these values to create user credentials specific to SAP, and finally connects to the Sun JCA Adapter for SAP.

            // Get backend credentials from provider's context
            SessionContext sessionContext = getSessionContext();
            String param = sessionContext.getUsername();
            if (param != null) {
                mJCAsapcustomerCommObj.getSAPConnectionParams().setUserid(param);
            }
            param = sessionContext.getPassword();
            if (param != null) {
                mJCAsapcustomerCommObj.getSAPConnectionParams().setPassword(param);
            }
            mJCAsapcustomerCommObj.connectWithNewParams();

            ic.close();
        }
        catch (Exception ex) {
            logger.debug("Initializing provider exception" + ex.getMessage());
            throw new RuntimeException(ex);
        }
    }

The terminate method closes the connection created by the initialize method:

    @Override
    public void terminate() {
        logger.debug("Terminating provider " + this);

        try {
            if (mJCAsapadapterConnection != null) {
                mJCAsapadapterConnection.close();
                logger.info("terminate provider close connection" 
                        + mJCAsapadapterConnection.toString());
            }
        }
        catch (Exception e) {
            logger.debug("terminating provider exception" + e.getMessage());
            throw new RuntimeException(e);
        }
    }

The provider code also implements a utility method, getSAPCustomerClient, which retrieves the customer communication object:

    /** 
     * @return SAPCustomerClient object that can be used to 
     * operate on a Customer BAPI.
     */
    public customer.Customer getSAPCustomerClient() {
        return mJCAsapcustomerCommObj;
    }

The getBusinessObjects method uses the getSAPCustomerClient method to retrieve the SAP customer data and store it in the Enterprise Connector's Customer object. It again calls methods on the communication object generated by the OTD wizard.

    @Override
    //Retrieve all IDocs and map here between Customer and VendorAccount Object
    public List<Customer> getBusinessObjects() {
        logger.debug("Getting objects from provider " + this);

        HashMap<String, Customer> customerMap = new HashMap<String, Customer>();

        try {
            // Getting customer list
            getSAPCustomerClient().getGetList().getIDRANGE(0).setOPTION("CP");
            getSAPCustomerClient().getGetList().getIDRANGE(0).setLOW("*");
            logger.info("Executing Customer with the following values Option " +
                "[" + getSAPCustomerClient().getGetList().getIDRANGE(0).getLOW() 
                + "] Option [" 
                + getSAPCustomerClient().getGetList().getIDRANGE(0).getOPTION() + "]");
            getSAPCustomerClient().getGetList().execute();

            // Process returned data and populate customer list
            customer.Customer.GetList.ExportParams.RETURN ret =
                getSAPCustomerClient().getGetList().getExportParams().getRETURN();
            logger.info("Retrieved [" 
                + getSAPCustomerClient().getGetList().countADDRESSDATA() 
                + "] customers");

            customer.Customer.GetList.ADDRESSDATA[] addressList =
                getSAPCustomerClient().getGetList().getADDRESSDATA();
            for (int i = 0; i < addressList.length; i++) {
                customer.Customer.GetList.ADDRESSDATA addr = addressList[i];

                // Ignore companies whose names start with "DELETED" -- hack
                if (!addr.getNAME().startsWith("DELETED")) {
                    // Ignore customers whose names are repeated
                    if (customerMap.containsKey(addr.getNAME())) {
                        continue;
                    }

                    // Create a new Customer instance 
                    Customer comp = new Customer(this);

                    // Set unique name for business object
                    comp.setName(addr.getNAME());

                    // Set customer number and name
                    comp.setCustomerNumber(addr.getCUSTOMER());
                    comp.setCustomerName(addr.getNAME());

                    // Get sales area
                    getSAPCustomerClient().getGetSalesAreas().getImportParams()
                        .setCUSTOMERNO(comp.getCustomerNumber());
                    getSAPCustomerClient().getGetSalesAreas().execute();
                    String retNo = getSAPCustomerClient().getGetSalesAreas()
                        .getExportParams().getRETURN().getMESSAGE();
                    String retMsg = getSAPCustomerClient().getGetSalesAreas()
                        .getExportParams().getRETURN().getCODE();
                    logger.info("Return Number [" + retNo + "] retMsg [" 
                        + retMsg + "].");
                    if (retNo.length() > 0) {
                        throw new RuntimeException(retMsg);
                    }

                    // Set sales related fields
                    comp.setSalesOrg(getSAPCustomerClient().getGetSalesAreas()
                        .getSALESAREAS(0).getSALESORG());
                    comp.setDistChannel(getSAPCustomerClient().getGetSalesAreas()
                        .getSALESAREAS(0).getDISTRCHN());
                    comp.setDivision(getSAPCustomerClient().getGetSalesAreas()
                        .getSALESAREAS(0).getDIVISION());

                    // Get detail on customer
                    getSAPCustomerClient().getGetDetail1().getImportParams()
                        .setCUSTOMERNO(comp.getCustomerNumber());
                    getSAPCustomerClient().getGetDetail1().getImportParams()
                        .setPI_SALESORG(comp.getSalesOrg());
                    getSAPCustomerClient().getGetDetail1().getImportParams()
                        .setPI_DISTR_CHAN(comp.getDistChannel());
                    getSAPCustomerClient().getGetDetail1().getImportParams()
                        .setPI_DIVISION(comp.getDivision());
                    getSAPCustomerClient().getGetDetail1().execute();
                    retNo = 
                        getSAPCustomerClient().getGetDetail1().getExportParams()
                            .getRETURN().getMESSAGE();
                    retMsg = 
                        getSAPCustomerClient().getGetDetail1().getExportParams()
                            .getRETURN().getNUMBER();
                    logger.info("Return Number [" + retNo + "] retMsg [" 
                        + retMsg + "].");
                    if (retNo.length() > 0) {
                        throw new RuntimeException(retMsg);
                    }

                    // Populate customer object data
                    customer.Customer.GetDetail1.ExportParams.PE_COMPANYDATA 
                        currAddr = getSAPCustomerClient().getGetDetail1()
                            .getExportParams().getPE_COMPANYDATA();
                    comp.setCity(currAddr.getCITY());
                    comp.setPostalCode(currAddr.getPOSTL_COD1());
                    comp.setStreet(currAddr.getSTREET());
                    comp.setCountryKey(currAddr.getCOUNTRY());
                    comp.setLanguageKey(currAddr.getLANGU_ISO());
                    comp.setRegion(currAddr.getREGION());
                    comp.setTelephone(currAddr.getTEL1_NUMBR());
                    comp.setFaxNumber(currAddr.getFAX_NUMBER());
                    comp.setCurrencyKey(currAddr.getCURRENCY());

                    customerMap.put(comp.getCustomerName(), comp);
                }
            }
            return new ArrayList<Customer>(customerMap.values());
        }
        catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

The getRepositoryName and newBusinessObject methods have implementations very similar to those in the MusicAlbumProvider class:

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

    @Override
    public Customer newBusinessObject() {
        return new Customer(this);
    }

The other methods in the provider class use the default BusinessObjectProvider implementation: getSessionContext, setSessionContect, and getTransactionManager.

Extending the BusinessObject Class to Access a Sun JCA Adapter

The BusinessObject class for an Enterprise Connector that accesses a Sun JCA Adapter may have straightforward implementations of the BusinessObject methods, but it may also require some additional utility methods. A SAP BAPI Customer object, for example, implements a large number of getter and setter methods for its properties. Its serialize and deserialize methods can be relatively simple.

The Customer object implementations of the getInsertCommand, getUpdateCommand, and getDeleteCommand methods call the constructors for the command classes, as expected. However, here the constructors take two arguments, and the second argument is the value returned by a utility method.

    /**
     * {@inheritDoc}
     */
    @Override
    public CustomerInsertCommand getInsertCommand() {
        return new CustomerInsertCommand(this, getInsertCustomer());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CustomerUpdateCommand getUpdateCommand() {
        return new CustomerUpdateCommand(this, getUpdateCustomer());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CustomerDeleteCommand getDeleteCommand() {
        return new CustomerDeleteCommand(this, getDeleteCustomer());
    }

The utility methods use the provider class's getSAPCustomerClient method to retrieve first the customer communication object, and then the CreateFromData1 object. For example, the getInsertCustomer method begins as follows:

    /**
     * Returns a Customer CreateFromData1 object to be used for insert.
     */
    public customer.Customer.CreateFromData1 getInsertCustomer() {
        customer.Customer.CreateFromData1 cfd = getBusinessObjectProvider()
                .getSAPCustomerClient().getCreateFromData1();

The rest of the getInsertCustomer method uses the CreateFromData1 object to assign the Customer properties to the SAP BAPI customer object. Finally, it returns the CreateFromData1 object.

       // Set import parameters
        cfd.getImportParams().getPI_COPYREFERENCE().setREF_CUSTMR(
            CustomerProvider.REF_CUSTOMER);
        cfd.getImportParams().getPI_COPYREFERENCE().setSALESORG(getSalesOrg());
        cfd.getImportParams().getPI_COPYREFERENCE().setDISTR_CHAN(getDistChannel());
        cfd.getImportParams().getPI_COPYREFERENCE().setDIVISION(getDivision());

        // Required import parameters
        cfd.getImportParams().getPI_COMPANYDATA().setNAME(getCustomerName());
        cfd.getImportParams().getPI_COMPANYDATA().setLANGU_ISO(getLanguageKey());
        cfd.getImportParams().getPI_COMPANYDATA().setCURRENCY(getCurrencyKey());
        cfd.getImportParams().getPI_COMPANYDATA().setCOUNTRY(getCountryKey());
        cfd.getImportParams().getPI_COMPANYDATA().setPOSTL_COD1(getPostalCode());
        cfd.getImportParams().getPI_COMPANYDATA().setCITY(getCity());

        // Additional import parameters
        cfd.getImportParams().getPI_COMPANYDATA().setSTREET(getStreet());
        cfd.getImportParams().getPI_COMPANYDATA().setREGION(getRegion());
        cfd.getImportParams().getPI_COMPANYDATA().setTEL1_NUMBR(getTelephone());
        cfd.getImportParams().getPI_COMPANYDATA().setFAX_NUMBER(getFaxNumber());

        return cfd;
    }

The getUpdateCustomer method is almost identical to the getInsertCustomer method except that it also marks the fields as having changed:

        // Mark fields to be changed
        String ex = "X";
        cfd.getImportParams().getPI_COMPANYDATAX().setNAME(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setLANGU_ISO(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setCURRENCY(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setCOUNTRY(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setPOSTL_COD1(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setCITY(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setSTREET(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setREGION(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setTEL1_NUMBR(ex);
        cfd.getImportParams().getPI_COMPANYDATAX().setFAX_NUMBER(ex);

Similarly, the getDeleteCustomer method informs SAP to delete a record by changing its name to begin with the string DELETED:

        // Mark customer as deleted by prepending "DELETE" to the name
        setCustomerName("DELETED " + getCustomerName());
        logger.fine("Changing NAME field to [" + getCustomerName() + "].");
        cfd.getImportParams().getPI_COMPANYDATA().setNAME(getCustomerName());
        cfd.getImportParams().getPI_COMPANYDATAX().setNAME("X");
        cfd.getImportParams().getPI_COMPANYDATA().setLANGU_ISO(getLanguageKey());
        cfd.getImportParams().getPI_COMPANYDATA().setCURRENCY(getCurrencyKey());
        cfd.getImportParams().getPI_COMPANYDATA().setCOUNTRY(getCountryKey());
        cfd.getImportParams().getPI_COMPANYDATA().setPOSTL_COD1(getPostalCode());
        cfd.getImportParams().getPI_COMPANYDATA().setCITY(getCity());
        cfd.getImportParams().setCUSTOMERNO(getCustomerNumber());
        cfd.getImportParams().setPI_SALESORG(getSalesOrg());
        cfd.getImportParams().setPI_DISTR_CHAN(getDistChannel());
        cfd.getImportParams().setPI_DIVISION(getDivision());

Extending the InsertCommand, UpdateCommand, and DeleteCommand Classes to Access a Sun JCA Adapter

For the three command classes, you need to use generated classes and methods from the OTD. For a SAP BAPI application, for example, you need to import the following packages for the CustomerInsertCommand implementation:

import customer.Customer.CreateFromData1;
import customer.Customer.CreateFromData1.ExportParams.RETURN;

You then use the first of the imported classes in the class constructor, which takes two arguments instead of the single argument of the default implementation:

    public CustomerInsertCommand(Customer bobject, CreateFromData1 cfd) {
        super(bobject);
        mCreateFromData = cfd;
        logger.debug("Creating instance " + this);
    }

The CreateFromData1 object passed to the constructor is the returned value from the Customer class's getInsertCustomer method.

You implement the execute command by calling the class's own execute method and retrieving any return value through the second imported class:

    @Override
    public void execute() {
        try {
            mCreateFromData.execute();
            RETURN ret = mCreateFromData.getExportParams().getRETURN();
            String retMsg = ret.getMESSAGE();
            String message = "SAP (" + ret.getNUMBER() + "): " + retMsg;
            logger.info(message);
            if (retMsg.length() > 0) {
                throw new RuntimeException(message);
            }
        }
        catch (RuntimeException e) {
            throw e;
        }
        catch (Exception e) {
            logger.severe(e.getMessage());
            throw new RuntimeException(e.getMessage(), e);
        }
    }

For both the CustomerDeleteCommand and the CustomerUpdateCommand implementations, you import the following:

import customer.Customer.ChangeFromData1;
import customer.Customer.ChangeFromData1.ExportParams.RETURN;

The constructors and the execute methods for these classes use the ChangeFromData1 object returned from the Customer.getUpdateCustomer and Customer.getDeleteCustomer methods. Otherwise the class implementations are identical to those of the CustomerInsertCommand implementation.