1 /* 2 * Copyright (c) 2003, 2010, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.rowset.internal; 27 28 import java.sql.*; 29 import javax.sql.*; 30 import java.util.*; 31 import java.io.*; 32 33 import com.sun.rowset.*; 34 import java.text.MessageFormat; 35 import javax.sql.rowset.*; 36 import javax.sql.rowset.serial.SQLInputImpl; 37 import javax.sql.rowset.serial.SerialArray; 38 import javax.sql.rowset.serial.SerialBlob; 39 import javax.sql.rowset.serial.SerialClob; 40 import javax.sql.rowset.serial.SerialStruct; 41 import javax.sql.rowset.spi.*; 42 43 44 /** 45 * The facility called on internally by the <code>RIOptimisticProvider</code> implementation to 46 * propagate changes back to the data source from which the rowset got its data. 47 * <P> 48 * A <code>CachedRowSetWriter</code> object, called a writer, has the public 49 * method <code>writeData</code> for writing modified data to the underlying data source. 50 * This method is invoked by the rowset internally and is never invoked directly by an application. 51 * A writer also has public methods for setting and getting 52 * the <code>CachedRowSetReader</code> object, called a reader, that is associated 53 * with the writer. The remainder of the methods in this class are private and 54 * are invoked internally, either directly or indirectly, by the method 55 * <code>writeData</code>. 56 * <P> 57 * Typically the <code>SyncFactory</code> manages the <code>RowSetReader</code> and 58 * the <code>RowSetWriter</code> implementations using <code>SyncProvider</code> objects. 59 * Standard JDBC RowSet implementations provide an object instance of this 60 * writer by invoking the <code>SyncProvider.getRowSetWriter()</code> method. 61 * 62 * @version 0.2 63 * @author Jonathan Bruce 64 * @see javax.sql.rowset.spi.SyncProvider 65 * @see javax.sql.rowset.spi.SyncFactory 66 * @see javax.sql.rowset.spi.SyncFactoryException 67 */ 68 public class CachedRowSetWriter implements TransactionalWriter, Serializable { 69 70 /** 71 * The <code>Connection</code> object that this writer will use to make a 72 * connection to the data source to which it will write data. 73 * 74 */ 75 private transient Connection con; 76 77 /** 78 * The SQL <code>SELECT</code> command that this writer will call 79 * internally. The method <code>initSQLStatements</code> builds this 80 * command by supplying the words "SELECT" and "FROM," and using 81 * metadata to get the table name and column names . 82 * 83 * @serial 84 */ 85 private String selectCmd; 86 87 /** 88 * The SQL <code>UPDATE</code> command that this writer will call 89 * internally to write data to the rowset's underlying data source. 90 * The method <code>initSQLStatements</code> builds this <code>String</code> 91 * object. 92 * 93 * @serial 94 */ 95 private String updateCmd; 96 97 /** 98 * The SQL <code>WHERE</code> clause the writer will use for update 99 * statements in the <code>PreparedStatement</code> object 100 * it sends to the underlying data source. 101 * 102 * @serial 103 */ 104 private String updateWhere; 105 106 /** 107 * The SQL <code>DELETE</code> command that this writer will call 108 * internally to delete a row in the rowset's underlying data source. 109 * 110 * @serial 111 */ 112 private String deleteCmd; 113 114 /** 115 * The SQL <code>WHERE</code> clause the writer will use for delete 116 * statements in the <code>PreparedStatement</code> object 117 * it sends to the underlying data source. 118 * 119 * @serial 120 */ 121 private String deleteWhere; 122 123 /** 124 * The SQL <code>INSERT INTO</code> command that this writer will internally use 125 * to insert data into the rowset's underlying data source. The method 126 * <code>initSQLStatements</code> builds this command with a question 127 * mark parameter placeholder for each column in the rowset. 128 * 129 * @serial 130 */ 131 private String insertCmd; 132 133 /** 134 * An array containing the column numbers of the columns that are 135 * needed to uniquely identify a row in the <code>CachedRowSet</code> object 136 * for which this <code>CachedRowSetWriter</code> object is the writer. 137 * 138 * @serial 139 */ 140 private int[] keyCols; 141 142 /** 143 * An array of the parameters that should be used to set the parameter 144 * placeholders in a <code>PreparedStatement</code> object that this 145 * writer will execute. 146 * 147 * @serial 148 */ 149 private Object[] params; 150 151 /** 152 * The <code>CachedRowSetReader</code> object that has been 153 * set as the reader for the <code>CachedRowSet</code> object 154 * for which this <code>CachedRowSetWriter</code> object is the writer. 155 * 156 * @serial 157 */ 158 private CachedRowSetReader reader; 159 160 /** 161 * The <code>ResultSetMetaData</code> object that contains information 162 * about the columns in the <code>CachedRowSet</code> object 163 * for which this <code>CachedRowSetWriter</code> object is the writer. 164 * 165 * @serial 166 */ 167 private ResultSetMetaData callerMd; 168 169 /** 170 * The number of columns in the <code>CachedRowSet</code> object 171 * for which this <code>CachedRowSetWriter</code> object is the writer. 172 * 173 * @serial 174 */ 175 private int callerColumnCount; 176 177 /** 178 * This <code>CachedRowSet<code> will hold the conflicting values 179 * retrieved from the db and hold it. 180 */ 181 private CachedRowSetImpl crsResolve; 182 183 /** 184 * This <code>ArrayList<code> will hold the values of SyncResolver.* 185 */ 186 private ArrayList<Integer> status; 187 188 /** 189 * This will check whether the same field value has changed both 190 * in database and CachedRowSet. 191 */ 192 private int iChangedValsInDbAndCRS; 193 194 /** 195 * This will hold the number of cols for which the values have 196 * changed only in database. 197 */ 198 private int iChangedValsinDbOnly ; 199 200 private JdbcRowSetResourceBundle resBundle; 201 202 public CachedRowSetWriter() { 203 try { 204 resBundle = JdbcRowSetResourceBundle.getJdbcRowSetResourceBundle(); 205 } catch(IOException ioe) { 206 throw new RuntimeException(ioe); 207 } 208 } 209 210 /** 211 * Propagates changes in the given <code>RowSet</code> object 212 * back to its underlying data source and returns <code>true</code> 213 * if successful. The writer will check to see if 214 * the data in the pre-modified rowset (the original values) differ 215 * from the data in the underlying data source. If data in the data 216 * source has been modified by someone else, there is a conflict, 217 * and in that case, the writer will not write to the data source. 218 * In other words, the writer uses an optimistic concurrency algorithm: 219 * It checks for conflicts before making changes rather than restricting 220 * access for concurrent users. 221 * <P> 222 * This method is called by the rowset internally when 223 * the application invokes the method <code>acceptChanges</code>. 224 * The <code>writeData</code> method in turn calls private methods that 225 * it defines internally. 226 * The following is a general summary of what the method 227 * <code>writeData</code> does, much of which is accomplished 228 * through calls to its own internal methods. 229 * <OL> 230 * <LI>Creates a <code>CachedRowSet</code> object from the given 231 * <code>RowSet</code> object 232 * <LI>Makes a connection with the data source 233 * <UL> 234 * <LI>Disables autocommit mode if it is not already disabled 235 * <LI>Sets the transaction isolation level to that of the rowset 236 * </UL> 237 * <LI>Checks to see if the reader has read new data since the writer 238 * was last called and, if so, calls the method 239 * <code>initSQLStatements</code> to initialize new SQL statements 240 * <UL> 241 * <LI>Builds new <code>SELECT</code>, <code>UPDATE</code>, 242 * <code>INSERT</code>, and <code>DELETE</code> statements 243 * <LI>Uses the <code>CachedRowSet</code> object's metadata to 244 * determine the table name, column names, and the columns 245 * that make up the primary key 246 * </UL> 247 * <LI>When there is no conflict, propagates changes made to the 248 * <code>CachedRowSet</code> object back to its underlying data source 249 * <UL> 250 * <LI>Iterates through each row of the <code>CachedRowSet</code> object 251 * to determine whether it has been updated, inserted, or deleted 252 * <LI>If the corresponding row in the data source has not been changed 253 * since the rowset last read its 254 * values, the writer will use the appropriate command to update, 255 * insert, or delete the row 256 * <LI>If any data in the data source does not match the original values 257 * for the <code>CachedRowSet</code> object, the writer will roll 258 * back any changes it has made to the row in the data source. 259 * </UL> 260 * </OL> 261 * 262 * @return <code>true</code> if changes to the rowset were successfully 263 * written to the rowset's underlying data source; 264 * <code>false</code> otherwise 265 */ 266 public boolean writeData(RowSetInternal caller) throws SQLException { 267 boolean conflict = false; 268 boolean showDel = false; 269 PreparedStatement pstmtIns = null; 270 iChangedValsInDbAndCRS = 0; 271 iChangedValsinDbOnly = 0; 272 273 // We assume caller is a CachedRowSet 274 CachedRowSetImpl crs = (CachedRowSetImpl)caller; 275 // crsResolve = new CachedRowSetImpl(); 276 this.crsResolve = new CachedRowSetImpl();; 277 278 // The reader is registered with the writer at design time. 279 // This is not required, in general. The reader has logic 280 // to get a JDBC connection, so call it. 281 282 con = reader.connect(caller); 283 284 285 if (con == null) { 286 throw new SQLException(resBundle.handleGetObject("crswriter.connect").toString()); 287 } 288 289 /* 290 // Fix 6200646. 291 // Don't change the connection or transaction properties. This will fail in a 292 // J2EE container. 293 if (con.getAutoCommit() == true) { 294 con.setAutoCommit(false); 295 } 296 297 con.setTransactionIsolation(crs.getTransactionIsolation()); 298 */ 299 300 initSQLStatements(crs); 301 int iColCount; 302 303 RowSetMetaDataImpl rsmdWrite = (RowSetMetaDataImpl)crs.getMetaData(); 304 RowSetMetaDataImpl rsmdResolv = new RowSetMetaDataImpl(); 305 306 iColCount = rsmdWrite.getColumnCount(); 307 int sz= crs.size()+1; 308 status = new ArrayList<>(sz); 309 310 status.add(0,null); 311 rsmdResolv.setColumnCount(iColCount); 312 313 for(int i =1; i <= iColCount; i++) { 314 rsmdResolv.setColumnType(i, rsmdWrite.getColumnType(i)); 315 rsmdResolv.setColumnName(i, rsmdWrite.getColumnName(i)); 316 rsmdResolv.setNullable(i, ResultSetMetaData.columnNullableUnknown); 317 } 318 this.crsResolve.setMetaData(rsmdResolv); 319 320 // moved outside the insert inner loop 321 //pstmtIns = con.prepareStatement(insertCmd); 322 323 if (callerColumnCount < 1) { 324 // No data, so return success. 325 if (reader.getCloseConnection() == true) 326 con.close(); 327 return true; 328 } 329 // We need to see rows marked for deletion. 330 showDel = crs.getShowDeleted(); 331 crs.setShowDeleted(true); 332 333 // Look at all the rows. 334 crs.beforeFirst(); 335 336 int rows =1; 337 while (crs.next()) { 338 if (crs.rowDeleted()) { 339 // The row has been deleted. 340 if (conflict = (deleteOriginalRow(crs, this.crsResolve)) == true) { 341 status.add(rows, SyncResolver.DELETE_ROW_CONFLICT); 342 } else { 343 // delete happened without any occurrence of conflicts 344 // so update status accordingly 345 status.add(rows, SyncResolver.NO_ROW_CONFLICT); 346 } 347 348 } else if (crs.rowInserted()) { 349 // The row has been inserted. 350 351 pstmtIns = con.prepareStatement(insertCmd); 352 if ( (conflict = insertNewRow(crs, pstmtIns, this.crsResolve)) == true) { 353 status.add(rows, SyncResolver.INSERT_ROW_CONFLICT); 354 } else { 355 // insert happened without any occurrence of conflicts 356 // so update status accordingly 357 status.add(rows, SyncResolver.NO_ROW_CONFLICT); 358 } 359 } else if (crs.rowUpdated()) { 360 // The row has been updated. 361 if ( conflict = (updateOriginalRow(crs)) == true) { 362 status.add(rows, SyncResolver.UPDATE_ROW_CONFLICT); 363 } else { 364 // update happened without any occurrence of conflicts 365 // so update status accordingly 366 status.add(rows, SyncResolver.NO_ROW_CONFLICT); 367 } 368 369 } else { 370 /** The row is neither of inserted, updated or deleted. 371 * So set nulls in the this.crsResolve for this row, 372 * as nothing is to be done for such rows. 373 * Also note that if such a row has been changed in database 374 * and we have not changed(inserted, updated or deleted) 375 * that is fine. 376 **/ 377 int icolCount = crs.getMetaData().getColumnCount(); 378 status.add(rows, SyncResolver.NO_ROW_CONFLICT); 379 380 this.crsResolve.moveToInsertRow(); 381 for(int cols=0;cols<iColCount;cols++) { 382 this.crsResolve.updateNull(cols+1); 383 } //end for 384 385 this.crsResolve.insertRow(); 386 this.crsResolve.moveToCurrentRow(); 387 388 } //end if 389 rows++; 390 } //end while 391 392 // close the insert statement 393 if(pstmtIns!=null) 394 pstmtIns.close(); 395 // reset 396 crs.setShowDeleted(showDel); 397 398 boolean boolConf = false; 399 for (int j=1;j<status.size();j++){ 400 // ignore status for index = 0 which is set to null 401 if(! ((status.get(j)).equals(SyncResolver.NO_ROW_CONFLICT))) { 402 // there is at least one conflict which needs to be resolved 403 boolConf = true; 404 break; 405 } 406 } 407 408 crs.beforeFirst(); 409 this.crsResolve.beforeFirst(); 410 411 if(boolConf) { 412 SyncProviderException spe = new SyncProviderException(status.size() - 1+resBundle.handleGetObject("crswriter.conflictsno").toString()); 413 //SyncResolver syncRes = spe.getSyncResolver(); 414 415 SyncResolverImpl syncResImpl = (SyncResolverImpl) spe.getSyncResolver(); 416 417 syncResImpl.setCachedRowSet(crs); 418 syncResImpl.setCachedRowSetResolver(this.crsResolve); 419 420 syncResImpl.setStatus(status); 421 syncResImpl.setCachedRowSetWriter(this); 422 423 throw spe; 424 } else { 425 return true; 426 } 427 /* 428 if (conflict == true) { 429 con.rollback(); 430 return false; 431 } else { 432 con.commit(); 433 if (reader.getCloseConnection() == true) { 434 con.close(); 435 } 436 return true; 437 } 438 */ 439 440 } //end writeData 441 442 /** 443 * Updates the given <code>CachedRowSet</code> object's underlying data 444 * source so that updates to the rowset are reflected in the original 445 * data source, and returns <code>false</code> if the update was successful. 446 * A return value of <code>true</code> indicates that there is a conflict, 447 * meaning that a value updated in the rowset has already been changed by 448 * someone else in the underlying data source. A conflict can also exist 449 * if, for example, more than one row in the data source would be affected 450 * by the update or if no rows would be affected. In any case, if there is 451 * a conflict, this method does not update the underlying data source. 452 * <P> 453 * This method is called internally by the method <code>writeData</code> 454 * if a row in the <code>CachedRowSet</code> object for which this 455 * <code>CachedRowSetWriter</code> object is the writer has been updated. 456 * 457 * @return <code>false</code> if the update to the underlying data source is 458 * successful; <code>true</code> otherwise 459 * @throws SQLException if a database access error occurs 460 */ 461 private boolean updateOriginalRow(CachedRowSet crs) 462 throws SQLException { 463 PreparedStatement pstmt; 464 int i = 0; 465 int idx = 0; 466 467 // Select the row from the database. 468 ResultSet origVals = crs.getOriginalRow(); 469 origVals.next(); 470 471 try { 472 updateWhere = buildWhereClause(updateWhere, origVals); 473 474 475 /** 476 * The following block of code is for checking a particular type of 477 * query where in there is a where clause. Without this block, if a 478 * SQL statement is built the "where" clause will appear twice hence 479 * the DB errors out and a SQLException is thrown. This code also 480 * considers that the where clause is in the right place as the 481 * CachedRowSet object would already have been populated with this 482 * query before coming to this point. 483 **/ 484 485 486 String tempselectCmd = selectCmd.toLowerCase(); 487 488 int idxWhere = tempselectCmd.indexOf("where"); 489 490 if(idxWhere != -1) 491 { 492 String tempSelect = selectCmd.substring(0,idxWhere); 493 selectCmd = tempSelect; 494 } 495 496 pstmt = con.prepareStatement(selectCmd + updateWhere, 497 ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); 498 499 for (i = 0; i < keyCols.length; i++) { 500 if (params[i] != null) { 501 pstmt.setObject(++idx, params[i]); 502 } else { 503 continue; 504 } 505 } 506 507 try { 508 pstmt.setMaxRows(crs.getMaxRows()); 509 pstmt.setMaxFieldSize(crs.getMaxFieldSize()); 510 pstmt.setEscapeProcessing(crs.getEscapeProcessing()); 511 pstmt.setQueryTimeout(crs.getQueryTimeout()); 512 } catch (Exception ex) { 513 // Older driver don't support these operations. 514 } 515 516 ResultSet rs = null; 517 rs = pstmt.executeQuery(); 518 ResultSetMetaData rsmd = rs.getMetaData(); 519 520 if (rs.next()) { 521 if (rs.next()) { 522 /** More than one row conflict. 523 * If rs has only one row we are able to 524 * uniquely identify the row where update 525 * have to happen else if more than one 526 * row implies we cannot uniquely identify the row 527 * where we have to do updates. 528 * crs.setKeyColumns needs to be set to 529 * come out of this situation. 530 */ 531 532 return true; 533 } 534 535 // don't close the rs 536 // we require the record in rs to be used. 537 // rs.close(); 538 // pstmt.close(); 539 rs.first(); 540 541 // how many fields need to be updated 542 int colsNotChanged = 0; 543 Vector<Integer> cols = new Vector<>(); 544 String updateExec = updateCmd; 545 Object orig; 546 Object curr; 547 Object rsval; 548 boolean boolNull = true; 549 Object objVal = null; 550 551 // There's only one row and the cursor 552 // needs to be on that row. 553 554 boolean first = true; 555 boolean flag = true; 556 557 this.crsResolve.moveToInsertRow(); 558 559 for (i = 1; i <= callerColumnCount; i++) { 560 orig = origVals.getObject(i); 561 curr = crs.getObject(i); 562 rsval = rs.getObject(i); 563 /* 564 * the following block creates equivalent objects 565 * that would have been created if this rs is populated 566 * into a CachedRowSet so that comparison of the column values 567 * from the ResultSet and CachedRowSet are possible 568 */ 569 Map<String, Class<?>> map = (crs.getTypeMap() == null)?con.getTypeMap():crs.getTypeMap(); 570 if (rsval instanceof Struct) { 571 572 Struct s = (Struct)rsval; 573 574 // look up the class in the map 575 Class<?> c = null; 576 c = map.get(s.getSQLTypeName()); 577 if (c != null) { 578 // create new instance of the class 579 SQLData obj = null; 580 try { 581 obj = (SQLData)c.newInstance(); 582 } catch (java.lang.InstantiationException ex) { 583 throw new SQLException(MessageFormat.format(resBundle.handleGetObject("cachedrowsetimpl.unableins").toString(), 584 ex.getMessage())); 585 } catch (java.lang.IllegalAccessException ex) { 586 throw new SQLException(MessageFormat.format(resBundle.handleGetObject("cachedrowsetimpl.unableins").toString(), 587 ex.getMessage())); 588 } 589 // get the attributes from the struct 590 Object attribs[] = s.getAttributes(map); 591 // create the SQLInput "stream" 592 SQLInputImpl sqlInput = new SQLInputImpl(attribs, map); 593 // read the values... 594 obj.readSQL(sqlInput, s.getSQLTypeName()); 595 rsval = obj; 596 } 597 } else if (rsval instanceof SQLData) { 598 rsval = new SerialStruct((SQLData)rsval, map); 599 } else if (rsval instanceof Blob) { 600 rsval = new SerialBlob((Blob)rsval); 601 } else if (rsval instanceof Clob) { 602 rsval = new SerialClob((Clob)rsval); 603 } else if (rsval instanceof java.sql.Array) { 604 rsval = new SerialArray((java.sql.Array)rsval, map); 605 } 606 607 // reset boolNull if it had been set 608 boolNull = true; 609 610 /** This addtional checking has been added when the current value 611 * in the DB is null, but the DB had a different value when the 612 * data was actaully fetched into the CachedRowSet. 613 **/ 614 615 if(rsval == null && orig != null) { 616 // value in db has changed 617 // don't proceed with synchronization 618 // get the value in db and pass it to the resolver. 619 620 iChangedValsinDbOnly++; 621 // Set the boolNull to false, 622 // in order to set the actual value; 623 boolNull = false; 624 objVal = rsval; 625 } 626 627 /** Adding the checking for rsval to be "not" null or else 628 * it would through a NullPointerException when the values 629 * are compared. 630 **/ 631 632 else if(rsval != null && (!rsval.equals(orig))) 633 { 634 // value in db has changed 635 // don't proceed with synchronization 636 // get the value in db and pass it to the resolver. 637 638 iChangedValsinDbOnly++; 639 // Set the boolNull to false, 640 // in order to set the actual value; 641 boolNull = false; 642 objVal = rsval; 643 } else if ( (orig == null || curr == null) ) { 644 645 /** Adding the additonal condition of checking for "flag" 646 * boolean variable, which would otherwise result in 647 * building a invalid query, as the comma would not be 648 * added to the query string. 649 **/ 650 651 if (first == false || flag == false) { 652 updateExec += ", "; 653 } 654 updateExec += crs.getMetaData().getColumnName(i); 655 cols.add(i); 656 updateExec += " = ? "; 657 first = false; 658 659 /** Adding the extra condition for orig to be "not" null as the 660 * condition for orig to be null is take prior to this, if this 661 * is not added it will result in a NullPointerException when 662 * the values are compared. 663 **/ 664 665 } else if (orig.equals(curr)) { 666 colsNotChanged++; 667 //nothing to update in this case since values are equal 668 669 /** Adding the extra condition for orig to be "not" null as the 670 * condition for orig to be null is take prior to this, if this 671 * is not added it will result in a NullPointerException when 672 * the values are compared. 673 **/ 674 675 } else if(orig.equals(curr) == false) { 676 // When values from db and values in CachedRowSet are not equal, 677 // if db value is same as before updation for each col in 678 // the row before fetching into CachedRowSet, 679 // only then we go ahead with updation, else we 680 // throw SyncProviderException. 681 682 // if value has changed in db after fetching from db 683 // for some cols of the row and at the same time, some other cols 684 // have changed in CachedRowSet, no synchronization happens 685 686 // Synchronization happens only when data when fetching is 687 // same or at most has changed in cachedrowset 688 689 // check orig value with what is there in crs for a column 690 // before updation in crs. 691 692 if(crs.columnUpdated(i)) { 693 if(rsval.equals(orig)) { 694 // At this point we are sure that 695 // the value updated in crs was from 696 // what is in db now and has not changed 697 if (flag == false || first == false) { 698 updateExec += ", "; 699 } 700 updateExec += crs.getMetaData().getColumnName(i); 701 cols.add(i); 702 updateExec += " = ? "; 703 flag = false; 704 } else { 705 // Here the value has changed in the db after 706 // data was fetched 707 // Plus store this row from CachedRowSet and keep it 708 // in a new CachedRowSet 709 boolNull= false; 710 objVal = rsval; 711 iChangedValsInDbAndCRS++; 712 } 713 } 714 } 715 716 if(!boolNull) { 717 this.crsResolve.updateObject(i,objVal); 718 } else { 719 this.crsResolve.updateNull(i); 720 } 721 } //end for 722 723 rs.close(); 724 pstmt.close(); 725 726 this.crsResolve.insertRow(); 727 this.crsResolve.moveToCurrentRow(); 728 729 /** 730 * if nothing has changed return now - this can happen 731 * if column is updated to the same value. 732 * if colsNotChanged == callerColumnCount implies we are updating 733 * the database with ALL COLUMNS HAVING SAME VALUES, 734 * so skip going to database, else do as usual. 735 **/ 736 if ( (first == false && cols.size() == 0) || 737 colsNotChanged == callerColumnCount ) { 738 return false; 739 } 740 741 if(iChangedValsInDbAndCRS != 0 || iChangedValsinDbOnly != 0) { 742 return true; 743 } 744 745 746 updateExec += updateWhere; 747 748 pstmt = con.prepareStatement(updateExec); 749 750 // Comments needed here 751 for (i = 0; i < cols.size(); i++) { 752 Object obj = crs.getObject(cols.get(i)); 753 if (obj != null) 754 pstmt.setObject(i + 1, obj); 755 else 756 pstmt.setNull(i + 1,crs.getMetaData().getColumnType(i + 1)); 757 } 758 idx = i; 759 760 // Comments needed here 761 for (i = 0; i < keyCols.length; i++) { 762 if (params[i] != null) { 763 pstmt.setObject(++idx, params[i]); 764 } else { 765 continue; 766 } 767 } 768 769 i = pstmt.executeUpdate(); 770 771 /** 772 * i should be equal to 1(row count), because we update 773 * one row(returned as row count) at a time, if all goes well. 774 * if 1 != 1, this implies we have not been able to 775 * do updations properly i.e there is a conflict in database 776 * versus what is in CachedRowSet for this particular row. 777 **/ 778 779 return false; 780 781 } else { 782 /** 783 * Cursor will be here, if the ResultSet may not return even a single row 784 * i.e. we can't find the row where to update because it has been deleted 785 * etc. from the db. 786 * Present the whole row as null to user, to force null to be sync'ed 787 * and hence nothing to be synced. 788 * 789 * NOTE: 790 * ------ 791 * In the database if a column that is mapped to java.sql.Types.REAL stores 792 * a Double value and is compared with value got from ResultSet.getFloat() 793 * no row is retrieved and will throw a SyncProviderException. For details 794 * see bug Id 5053830 795 **/ 796 return true; 797 } 798 } catch (SQLException ex) { 799 ex.printStackTrace(); 800 // if executeUpdate fails it will come here, 801 // update crsResolve with null rows 802 this.crsResolve.moveToInsertRow(); 803 804 for(i = 1; i <= callerColumnCount; i++) { 805 this.crsResolve.updateNull(i); 806 } 807 808 this.crsResolve.insertRow(); 809 this.crsResolve.moveToCurrentRow(); 810 811 return true; 812 } 813 } 814 815 /** 816 * Inserts a row that has been inserted into the given 817 * <code>CachedRowSet</code> object into the data source from which 818 * the rowset is derived, returning <code>false</code> if the insertion 819 * was successful. 820 * 821 * @param crs the <code>CachedRowSet</code> object that has had a row inserted 822 * and to whose underlying data source the row will be inserted 823 * @param pstmt the <code>PreparedStatement</code> object that will be used 824 * to execute the insertion 825 * @return <code>false</code> to indicate that the insertion was successful; 826 * <code>true</code> otherwise 827 * @throws SQLException if a database access error occurs 828 */ 829 private boolean insertNewRow(CachedRowSet crs, 830 PreparedStatement pstmt, CachedRowSetImpl crsRes) throws SQLException { 831 int i = 0; 832 int icolCount = crs.getMetaData().getColumnCount(); 833 834 boolean returnVal = false; 835 PreparedStatement pstmtSel = con.prepareStatement(selectCmd, 836 ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); 837 ResultSet rs, rs2 = null; 838 DatabaseMetaData dbmd = con.getMetaData(); 839 rs = pstmtSel.executeQuery(); 840 String table = crs.getTableName(); 841 rs2 = dbmd.getPrimaryKeys(null, null, table); 842 String [] primaryKeys = new String[icolCount]; 843 int k = 0; 844 while(rs2.next()) { 845 String pkcolname = rs2.getString("COLUMN_NAME"); 846 primaryKeys[k] = pkcolname; 847 k++; 848 } 849 850 if(rs.next()) { 851 for(int j=0;j<primaryKeys.length;j++) { 852 if(primaryKeys[j] != null) { 853 if(crs.getObject(primaryKeys[j]) == null){ 854 break; 855 } 856 String crsPK = (crs.getObject(primaryKeys[j])).toString(); 857 String rsPK = (rs.getObject(primaryKeys[j])).toString(); 858 if(crsPK.equals(rsPK)) { 859 returnVal = true; 860 this.crsResolve.moveToInsertRow(); 861 for(i = 1; i <= icolCount; i++) { 862 String colname = (rs.getMetaData()).getColumnName(i); 863 if(colname.equals(primaryKeys[j])) 864 this.crsResolve.updateObject(i,rsPK); 865 else 866 this.crsResolve.updateNull(i); 867 } 868 this.crsResolve.insertRow(); 869 this.crsResolve.moveToCurrentRow(); 870 } 871 } 872 } 873 } 874 if(returnVal) 875 return returnVal; 876 877 try { 878 for (i = 1; i <= icolCount; i++) { 879 Object obj = crs.getObject(i); 880 if (obj != null) { 881 pstmt.setObject(i, obj); 882 } else { 883 pstmt.setNull(i,crs.getMetaData().getColumnType(i)); 884 } 885 } 886 887 i = pstmt.executeUpdate(); 888 return false; 889 890 } catch (SQLException ex) { 891 /** 892 * Cursor will come here if executeUpdate fails. 893 * There can be many reasons why the insertion failed, 894 * one can be violation of primary key. 895 * Hence we cannot exactly identify why the insertion failed 896 * Present the current row as a null row to the user. 897 **/ 898 this.crsResolve.moveToInsertRow(); 899 900 for(i = 1; i <= icolCount; i++) { 901 this.crsResolve.updateNull(i); 902 } 903 904 this.crsResolve.insertRow(); 905 this.crsResolve.moveToCurrentRow(); 906 907 return true; 908 } 909 } 910 911 /** 912 * Deletes the row in the underlying data source that corresponds to 913 * a row that has been deleted in the given <code> CachedRowSet</code> object 914 * and returns <code>false</code> if the deletion was successful. 915 * <P> 916 * This method is called internally by this writer's <code>writeData</code> 917 * method when a row in the rowset has been deleted. The values in the 918 * deleted row are the same as those that are stored in the original row 919 * of the given <code>CachedRowSet</code> object. If the values in the 920 * original row differ from the row in the underlying data source, the row 921 * in the data source is not deleted, and <code>deleteOriginalRow</code> 922 * returns <code>true</code> to indicate that there was a conflict. 923 * 924 * 925 * @return <code>false</code> if the deletion was successful, which means that 926 * there was no conflict; <code>true</code> otherwise 927 * @throws SQLException if there was a database access error 928 */ 929 private boolean deleteOriginalRow(CachedRowSet crs, CachedRowSetImpl crsRes) throws SQLException { 930 PreparedStatement pstmt; 931 int i; 932 int idx = 0; 933 String strSelect; 934 // Select the row from the database. 935 ResultSet origVals = crs.getOriginalRow(); 936 origVals.next(); 937 938 deleteWhere = buildWhereClause(deleteWhere, origVals); 939 pstmt = con.prepareStatement(selectCmd + deleteWhere, 940 ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); 941 942 for (i = 0; i < keyCols.length; i++) { 943 if (params[i] != null) { 944 pstmt.setObject(++idx, params[i]); 945 } else { 946 continue; 947 } 948 } 949 950 try { 951 pstmt.setMaxRows(crs.getMaxRows()); 952 pstmt.setMaxFieldSize(crs.getMaxFieldSize()); 953 pstmt.setEscapeProcessing(crs.getEscapeProcessing()); 954 pstmt.setQueryTimeout(crs.getQueryTimeout()); 955 } catch (Exception ex) { 956 /* 957 * Older driver don't support these operations... 958 */ 959 ; 960 } 961 962 ResultSet rs = pstmt.executeQuery(); 963 964 if (rs.next() == true) { 965 if (rs.next()) { 966 // more than one row 967 return true; 968 } 969 rs.first(); 970 971 // Now check all the values in rs to be same in 972 // db also before actually going ahead with deleting 973 boolean boolChanged = false; 974 975 crsRes.moveToInsertRow(); 976 977 for (i = 1; i <= crs.getMetaData().getColumnCount(); i++) { 978 979 Object original = origVals.getObject(i); 980 Object changed = rs.getObject(i); 981 982 if(original != null && changed != null ) { 983 if(! (original.toString()).equals(changed.toString()) ) { 984 boolChanged = true; 985 crsRes.updateObject(i,origVals.getObject(i)); 986 } 987 } else { 988 crsRes.updateNull(i); 989 } 990 } 991 992 crsRes.insertRow(); 993 crsRes.moveToCurrentRow(); 994 995 if(boolChanged) { 996 // do not delete as values in db have changed 997 // deletion will not happen for this row from db 998 // exit now returning true. i.e. conflict 999 return true; 1000 } else { 1001 // delete the row. 1002 // Go ahead with deleting, 1003 // don't do anything here 1004 } 1005 1006 String cmd = deleteCmd + deleteWhere; 1007 pstmt = con.prepareStatement(cmd); 1008 1009 idx = 0; 1010 for (i = 0; i < keyCols.length; i++) { 1011 if (params[i] != null) { 1012 pstmt.setObject(++idx, params[i]); 1013 } else { 1014 continue; 1015 } 1016 } 1017 1018 if (pstmt.executeUpdate() != 1) { 1019 return true; 1020 } 1021 pstmt.close(); 1022 } else { 1023 // didn't find the row 1024 return true; 1025 } 1026 1027 // no conflict 1028 return false; 1029 } 1030 1031 /** 1032 * Sets the reader for this writer to the given reader. 1033 * 1034 * @throws SQLException if a database access error occurs 1035 */ 1036 public void setReader(CachedRowSetReader reader) throws SQLException { 1037 this.reader = reader; 1038 } 1039 1040 /** 1041 * Gets the reader for this writer. 1042 * 1043 * @throws SQLException if a database access error occurs 1044 */ 1045 public CachedRowSetReader getReader() throws SQLException { 1046 return reader; 1047 } 1048 1049 /** 1050 * Composes a <code>SELECT</code>, <code>UPDATE</code>, <code>INSERT</code>, 1051 * and <code>DELETE</code> statement that can be used by this writer to 1052 * write data to the data source backing the given <code>CachedRowSet</code> 1053 * object. 1054 * 1055 * @ param caller a <code>CachedRowSet</code> object for which this 1056 * <code>CachedRowSetWriter</code> object is the writer 1057 * @throws SQLException if a database access error occurs 1058 */ 1059 private void initSQLStatements(CachedRowSet caller) throws SQLException { 1060 1061 int i; 1062 1063 callerMd = caller.getMetaData(); 1064 callerColumnCount = callerMd.getColumnCount(); 1065 if (callerColumnCount < 1) 1066 // No data, so return. 1067 return; 1068 1069 /* 1070 * If the RowSet has a Table name we should use it. 1071 * This is really a hack to get round the fact that 1072 * a lot of the jdbc drivers can't provide the tab. 1073 */ 1074 String table = caller.getTableName(); 1075 if (table == null) { 1076 /* 1077 * attempt to build a table name using the info 1078 * that the driver gave us for the first column 1079 * in the source result set. 1080 */ 1081 table = callerMd.getTableName(1); 1082 if (table == null || table.length() == 0) { 1083 throw new SQLException(resBundle.handleGetObject("crswriter.tname").toString()); 1084 } 1085 } 1086 String catalog = callerMd.getCatalogName(1); 1087 String schema = callerMd.getSchemaName(1); 1088 DatabaseMetaData dbmd = con.getMetaData(); 1089 1090 /* 1091 * Compose a SELECT statement. There are three parts. 1092 */ 1093 1094 // Project List 1095 selectCmd = "SELECT "; 1096 for (i=1; i <= callerColumnCount; i++) { 1097 selectCmd += callerMd.getColumnName(i); 1098 if ( i < callerMd.getColumnCount() ) 1099 selectCmd += ", "; 1100 else 1101 selectCmd += " "; 1102 } 1103 1104 // FROM clause. 1105 selectCmd += "FROM " + buildTableName(dbmd, catalog, schema, table); 1106 1107 /* 1108 * Compose an UPDATE statement. 1109 */ 1110 updateCmd = "UPDATE " + buildTableName(dbmd, catalog, schema, table); 1111 1112 1113 /** 1114 * The following block of code is for checking a particular type of 1115 * query where in there is a where clause. Without this block, if a 1116 * SQL statement is built the "where" clause will appear twice hence 1117 * the DB errors out and a SQLException is thrown. This code also 1118 * considers that the where clause is in the right place as the 1119 * CachedRowSet object would already have been populated with this 1120 * query before coming to this point. 1121 **/ 1122 1123 String tempupdCmd = updateCmd.toLowerCase(); 1124 1125 int idxupWhere = tempupdCmd.indexOf("where"); 1126 1127 if(idxupWhere != -1) 1128 { 1129 updateCmd = updateCmd.substring(0,idxupWhere); 1130 } 1131 updateCmd += "SET "; 1132 1133 /* 1134 * Compose an INSERT statement. 1135 */ 1136 insertCmd = "INSERT INTO " + buildTableName(dbmd, catalog, schema, table); 1137 // Column list 1138 insertCmd += "("; 1139 for (i=1; i <= callerColumnCount; i++) { 1140 insertCmd += callerMd.getColumnName(i); 1141 if ( i < callerMd.getColumnCount() ) 1142 insertCmd += ", "; 1143 else 1144 insertCmd += ") VALUES ("; 1145 } 1146 for (i=1; i <= callerColumnCount; i++) { 1147 insertCmd += "?"; 1148 if (i < callerColumnCount) 1149 insertCmd += ", "; 1150 else 1151 insertCmd += ")"; 1152 } 1153 1154 /* 1155 * Compose a DELETE statement. 1156 */ 1157 deleteCmd = "DELETE FROM " + buildTableName(dbmd, catalog, schema, table); 1158 1159 /* 1160 * set the key desriptors that will be 1161 * needed to construct where clauses. 1162 */ 1163 buildKeyDesc(caller); 1164 } 1165 1166 /** 1167 * Returns a fully qualified table name built from the given catalog and 1168 * table names. The given metadata object is used to get the proper order 1169 * and separator. 1170 * 1171 * @param dbmd a <code>DatabaseMetaData</code> object that contains metadata 1172 * about this writer's <code>CachedRowSet</code> object 1173 * @param catalog a <code>String</code> object with the rowset's catalog 1174 * name 1175 * @param table a <code>String</code> object with the name of the table from 1176 * which this writer's rowset was derived 1177 * @return a <code>String</code> object with the fully qualified name of the 1178 * table from which this writer's rowset was derived 1179 * @throws SQLException if a database access error occurs 1180 */ 1181 private String buildTableName(DatabaseMetaData dbmd, 1182 String catalog, String schema, String table) throws SQLException { 1183 1184 // trim all the leading and trailing whitespaces, 1185 // white spaces can never be catalog, schema or a table name. 1186 1187 String cmd = ""; 1188 1189 catalog = catalog.trim(); 1190 schema = schema.trim(); 1191 table = table.trim(); 1192 1193 if (dbmd.isCatalogAtStart() == true) { 1194 if (catalog != null && catalog.length() > 0) { 1195 cmd += catalog + dbmd.getCatalogSeparator(); 1196 } 1197 if (schema != null && schema.length() > 0) { 1198 cmd += schema + "."; 1199 } 1200 cmd += table; 1201 } else { 1202 if (schema != null && schema.length() > 0) { 1203 cmd += schema + "."; 1204 } 1205 cmd += table; 1206 if (catalog != null && catalog.length() > 0) { 1207 cmd += dbmd.getCatalogSeparator() + catalog; 1208 } 1209 } 1210 cmd += " "; 1211 return cmd; 1212 } 1213 1214 /** 1215 * Assigns to the given <code>CachedRowSet</code> object's 1216 * <code>params</code> 1217 * field an array whose length equals the number of columns needed 1218 * to uniquely identify a row in the rowset. The array is given 1219 * values by the method <code>buildWhereClause</code>. 1220 * <P> 1221 * If the <code>CachedRowSet</code> object's <code>keyCols</code> 1222 * field has length <code>0</code> or is <code>null</code>, the array 1223 * is set with the column number of every column in the rowset. 1224 * Otherwise, the array in the field <code>keyCols</code> is set with only 1225 * the column numbers of the columns that are required to form a unique 1226 * identifier for a row. 1227 * 1228 * @param crs the <code>CachedRowSet</code> object for which this 1229 * <code>CachedRowSetWriter</code> object is the writer 1230 * 1231 * @throws SQLException if a database access error occurs 1232 */ 1233 private void buildKeyDesc(CachedRowSet crs) throws SQLException { 1234 1235 keyCols = crs.getKeyColumns(); 1236 ResultSetMetaData resultsetmd = crs.getMetaData(); 1237 if (keyCols == null || keyCols.length == 0) { 1238 ArrayList<Integer> listKeys = new ArrayList<Integer>(); 1239 1240 for (int i = 0; i < callerColumnCount; i++ ) { 1241 if(resultsetmd.getColumnType(i+1) != java.sql.Types.CLOB && 1242 resultsetmd.getColumnType(i+1) != java.sql.Types.STRUCT && 1243 resultsetmd.getColumnType(i+1) != java.sql.Types.SQLXML && 1244 resultsetmd.getColumnType(i+1) != java.sql.Types.BLOB && 1245 resultsetmd.getColumnType(i+1) != java.sql.Types.ARRAY && 1246 resultsetmd.getColumnType(i+1) != java.sql.Types.OTHER ) 1247 listKeys.add(i+1); 1248 } 1249 keyCols = new int[listKeys.size()]; 1250 for (int i = 0; i < listKeys.size(); i++ ) 1251 keyCols[i] = listKeys.get(i); 1252 } 1253 params = new Object[keyCols.length]; 1254 } 1255 1256 /** 1257 * Constructs an SQL <code>WHERE</code> clause using the given 1258 * string as a starting point. The resulting clause will contain 1259 * a column name and " = ?" for each key column, that is, each column 1260 * that is needed to form a unique identifier for a row in the rowset. 1261 * This <code>WHERE</code> clause can be added to 1262 * a <code>PreparedStatement</code> object that updates, inserts, or 1263 * deletes a row. 1264 * <P> 1265 * This method uses the given result set to access values in the 1266 * <code>CachedRowSet</code> object that called this writer. These 1267 * values are used to build the array of parameters that will serve as 1268 * replacements for the "?" parameter placeholders in the 1269 * <code>PreparedStatement</code> object that is sent to the 1270 * <code>CachedRowSet</code> object's underlying data source. 1271 * 1272 * @param whereClause a <code>String</code> object that is an empty 1273 * string ("") 1274 * @param rs a <code>ResultSet</code> object that can be used 1275 * to access the <code>CachedRowSet</code> object's data 1276 * @return a <code>WHERE</code> clause of the form "<code>WHERE</code> 1277 * columnName = ? AND columnName = ? AND columnName = ? ..." 1278 * @throws SQLException if a database access error occurs 1279 */ 1280 private String buildWhereClause(String whereClause, 1281 ResultSet rs) throws SQLException { 1282 whereClause = "WHERE "; 1283 1284 for (int i = 0; i < keyCols.length; i++) { 1285 if (i > 0) { 1286 whereClause += "AND "; 1287 } 1288 whereClause += callerMd.getColumnName(keyCols[i]); 1289 params[i] = rs.getObject(keyCols[i]); 1290 if (rs.wasNull() == true) { 1291 whereClause += " IS NULL "; 1292 } else { 1293 whereClause += " = ? "; 1294 } 1295 } 1296 return whereClause; 1297 } 1298 1299 void updateResolvedConflictToDB(CachedRowSet crs, Connection con) throws SQLException { 1300 //String updateExe = ; 1301 PreparedStatement pStmt ; 1302 String strWhere = "WHERE " ; 1303 String strExec =" "; 1304 String strUpdate = "UPDATE "; 1305 int icolCount = crs.getMetaData().getColumnCount(); 1306 int keyColumns[] = crs.getKeyColumns(); 1307 Object param[]; 1308 String strSet=""; 1309 1310 strWhere = buildWhereClause(strWhere, crs); 1311 1312 if (keyColumns == null || keyColumns.length == 0) { 1313 keyColumns = new int[icolCount]; 1314 for (int i = 0; i < keyColumns.length; ) { 1315 keyColumns[i] = ++i; 1316 } 1317 } 1318 param = new Object[keyColumns.length]; 1319 1320 strUpdate = "UPDATE " + buildTableName(con.getMetaData(), 1321 crs.getMetaData().getCatalogName(1), 1322 crs.getMetaData().getSchemaName(1), 1323 crs.getTableName()); 1324 1325 // changed or updated values will become part of 1326 // set clause here 1327 strUpdate += "SET "; 1328 1329 boolean first = true; 1330 1331 for (int i=1; i<=icolCount;i++) { 1332 if (crs.columnUpdated(i)) { 1333 if (first == false) { 1334 strSet += ", "; 1335 } 1336 strSet += crs.getMetaData().getColumnName(i); 1337 strSet += " = ? "; 1338 first = false; 1339 } //end if 1340 } //end for 1341 1342 // keycols will become part of where clause 1343 strUpdate += strSet; 1344 strWhere = "WHERE "; 1345 1346 for (int i = 0; i < keyColumns.length; i++) { 1347 if (i > 0) { 1348 strWhere += "AND "; 1349 } 1350 strWhere += crs.getMetaData().getColumnName(keyColumns[i]); 1351 param[i] = crs.getObject(keyColumns[i]); 1352 if (crs.wasNull() == true) { 1353 strWhere += " IS NULL "; 1354 } else { 1355 strWhere += " = ? "; 1356 } 1357 } 1358 strUpdate += strWhere; 1359 1360 pStmt = con.prepareStatement(strUpdate); 1361 1362 int idx =0; 1363 for (int i = 0; i < icolCount; i++) { 1364 if(crs.columnUpdated(i+1)) { 1365 Object obj = crs.getObject(i+1); 1366 if (obj != null) { 1367 pStmt.setObject(++idx, obj); 1368 } else { 1369 pStmt.setNull(i + 1,crs.getMetaData().getColumnType(i + 1)); 1370 } //end if ..else 1371 } //end if crs.column... 1372 } //end for 1373 1374 // Set the key cols for after WHERE =? clause 1375 for (int i = 0; i < keyColumns.length; i++) { 1376 if (param[i] != null) { 1377 pStmt.setObject(++idx, param[i]); 1378 } 1379 } 1380 1381 int id = pStmt.executeUpdate(); 1382 } 1383 1384 1385 /** 1386 * 1387 */ 1388 public void commit() throws SQLException { 1389 con.commit(); 1390 if (reader.getCloseConnection() == true) { 1391 con.close(); 1392 } 1393 } 1394 1395 public void commit(CachedRowSetImpl crs, boolean updateRowset) throws SQLException { 1396 con.commit(); 1397 if(updateRowset) { 1398 if(crs.getCommand() != null) 1399 crs.execute(con); 1400 } 1401 1402 if (reader.getCloseConnection() == true) { 1403 con.close(); 1404 } 1405 } 1406 1407 /** 1408 * 1409 */ 1410 public void rollback() throws SQLException { 1411 con.rollback(); 1412 if (reader.getCloseConnection() == true) { 1413 con.close(); 1414 } 1415 } 1416 1417 /** 1418 * 1419 */ 1420 public void rollback(Savepoint s) throws SQLException { 1421 con.rollback(s); 1422 if (reader.getCloseConnection() == true) { 1423 con.close(); 1424 } 1425 } 1426 1427 private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { 1428 // Default state initialization happens here 1429 ois.defaultReadObject(); 1430 // Initialization of Res Bundle happens here . 1431 try { 1432 resBundle = JdbcRowSetResourceBundle.getJdbcRowSetResourceBundle(); 1433 } catch(IOException ioe) { 1434 throw new RuntimeException(ioe); 1435 } 1436 1437 } 1438 1439 static final long serialVersionUID =-8506030970299413976L; 1440 }