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