Application Techniques



Using a setDataSource DSO

How to implement the AgiDataSource interface to access and update data from and to any data source.

About this technique

Details

Category

Data Access Techniques> Data source objects

Description

You'll learn about:

You can run this technique code from:

NOTE   First make sure that database is running on your localhost SilverStream Server

Related reading

See the chapter on using data source business objects in the Programmer's Guide

Objects and data flow   Top of page

This application demonstrates all of the SilverStream objects that you must implement to create an updateable DSO (also called a setResultSet DSO). This technique documents some, but not all, of the methods that you must implement.

    For a complete description of all of the methods you need to implement, see the chapter on using data source business objects in the Programmer's Guide

This application implements the following objects:

frmRDBEmployeeDSO

A SilverStream form containing an AgcData control called dataRDBDSO. The dataRDBDSO object is bound to the DSO called DSORDBEmployees. This form invokes DSORDBEmployees.

DSORDBEmployees

A SilverStream data source object. It instantiates a utility object, called objRDBDataSource, that implements the SilverStream AgiDataSource interface, and sets the data source.

objRDBDataSource

A utility class that implements the com.sssw.rt.util.AgiDataSource interface. It obtains the data from the external data source, formats it in the appropriate row and column format and populates the server-side data buffer for download to the client.

objRDBSingleRowEnumerator

A standard Java utility class that returns the next available row. It is used by the objRDBDataSource.getNextRows() method, one of the methods that is part of the AgiDataSource implementation.

objRDBBandDescriptor

A utility class that implements the com.sssw.rt.util.AgiBandDescriptor interface. It provides important column information.

objRDBDataUpdateRequest

A utility class that extends the com.sssw.rt.util.AgoDataUpdateRequest object. It gathers the updated records from the AgcData on the client and contains methods that define how the updates should be committed to the external datasource. It defines methods that provide the mechanics of an update.

objRDBDataUpdateRow

A utility class that extends the com.sssw.rt.util.AgoDataUpdateRow object.

objRDBTransactionHandle

A utility class that implements the com.sssw.rt.util.AgiTransactionHandle interface. It connects and disconnects from the database.

This diagram illustrates the relationship among these objects.

Invoking the DSO   Top of page

The form RDBEmployeeDSO displays records from the employees table. The user can navigate the data set and can also add, delete, or update the data set. The interface looks like this:

In the FormActivate event, the invokeQuery() method is called passing parameters as an array of objects. The first parameter is a String that represents and Order by clause, the second represents the Query string. The AgcData calls gotoFirst() to download data to the form.

  protected void formActivate() 
  { 
    try 
    { 
     // Define query parameters 
       String sOrderBy = "employeeid"; 
       String sQuery = ""; 
       Object[] objParms = new Object[2]; 
       objParms[0] =  sOrderBy; 
       objParms[1] = sQuery; 
    // Call the DSO, passing the query 
       Object objRDBResults = dataRDBDSO.invokeQuery(objParms); 
    // Download data from the AgcData object 
       dataRDBDSO.gotoFirst(); 
    } 
    catch (Exception e) 
    { 
      System.out.println("Exception calling invokeQuery on 
         dataRDBDSO:" + e); 
       agDialog.displayError(e); 
    } 
  } 

Instantiating and setting the AgiDataSource   Top of page

The DSORDBEmployees is the object that describes the data set and provides bind information to the form. The form calls the DSO that is bound to the form's AgcData control. The DSO defines these columns:

The columns that are defined by the DSO must also be defined by the AgiDataSource object (in this case the objRDBEmployees object). The column list does not have to include the entire result set returned by the AgiDataSource; however, any column defined in the data source properties is available for design time data binding.

The Updatable check box is also checked which means that this DSO can perform updates, thus it implements the AgiDataUpdateRequest in addition to the AgiDataSourceListener interface.

Coding the invokeQuery event

In the DSO's invokeQuery event, the DSO instantiates objRDBDataSource and sets it as the data source for the AgcData control.

  // ... code from the invokeQuery() event... 
  // Set up the parameters for the this Data Source Object 
     Hashtable hshParms = new Hashtable(); 
      // the SilverStream Server 
      hshParms.put("Server", evt.getServer()); 
      // ODBC DataSource Name         
      hshParms.put ("DBName", ((AgiDatabase) 
             evt.getDatabase()).getName()); 
      // user id     
      hshParms.put("UserID", USER_ID); 
      //user password                 
      hshParms.put("Password", PASSWORD); 
      // table name             
      hshParms.put ("TableName", TABLE_NAME); 
      // array of the column names 
      hshParms.put ("FieldNames", COLUMN_NAMES);  
      // array of the column date types 
      hshParms.put ("DataTypes", COLUMN_DATA_TYPES) 
      // array of booleans whether column may be included in a query 
      hshParms.put ("CanQuery", COLUMN_CAN_QUERY); 
     // name of autoincrement column, if any 
      hshParms.put("AutoIncrementColumn", AUTO_INCREMENT);     
   
  // See if a parameter was passed by the Form or Page.   
  // No parameter is required, however, if there is a parameter,  
  // it expects and array of 2 objects, the first being the  
  // OrderBy string and the second being the Query string. 
      Object[] objParms = (Object[]) evt.getParameter(); 
          if (objParms != null) 
          { 
              if (objParms.length != 2) 
              { 
                System.out.println(this + " - " + 
                    ERROR_INVALID_PARAMETER_LIST); 
                  // If a parm is passed, we require 2 objects 
                  throw new Exception(this + " - " + 
                        ERROR_INVALID_PARAMETER_LIST); 
              } 
           hshParms.put("OrderBy", objParms[0]); 
           hshParms.put("QueryString", objParms[1]); 
          } 
           
   // Create the data source object, set the data source 
   // object in the event, set the result to ok, and return. 
      AgiDataSource objRDBDataSource = new 
                   ObjRDBDataSource(hshParms); 
      evt.setDataSource(objRDBDataSource); 
      evt.setResult("ok");     
      return; 

Implementing AgiDataSource   Top of page

The objRDBDataSource is the utility class that implements the com.sssw.rt.util.AgiDataSource interface. It is located in the Objects/com/examples/rdbdso directory.

This objRDBDataSource object is responsible for accessing and updating the external data source. It gets instantiated by the DSORDBEmployees call to the setDataSource() method in the DSOs invokeQuery event.

Declared variables

The objRDBDataSource declares the following variables in the General Declarations section to save the objects across calls to other objects.

  private Connection m_connection; // Connection 
  private AgiDataRowFactory m_rowFactory;// data row factory 
  protected ObjRDBBandDescriptor m_bandDescriptor; // bandDescriptor 
  private PreparedStatement m_preparedStatement;// PreparedStatement 
  private ResultSet m_resultSet; // result set 
  private Hashtable m_hshParms; // Hashtable parameters 
  private String m_sTableName; // the table name 
  private String[] m_sColumns; // the column list 
  private String m_sQuery;     // Query String 
  private String m_sOrderBy;   // Order by columns 
  protected String m_autoIncrementColumn; // Autoinc column name 
  int m_iColumnCount;         // Column count 

Imports

The objRDBDataSource object imports these packages in the Imports section:

  import com.sssw.rt.util.*;// for the AgiDataRowFactory 
  import com.sssw.srv.api.*;// for the AgiDatabase     
  import java.sql.*;// for the connection 
  import java.util.*;// for the Hashtable 

Constructor

The constructor for the object is defined in the General section. It has this declaration.

  public objRDBDataSource(Hashtable phshParms) 

Where phshParms is a Hashtable that contains the database, table and column information.

Required methods

When implementing the AgiDataSource there are several method implementations that you must include. You can see the code for these methods when you open objRDBDataSource in in the Examples3_Java database.

    For information about these methods, see Implementing AgiDataSource in the Data Source Objects chapter of the Programmer's Guide.

The examples that follows shows how to implement the requery() and getNextRows() methods.

Implementing reQuery()

This method re-executes the current query on the data source:

  public void reQuery() throws com.sssw.rt.util.AgoApiException 
    { 
      String sODBCDsn = "jdbc:sssw:odbc:";     
      // First, read the Hashtable dbname and connect  
      // to the database 
      try 
         {   
         // Close any current connections 
         close(); 
     
        // Get the parameters from the Hashtable and connect  
        // to the database 
        String sDatabaseName = (String) m_hshParms.get ("DBName"); 
        sODBCDsn += sDatabaseName;  
        String sUserID = (String) m_hshParms.get ("UserID"); 
        String sPassword = (String) m_hshParms.get("Password"); 
        AgiServer srvServer = (AgiServer) m_hshParms.get("Server"); 
         
        // Initialize our database object  
        AgiDatabase database = null ;         
        // Get our database object from the SilverStream Server 
        try  
          { 
          database = srvServer.getDatabase(sDatabaseName); 
          m_connection = database.getConnection(true); 
          m_bConnectionPool = true; 
          } 
        // Otherwise, we need to connect to it using  
        // the SQL DriverManager 
        catch (Exception e)   
           { 
          m_connection = DriverManager.getConnection  
             (sODBCDsn, sUserID, sPassword); 
           }             
        }       
      } 
      catch (Exception ex) { 
        if (DEBUG_ON) 
          System.out.println("----> reQuery.getConnection failed:  " 
           + ex.toString() ); 
     
            // Close any open connection 
        close(); 
     
        // Throw the Exception back to the caller 
        throw new AgoSystemDatabaseException(ex.toString()); 
      }       
     
      // Now, build the SELECT statement 
      try   
        { 
          if (m_bandDescriptor == null) 
          getBandDescriptor(); 
   
        m_iColumnCount = m_bandDescriptor.getColumnCount(); 
     
        // Construct the Select statement 
        String sStatement = "SELECT "; 
        for (int i = 0; i < m_iColumnCount; i++ ) { 
          if (i > 0) 
            sStatement += ", ";           
          sStatement = sStatement + " " + m_sTableName + "." + 
             m_sColumns[i]; 
        } 
        sStatement = sStatement + " FROM " + m_sTableName; 
         
        // Append where clause as passed in Query statement 
        if (m_sQuery != null && !m_sQuery.equals("")) 
          sStatement = sStatement + " WHERE " + m_sQuery; 
         
        // Use Order BY as passed in Query statement or use default   
        if (m_sOrderBy != null && !m_sOrderBy.equals("")) 
          sStatement = sStatement + " ORDER BY " + m_sOrderBy; 
        if (DEBUG_ON) 
          System.out.println(sStatement); 
       
        // Compile the Statement and execute it     
        m_preparedStatement = 
           m_connection.prepareStatement(sStatement); 
        m_resultSet = m_preparedStatement.executeQuery(); 
        }  
     
      catch (SQLException excpError)  
       { 
                  // Close any open connection 
        close(); 
         
        // Throw the Exception back to the caller 
        throw new AgoSystemDatabaseException(excpError.toString()); 
      } 
    } 

Implementing getNextRows()

This method gets the next block of rows from the result set of the data source.

  public java.util.Enumeration getNextRows(int int0) throws com.sssw.rt.util.AgoEndOfRowsException, com.sssw.rt.util.AgoApiException 
  { 
        if (m_resultSet == null) 
             throw new AgoEndOfRowsException(null); 
          
        try  
        { 
             if (!m_resultSet.next())  
           { 
               close(); 
               return null; 
           } 
      
           // Create a data row 
            AgiDataRow row = m_rowFactory.createDataRow(null); 
      
           // Loop through the column list and set the data row  
           //column values with the result set data.  
           for (int i = 0; i < m_iColumnCount; i++) 
           { 
                 Object objData = m_resultSet.getObject(i+1);             
              row.setData(i, objData); 
           } 
            // get next row from row enumerator utility class  
            return new ObjRDBSingleRowEnumeration(row); 
         }  
        catch (SQLException ex)  
        { 
              System.out.println(M_THIS + " - Error getting data.  
                  \r\nDescription:  " + ex.toString()); 
               
             // Close any open connection 
           close(); 
           throw new AgoSystemDatabaseException(ex.toString()); 
         } 
        catch (Exception e) 
        { 
            System.out.println("Error getting data. 
               \r\nDescription:  " + e.toString()); 
            
             // Close any open connection 
            close(); 
             throw new AgoSystemDatabaseException(e.toString()); 
        } 
  } 

Implementing AgiBandDescriptor   Top of page

The objRDBBandDescriptor describes the columns provided by a particular band for an AgiRowCursor object. The AgiDataUpdateRow calls the AgiDataSource getBandDescriptor() method to create the AgiBandDescriptor object.

The objRDBBandDescriptor is instantiated by the AgiDataSource object when it call the getBandDescriptor() method. This object is located in the Objects/com/examples/rdbdso directory.

Constructor

the constructor for the BandDescriptor object takes a hashtable:

  public objRDBBandDescriptor(Hashtable phshParms) 

Where Hashtable is a Hashtable containing database, table and column information

Required methods

When implementing the AgiBandDescriptor there are several method implementations that must be included. You can see the code for these methods when you open objRDBBandDescripto in the Examples3_Java database.

    For information about these methods, see Implementing AgiBandDescriptor in the Data Source Objects chapter of the Programmer's Guide.

getColumnType()

This is the implementation for the getColumnType() method. The not at the beginning lists the valid return types for the method:

  /**  
   *  Method: 
   *      getColumnType() 
   *  Description: 
   *  Return the data type (from DatatypeCodes) of the column. 
   *  Parameters:   
   *      int -  the column number 
   *  Returns:   
   *      char - the data type 
   *         Valid DatatypeCodes are: 
   *      DatatypeCodes.TYPE_BOOLEAN 
   *      DatatypeCodes.TYPE_CHAR 
   *      DatatypeCodes.TYPE_BYTE 
   *      DatatypeCodes.TYPE_SHORT 
   *      DatatypeCodes.TYPE_INT 
   *      DatatypeCodes.TYPE_LONG 
   *      DatatypeCodes.TYPE_FLOAT 
   *      DatatypeCodes.TYPE_DOUBLE 
   *      DatatypeCodes.TYPE_BYTEARRAY 
   *      DatatypeCodes.TYPE_STRING 
   *      DatatypeCodes.TYPE_NUMERIC 
   *      DatatypeCodes.TYPE_DATE 
   *      DatatypeCodes.TYPE_TIME 
   *      DatatypeCodes.TYPE_TIMESTAMP 
   */    
     public char getColumnType(int int0) 
     { 
     // Get the list of column types and the return the  
     // data type of the requested column. 
     char dataTypes[] = (char[]) m_hshParms.get("DataTypes"); 
     if (int0 <= dataTypes.length) 
        return dataTypes [int0]; 
     else 
             throw new IllegalArgumentException(); 
     } 

Extending AgoDataUpdateRequest   Top of page

The objRDBDataUpdateRequest is a utility class that extends the AgoDataUpdateRequest object. It acts as the basket for gathering the rows that the client has updated, and then doing the work necessary to update those rows to the external data source. This object is located in the Objects/com/examples/rdbdso directory.

It gets instantiated by the object that implements AgiDataSource during the prepareUpdateRequest() method. It in turn creates the transaction handle object and the AgoDataUpdateRow. The objRDBDataUpdateRequest object relies on the AgoDataUpdateRow object and the AgiTransactionHandle objects.

Variables

This implementation defines the following variables in the General Declarations section. This is done in oder to save the Hashtable of parameters across calls and to save the transaction object as well.

  // save the Hashtable parameters across calls 
  private Hashtable m_hshParms; 
   
  // save the transaction object calls 
  private ObjRDBTransactionHandle m_transaction; 

Imports

The object defines these imports in the Imports section for the Connection, Hashtable and database Exceptions:

  import java.sql.*;  // for the Connection 
  import java.util.*; // for the Hashtable parameters 
  import com.sssw.rt.util.*; // for database Exceptions 

createUpdateRow()

As part of the mechanics for updating the external data source, the update request has to create the rows that need to be updated. It uses the createUpdateRow() method to do so. There are two variants of this method. One lets you create rows for insert or delete. The return statement is as follows:

  return new objRDBDataUpdateRow(char0, agiDataSource1, 
  m_rowFactory, agiDataRow2); 

Where:

char0 is the kind of operation (from CommandCodes)

AgiDataSource1 is the data source object

m_rowFactory is the row factory

agiDataRow2 is the data row

The other variant is used for rows that have been modified.

  return new objRDBDataUpdateRow(char0, agiDataSource1, 
   m_rowFactory, agiDataRow2, agiDataRow3); 

Where:

char0 is the kind of operation (from CommandCodes)

AgiDataSource1 is the data source object

m_rowFactory is the row factory

agiDataRow2 is the old data row

agiDataRow3 is the new data row

beginTransaction()

The data update request is responsible for defining the beginning and ending of transactions and specifying whether a transaction should be committed or aborted. This is how the beginTransaction() method is implemented.

  protected com.sssw.rt.util.AgiTransactionHandle beginTransaction() 
      throws com.sssw.rt.util.AgoApiException 
  { 
  // Create our transaction handle object, passing the  
  // Hashtable parameters and return it to the caller 
     m_transaction = new ObjRDBTransactionHandle(m_hshParms); 
     return m_transaction; 
  } 

commitTransaction()

In this implementation, transactions are committed by calling the commitTransaction() method. This is how it is implemented.

  protected void 
  commitTransaction(com.sssw.rt.util.AgiTransactionHandle 
  agiTransactionHandle0) throws com.sssw.rt.util.AgoApiException 
  { 
     // Cast the parameter to our transaction handle object, 
     // get our Connection, commit it, then release it. 
     try  
     { 
       ObjRDBTransactionHandle hTransaction =  
       (ObjRDBTransactionHandle) agiTransactionHandle0; 
       Connection connection = hTransaction.getConnection(); 
       connection.commit(); 
       hTransaction.releaseConnection(); 
     }  
     catch (SQLException ex)  
     { 
     throw new AgoSystemDatabaseException(ex, THIS + 
           ".commitTransaction():  Exception during commit"); 
     } 
  } 

Extending AgoDataUpdateRow   Top of page

The objRDBDataUpdateRow is a utility class that extends the AgoDataRow object. This object is located in the Objects/com/examples/rdbdso directory.

It holds the AgiDataRow objects that represent the rows that the user has added, deleted, or modified. The AgiDataSource object is saved in the constructor so that it can call the getBandDescriptor() method to get the AgiBandDescriptor object to use when building SQL.

The objRDBDataUpdateRow object must implement the checkStatment() and the prepareAndExecuteUpdate() methods. These method actually do the work required by the row.

Variables

The object defines these variables in the General Declarations section.

  // save the data source and transaction handle across calls 
  ObjRDBDataSource m_dataSource; 
  AgiDataRowFactory m_rowFactory; 
  AgiDataRow m_rowMain; 
  AgiDataRow m_rowUpdate; 
  char m_operation; 

Imports

This object imports the following:

  import java.sql.*;// for the Connection 
  import com.sssw.rt.util.*;// for the prepareAndExecuteUpdate 
  import com.sssw.srv.api.*;// for the AgiDatabase 
  import java.math.*;// for BigDecimal 

prepareAndExecuteUpdate() method

This method does most of the work. It builds the appropriate SQL to delete, insert, or update the row. (This is based on the requirements of the external data source, which is a relational database in this example.) The command codes passed by the client determine the insert, update, or delete operation.

For a list of command codes see the on-line API help page for CommandCodes.

This is these code for prepareAndExecuteUpdate:

  public com.sssw.rt.util.AgiDataRow 
  prepareAndExecuteUpdate(com.sssw.rt.util.AgiTransactionHandle 
  agiTransactionHandle0) throws com.sssw.rt.util.AgoApiException 
  { 
      // Cast our transaction handle to our transaction handle 
      // object and get the connection 
      ObjRDBTransactionHandle hTransaction = 
         (ObjRDBTransactionHandle) agiTransactionHandle0; 
      Connection connection = hTransaction.getConnection(); 
     
      // Based on the operation type, build, compile,  
      // bind and execute the appropriate SQL statement 
       
      PreparedStatement prepStatement = null; 
      AgiDataRow newRow = null; 
      try  
      { 
        switch (m_operation) 
        { 
          case CommandCodes.CCINSERT: 
            // Build an insert statement 
            newRow = buildAndExecuteInsertStatement (connection); 
                  break; 
            case CommandCodes.CCUPDATE: 
            // Build an update statement 
            buildAndExecuteUpdateStatement (connection);   
                  break; 
          case CommandCodes.CCDELETE: 
            // Build a delete statement 
            buildAndExecuteDeleteStatement (connection); 
                  break; 
          default: 
                    throw new AgoUnrecoverableSystemException(M_THIS 
                  + ".prepareAndExecuteUpdate():   
                  Unsupported update operation type '" +  
                  m_operation + "' on row " +  
                  m_rowMain.getRowKey()); 
        } 
      }  
      catch (Exception e)  
      { 
        System.out.println(M_THIS + ".prepareAndExecuteUpdate() 
             SQLException:  " +  m_rowMain.getRowKey() + ": " + 
              e.getMessage()); 
        throw new AgoSystemDatabaseException(e, M_THIS + 
              ".prepareAndExecuteUpdate():  SQLException occurred 
                during update of row " +  
                m_rowMain.getRowKey() + "\r\nDescription: " + 
                 e.toString() +  "\r\nDetail:  " + e.getMessage()); 
        } 
        return newRow;    
    } 

Implementing AgiTransactionHandle   Top of page

The objRDBTransactionHandle is a utility class that implements the AgiTransaction handle interface. This object is located in the Objects/com/examples/rdbdso directory.

A transaction handle represents an in-progress database transaction - whatever that might mean for the underlying external data source. You can use it to store information that you need to pass to each updatable row during the update operation. In this example, the transaction handle gets created by the AgoDataUpdateRequest.beginTransaction() method.

This implementation includes a constructor, a getConnection() and a releaseConnection() method. Note that this example is specific to a SQL RDBMS that uses the driver manager to connect to the database. Depending on the external data source that you are using, you might not implement methods like these. For example, if you are trying to access an object database, you will have to use the vendor's proprietary connection mechanism.

Variables

The objRDBTransactionHandle defines two variables used by the class Constructor:

  // Save the connection across calls 
  private Connection m_connection; 
  // true if we were able to connect using the connection pool 
  private boolean m_bConnectionPool = false; 

Imports

The object imports these packages:

  import java.sql.*;// for the Connection 
  import java.util.*;// for the Hashtable parameters 
  import com.sssw.srv.api.*;// for the AgiDatabase and AgiServer 

Constructor

The constructor connects to the datasource, and uses the member variables to save the connection across calls to the database. If the database is known by the SilverStream Server, it gets a connection from the connection pool. Otherwise it connects to the database using the SQL Driver Manager:

  public ObjRDBTransactionHandle(Hashtable phshParms) 
    { 
      m_hshParms = phshParms; // save the parameters across calls 
       
      try  
       { 
        m_bConnectionPool = false; 
        String sDatabaseName = (String) phshParms.get("DBName"); 
        String sODBCDsn = "jdbc:sssw:odbc:" + sDatabaseName; 
        String sUserID = (String) phshParms.get ("UserID"); 
        String sPassword = (String) phshParms.get("Password"); 
        AgiServer server = (AgiServer) phshParms.get("Server"); 
     
        // Initialize our database object  
        AgiDatabase dbConnect = null ;   
       
        // Get our database object from the SilverStream Server 
        try  
         { 
          dbConnect = server.getDatabase(sDatabaseName); 
          m_connection = dbConnect.getConnection(true); 
          m_bConnectionPool = true; 
           
         } 
        // Otherwise, we need to connect to it using  
        // the SQL DriverManager 
        catch (Exception e)  
          { 
          m_connection = DriverManager.getConnection (sODBCDsn, 
             sUserID, sPassword); 
          } 
        } 
      catch (Exception excpError)   
      { 
        System.out.println("----> Constructor failed!"); 
      } 
    } 

getConnection() method

This method simply gets the database connection and saves it to the member variable

  public Connection getConnection() 
  { 
      return m_connection; 
  } 

releaseConnection() method

This method closes the connection, first determining whether the connection came from the connection pool on the SilverStream Server or from an SQL Driver Manager:

  public void releaseConnection() 
  { 
      if (m_connection != null)  
        { 
           try 
           { 
              if (m_bConnectionPool) 
              { 
                 AgiServer server = (AgiServer) 
                     m_hshParms.get("Server"); 
                 String sDatabaseName = (String) m_hshParms.get 
                     ("DBName"); 
                 AgiDatabase dbObject = 
                      server.getDatabase(sDatabaseName); 
                 server.getDatabase(sDatabaseName). 
                   releaseConnection(m_connection); 
              } 
              else 
              { 
                 //Release an ODBC SQL DriverManager connection 
                 m_connection.close(); 
              } 
           } 
           catch (Exception excpError) 
           { 
              System.out.println("====> releaseConnection failed!"); 
           } 
            
           m_connection = null; 
           m_bConnectionPool = false; 
        } 
  } 






Copyright © 2000, SilverStream Software, Inc. All rights reserved.