KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > continuent > sequoia > controller > backend > result > ControllerResultSet


1 /**
2  * Sequoia: Database clustering technology.
3  * Copyright (C) 2002-2004 French National Institute For Research In Computer
4  * Science And Control (INRIA).
5  * Copyright (C) 2005 AmicoSoft, Inc. dba Emic Networks
6  * Contact: sequoia@continuent.org
7  *
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  *
20  * Initial developer(s): Emmanuel Cecchet.
21  * Contributor(s): Diego Malpica.
22  */

23
24 package org.continuent.sequoia.controller.backend.result;
25
26 import java.io.IOException JavaDoc;
27 import java.io.Serializable JavaDoc;
28 import java.sql.ResultSet JavaDoc;
29 import java.sql.SQLException JavaDoc;
30 import java.sql.SQLWarning JavaDoc;
31 import java.sql.Statement JavaDoc;
32 import java.util.ArrayList JavaDoc;
33 import java.util.Iterator JavaDoc;
34
35 import org.continuent.sequoia.common.exceptions.NotImplementedException;
36 import org.continuent.sequoia.common.exceptions.driver.protocol.BackendDriverException;
37 import org.continuent.sequoia.common.protocol.Field;
38 import org.continuent.sequoia.common.protocol.SQLDataSerialization;
39 import org.continuent.sequoia.common.protocol.TypeTag;
40 import org.continuent.sequoia.common.stream.DriverBufferedOutputStream;
41 import org.continuent.sequoia.controller.cache.metadata.MetadataCache;
42 import org.continuent.sequoia.controller.core.ControllerConstants;
43 import org.continuent.sequoia.controller.requests.AbstractRequest;
44
45 /**
46  * A <code>ControllerResultSet</code> is a lightweight ResultSet for the
47  * controller side. It only contains row data and column metadata. The real
48  * ResultSet is constructed on by the driver (DriverResultSet object) on the
49  * client side from the ControllerResultSet information.
50  *
51  * @author <a HREF="mailto:Emmanuel.Cecchet@inria.fr">Emmanuel Cecchet </a>
52  * @version 1.0
53  */

54 public class ControllerResultSet extends AbstractResult implements Serializable JavaDoc
55 {
56   private static final long serialVersionUID = 4109773059200535129L;
57
58   /** The results */
59   private ArrayList JavaDoc data = null;
60   /** The fields */
61   private Field[] fields = null;
62   /** Cursor name for this ResultSet (not used yet) */
63   private String JavaDoc cursorName = null;
64   /** Fetch size if we need to fetch only a subset of the ResultSet */
65   private int fetchSize = 0;
66   /** Backend ResultSet. We need to hold a ref to it when streaming. */
67   private transient ResultSet JavaDoc dbResultSet = null;
68   /**
69    * Temporary reference to Statement when we are built from a dbResultSet.
70    * Maybe this should be a local variable.
71    */

72   private transient Statement JavaDoc owningStatement = null;
73   /** True if the underlying database ResultSet is closed */
74   private boolean dbResultSetClosed = true;
75   /** True if there is still more data to fetch from dbResultSet */
76   private boolean hasMoreData = false;
77   /** Maximum number of rows remaining to fetch */
78   private int maxRows = 0;
79   /** Pointers to column-specific de/serializer */
80   private SQLDataSerialization.Serializer[] serializers;
81   /** SQL Warning attached to this resultset */
82   private SQLWarning JavaDoc warnings = null;
83
84   /**
85    * Build a Sequoia ResultSet from a database specific ResultSet. The metadata
86    * can be retrieved from the MetadataCache if provided. If a metadata cache is
87    * provided but the data is not in the cache, the MetadataCache is updated
88    * accordingly. The remaining code is a straightforward copy of both metadata
89    * and data. Used for actual queries, as opposed to metadata ResultSets.
90    * <p>
91    * The statement used to execute the query will be closed when the ResultSet
92    * has been completely copied or when the ResultSet is closed while in
93    * streaming mode.
94    *
95    * @param request Request to which this ResultSet belongs
96    * @param rs The database specific ResultSet
97    * @param metadataCache MetadataCache (null if none)
98    * @param s Statement used to get rs
99    * @param isPartOfMultipleResults true if this ResultSet is part of a call to
100    * a query that returns multiple ResultSets
101    * @throws SQLException if an error occurs
102    */

103   public ControllerResultSet(AbstractRequest request, java.sql.ResultSet JavaDoc rs,
104       MetadataCache metadataCache, Statement JavaDoc s, boolean isPartOfMultipleResults)
105       throws SQLException JavaDoc
106   {
107     this.owningStatement = s;
108     try
109     {
110       if (rs == null)
111         throw new SQLException JavaDoc(
112             "Cannot build a ControllerResultSet with a null java.sql.ResultSet");
113
114       // This is already a result coming from another controller.
115
// if (rs instanceof org.continuent.sequoia.driver.ResultSet)
116
// return (org.continuent.sequoia.driver.ResultSet) rs;
117

118       // Build the ResultSet metaData
119
if ((metadataCache != null) && !isPartOfMultipleResults)
120         fields = metadataCache.getMetadata(request);
121
122       if (fields == null)
123       { // Metadata Cache miss or part of multiple results
124
// Build the fields from the MetaData
125
java.sql.ResultSetMetaData JavaDoc metaData = rs.getMetaData();
126         fields = ControllerConstants.CONTROLLER_FACTORY
127             .getResultSetMetaDataFactory().copyResultSetMetaData(metaData,
128                 metadataCache);
129         if ((metadataCache != null) && !isPartOfMultipleResults)
130           metadataCache.addMetadata(request, fields);
131       }
132       // Copy the warnings
133
warnings = null;
134       if (request.getRetrieveSQLWarnings())
135       {
136         warnings = rs.getWarnings();
137       }
138
139       // Build the ResultSet data
140
data = new ArrayList JavaDoc();
141       if (rs.next()) // not empty RS
142
{
143         cursorName = request.getCursorName();
144         fetchSize = request.getFetchSize();
145         maxRows = request.getMaxRows();
146         if (maxRows == 0)
147           maxRows = Integer.MAX_VALUE; // Infinite number of rows
148

149         // Note that fetchData updates the data field
150
dbResultSet = rs;
151         fetchData();
152         if (hasMoreData && (cursorName == null))
153           // hashCode() is not guaranteed to be injective in theory,
154
// but returns the address of the object in practice.
155
cursorName = String.valueOf(dbResultSet.hashCode());
156       }
157       else
158       // empty RS
159
{
160         hasMoreData = false;
161         dbResultSet = null;
162         dbResultSetClosed = true;
163         rs.close();
164         if (owningStatement != null)
165         {
166           try
167           {
168             owningStatement.close();
169           }
170           catch (SQLException JavaDoc ignore)
171           {
172           }
173           owningStatement = null;
174         }
175       }
176     }
177     catch (SQLException JavaDoc e)
178     {
179       throw (SQLException JavaDoc) new SQLException JavaDoc(
180           "Error while building Sequoia ResultSet (" + e.getLocalizedMessage()
181               + ")", e.getSQLState(), e.getErrorCode()).initCause(e);
182     }
183   }
184
185   /**
186    * Creates a new <code>ControllerResultSet</code> object from already built
187    * data. Used for ResultSets holding metadata.
188    *
189    * @param fields ResultSet metadata fields
190    * @param data ResultSet data (an ArrayList of Object[] representing row
191    * content)
192    */

193   public ControllerResultSet(Field[] fields, ArrayList JavaDoc data)
194   {
195     if (data == null)
196       throw new IllegalArgumentException JavaDoc(
197           "Cannot build a ControllerResultSet with null data ArrayList");
198
199     this.fields = fields;
200     this.data = data;
201     warnings = null;
202   }
203
204   /**
205    * Closes the database ResultSet to release the resource and garbage collect
206    * data.
207    */

208   public void closeResultSet()
209   {
210     if ((dbResultSet != null) && !dbResultSetClosed)
211     {
212       try
213       {
214         dbResultSet.close();
215       }
216       catch (SQLException JavaDoc ignore)
217       {
218       }
219       dbResultSet = null; // to allow GC to work properly
220

221       // TODO: explain how owningStatement could be not null since we set it to
222
// null at end of constructor ??
223
if (owningStatement != null)
224       {
225         try
226         {
227           owningStatement.close();
228         }
229         catch (SQLException JavaDoc ignore)
230         {
231         }
232         owningStatement = null;
233       }
234     }
235   }
236
237   /**
238    * Sets the fetch size and calls fetchData()
239    *
240    * @param fetchSizeParam the number of rows to fetch
241    * @throws SQLException if an error occurs
242    * @see #fetchData()
243    */

244   public void fetchData(int fetchSizeParam) throws SQLException JavaDoc
245   {
246     this.fetchSize = fetchSizeParam;
247     fetchData();
248     if (!hasMoreData)
249     {
250       if (owningStatement != null)
251       {
252         try
253         {
254           owningStatement.close();
255         }
256         catch (SQLException JavaDoc ignore)
257         {
258         }
259         owningStatement = null;
260       }
261     }
262   }
263
264   /**
265    * Fetch the next rows of data from dbResultSet according to fetchSize and
266    * maxRows parameters. This methods directly updates the data and hasMoreData
267    * fields returned by getData() and hadMoreData() accessors.
268    *
269    * @throws SQLException from the backend or if dbResultSet is closed. Maybe we
270    * should use a different type internally.
271    */

272   public void fetchData() throws SQLException JavaDoc
273   {
274     if (dbResultSet == null)
275       throw new SQLException JavaDoc("Backend ResultSet is closed");
276
277     Object JavaDoc[] row;
278     // We directly update the data field
279

280     // Re-use the existing ArrayList with the same size: more efficient in the
281
// usual case (constant fetchSize)
282
data.clear();
283     int toFetch;
284     if (fetchSize > 0)
285     {
286       toFetch = fetchSize < maxRows ? fetchSize : maxRows;
287       // instead of remembering how much we sent, it's simpler to decrease how
288
// much we still may send.
289
maxRows -= toFetch;
290     }
291     else
292       toFetch = maxRows;
293     int nbColumn = fields.length;
294     Object JavaDoc object;
295     do
296     {
297       row = new Object JavaDoc[nbColumn];
298       for (int i = 0; i < nbColumn; i++)
299       {
300         object = ControllerConstants.CONTROLLER_FACTORY
301             .getBackendObjectConverter().convertResultSetObject(dbResultSet, i,
302                 fields[i]);
303         row[i] = object;
304       }
305       data.add(row);
306       toFetch--;
307       hasMoreData = dbResultSet.next();
308     }
309     while (hasMoreData && (toFetch > 0));
310     if (hasMoreData && (fetchSize > 0) && (maxRows > 0))
311     { // More data to fetch later on
312
maxRows += toFetch;
313       dbResultSetClosed = false;
314     }
315     else
316     {
317       hasMoreData = false;
318       dbResultSet.close();
319       if (owningStatement != null)
320         owningStatement.close();
321       dbResultSet = null;
322       dbResultSetClosed = true;
323     }
324   }
325
326   /**
327    * Get the name of the SQL cursor used by this ResultSet
328    *
329    * @return the ResultSet's SQL cursor name.
330    */

331   public String JavaDoc getCursorName()
332   {
333     return cursorName;
334   }
335
336   /**
337    * Returns the data value.
338    *
339    * @return Returns the data.
340    */

341   public ArrayList JavaDoc getData()
342   {
343     return data;
344   }
345
346   /**
347    * Returns the fields value.
348    *
349    * @return Returns the fields.
350    */

351   public Field[] getFields()
352   {
353     return fields;
354   }
355
356   /**
357    * Returns the hasMoreData value.
358    *
359    * @return Returns the hasMoreData.
360    */

361   public boolean hasMoreData()
362   {
363     return hasMoreData;
364   }
365
366   //
367
// Serialization
368
//
369

370   /**
371    * Serialize the <code>DriverResultSet</code> on the output stream by
372    * sending only the needed parameters to reconstruct it on the driver. Caller
373    * MUST have called #initSerializers() before. MUST mirror the following
374    * deserialization method:
375    * {@link org.continuent.sequoia.driver.DriverResultSet#DriverResultSet(org.continuent.sequoia.driver.Connection)}
376    *
377    * @param output destination stream
378    * @throws IOException if a network error occurs
379    */

380
381   public void sendToStream(
382       org.continuent.sequoia.common.stream.DriverBufferedOutputStream output)
383       throws IOException JavaDoc
384   {
385     // Serialize SQL warning chain first (in case of result streaming, results
386
// must be the last ones to be sent
387
if (warnings != null)
388     {
389       output.writeBoolean(true);
390       new BackendDriverException(warnings).sendToStream(output);
391     }
392     else
393       output.writeBoolean(false);
394
395     int nbOfColumns = fields.length;
396     int nbOfRows = data.size();
397     // serialize columns information
398
output.writeInt(nbOfColumns);
399     for (int f = 0; f < nbOfColumns; f++)
400       this.fields[f].sendToStream(output);
401
402     TypeTag.COL_TYPES.sendToStream(output);
403
404     // This could be just a boolean, see next line. But there is no real need
405
// for change.
406
output.writeInt(nbOfRows);
407
408     // Send Java columns type. We need to do it only once: not for every row!
409
if (nbOfRows > 0)
410     {
411       if (null == this.serializers)
412         throw new IllegalStateException JavaDoc(
413             "Bug: forgot to initialize serializers of a non empty ControllerResultSet");
414
415       for (int col = 0; col < nbOfColumns; col++)
416         serializers[col].getTypeTag().sendToStream(output);
417     }
418
419     // Finally send the actual data
420
sendRowsToStream(output);
421
422     if (this.hasMoreData)
423     { // Send the cursor name for further references
424
output.writeLongUTF(this.cursorName);
425     }
426     output.flush();
427   }
428
429   /**
430    * Initialize serializers based on the analysis of actual Java Objects of the
431    * ResultSet to send (typically issued by backend's driver readObject()
432    * method). MUST be called before #sendToStream()
433    *
434    * @throws NotImplementedException in case we don't know how to serialize
435    * something
436    */

437   public void initSerializers() throws NotImplementedException
438   {
439     /* we don't expect the column types of "this" result set to change */
440     if (this.serializers != null)
441       return;
442
443     if (data.size() == 0)
444       return;
445
446     final int nbOfColumns = fields.length;
447     this.serializers = new SQLDataSerialization.Serializer[nbOfColumns];
448
449     for (int col = 0; col < nbOfColumns; col++)
450     {
451       int rowIdx = -1;
452       while (serializers[col] == null)
453       {
454         rowIdx++;
455
456         // We browsed the whole column and found nothing but NULLs
457
if (rowIdx >= data.size()) // ? || rowIdx > 100)
458
break;
459
460         final Object JavaDoc[] row = (Object JavaDoc[]) data.get(rowIdx);
461         final Object JavaDoc sqlObj = row[col];
462
463         /*
464          * If SQL was NULL, we only have a null reference and can't do much with
465          * it. Move down to next row
466          */

467         if (sqlObj == null)
468           continue;
469
470         try
471         {
472           serializers[col] = SQLDataSerialization.getSerializer(sqlObj);
473         }
474         catch (NotImplementedException nie)
475         {
476           if (sqlObj instanceof Short JavaDoc)
477           {
478             /**
479              * This is a workaround for a bug in (at least) PostgreSQL's driver.
480              * This bug has been only very recently fixed: 8 jun 2005 in version
481              * 1.75 of source file
482              * pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java
483              * http://jdbc.postgresql.org/development/cvs.html.
484              * <p>
485              * It seems this java.lang.Short bug happens with multiple DBMS:
486              * http://archives.postgresql.org/pgsql-jdbc/2005-07/threads.php#00382
487              */

488
489             // FIXME: we should probably convert to Integer sooner in
490
// backendObjectConverter. Or just set a different serializer.
491
// Unfortunately we have not access to any logger at this point.
492
// TODO: append the following SQLwarning() to this resultset
493
// "Buggy backend driver returns a java.lang.Short"
494
// + " for column number " + col + ", converting to Integer"
495
serializers[col] = SQLDataSerialization
496                 .getSerializer(TypeTag.INTEGER);
497
498           } // no known serialization workaround
499
else
500           {
501             NotImplementedException betterNIE = new NotImplementedException(
502                 "Backend driver gave an object of an unsupported java type:"
503                     + sqlObj.getClass().getName() + ", at colum number " + col
504                     + " of name " + fields[col].getFieldName());
505             /**
506              * No need for this, see
507              * {@link SQLDataSerialization#getSerializer(Object)}
508              */

509             // betterNIE.initCause(nie);
510
throw betterNIE;
511           }
512         }
513
514       } // while (serializers[col] == null)
515

516       if (serializers[col] == null) // we found nothing
517
{
518         // TODO: add the following SQLWarning() to this resultset
519
// "The whole column number " + col + " was null"
520

521         /**
522          * The whole column is null. Fall back on the JDBC type provided by
523          * backend's metaData.getColumnType(), hoping it's right. Since we are
524          * sending just nulls, a wrong typing should not do much harm anyway ?
525          *
526          * @see org.continuent.sequoia.controller.virtualdatabase.ControllerResultSet#ControllerResultSet(AbstractRequest,
527          * java.sql.ResultSet, MetadataCache, Statement)
528          */

529         /**
530          * We could (should ?) also use {@link Field#getColumnClassName()}, and
531          * do some reflection instead. This is depending on the behaviour and
532          * quality of the JDBC driver of the backends we want to support.
533          */

534
535         final TypeTag javaObjectType = TypeTag.jdbcToJavaObjectType(fields[col]
536             .getSqlType());
537
538         if (!TypeTag.TYPE_ERROR.equals(javaObjectType))
539
540           serializers[col] = SQLDataSerialization.getSerializer(javaObjectType);
541
542         else
543         {
544           // set the undefined serializer (we promise not to use it)
545
serializers[col] = SQLDataSerialization.getSerializer(null);
546
547           // TODO: add the following SQLWarning() to this resultset
548
// "The whole column number " + col + " was null"
549
// **AND** there was an unknown JDBC type number
550

551           // throw new NotImplementedException(
552
// "Could not guess type of column number " + (col + 1)
553
// + ". Whole column is null and backend provides "
554
// + "unknown JDBC type number: " + fields[col].getSqlType());
555
}
556
557       } // if (serializers[col] == null) we found nothing for this whole column
558

559     } // for (column)
560
}
561
562   /**
563    * Serialize only rows, not any metadata. Useful for streaming. Called by the
564    * controller side. This method MUST mirror the following deserialization
565    * method: {@link org.continuent.sequoia.driver.DriverResultSet#receiveRows()}
566    *
567    * @param output destination stream
568    * @throws IOException on stream error
569    */

570   public void sendRowsToStream(DriverBufferedOutputStream output)
571       throws IOException JavaDoc
572   {
573
574     output.writeInt(data.size());
575
576     boolean[] nulls = new boolean[fields.length];
577
578     Iterator JavaDoc rowsIter = this.data.iterator();
579     while (rowsIter.hasNext())
580     {
581       Object JavaDoc[] row = (Object JavaDoc[]) rowsIter.next();
582       TypeTag.ROW.sendToStream(output);
583
584       // first flag null values
585
for (int col = 0; col < row.length; col++)
586       {
587         if (null == row[col])
588           nulls[col] = true;
589         else
590           nulls[col] = false;
591         // TODO: we should compress this
592
output.writeBoolean(nulls[col]);
593       }
594
595       for (int col = 0; col < row.length; col++)
596         if (!nulls[col]) // send only non-nulls
597
{
598           try
599           {
600             /**
601              * Here we are sure that serializers are initialized because:
602              * <p>
603              * (1) we went through
604              * {@link #sendToStream(DriverBufferedOutputStream)} at least once
605              * before
606              * <p>
607              * (2) and there was a non-zero ResultSet transfered, else we would
608              * not come here again.
609              */

610             serializers[col].sendToStream(row[col], output);
611           }
612           catch (ClassCastException JavaDoc cce1)
613           {
614             ClassCastException JavaDoc cce2 = new ClassCastException JavaDoc("Serializer "
615                 + serializers[col] + " failed on Java object: " + row[col]
616                 + " found in column: " + col + ", because of unexpected type "
617                 + row[col].getClass().getName());
618             cce2.initCause(cce1);
619             throw cce2;
620           }
621         } // if !null
622

623     } // while (rows)
624

625     output.writeBoolean(this.hasMoreData);
626     output.flush();
627   }
628
629 }
Popular Tags