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</code> implementation to
47 * propagate changes back to the data source from which the rowset got its data.
48 * <P>
49 * A <code>CachedRowSetWriter</code> object, called a writer, has the public
50 * method <code>writeData</code> 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</code> 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</code>.
57 * <P>
58 * Typically the <code>SyncFactory</code> manages the <code>RowSetReader</code> and
59 * the <code>RowSetWriter</code> implementations using <code>SyncProvider</code> objects.
60 * Standard JDBC RowSet implementations provide an object instance of this
61 * writer by invoking the <code>SyncProvider.getRowSetWriter()</code> 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</code> 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</code> command that this writer will call
80 * internally. The method <code>initSQLStatements</code> 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</code> command that this writer will call
90 * internally to write data to the rowset's underlying data source.
91 * The method <code>initSQLStatements</code> builds this <code>String</code>
92 * object.
93 *
94 * @serial
95 */
96 private String updateCmd;
97
98 /**
99 * The SQL <code>WHERE</code> clause the writer will use for update
100 * statements in the <code>PreparedStatement</code> object
101 * it sends to the underlying data source.
102 *
103 * @serial
104 */
105 private String updateWhere;
106
107 /**
108 * The SQL <code>DELETE</code> 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</code> clause the writer will use for delete
117 * statements in the <code>PreparedStatement</code> 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</code> command that this writer will internally use
126 * to insert data into the rowset's underlying data source. The method
127 * <code>initSQLStatements</code> 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</code> object
137 * for which this <code>CachedRowSetWriter</code> 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</code> object that this
146 * writer will execute.
147 *
148 * @serial
149 */
150 private Object[] params;
151
152 /**
153 * The <code>CachedRowSetReader</code> object that has been
154 * set as the reader for the <code>CachedRowSet</code> object
155 * for which this <code>CachedRowSetWriter</code> object is the writer.
156 *
157 * @serial
158 */
159 private CachedRowSetReader reader;
160
161 /**
162 * The <code>ResultSetMetaData</code> object that contains information
163 * about the columns in the <code>CachedRowSet</code> object
164 * for which this <code>CachedRowSetWriter</code> object is the writer.
165 *
166 * @serial
167 */
168 private ResultSetMetaData callerMd;
169
170 /**
171 * The number of columns in the <code>CachedRowSet</code> object
172 * for which this <code>CachedRowSetWriter</code> object is the writer.
173 *
174 * @serial
175 */
176 private int callerColumnCount;
177
178 /**
179 * This <code>CachedRowSet<code> 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</code> object
213 * back to its underlying data source and returns <code>true</code>
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</code>.
225 * The <code>writeData</code> 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</code> does, much of which is accomplished
229 * through calls to its own internal methods.
230 * <OL>
231 * <LI>Creates a <code>CachedRowSet</code> object from the given
232 * <code>RowSet</code> 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</code> to initialize new SQL statements
241 * <UL>
242 * <LI>Builds new <code>SELECT</code>, <code>UPDATE</code>,
243 * <code>INSERT</code>, and <code>DELETE</code> statements
244 * <LI>Uses the <code>CachedRowSet</code> 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</code> object back to its underlying data source
250 * <UL>
251 * <LI>Iterates through each row of the <code>CachedRowSet</code> 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</code> 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</code> if changes to the rowset were successfully
264 * written to the rowset's underlying data source;
265 * <code>false</code> 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</code> object's underlying data
439 * source so that updates to the rowset are reflected in the original
440 * data source, and returns <code>false</code> if the update was successful.
441 * A return value of <code>true</code> 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</code>
449 * if a row in the <code>CachedRowSet</code> object for which this
450 * <code>CachedRowSetWriter</code> object is the writer has been updated.
451 *
452 * @return <code>false</code> if the update to the underlying data source is
453 * successful; <code>true</code> 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 obj = (SQLData)ReflectUtil.newInstance(c);
577 } catch (Exception ex) {
578 throw new SQLException("Unable to Instantiate: ", ex);
579 }
580 // get the attributes from the struct
581 Object attribs[] = s.getAttributes(map);
582 // create the SQLInput "stream"
583 SQLInputImpl sqlInput = new SQLInputImpl(attribs, map);
584 // read the values...
585 obj.readSQL(sqlInput, s.getSQLTypeName());
586 rsval = obj;
587 }
588 } else if (rsval instanceof SQLData) {
589 rsval = new SerialStruct((SQLData)rsval, map);
590 } else if (rsval instanceof Blob) {
591 rsval = new SerialBlob((Blob)rsval);
592 } else if (rsval instanceof Clob) {
593 rsval = new SerialClob((Clob)rsval);
594 } else if (rsval instanceof java.sql.Array) {
595 rsval = new SerialArray((java.sql.Array)rsval, map);
596 }
597
598 // reset boolNull if it had been set
599 boolNull = true;
600
601 /** This addtional checking has been added when the current value
602 * in the DB is null, but the DB had a different value when the
603 * data was actaully fetched into the CachedRowSet.
604 **/
605
606 if(rsval == null && orig != null) {
607 // value in db has changed
608 // don't proceed with synchronization
609 // get the value in db and pass it to the resolver.
610
611 iChangedValsinDbOnly++;
612 // Set the boolNull to false,
613 // in order to set the actual value;
614 boolNull = false;
615 objVal = rsval;
616 }
617
618 /** Adding the checking for rsval to be "not" null or else
619 * it would through a NullPointerException when the values
620 * are compared.
621 **/
622
623 else if(rsval != null && (!rsval.equals(orig)))
624 {
625 // value in db has changed
626 // don't proceed with synchronization
627 // get the value in db and pass it to the resolver.
628
629 iChangedValsinDbOnly++;
630 // Set the boolNull to false,
631 // in order to set the actual value;
632 boolNull = false;
633 objVal = rsval;
634 } else if ( (orig == null || curr == null) ) {
635
636 /** Adding the additonal condition of checking for "flag"
637 * boolean variable, which would otherwise result in
638 * building a invalid query, as the comma would not be
639 * added to the query string.
640 **/
641
642 if (first == false || flag == false) {
643 updateExec += ", ";
644 }
645 updateExec += crs.getMetaData().getColumnName(i);
646 cols.add(i);
647 updateExec += " = ? ";
648 first = false;
649
650 /** Adding the extra condition for orig to be "not" null as the
651 * condition for orig to be null is take prior to this, if this
652 * is not added it will result in a NullPointerException when
653 * the values are compared.
654 **/
655
656 } else if (orig.equals(curr)) {
657 colsNotChanged++;
658 //nothing to update in this case since values are equal
659
660 /** Adding the extra condition for orig to be "not" null as the
661 * condition for orig to be null is take prior to this, if this
662 * is not added it will result in a NullPointerException when
663 * the values are compared.
664 **/
665
666 } else if(orig.equals(curr) == false) {
667 // When values from db and values in CachedRowSet are not equal,
668 // if db value is same as before updation for each col in
669 // the row before fetching into CachedRowSet,
670 // only then we go ahead with updation, else we
671 // throw SyncProviderException.
672
673 // if value has changed in db after fetching from db
674 // for some cols of the row and at the same time, some other cols
675 // have changed in CachedRowSet, no synchronization happens
676
677 // Synchronization happens only when data when fetching is
678 // same or at most has changed in cachedrowset
679
680 // check orig value with what is there in crs for a column
681 // before updation in crs.
682
683 if(crs.columnUpdated(i)) {
684 if(rsval.equals(orig)) {
685 // At this point we are sure that
686 // the value updated in crs was from
687 // what is in db now and has not changed
688 if (flag == false || first == false) {
689 updateExec += ", ";
690 }
691 updateExec += crs.getMetaData().getColumnName(i);
692 cols.add(i);
693 updateExec += " = ? ";
694 flag = false;
695 } else {
696 // Here the value has changed in the db after
697 // data was fetched
698 // Plus store this row from CachedRowSet and keep it
699 // in a new CachedRowSet
700 boolNull= false;
701 objVal = rsval;
702 iChangedValsInDbAndCRS++;
703 }
704 }
705 }
706
707 if(!boolNull) {
708 this.crsResolve.updateObject(i,objVal);
709 } else {
710 this.crsResolve.updateNull(i);
711 }
712 } //end for
713
714 rs.close();
715 pstmt.close();
716
717 this.crsResolve.insertRow();
718 this.crsResolve.moveToCurrentRow();
719
720 /**
721 * if nothing has changed return now - this can happen
722 * if column is updated to the same value.
723 * if colsNotChanged == callerColumnCount implies we are updating
724 * the database with ALL COLUMNS HAVING SAME VALUES,
725 * so skip going to database, else do as usual.
726 **/
727 if ( (first == false && cols.size() == 0) ||
728 colsNotChanged == callerColumnCount ) {
729 return false;
730 }
731
732 if(iChangedValsInDbAndCRS != 0 || iChangedValsinDbOnly != 0) {
733 return true;
734 }
735
736
737 updateExec += updateWhere;
738
739 pstmt = con.prepareStatement(updateExec);
740
741 // Comments needed here
742 for (i = 0; i < cols.size(); i++) {
743 Object obj = crs.getObject(cols.get(i));
744 if (obj != null)
745 pstmt.setObject(i + 1, obj);
746 else
747 pstmt.setNull(i + 1,crs.getMetaData().getColumnType(i + 1));
748 }
749 idx = i;
750
751 // Comments needed here
752 for (i = 0; i < keyCols.length; i++) {
753 if (params[i] != null) {
754 pstmt.setObject(++idx, params[i]);
755 } else {
756 continue;
757 }
758 }
759
760 i = pstmt.executeUpdate();
761
762 /**
763 * i should be equal to 1(row count), because we update
764 * one row(returned as row count) at a time, if all goes well.
765 * if 1 != 1, this implies we have not been able to
766 * do updations properly i.e there is a conflict in database
767 * versus what is in CachedRowSet for this particular row.
768 **/
769
770 return false;
771
772 } else {
773 /**
774 * Cursor will be here, if the ResultSet may not return even a single row
775 * i.e. we can't find the row where to update because it has been deleted
776 * etc. from the db.
777 * Present the whole row as null to user, to force null to be sync'ed
778 * and hence nothing to be synced.
779 *
780 * NOTE:
781 * ------
782 * In the database if a column that is mapped to java.sql.Types.REAL stores
783 * a Double value and is compared with value got from ResultSet.getFloat()
784 * no row is retrieved and will throw a SyncProviderException. For details
785 * see bug Id 5053830
786 **/
787 return true;
788 }
789 } catch (SQLException ex) {
790 ex.printStackTrace();
791 // if executeUpdate fails it will come here,
792 // update crsResolve with null rows
793 this.crsResolve.moveToInsertRow();
794
795 for(i = 1; i <= callerColumnCount; i++) {
796 this.crsResolve.updateNull(i);
797 }
798
799 this.crsResolve.insertRow();
800 this.crsResolve.moveToCurrentRow();
801
802 return true;
803 }
804 }
805
806 /**
807 * Inserts a row that has been inserted into the given
808 * <code>CachedRowSet</code> object into the data source from which
809 * the rowset is derived, returning <code>false</code> if the insertion
810 * was successful.
811 *
812 * @param crs the <code>CachedRowSet</code> object that has had a row inserted
813 * and to whose underlying data source the row will be inserted
814 * @param pstmt the <code>PreparedStatement</code> object that will be used
815 * to execute the insertion
816 * @return <code>false</code> to indicate that the insertion was successful;
817 * <code>true</code> otherwise
818 * @throws SQLException if a database access error occurs
819 */
820 private boolean insertNewRow(CachedRowSet crs,
821 PreparedStatement pstmt, CachedRowSetImpl crsRes) throws SQLException {
822
823 boolean returnVal = false;
824
825 try (PreparedStatement pstmtSel = con.prepareStatement(selectCmd,
826 ResultSet.TYPE_SCROLL_SENSITIVE,
827 ResultSet.CONCUR_READ_ONLY);
828 ResultSet rs = pstmtSel.executeQuery();
829 ResultSet rs2 = con.getMetaData().getPrimaryKeys(null, null,
830 crs.getTableName())
831 ) {
832
833 ResultSetMetaData rsmd = crs.getMetaData();
834 int icolCount = rsmd.getColumnCount();
835 String[] primaryKeys = new String[icolCount];
836 int k = 0;
837 while (rs2.next()) {
838 primaryKeys[k] = rs2.getString("COLUMN_NAME");
839 k++;
840 }
841
842 if (rs.next()) {
843 for (String pkName : primaryKeys) {
844 if (!isPKNameValid(pkName, rsmd)) {
845
846 /* We came here as one of the primary keys
847 * of the table is not present in the cached
848 * rowset object, it should be an autoincrement column
849 * and not included while creating CachedRowSet
850 * Object, proceed to check for other primary keys
851 */
852 continue;
853 }
854
855 Object crsPK = crs.getObject(pkName);
856 if (crsPK == null) {
857 /*
858 * It is possible that the PK is null on some databases
859 * and will be filled in at insert time (MySQL for example)
860 */
861 break;
862 }
863
864 String rsPK = rs.getObject(pkName).toString();
865 if (crsPK.toString().equals(rsPK)) {
866 returnVal = true;
867 this.crsResolve.moveToInsertRow();
868 for (int i = 1; i <= icolCount; i++) {
869 String colname = (rs.getMetaData()).getColumnName(i);
870 if (colname.equals(pkName))
871 this.crsResolve.updateObject(i,rsPK);
872 else
873 this.crsResolve.updateNull(i);
874 }
875 this.crsResolve.insertRow();
876 this.crsResolve.moveToCurrentRow();
877 }
878 }
879 }
880
881 if (returnVal) {
882 return returnVal;
883 }
884
885 try {
886 for (int i = 1; i <= icolCount; i++) {
887 Object obj = crs.getObject(i);
888 if (obj != null) {
889 pstmt.setObject(i, obj);
890 } else {
891 pstmt.setNull(i,crs.getMetaData().getColumnType(i));
892 }
893 }
894
895 pstmt.executeUpdate();
896 return false;
897
898 } catch (SQLException ex) {
899 /*
900 * Cursor will come here if executeUpdate fails.
901 * There can be many reasons why the insertion failed,
902 * one can be violation of primary key.
903 * Hence we cannot exactly identify why the insertion failed,
904 * present the current row as a null row to the caller.
905 */
906 this.crsResolve.moveToInsertRow();
907
908 for (int i = 1; i <= icolCount; i++) {
909 this.crsResolve.updateNull(i);
910 }
911
912 this.crsResolve.insertRow();
913 this.crsResolve.moveToCurrentRow();
914
915 return true;
916 }
917 }
918 }
919
920 /**
921 * Deletes the row in the underlying data source that corresponds to
922 * a row that has been deleted in the given <code> CachedRowSet</code> object
923 * and returns <code>false</code> if the deletion was successful.
924 * <P>
925 * This method is called internally by this writer's <code>writeData</code>
926 * method when a row in the rowset has been deleted. The values in the
927 * deleted row are the same as those that are stored in the original row
928 * of the given <code>CachedRowSet</code> object. If the values in the
929 * original row differ from the row in the underlying data source, the row
930 * in the data source is not deleted, and <code>deleteOriginalRow</code>
931 * returns <code>true</code> to indicate that there was a conflict.
932 *
933 *
934 * @return <code>false</code> if the deletion was successful, which means that
935 * there was no conflict; <code>true</code> otherwise
936 * @throws SQLException if there was a database access error
937 */
938 private boolean deleteOriginalRow(CachedRowSet crs, CachedRowSetImpl crsRes) throws SQLException {
939 PreparedStatement pstmt;
940 int i;
941 int idx = 0;
942 String strSelect;
943 // Select the row from the database.
944 ResultSet origVals = crs.getOriginalRow();
945 origVals.next();
946
947 deleteWhere = buildWhereClause(deleteWhere, origVals);
948 pstmt = con.prepareStatement(selectCmd + deleteWhere,
949 ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
950
951 for (i = 0; i < keyCols.length; i++) {
952 if (params[i] != null) {
953 pstmt.setObject(++idx, params[i]);
954 } else {
955 continue;
956 }
957 }
958
959 try {
960 pstmt.setMaxRows(crs.getMaxRows());
961 pstmt.setMaxFieldSize(crs.getMaxFieldSize());
962 pstmt.setEscapeProcessing(crs.getEscapeProcessing());
963 pstmt.setQueryTimeout(crs.getQueryTimeout());
964 } catch (Exception ex) {
965 /*
966 * Older driver don't support these operations...
967 */
968 ;
969 }
970
971 ResultSet rs = pstmt.executeQuery();
972
973 if (rs.next() == true) {
974 if (rs.next()) {
975 // more than one row
976 return true;
977 }
978 rs.first();
979
980 // Now check all the values in rs to be same in
981 // db also before actually going ahead with deleting
982 boolean boolChanged = false;
983
984 crsRes.moveToInsertRow();
985
986 for (i = 1; i <= crs.getMetaData().getColumnCount(); i++) {
987
988 Object original = origVals.getObject(i);
989 Object changed = rs.getObject(i);
990
991 if(original != null && changed != null ) {
992 if(! (original.toString()).equals(changed.toString()) ) {
993 boolChanged = true;
994 crsRes.updateObject(i,origVals.getObject(i));
995 }
996 } else {
997 crsRes.updateNull(i);
998 }
999 }
1000
1001 crsRes.insertRow();
1002 crsRes.moveToCurrentRow();
1003
1004 if(boolChanged) {
1005 // do not delete as values in db have changed
1006 // deletion will not happen for this row from db
1007 // exit now returning true. i.e. conflict
1008 return true;
1009 } else {
1010 // delete the row.
1011 // Go ahead with deleting,
1012 // don't do anything here
1013 }
1014
1015 String cmd = deleteCmd + deleteWhere;
1016 pstmt = con.prepareStatement(cmd);
1017
1018 idx = 0;
1019 for (i = 0; i < keyCols.length; i++) {
1020 if (params[i] != null) {
1021 pstmt.setObject(++idx, params[i]);
1022 } else {
1023 continue;
1024 }
1025 }
1026
1027 if (pstmt.executeUpdate() != 1) {
1028 return true;
1029 }
1030 pstmt.close();
1031 } else {
1032 // didn't find the row
1033 return true;
1034 }
1035
1036 // no conflict
1037 return false;
1038 }
1039
1040 /**
1041 * Sets the reader for this writer to the given reader.
1042 *
1043 * @throws SQLException if a database access error occurs
1044 */
1045 public void setReader(CachedRowSetReader reader) throws SQLException {
1046 this.reader = reader;
1047 }
1048
1049 /**
1050 * Gets the reader for this writer.
1051 *
1052 * @throws SQLException if a database access error occurs
1053 */
1054 public CachedRowSetReader getReader() throws SQLException {
1055 return reader;
1056 }
1057
1058 /**
1059 * Composes a <code>SELECT</code>, <code>UPDATE</code>, <code>INSERT</code>,
1060 * and <code>DELETE</code> statement that can be used by this writer to
1061 * write data to the data source backing the given <code>CachedRowSet</code>
1062 * object.
1063 *
1064 * @ param caller a <code>CachedRowSet</code> object for which this
1065 * <code>CachedRowSetWriter</code> object is the writer
1066 * @throws SQLException if a database access error occurs
1067 */
1068 private void initSQLStatements(CachedRowSet caller) throws SQLException {
1069
1070 int i;
1071
1072 callerMd = caller.getMetaData();
1073 callerColumnCount = callerMd.getColumnCount();
1074 if (callerColumnCount < 1)
1075 // No data, so return.
1076 return;
1077
1078 /*
1079 * If the RowSet has a Table name we should use it.
1080 * This is really a hack to get round the fact that
1081 * a lot of the jdbc drivers can't provide the tab.
1082 */
1083 String table = caller.getTableName();
1084 if (table == null) {
1085 /*
1086 * attempt to build a table name using the info
1087 * that the driver gave us for the first column
1088 * in the source result set.
1089 */
1090 table = callerMd.getTableName(1);
1091 if (table == null || table.length() == 0) {
1092 throw new SQLException(resBundle.handleGetObject("crswriter.tname").toString());
1093 }
1094 }
1095 String catalog = callerMd.getCatalogName(1);
1096 String schema = callerMd.getSchemaName(1);
1097 DatabaseMetaData dbmd = con.getMetaData();
1098
1099 /*
1100 * Compose a SELECT statement. There are three parts.
1101 */
1102
1103 // Project List
1104 selectCmd = "SELECT ";
1105 for (i=1; i <= callerColumnCount; i++) {
1106 selectCmd += callerMd.getColumnName(i);
1107 if ( i < callerMd.getColumnCount() )
1108 selectCmd += ", ";
1109 else
1110 selectCmd += " ";
1111 }
1112
1113 // FROM clause.
1114 selectCmd += "FROM " + buildTableName(dbmd, catalog, schema, table);
1115
1116 /*
1117 * Compose an UPDATE statement.
1118 */
1119 updateCmd = "UPDATE " + buildTableName(dbmd, catalog, schema, table);
1120
1121
1122 /**
1123 * The following block of code is for checking a particular type of
1124 * query where in there is a where clause. Without this block, if a
1125 * SQL statement is built the "where" clause will appear twice hence
1126 * the DB errors out and a SQLException is thrown. This code also
1127 * considers that the where clause is in the right place as the
1128 * CachedRowSet object would already have been populated with this
1129 * query before coming to this point.
1130 **/
1131
1132 String tempupdCmd = updateCmd.toLowerCase();
1133
1134 int idxupWhere = tempupdCmd.indexOf("where");
1135
1136 if(idxupWhere != -1)
1137 {
1138 updateCmd = updateCmd.substring(0,idxupWhere);
1139 }
1140 updateCmd += "SET ";
1141
1142 /*
1143 * Compose an INSERT statement.
1144 */
1145 insertCmd = "INSERT INTO " + buildTableName(dbmd, catalog, schema, table);
1146 // Column list
1147 insertCmd += "(";
1148 for (i=1; i <= callerColumnCount; i++) {
1149 insertCmd += callerMd.getColumnName(i);
1150 if ( i < callerMd.getColumnCount() )
1151 insertCmd += ", ";
1152 else
1153 insertCmd += ") VALUES (";
1154 }
1155 for (i=1; i <= callerColumnCount; i++) {
1156 insertCmd += "?";
1157 if (i < callerColumnCount)
1158 insertCmd += ", ";
1159 else
1160 insertCmd += ")";
1161 }
1162
1163 /*
1164 * Compose a DELETE statement.
1165 */
1166 deleteCmd = "DELETE FROM " + buildTableName(dbmd, catalog, schema, table);
1167
1168 /*
1169 * set the key desriptors that will be
1170 * needed to construct where clauses.
1171 */
1172 buildKeyDesc(caller);
1173 }
1174
1175 /**
1176 * Returns a fully qualified table name built from the given catalog and
1177 * table names. The given metadata object is used to get the proper order
1178 * and separator.
1179 *
1180 * @param dbmd a <code>DatabaseMetaData</code> object that contains metadata
1181 * about this writer's <code>CachedRowSet</code> object
1182 * @param catalog a <code>String</code> object with the rowset's catalog
1183 * name
1184 * @param table a <code>String</code> object with the name of the table from
1185 * which this writer's rowset was derived
1186 * @return a <code>String</code> object with the fully qualified name of the
1187 * table from which this writer's rowset was derived
1188 * @throws SQLException if a database access error occurs
1189 */
1190 private String buildTableName(DatabaseMetaData dbmd,
1191 String catalog, String schema, String table) throws SQLException {
1192
1193 // trim all the leading and trailing whitespaces,
1194 // white spaces can never be catalog, schema or a table name.
1195
1196 String cmd = "";
1197
1198 catalog = catalog.trim();
1199 schema = schema.trim();
1200 table = table.trim();
1201
1202 if (dbmd.isCatalogAtStart() == true) {
1203 if (catalog != null && catalog.length() > 0) {
1204 cmd += catalog + dbmd.getCatalogSeparator();
1205 }
1206 if (schema != null && schema.length() > 0) {
1207 cmd += schema + ".";
1208 }
1209 cmd += table;
1210 } else {
1211 if (schema != null && schema.length() > 0) {
1212 cmd += schema + ".";
1213 }
1214 cmd += table;
1215 if (catalog != null && catalog.length() > 0) {
1216 cmd += dbmd.getCatalogSeparator() + catalog;
1217 }
1218 }
1219 cmd += " ";
1220 return cmd;
1221 }
1222
1223 /**
1224 * Assigns to the given <code>CachedRowSet</code> object's
1225 * <code>params</code>
1226 * field an array whose length equals the number of columns needed
1227 * to uniquely identify a row in the rowset. The array is given
1228 * values by the method <code>buildWhereClause</code>.
1229 * <P>
1230 * If the <code>CachedRowSet</code> object's <code>keyCols</code>
1231 * field has length <code>0</code> or is <code>null</code>, the array
1232 * is set with the column number of every column in the rowset.
1233 * Otherwise, the array in the field <code>keyCols</code> is set with only
1234 * the column numbers of the columns that are required to form a unique
1235 * identifier for a row.
1236 *
1237 * @param crs the <code>CachedRowSet</code> object for which this
1238 * <code>CachedRowSetWriter</code> object is the writer
1239 *
1240 * @throws SQLException if a database access error occurs
1241 */
1242 private void buildKeyDesc(CachedRowSet crs) throws SQLException {
1243
1244 keyCols = crs.getKeyColumns();
1245 ResultSetMetaData resultsetmd = crs.getMetaData();
1246 if (keyCols == null || keyCols.length == 0) {
1247 ArrayList<Integer> listKeys = new ArrayList<Integer>();
1248
1249 for (int i = 0; i < callerColumnCount; i++ ) {
1250 if(resultsetmd.getColumnType(i+1) != java.sql.Types.CLOB &&
1251 resultsetmd.getColumnType(i+1) != java.sql.Types.STRUCT &&
1252 resultsetmd.getColumnType(i+1) != java.sql.Types.SQLXML &&
1253 resultsetmd.getColumnType(i+1) != java.sql.Types.BLOB &&
1254 resultsetmd.getColumnType(i+1) != java.sql.Types.ARRAY &&
1255 resultsetmd.getColumnType(i+1) != java.sql.Types.OTHER )
1256 listKeys.add(i+1);
1257 }
1258 keyCols = new int[listKeys.size()];
1259 for (int i = 0; i < listKeys.size(); i++ )
1260 keyCols[i] = listKeys.get(i);
1261 }
1262 params = new Object[keyCols.length];
1263 }
1264
1265 /**
1266 * Constructs an SQL <code>WHERE</code> clause using the given
1267 * string as a starting point. The resulting clause will contain
1268 * a column name and " = ?" for each key column, that is, each column
1269 * that is needed to form a unique identifier for a row in the rowset.
1270 * This <code>WHERE</code> clause can be added to
1271 * a <code>PreparedStatement</code> object that updates, inserts, or
1272 * deletes a row.
1273 * <P>
1274 * This method uses the given result set to access values in the
1275 * <code>CachedRowSet</code> object that called this writer. These
1276 * values are used to build the array of parameters that will serve as
1277 * replacements for the "?" parameter placeholders in the
1278 * <code>PreparedStatement</code> object that is sent to the
1279 * <code>CachedRowSet</code> object's underlying data source.
1280 *
1281 * @param whereClause a <code>String</code> object that is an empty
1282 * string ("")
1283 * @param rs a <code>ResultSet</code> object that can be used
1284 * to access the <code>CachedRowSet</code> object's data
1285 * @return a <code>WHERE</code> clause of the form "<code>WHERE</code>
1286 * columnName = ? AND columnName = ? AND columnName = ? ..."
1287 * @throws SQLException if a database access error occurs
1288 */
1289 private String buildWhereClause(String whereClause,
1290 ResultSet rs) throws SQLException {
1291 whereClause = "WHERE ";
1292
1293 for (int i = 0; i < keyCols.length; i++) {
1294 if (i > 0) {
1295 whereClause += "AND ";
1296 }
1297 whereClause += callerMd.getColumnName(keyCols[i]);
1298 params[i] = rs.getObject(keyCols[i]);
1299 if (rs.wasNull() == true) {
1300 whereClause += " IS NULL ";
1301 } else {
1302 whereClause += " = ? ";
1303 }
1304 }
1305 return whereClause;
1306 }
1307
1308 void updateResolvedConflictToDB(CachedRowSet crs, Connection con) throws SQLException {
1309 //String updateExe = ;
1310 PreparedStatement pStmt ;
1311 String strWhere = "WHERE " ;
1312 String strExec =" ";
1313 String strUpdate = "UPDATE ";
1314 int icolCount = crs.getMetaData().getColumnCount();
1315 int keyColumns[] = crs.getKeyColumns();
1316 Object param[];
1317 String strSet="";
1318
1319 strWhere = buildWhereClause(strWhere, crs);
1320
1321 if (keyColumns == null || keyColumns.length == 0) {
1322 keyColumns = new int[icolCount];
1323 for (int i = 0; i < keyColumns.length; ) {
1324 keyColumns[i] = ++i;
1325 }
1326 }
1327 param = new Object[keyColumns.length];
1328
1329 strUpdate = "UPDATE " + buildTableName(con.getMetaData(),
1330 crs.getMetaData().getCatalogName(1),
1331 crs.getMetaData().getSchemaName(1),
1332 crs.getTableName());
1333
1334 // changed or updated values will become part of
1335 // set clause here
1336 strUpdate += "SET ";
1337
1338 boolean first = true;
1339
1340 for (int i=1; i<=icolCount;i++) {
1341 if (crs.columnUpdated(i)) {
1342 if (first == false) {
1343 strSet += ", ";
1344 }
1345 strSet += crs.getMetaData().getColumnName(i);
1346 strSet += " = ? ";
1347 first = false;
1348 } //end if
1349 } //end for
1350
1351 // keycols will become part of where clause
1352 strUpdate += strSet;
1353 strWhere = "WHERE ";
1354
1355 for (int i = 0; i < keyColumns.length; i++) {
1356 if (i > 0) {
1357 strWhere += "AND ";
1358 }
1359 strWhere += crs.getMetaData().getColumnName(keyColumns[i]);
1360 param[i] = crs.getObject(keyColumns[i]);
1361 if (crs.wasNull() == true) {
1362 strWhere += " IS NULL ";
1363 } else {
1364 strWhere += " = ? ";
1365 }
1366 }
1367 strUpdate += strWhere;
1368
1369 pStmt = con.prepareStatement(strUpdate);
1370
1371 int idx =0;
1372 for (int i = 0; i < icolCount; i++) {
1373 if(crs.columnUpdated(i+1)) {
1374 Object obj = crs.getObject(i+1);
1375 if (obj != null) {
1376 pStmt.setObject(++idx, obj);
1377 } else {
1378 pStmt.setNull(i + 1,crs.getMetaData().getColumnType(i + 1));
1379 } //end if ..else
1380 } //end if crs.column...
1381 } //end for
1382
1383 // Set the key cols for after WHERE =? clause
1384 for (int i = 0; i < keyColumns.length; i++) {
1385 if (param[i] != null) {
1386 pStmt.setObject(++idx, param[i]);
1387 }
1388 }
1389
1390 int id = pStmt.executeUpdate();
1391 }
1392
1393
1394 /**
1395 *
1396 */
1397 public void commit() throws SQLException {
1398 con.commit();
1399 if (reader.getCloseConnection() == true) {
1400 con.close();
1401 }
1402 }
1403
1404 public void commit(CachedRowSetImpl crs, boolean updateRowset) throws SQLException {
1405 con.commit();
1406 if(updateRowset) {
1407 if(crs.getCommand() != null)
1408 crs.execute(con);
1409 }
1410
1411 if (reader.getCloseConnection() == true) {
1412 con.close();
1413 }
1414 }
1415
1416 /**
1417 *
1418 */
1419 public void rollback() throws SQLException {
1420 con.rollback();
1421 if (reader.getCloseConnection() == true) {
1422 con.close();
1423 }
1424 }
1425
1426 /**
1427 *
1428 */
1429 public void rollback(Savepoint s) throws SQLException {
1430 con.rollback(s);
1431 if (reader.getCloseConnection() == true) {
1432 con.close();
1433 }
1434 }
1435
1436 private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1437 // Default state initialization happens here
1438 ois.defaultReadObject();
1439 // Initialization of Res Bundle happens here .
1440 try {
1441 resBundle = JdbcRowSetResourceBundle.getJdbcRowSetResourceBundle();
1442 } catch(IOException ioe) {
1443 throw new RuntimeException(ioe);
1444 }
1445
1446 }
1447
1448 static final long serialVersionUID =-8506030970299413976L;
1449
1450 /**
1451 * Validate whether the Primary Key is known to the CachedRowSet. If it is
1452 * not, it is an auto-generated key
1453 * @param pk - Primary Key to validate
1454 * @param rsmd - ResultSetMetadata for the RowSet
1455 * @return true if found, false otherwise (auto generated key)
1456 */
1457 private boolean isPKNameValid(String pk, ResultSetMetaData rsmd) throws SQLException {
1458 boolean isValid = false;
1459 int cols = rsmd.getColumnCount();
1460 for(int i = 1; i<= cols; i++) {
1461 String colName = rsmd.getColumnClassName(i);
1462 if(colName.equalsIgnoreCase(pk)) {
1463 isValid = true;
1464 break;
1465 }
1466 }
1467
1468 return isValid;
1469 }
1470 }
--- EOF ---