blob: 08ec71c0eb794098267fc8ea8a52819024a2c2d4 [file] [log] [blame]
/*
* Copyright (c) 2003, 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.rowset.internal;
import java.sql.*;
import javax.sql.*;
import java.util.*;
import java.io.*;
import com.sun.rowset.*;
import javax.sql.rowset.*;
import javax.sql.rowset.spi.*;
/**
* The facility called on internally by the <code>RIOptimisticProvider</code> implementation to
* propagate changes back to the data source from which the rowset got its data.
* <P>
* A <code>CachedRowSetWriter</code> object, called a writer, has the public
* method <code>writeData</code> for writing modified data to the underlying data source.
* This method is invoked by the rowset internally and is never invoked directly by an application.
* A writer also has public methods for setting and getting
* the <code>CachedRowSetReader</code> object, called a reader, that is associated
* with the writer. The remainder of the methods in this class are private and
* are invoked internally, either directly or indirectly, by the method
* <code>writeData</code>.
* <P>
* Typically the <code>SyncFactory</code> manages the <code>RowSetReader</code> and
* the <code>RowSetWriter</code> implementations using <code>SyncProvider</code> objects.
* Standard JDBC RowSet implementations provide an object instance of this
* writer by invoking the <code>SyncProvider.getRowSetWriter()</code> method.
*
* @author Jonathan Bruce
* @see javax.sql.rowset.spi.SyncProvider
* @see javax.sql.rowset.spi.SyncFactory
* @see javax.sql.rowset.spi.SyncFactoryException
*/
public class CachedRowSetWriter implements TransactionalWriter, Serializable {
/**
* The <code>Connection</code> object that this writer will use to make a
* connection to the data source to which it will write data.
*
*/
private transient Connection con;
/**
* The SQL <code>SELECT</code> command that this writer will call
* internally. The method <code>initSQLStatements</code> builds this
* command by supplying the words "SELECT" and "FROM," and using
* metadata to get the table name and column names .
*
* @serial
*/
private String selectCmd;
/**
* The SQL <code>UPDATE</code> command that this writer will call
* internally to write data to the rowset's underlying data source.
* The method <code>initSQLStatements</code> builds this <code>String</code>
* object.
*
* @serial
*/
private String updateCmd;
/**
* The SQL <code>WHERE</code> clause the writer will use for update
* statements in the <code>PreparedStatement</code> object
* it sends to the underlying data source.
*
* @serial
*/
private String updateWhere;
/**
* The SQL <code>DELETE</code> command that this writer will call
* internally to delete a row in the rowset's underlying data source.
*
* @serial
*/
private String deleteCmd;
/**
* The SQL <code>WHERE</code> clause the writer will use for delete
* statements in the <code>PreparedStatement</code> object
* it sends to the underlying data source.
*
* @serial
*/
private String deleteWhere;
/**
* The SQL <code>INSERT INTO</code> command that this writer will internally use
* to insert data into the rowset's underlying data source. The method
* <code>initSQLStatements</code> builds this command with a question
* mark parameter placeholder for each column in the rowset.
*
* @serial
*/
private String insertCmd;
/**
* An array containing the column numbers of the columns that are
* needed to uniquely identify a row in the <code>CachedRowSet</code> object
* for which this <code>CachedRowSetWriter</code> object is the writer.
*
* @serial
*/
private int[] keyCols;
/**
* An array of the parameters that should be used to set the parameter
* placeholders in a <code>PreparedStatement</code> object that this
* writer will execute.
*
* @serial
*/
private Object[] params;
/**
* The <code>CachedRowSetReader</code> object that has been
* set as the reader for the <code>CachedRowSet</code> object
* for which this <code>CachedRowSetWriter</code> object is the writer.
*
* @serial
*/
private CachedRowSetReader reader;
/**
* The <code>ResultSetMetaData</code> object that contains information
* about the columns in the <code>CachedRowSet</code> object
* for which this <code>CachedRowSetWriter</code> object is the writer.
*
* @serial
*/
private ResultSetMetaData callerMd;
/**
* The number of columns in the <code>CachedRowSet</code> object
* for which this <code>CachedRowSetWriter</code> object is the writer.
*
* @serial
*/
private int callerColumnCount;
/**
* This <code>CachedRowSet<code> will hold the conflicting values
* retrieved from the db and hold it.
*/
private CachedRowSetImpl crsResolve;
/**
* This <code>ArrayList<code> will hold the values of SyncResolver.*
*/
private ArrayList status;
/**
* This will check whether the same field value has changed both
* in database and CachedRowSet.
*/
private int iChangedValsInDbAndCRS;
/**
* This will hold the number of cols for which the values have
* changed only in database.
*/
private int iChangedValsinDbOnly ;
private JdbcRowSetResourceBundle resBundle;
public CachedRowSetWriter() {
try {
resBundle = JdbcRowSetResourceBundle.getJdbcRowSetResourceBundle();
} catch(IOException ioe) {
throw new RuntimeException(ioe);
}
}
/**
* Propagates changes in the given <code>RowSet</code> object
* back to its underlying data source and returns <code>true</code>
* if successful. The writer will check to see if
* the data in the pre-modified rowset (the original values) differ
* from the data in the underlying data source. If data in the data
* source has been modified by someone else, there is a conflict,
* and in that case, the writer will not write to the data source.
* In other words, the writer uses an optimistic concurrency algorithm:
* It checks for conflicts before making changes rather than restricting
* access for concurrent users.
* <P>
* This method is called by the rowset internally when
* the application invokes the method <code>acceptChanges</code>.
* The <code>writeData</code> method in turn calls private methods that
* it defines internally.
* The following is a general summary of what the method
* <code>writeData</code> does, much of which is accomplished
* through calls to its own internal methods.
* <OL>
* <LI>Creates a <code>CachedRowSet</code> object from the given
* <code>RowSet</code> object
* <LI>Makes a connection with the data source
* <UL>
* <LI>Disables autocommit mode if it is not already disabled
* <LI>Sets the transaction isolation level to that of the rowset
* </UL>
* <LI>Checks to see if the reader has read new data since the writer
* was last called and, if so, calls the method
* <code>initSQLStatements</code> to initialize new SQL statements
* <UL>
* <LI>Builds new <code>SELECT</code>, <code>UPDATE</code>,
* <code>INSERT</code>, and <code>DELETE</code> statements
* <LI>Uses the <code>CachedRowSet</code> object's metadata to
* determine the table name, column names, and the columns
* that make up the primary key
* </UL>
* <LI>When there is no conflict, propagates changes made to the
* <code>CachedRowSet</code> object back to its underlying data source
* <UL>
* <LI>Iterates through each row of the <code>CachedRowSet</code> object
* to determine whether it has been updated, inserted, or deleted
* <LI>If the corresponding row in the data source has not been changed
* since the rowset last read its
* values, the writer will use the appropriate command to update,
* insert, or delete the row
* <LI>If any data in the data source does not match the original values
* for the <code>CachedRowSet</code> object, the writer will roll
* back any changes it has made to the row in the data source.
* </UL>
* </OL>
*
* @return <code>true</code> if changes to the rowset were successfully
* written to the rowset's underlying data source;
* <code>false</code> otherwise
*/
public boolean writeData(RowSetInternal caller) throws SQLException {
boolean conflict = false;
boolean showDel = false;
PreparedStatement pstmtIns = null;
iChangedValsInDbAndCRS = 0;
iChangedValsinDbOnly = 0;
// We assume caller is a CachedRowSet
CachedRowSetImpl crs = (CachedRowSetImpl)caller;
// crsResolve = new CachedRowSetImpl();
this.crsResolve = new CachedRowSetImpl();;
// The reader is registered with the writer at design time.
// This is not required, in general. The reader has logic
// to get a JDBC connection, so call it.
con = reader.connect(caller);
if (con == null) {
throw new SQLException(resBundle.handleGetObject("crswriter.connect").toString());
}
/*
// Fix 6200646.
// Don't change the connection or transaction properties. This will fail in a
// J2EE container.
if (con.getAutoCommit() == true) {
con.setAutoCommit(false);
}
con.setTransactionIsolation(crs.getTransactionIsolation());
*/
initSQLStatements(crs);
int iColCount;
RowSetMetaDataImpl rsmdWrite = (RowSetMetaDataImpl)crs.getMetaData();
RowSetMetaDataImpl rsmdResolv = new RowSetMetaDataImpl();
iColCount = rsmdWrite.getColumnCount();
int sz= crs.size()+1;
status = new ArrayList(sz);
status.add(0,null);
rsmdResolv.setColumnCount(iColCount);
for(int i =1; i <= iColCount; i++) {
rsmdResolv.setColumnType(i, rsmdWrite.getColumnType(i));
rsmdResolv.setColumnName(i, rsmdWrite.getColumnName(i));
rsmdResolv.setNullable(i, ResultSetMetaData.columnNullableUnknown);
}
this.crsResolve.setMetaData(rsmdResolv);
// moved outside the insert inner loop
//pstmtIns = con.prepareStatement(insertCmd);
if (callerColumnCount < 1) {
// No data, so return success.
if (reader.getCloseConnection() == true)
con.close();
return true;
}
// We need to see rows marked for deletion.
showDel = crs.getShowDeleted();
crs.setShowDeleted(true);
// Look at all the rows.
crs.beforeFirst();
int rows =1;
while (crs.next()) {
if (crs.rowDeleted()) {
// The row has been deleted.
if (conflict = (deleteOriginalRow(crs, this.crsResolve)) == true) {
status.add(rows, new Integer(SyncResolver.DELETE_ROW_CONFLICT));
} else {
// delete happened without any occurrence of conflicts
// so update status accordingly
status.add(rows, new Integer(SyncResolver.NO_ROW_CONFLICT));
}
} else if (crs.rowInserted()) {
// The row has been inserted.
pstmtIns = con.prepareStatement(insertCmd);
if ( (conflict = insertNewRow(crs, pstmtIns, this.crsResolve)) == true) {
status.add(rows, new Integer(SyncResolver.INSERT_ROW_CONFLICT));
} else {
// insert happened without any occurrence of conflicts
// so update status accordingly
status.add(rows, new Integer(SyncResolver.NO_ROW_CONFLICT));
}
} else if (crs.rowUpdated()) {
// The row has been updated.
if ( conflict = (updateOriginalRow(crs)) == true) {
status.add(rows, new Integer(SyncResolver.UPDATE_ROW_CONFLICT));
} else {
// update happened without any occurrence of conflicts
// so update status accordingly
status.add(rows, new Integer(SyncResolver.NO_ROW_CONFLICT));
}
} else {
/** The row is neither of inserted, updated or deleted.
* So set nulls in the this.crsResolve for this row,
* as nothing is to be done for such rows.
* Also note that if such a row has been changed in database
* and we have not changed(inserted, updated or deleted)
* that is fine.
**/
int icolCount = crs.getMetaData().getColumnCount();
status.add(rows, new Integer(SyncResolver.NO_ROW_CONFLICT));
this.crsResolve.moveToInsertRow();
for(int cols=0;cols<iColCount;cols++) {
this.crsResolve.updateNull(cols+1);
} //end for
this.crsResolve.insertRow();
this.crsResolve.moveToCurrentRow();
} //end if
rows++;
} //end while
// close the insert statement
if(pstmtIns!=null)
pstmtIns.close();
// reset
crs.setShowDeleted(showDel);
boolean boolConf = false;
for (int j=1;j<status.size();j++){
// ignore status for index = 0 which is set to null
if(! ((status.get(j)).equals(new Integer(SyncResolver.NO_ROW_CONFLICT)))) {
// there is at least one conflict which needs to be resolved
boolConf = true;
break;
}
}
crs.beforeFirst();
this.crsResolve.beforeFirst();
if(boolConf) {
SyncProviderException spe = new SyncProviderException(status.size() - 1+resBundle.handleGetObject("crswriter.conflictsno").toString());
//SyncResolver syncRes = spe.getSyncResolver();
SyncResolverImpl syncResImpl = (SyncResolverImpl) spe.getSyncResolver();
syncResImpl.setCachedRowSet(crs);
syncResImpl.setCachedRowSetResolver(this.crsResolve);
syncResImpl.setStatus(status);
syncResImpl.setCachedRowSetWriter(this);
throw spe;
} else {
return true;
}
/*
if (conflict == true) {
con.rollback();
return false;
} else {
con.commit();
if (reader.getCloseConnection() == true) {
con.close();
}
return true;
}
*/
} //end writeData
/**
* Updates the given <code>CachedRowSet</code> object's underlying data
* source so that updates to the rowset are reflected in the original
* data source, and returns <code>false</code> if the update was successful.
* A return value of <code>true</code> indicates that there is a conflict,
* meaning that a value updated in the rowset has already been changed by
* someone else in the underlying data source. A conflict can also exist
* if, for example, more than one row in the data source would be affected
* by the update or if no rows would be affected. In any case, if there is
* a conflict, this method does not update the underlying data source.
* <P>
* This method is called internally by the method <code>writeData</code>
* if a row in the <code>CachedRowSet</code> object for which this
* <code>CachedRowSetWriter</code> object is the writer has been updated.
*
* @return <code>false</code> if the update to the underlying data source is
* successful; <code>true</code> otherwise
* @throws SQLException if a database access error occurs
*/
private boolean updateOriginalRow(CachedRowSet crs)
throws SQLException {
PreparedStatement pstmt;
int i = 0;
int idx = 0;
// Select the row from the database.
ResultSet origVals = crs.getOriginalRow();
origVals.next();
try {
updateWhere = buildWhereClause(updateWhere, origVals);
/**
* The following block of code is for checking a particular type of
* query where in there is a where clause. Without this block, if a
* SQL statement is built the "where" clause will appear twice hence
* the DB errors out and a SQLException is thrown. This code also
* considers that the where clause is in the right place as the
* CachedRowSet object would already have been populated with this
* query before coming to this point.
**/
String tempselectCmd = selectCmd.toLowerCase();
int idxWhere = tempselectCmd.indexOf("where");
if(idxWhere != -1)
{
String tempSelect = selectCmd.substring(0,idxWhere);
selectCmd = tempSelect;
}
pstmt = con.prepareStatement(selectCmd + updateWhere,
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
for (i = 0; i < keyCols.length; i++) {
if (params[i] != null) {
pstmt.setObject(++idx, params[i]);
} else {
continue;
}
}
try {
pstmt.setMaxRows(crs.getMaxRows());
pstmt.setMaxFieldSize(crs.getMaxFieldSize());
pstmt.setEscapeProcessing(crs.getEscapeProcessing());
pstmt.setQueryTimeout(crs.getQueryTimeout());
} catch (Exception ex) {
// Older driver don't support these operations.
}
ResultSet rs = null;
rs = pstmt.executeQuery();
if (rs.next() == true) {
if (rs.next()) {
/** More than one row conflict.
* If rs has only one row we are able to
* uniquely identify the row where update
* have to happen else if more than one
* row implies we cannot uniquely identify the row
* where we have to do updates.
* crs.setKeyColumns needs to be set to
* come out of this situation.
*/
return true;
}
// don't close the rs
// we require the record in rs to be used.
// rs.close();
// pstmt.close();
rs.first();
// how many fields need to be updated
int colsNotChanged = 0;
Vector cols = new Vector();
String updateExec = new String(updateCmd);
Object orig;
Object curr;
Object rsval;
boolean boolNull = true;
Object objVal = null;
// There's only one row and the cursor
// needs to be on that row.
boolean first = true;
boolean flag = true;
this.crsResolve.moveToInsertRow();
for (i = 1; i <= callerColumnCount; i++) {
orig = origVals.getObject(i);
curr = crs.getObject(i);
rsval = rs.getObject(i);
// reset boolNull if it had been set
boolNull = true;
/** This addtional checking has been added when the current value
* in the DB is null, but the DB had a different value when the
* data was actaully fetched into the CachedRowSet.
**/
if(rsval == null && orig != null) {
// value in db has changed
// don't proceed with synchronization
// get the value in db and pass it to the resolver.
iChangedValsinDbOnly++;
// Set the boolNull to false,
// in order to set the actual value;
boolNull = false;
objVal = rsval;
}
/** Adding the checking for rsval to be "not" null or else
* it would through a NullPointerException when the values
* are compared.
**/
else if(rsval != null && (!rsval.equals(orig)))
{
// value in db has changed
// don't proceed with synchronization
// get the value in db and pass it to the resolver.
iChangedValsinDbOnly++;
// Set the boolNull to false,
// in order to set the actual value;
boolNull = false;
objVal = rsval;
} else if ( (orig == null || curr == null) ) {
/** Adding the additonal condition of checking for "flag"
* boolean variable, which would otherwise result in
* building a invalid query, as the comma would not be
* added to the query string.
**/
if (first == false || flag == false) {
updateExec += ", ";
}
updateExec += crs.getMetaData().getColumnName(i);
cols.add(new Integer(i));
updateExec += " = ? ";
first = false;
/** Adding the extra condition for orig to be "not" null as the
* condition for orig to be null is take prior to this, if this
* is not added it will result in a NullPointerException when
* the values are compared.
**/
} else if (orig.equals(curr)) {
colsNotChanged++;
//nothing to update in this case since values are equal
/** Adding the extra condition for orig to be "not" null as the
* condition for orig to be null is take prior to this, if this
* is not added it will result in a NullPointerException when
* the values are compared.
**/
} else if(orig.equals(curr) == false) {
// When values from db and values in CachedRowSet are not equal,
// if db value is same as before updation for each col in
// the row before fetching into CachedRowSet,
// only then we go ahead with updation, else we
// throw SyncProviderException.
// if value has changed in db after fetching from db
// for some cols of the row and at the same time, some other cols
// have changed in CachedRowSet, no synchronization happens
// Synchronization happens only when data when fetching is
// same or at most has changed in cachedrowset
// check orig value with what is there in crs for a column
// before updation in crs.
if(crs.columnUpdated(i)) {
if(rsval.equals(orig)) {
// At this point we are sure that
// the value updated in crs was from
// what is in db now and has not changed
if (flag == false || first == false) {
updateExec += ", ";
}
updateExec += crs.getMetaData().getColumnName(i);
cols.add(new Integer(i));
updateExec += " = ? ";
flag = false;
} else {
// Here the value has changed in the db after
// data was fetched
// Plus store this row from CachedRowSet and keep it
// in a new CachedRowSet
boolNull= false;
objVal = rsval;
iChangedValsInDbAndCRS++;
}
}
}
if(!boolNull) {
this.crsResolve.updateObject(i,objVal);
} else {
this.crsResolve.updateNull(i);
}
} //end for
this.crsResolve.insertRow();
this.crsResolve.moveToCurrentRow();
/**
* if nothing has changed return now - this can happen
* if column is updated to the same value.
* if colsNotChanged == callerColumnCount implies we are updating
* the database with ALL COLUMNS HAVING SAME VALUES,
* so skip going to database, else do as usual.
**/
if ( (first == false && cols.size() == 0) ||
colsNotChanged == callerColumnCount ) {
return false;
}
if(iChangedValsInDbAndCRS != 0 || iChangedValsinDbOnly != 0) {
return true;
}
updateExec += updateWhere;
pstmt = con.prepareStatement(updateExec);
// Comments needed here
for (i = 0; i < cols.size(); i++) {
Object obj = crs.getObject(((Integer)cols.get(i)).intValue());
if (obj != null)
pstmt.setObject(i + 1, obj);
else
pstmt.setNull(i + 1,crs.getMetaData().getColumnType(i + 1));
}
idx = i;
// Comments needed here
for (i = 0; i < keyCols.length; i++) {
if (params[i] != null) {
pstmt.setObject(++idx, params[i]);
} else {
continue;
}
}
i = pstmt.executeUpdate();
/**
* i should be equal to 1(row count), because we update
* one row(returned as row count) at a time, if all goes well.
* if 1 != 1, this implies we have not been able to
* do updations properly i.e there is a conflict in database
* versus what is in CachedRowSet for this particular row.
**/
return false;
} else {
/**
* Cursor will be here, if the ResultSet may not return even a single row
* i.e. we can't find the row where to update because it has been deleted
* etc. from the db.
* Present the whole row as null to user, to force null to be sync'ed
* and hence nothing to be synced.
*
* NOTE:
* ------
* In the database if a column that is mapped to java.sql.Types.REAL stores
* a Double value and is compared with value got from ResultSet.getFloat()
* no row is retrieved and will throw a SyncProviderException. For details
* see bug Id 5053830
**/
return true;
}
} catch (SQLException ex) {
ex.printStackTrace();
// if executeUpdate fails it will come here,
// update crsResolve with null rows
this.crsResolve.moveToInsertRow();
for(i = 1; i <= callerColumnCount; i++) {
this.crsResolve.updateNull(i);
}
this.crsResolve.insertRow();
this.crsResolve.moveToCurrentRow();
return true;
}
}
/**
* Inserts a row that has been inserted into the given
* <code>CachedRowSet</code> object into the data source from which
* the rowset is derived, returning <code>false</code> if the insertion
* was successful.
*
* @param crs the <code>CachedRowSet</code> object that has had a row inserted
* and to whose underlying data source the row will be inserted
* @param pstmt the <code>PreparedStatement</code> object that will be used
* to execute the insertion
* @return <code>false</code> to indicate that the insertion was successful;
* <code>true</code> otherwise
* @throws SQLException if a database access error occurs
*/
private boolean insertNewRow(CachedRowSet crs,
PreparedStatement pstmt, CachedRowSetImpl crsRes) throws SQLException {
int i = 0;
int icolCount = crs.getMetaData().getColumnCount();
boolean returnVal = false;
PreparedStatement pstmtSel = con.prepareStatement(selectCmd,
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rs, rs2 = null;
DatabaseMetaData dbmd = con.getMetaData();
rs = pstmtSel.executeQuery();
String table = crs.getTableName();
rs2 = dbmd.getPrimaryKeys(null, null, table);
String [] primaryKeys = new String[icolCount];
int k = 0;
while(rs2.next()) {
String pkcolname = rs2.getString("COLUMN_NAME");
primaryKeys[k] = pkcolname;
k++;
}
if(rs.next()) {
for(int j=0;j<primaryKeys.length;j++) {
if(primaryKeys[j] != null) {
if(crs.getObject(primaryKeys[j]) == null){
break;
}
String crsPK = (crs.getObject(primaryKeys[j])).toString();
String rsPK = (rs.getObject(primaryKeys[j])).toString();
if(crsPK.equals(rsPK)) {
returnVal = true;
this.crsResolve.moveToInsertRow();
for(i = 1; i <= icolCount; i++) {
String colname = (rs.getMetaData()).getColumnName(i);
if(colname.equals(primaryKeys[j]))
this.crsResolve.updateObject(i,rsPK);
else
this.crsResolve.updateNull(i);
}
this.crsResolve.insertRow();
this.crsResolve.moveToCurrentRow();
}
}
}
}
if(returnVal)
return returnVal;
try {
for (i = 1; i <= icolCount; i++) {
Object obj = crs.getObject(i);
if (obj != null) {
pstmt.setObject(i, obj);
} else {
pstmt.setNull(i,crs.getMetaData().getColumnType(i));
}
}
i = pstmt.executeUpdate();
return false;
} catch (SQLException ex) {
/**
* Cursor will come here if executeUpdate fails.
* There can be many reasons why the insertion failed,
* one can be violation of primary key.
* Hence we cannot exactly identify why the insertion failed
* Present the current row as a null row to the user.
**/
this.crsResolve.moveToInsertRow();
for(i = 1; i <= icolCount; i++) {
this.crsResolve.updateNull(i);
}
this.crsResolve.insertRow();
this.crsResolve.moveToCurrentRow();
return true;
}
}
/**
* Deletes the row in the underlying data source that corresponds to
* a row that has been deleted in the given <code> CachedRowSet</code> object
* and returns <code>false</code> if the deletion was successful.
* <P>
* This method is called internally by this writer's <code>writeData</code>
* method when a row in the rowset has been deleted. The values in the
* deleted row are the same as those that are stored in the original row
* of the given <code>CachedRowSet</code> object. If the values in the
* original row differ from the row in the underlying data source, the row
* in the data source is not deleted, and <code>deleteOriginalRow</code>
* returns <code>true</code> to indicate that there was a conflict.
*
*
* @return <code>false</code> if the deletion was successful, which means that
* there was no conflict; <code>true</code> otherwise
* @throws SQLException if there was a database access error
*/
private boolean deleteOriginalRow(CachedRowSet crs, CachedRowSetImpl crsRes) throws SQLException {
PreparedStatement pstmt;
int i;
int idx = 0;
String strSelect;
// Select the row from the database.
ResultSet origVals = crs.getOriginalRow();
origVals.next();
deleteWhere = buildWhereClause(deleteWhere, origVals);
pstmt = con.prepareStatement(selectCmd + deleteWhere,
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
for (i = 0; i < keyCols.length; i++) {
if (params[i] != null) {
pstmt.setObject(++idx, params[i]);
} else {
continue;
}
}
try {
pstmt.setMaxRows(crs.getMaxRows());
pstmt.setMaxFieldSize(crs.getMaxFieldSize());
pstmt.setEscapeProcessing(crs.getEscapeProcessing());
pstmt.setQueryTimeout(crs.getQueryTimeout());
} catch (Exception ex) {
/*
* Older driver don't support these operations...
*/
;
}
ResultSet rs = pstmt.executeQuery();
if (rs.next() == true) {
if (rs.next()) {
// more than one row
return true;
}
rs.first();
// Now check all the values in rs to be same in
// db also before actually going ahead with deleting
boolean boolChanged = false;
crsRes.moveToInsertRow();
for (i = 1; i <= crs.getMetaData().getColumnCount(); i++) {
Object original = origVals.getObject(i);
Object changed = rs.getObject(i);
if(original != null && changed != null ) {
if(! (original.toString()).equals(changed.toString()) ) {
boolChanged = true;
crsRes.updateObject(i,origVals.getObject(i));
}
} else {
crsRes.updateNull(i);
}
}
crsRes.insertRow();
crsRes.moveToCurrentRow();
if(boolChanged) {
// do not delete as values in db have changed
// deletion will not happen for this row from db
// exit now returning true. i.e. conflict
return true;
} else {
// delete the row.
// Go ahead with deleting,
// don't do anything here
}
String cmd = deleteCmd + deleteWhere;
pstmt = con.prepareStatement(cmd);
idx = 0;
for (i = 0; i < keyCols.length; i++) {
if (params[i] != null) {
pstmt.setObject(++idx, params[i]);
} else {
continue;
}
}
if (pstmt.executeUpdate() != 1) {
return true;
}
pstmt.close();
} else {
// didn't find the row
return true;
}
// no conflict
return false;
}
/**
* Sets the reader for this writer to the given reader.
*
* @throws SQLException if a database access error occurs
*/
public void setReader(CachedRowSetReader reader) throws SQLException {
this.reader = reader;
}
/**
* Gets the reader for this writer.
*
* @throws SQLException if a database access error occurs
*/
public CachedRowSetReader getReader() throws SQLException {
return reader;
}
/**
* Composes a <code>SELECT</code>, <code>UPDATE</code>, <code>INSERT</code>,
* and <code>DELETE</code> statement that can be used by this writer to
* write data to the data source backing the given <code>CachedRowSet</code>
* object.
*
* @ param caller a <code>CachedRowSet</code> object for which this
* <code>CachedRowSetWriter</code> object is the writer
* @throws SQLException if a database access error occurs
*/
private void initSQLStatements(CachedRowSet caller) throws SQLException {
int i;
callerMd = caller.getMetaData();
callerColumnCount = callerMd.getColumnCount();
if (callerColumnCount < 1)
// No data, so return.
return;
/*
* If the RowSet has a Table name we should use it.
* This is really a hack to get round the fact that
* a lot of the jdbc drivers can't provide the tab.
*/
String table = caller.getTableName();
if (table == null) {
/*
* attempt to build a table name using the info
* that the driver gave us for the first column
* in the source result set.
*/
table = callerMd.getTableName(1);
if (table == null || table.length() == 0) {
throw new SQLException(resBundle.handleGetObject("crswriter.tname").toString());
}
}
String catalog = callerMd.getCatalogName(1);
String schema = callerMd.getSchemaName(1);
DatabaseMetaData dbmd = con.getMetaData();
/*
* Compose a SELECT statement. There are three parts.
*/
// Project List
selectCmd = "SELECT ";
for (i=1; i <= callerColumnCount; i++) {
selectCmd += callerMd.getColumnName(i);
if ( i < callerMd.getColumnCount() )
selectCmd += ", ";
else
selectCmd += " ";
}
// FROM clause.
selectCmd += "FROM " + buildTableName(dbmd, catalog, schema, table);
/*
* Compose an UPDATE statement.
*/
updateCmd = "UPDATE " + buildTableName(dbmd, catalog, schema, table);
/**
* The following block of code is for checking a particular type of
* query where in there is a where clause. Without this block, if a
* SQL statement is built the "where" clause will appear twice hence
* the DB errors out and a SQLException is thrown. This code also
* considers that the where clause is in the right place as the
* CachedRowSet object would already have been populated with this
* query before coming to this point.
**/
String tempupdCmd = updateCmd.toLowerCase();
int idxupWhere = tempupdCmd.indexOf("where");
if(idxupWhere != -1)
{
updateCmd = updateCmd.substring(0,idxupWhere);
}
updateCmd += "SET ";
/*
* Compose an INSERT statement.
*/
insertCmd = "INSERT INTO " + buildTableName(dbmd, catalog, schema, table);
// Column list
insertCmd += "(";
for (i=1; i <= callerColumnCount; i++) {
insertCmd += callerMd.getColumnName(i);
if ( i < callerMd.getColumnCount() )
insertCmd += ", ";
else
insertCmd += ") VALUES (";
}
for (i=1; i <= callerColumnCount; i++) {
insertCmd += "?";
if (i < callerColumnCount)
insertCmd += ", ";
else
insertCmd += ")";
}
/*
* Compose a DELETE statement.
*/
deleteCmd = "DELETE FROM " + buildTableName(dbmd, catalog, schema, table);
/*
* set the key desriptors that will be
* needed to construct where clauses.
*/
buildKeyDesc(caller);
}
/**
* Returns a fully qualified table name built from the given catalog and
* table names. The given metadata object is used to get the proper order
* and separator.
*
* @param dbmd a <code>DatabaseMetaData</code> object that contains metadata
* about this writer's <code>CachedRowSet</code> object
* @param catalog a <code>String</code> object with the rowset's catalog
* name
* @param table a <code>String</code> object with the name of the table from
* which this writer's rowset was derived
* @return a <code>String</code> object with the fully qualified name of the
* table from which this writer's rowset was derived
* @throws SQLException if a database access error occurs
*/
private String buildTableName(DatabaseMetaData dbmd,
String catalog, String schema, String table) throws SQLException {
// trim all the leading and trailing whitespaces,
// white spaces can never be catalog, schema or a table name.
String cmd = new String();
catalog = catalog.trim();
schema = schema.trim();
table = table.trim();
if (dbmd.isCatalogAtStart() == true) {
if (catalog != null && catalog.length() > 0) {
cmd += catalog + dbmd.getCatalogSeparator();
}
if (schema != null && schema.length() > 0) {
cmd += schema + ".";
}
cmd += table;
} else {
if (schema != null && schema.length() > 0) {
cmd += schema + ".";
}
cmd += table;
if (catalog != null && catalog.length() > 0) {
cmd += dbmd.getCatalogSeparator() + catalog;
}
}
cmd += " ";
return cmd;
}
/**
* Assigns to the given <code>CachedRowSet</code> object's
* <code>params</code>
* field an array whose length equals the number of columns needed
* to uniquely identify a row in the rowset. The array is given
* values by the method <code>buildWhereClause</code>.
* <P>
* If the <code>CachedRowSet</code> object's <code>keyCols</code>
* field has length <code>0</code> or is <code>null</code>, the array
* is set with the column number of every column in the rowset.
* Otherwise, the array in the field <code>keyCols</code> is set with only
* the column numbers of the columns that are required to form a unique
* identifier for a row.
*
* @param crs the <code>CachedRowSet</code> object for which this
* <code>CachedRowSetWriter</code> object is the writer
*
* @throws SQLException if a database access error occurs
*/
private void buildKeyDesc(CachedRowSet crs) throws SQLException {
keyCols = crs.getKeyColumns();
if (keyCols == null || keyCols.length == 0) {
keyCols = new int[callerColumnCount];
for (int i = 0; i < keyCols.length; ) {
keyCols[i] = ++i;
}
}
params = new Object[keyCols.length];
}
/**
* Constructs an SQL <code>WHERE</code> clause using the given
* string as a starting point. The resulting clause will contain
* a column name and " = ?" for each key column, that is, each column
* that is needed to form a unique identifier for a row in the rowset.
* This <code>WHERE</code> clause can be added to
* a <code>PreparedStatement</code> object that updates, inserts, or
* deletes a row.
* <P>
* This method uses the given result set to access values in the
* <code>CachedRowSet</code> object that called this writer. These
* values are used to build the array of parameters that will serve as
* replacements for the "?" parameter placeholders in the
* <code>PreparedStatement</code> object that is sent to the
* <code>CachedRowSet</code> object's underlying data source.
*
* @param whereClause a <code>String</code> object that is an empty
* string ("")
* @param rs a <code>ResultSet</code> object that can be used
* to access the <code>CachedRowSet</code> object's data
* @return a <code>WHERE</code> clause of the form "<code>WHERE</code>
* columnName = ? AND columnName = ? AND columnName = ? ..."
* @throws SQLException if a database access error occurs
*/
private String buildWhereClause(String whereClause,
ResultSet rs) throws SQLException {
whereClause = "WHERE ";
for (int i = 0; i < keyCols.length; i++) {
if (i > 0) {
whereClause += "AND ";
}
whereClause += callerMd.getColumnName(keyCols[i]);
params[i] = rs.getObject(keyCols[i]);
if (rs.wasNull() == true) {
whereClause += " IS NULL ";
} else {
whereClause += " = ? ";
}
}
return whereClause;
}
void updateResolvedConflictToDB(CachedRowSet crs, Connection con) throws SQLException {
//String updateExe = ;
PreparedStatement pStmt ;
String strWhere = "WHERE " ;
String strExec =" ";
String strUpdate = "UPDATE ";
int icolCount = crs.getMetaData().getColumnCount();
int keyColumns[] = crs.getKeyColumns();
Object param[];
String strSet="";
strWhere = buildWhereClause(strWhere, crs);
if (keyColumns == null || keyColumns.length == 0) {
keyColumns = new int[icolCount];
for (int i = 0; i < keyColumns.length; ) {
keyColumns[i] = ++i;
}
}
param = new Object[keyColumns.length];
strUpdate = "UPDATE " + buildTableName(con.getMetaData(),
crs.getMetaData().getCatalogName(1),
crs.getMetaData().getSchemaName(1),
crs.getTableName());
// changed or updated values will become part of
// set clause here
strUpdate += "SET ";
boolean first = true;
for (int i=1; i<=icolCount;i++) {
if (crs.columnUpdated(i)) {
if (first == false) {
strSet += ", ";
}
strSet += crs.getMetaData().getColumnName(i);
strSet += " = ? ";
first = false;
} //end if
} //end for
// keycols will become part of where clause
strUpdate += strSet;
strWhere = "WHERE ";
for (int i = 0; i < keyColumns.length; i++) {
if (i > 0) {
strWhere += "AND ";
}
strWhere += crs.getMetaData().getColumnName(keyColumns[i]);
param[i] = crs.getObject(keyColumns[i]);
if (crs.wasNull() == true) {
strWhere += " IS NULL ";
} else {
strWhere += " = ? ";
}
}
strUpdate += strWhere;
pStmt = con.prepareStatement(strUpdate);
int idx =0;
for (int i = 0; i < icolCount; i++) {
if(crs.columnUpdated(i+1)) {
Object obj = crs.getObject(i+1);
if (obj != null) {
pStmt.setObject(++idx, obj);
} else {
pStmt.setNull(i + 1,crs.getMetaData().getColumnType(i + 1));
} //end if ..else
} //end if crs.column...
} //end for
// Set the key cols for after WHERE =? clause
for (int i = 0; i < keyColumns.length; i++) {
if (param[i] != null) {
pStmt.setObject(++idx, param[i]);
}
}
int id = pStmt.executeUpdate();
}
/**
*
*/
public void commit() throws SQLException {
con.commit();
if (reader.getCloseConnection() == true) {
con.close();
}
}
public void commit(CachedRowSetImpl crs, boolean updateRowset) throws SQLException {
con.commit();
if(updateRowset) {
if(crs.getCommand() != null)
crs.execute(con);
}
if (reader.getCloseConnection() == true) {
con.close();
}
}
/**
*
*/
public void rollback() throws SQLException {
con.rollback();
if (reader.getCloseConnection() == true) {
con.close();
}
}
/**
*
*/
public void rollback(Savepoint s) throws SQLException {
con.rollback(s);
if (reader.getCloseConnection() == true) {
con.close();
}
}
}