/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.test.client.ctc;
import java.io.Externalizable;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.teiid.common.buffer.TupleBatch;
import org.teiid.core.types.DataTypeManager;
import org.teiid.query.sql.symbol.AliasSymbol;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.query.sql.symbol.SingleElementSymbol;
/**
* This class encapsulates results associated with a query.
* <p>
* Results are conceptually organized as a table of columns and rows, where the columns are the data fields that were specified in
* the query select statement, and the rows are individual records returned from the data set. The data values are arbitrary Java
* objects in each field/record cell.
* <p>
*
* <pre>
*
*
* Record # | Field1 Field2 Field3 ... FieldN
* ----------|---------------------------------------------
* 1 | Value11 Value12 Value13 Value1N
* 2 | Value21 Value22 Value23 Value2N
* : | : : : :
* M | ValueM1 ValueM2 ValueM3 ValueMN
*
*
* </pre>
*
* <p>
* Methods are provided to access data by:
* <p>
* <ul>
* <li>Cell value - specify field identifier and record number</li>
* <li>Field values - specify field identifier</li>
* <li>Record values - specify record number</li>
* <li>Record - specify record number; returns field idents mapped to values</li>
* </ul>
* <p>
* Results can be specified to be sorted based on a user-provided ordering. The ordering is a List of ElementSymbols, which should
* match the identifiers for the results fields. This list will typically be in the order that the parameters were specified in
* the query select statement. If no ordering list is specified, the order is the same as results fields are added to this object.
* <p>
*/
public class QueryResults implements
Externalizable {
/**
* Serialization ID - this must be changed if this class is no longer serialization-compatible with old versions.
*/
static final long serialVersionUID = 5397138282301824378L;
/**
* The fields in the results object: List of String
*/
private List fields;
/**
* The column info for each field: Map of String --> ColumnInfo
*/
private Map columnInfos;
/**
* The set of results. Each result is keyed off the variable identifier that was defined in the query's select clause. This
* field will never be null.
*/
private List records; // Rows of columns: List<List<Object>>
// =========================================================================
// C O N S T R U C T O R S
// =========================================================================
/**
* Construct a default instance of this class.
* <p>
* The number of fields returned by the {@link #getFieldCount}method will be 0 after this constructor has completed. The
* number of records returned by the {@link #getRecordCount}method will be 0 after this constructor has completed.
* <p>
*/
public QueryResults() {
}
/**
* Construct an instance of this class, specifying the order that the elements should be inserted into the map. The number of
* fields returned by the {@link #getFieldCount}method will be the same as the number of <code>fields</code> passed in
* after this constructor has completed. The number of records returned by the {@link #getRecordCount}method will be 0 after
* this constructor has completed.
* <p>
*
* @param fields
* The set of field identifiers that will be in the result set
*/
public QueryResults(List fields) {
this(fields, 0);
}
/**
* Construct an instance of this class, specifying the fields and the number of records that the result set should hold. The
* fields and number of records are used to pre-allocate memory for all the values that are expected to be indested into the
* results set.
* <p>
* The number of records returned by the {@link #getRecordCount}method will be <code>numberOfRecords</code> after this
* constructor has completed. The number of fields returned by the {@link #getFieldCount}will be the same as the size of the
* list of fields passed in after this constructor has completed.
* <p>
*
* @param fields
* The ordered list of variables in select statement
* @param numberOfRecords
* The number of blank records to create; records will all contain <code>null</code> values for all the fields
* @see #addField
*/
public QueryResults(List fields,
int numberOfRecords) {
if (fields != null) {
Iterator fieldIter = fields.iterator();
while (fieldIter.hasNext()) {
ColumnInfo info = (ColumnInfo)fieldIter.next();
addField(info);
}
for (int k = 0; k < numberOfRecords; k++) {
addRecord();
}
}
}
/**
* Construct a QueryResults from a TupleBatch. Take all rows from the QueryBatch and put them into the QueryResults.
*
* @param elements
* List of SingleElementSymbols
* @param tupleBatch
* Batch of rows
*/
public QueryResults(List elements,
TupleBatch tupleBatch) {
// Add fields
List columnInfos = createColumnInfos(elements);
for (int i = 0; i < columnInfos.size(); i++) {
ColumnInfo info = (ColumnInfo)columnInfos.get(i);
addField(info);
}
// Add records in bulk -
this.records = Arrays.asList(tupleBatch.getAllTuples());
}
// =========================================================================
// D A T A A C C E S S M E T H O D S
// =========================================================================
/**
* Returns all the field identifiers. If the parameters in the query select statement have been provided, then the set of
* field identifiers should be a subset of them, and ordered the same.
* <p>
* This method will never return <code>null</code>. The list of identifiers returned is not mutable -- changes made to this
* list will not affect the QueryResults object.
*
* @return The field identifiers
*/
public List getFieldIdents() {
return (fields != null) ? fields : new ArrayList();
}
/**
* Get the column information given the column name.
*
* @param columnName
* The name of the column.
* @return Column information
*/
public ColumnInfo getColumnInfo(String columnName) {
if (columnInfos != null) {
return (ColumnInfo)columnInfos.get(columnName);
}
return null;
}
/**
* Returns the number of fields in the result set.
*
* @return The number of fields
*/
public int getFieldCount() {
return (fields != null) ? fields.size() : 0;
}
/**
* Returns the number of records in the result set.
* <p>
*
* @return The number of records
*/
public int getRecordCount() {
return (records != null) ? records.size() : 0;
}
/**
* Get the value for the specified field and record.
* <p>
* The value returned is not mutable -- changes made to this value will not affect the QueryResults object.
* <p>
* <b>Note that results must be retrieved with the same type of data node identifier that was specified in the select
* statement. </b>
* <p>
*
* @param columnName
* The unique data element identifier for the field
* @param recordNumber
* The record number
* @return The data value at the specified field and record
* @exception IllegalArgumentException
* If field is not in result set
* @exception IndexOutOfBoundsException
* If record is not in result set
*/
public Object getValue(String columnName,
int recordNumber) throws IllegalArgumentException,
IndexOutOfBoundsException {
// This throws an IllegalArgumentException if field not in result set
int columnNumber = getIndexOfField(columnName);
return (records != null) ? ((List)records.get(recordNumber)).get(columnNumber) : null;
}
/**
* Returns the values for the specified record. The values are ordered the same as the field identifiers in the result set,
* which will be the same as the order of the query select parameters if they have been provided.
* <p>
* The list of values returned is not mutable -- changes made to this list will not affect the QueryResults object.
* <p>
*
* @param recordNumber
* The record number
* @return A list containing the field values for the specified record, ordered according to the original select parameters,
* if defined
*/
public List getRecordValues(int recordNumber) throws IndexOutOfBoundsException {
if (records != null) {
return (List)records.get(recordNumber);
}
throw new IndexOutOfBoundsException("Record number " + recordNumber + " is not valid.");
}
/**
* Get the records contained in this result. The records are returned as a list of field values (a list of lists).
*
* @return A list of lists contains the field values for each row.
*/
public List getRecords() {
return records;
}
/**
* Returns true if the specified field is in the result set.
*
* @param field
* Unique identifier for a data element specified in result set
*/
public boolean containsField(String field) {
if (fields != null && field != null) {
Iterator iter = fields.iterator();
while (iter.hasNext()) {
if (((String)iter.next()).equalsIgnoreCase(field)) {
return true;
}
}
}
return false;
}
public List getTypes() {
List typeNames = new ArrayList();
int nFields = getFieldCount();
for (int i = 0; i < nFields; i++) {
String aField = (String)fields.get(i);
typeNames.add(((ColumnInfo)columnInfos.get(aField)).getDataType());
}
return typeNames;
}
// =========================================================================
// D A T A M A N I P U L A T I O N M E T H O D S
// =========================================================================
/**
* Add a new field into this result set. The field will be inserted in the order of the parameters in the select statement if
* those parameters were specified upon construction of the result set; otherwise, the field will be appended to the result
* set.
* <p>
*
* @param info
* The column information.
*/
public void addField(ColumnInfo info) {
// Add to ordered list of fields
if (fields == null) {
fields = new ArrayList();
}
fields.add(info.getName());
// Save column information
if (columnInfos == null) {
columnInfos = new HashMap();
}
columnInfos.put(info.getName(), info);
// Add new field to each record
if (records != null) {
for (int i = 0; i < records.size(); i++) {
List record = (List)records.get(i);
record.add(null);
}
}
}
/**
* Add a set of fields into this result set. The fields will be inserted in the order of the parameters in the select
* statement if those parameters were specified upon construction of the result set; otherwise, the field will be appended to
* the result set.
* <p>
*
* @param fields
* The field identifiers.
*/
public void addFields(Collection fields) {
Iterator idents = fields.iterator();
while (idents.hasNext()) {
ColumnInfo ident = (ColumnInfo)idents.next();
addField(ident);
}
}
/**
* Add a new record for all fields. The record is populated with all null values, which act as placeholders for subsequent
* <code>setValue
* </code> calls.
* <p>
* Before this method is called, the fields must already be defined.
* <p>
*
* @return The updated number of records
*/
public int addRecord() {
// Create a place-holder record
int nField = getFieldCount();
if (nField == 0) {
throw new IllegalArgumentException("Cannot add record; no fields have been defined");
}
// Create a record with all null values, one for each field
List record = new ArrayList(nField);
for (int j = 0; j < nField; j++) {
record.add(null);
}
return addRecord(record);
}
/**
* Add a new record for all fields. The record must contain the same number of values as there are fields.
* <p>
* Before this method is called, the fields must already be defined.
* <p>
*
* @return The updated number of records
*/
public int addRecord(List record) {
if (record == null) {
throw new IllegalArgumentException("Attempt to add null record.");
}
if (record.size() != getFieldCount()) {
throw new IllegalArgumentException("Attempt to add record with " + record.size() + " values when " + getFieldCount() + " fields are defined.");
}
if (records == null) {
records = new ArrayList();
}
records.add(record);
return records.size();
}
/**
* Set the value at a particular record for a field.
* <p>
* The specified field and record must already exist in the data set, or an exception will be thrown. The
* {@link #addField(ColumnInfo)}method can be used to append values or new records for fields.
*
* @param field
* The unique data element identifier for the field
* @param recordNumber
* The record number
* @exception IndexOutOfBoundsException
* If the specified record does not exist
*/
public void setValue(String field,
int recordNumber,
Object value) throws IllegalArgumentException,
IndexOutOfBoundsException {
List record = (List)records.get(recordNumber);
int fieldIndex = getIndexOfField(field);
record.set(fieldIndex, value);
}
// =========================================================================
// H E L P E R M E T H O D S
// =========================================================================
/**
* Returns the index of the specified field is in the result set. An exception is thrown if the field is not in the set.
*
* @param field
* Unique identifier for a data element specified in result set
* @return The index of the field in the set of fields
* @exception IllegalArgumentException
* If field is not in result set
*/
public int getIndexOfField(String field) throws IllegalArgumentException {
int index = -1;
if (fields != null && field != null) {
Iterator iter = fields.iterator();
for (int i = 0; iter.hasNext(); i++) {
if (field.equalsIgnoreCase((String)iter.next())) {
index = i;
break;
}
}
}
if (index == -1) {
throw new IllegalArgumentException("Field with identifier " + field + " is not in result set");
}
return index;
}
/**
* Convert a list of SingleElementSymbols to a List of ColumnInfo objects.
*
* @param symbols
* List of SingleElementSymbols
* @return List of ColumnInfos
*/
public static List createColumnInfos(List symbols) {
List infos = new ArrayList(symbols.size());
Iterator iter = symbols.iterator();
while (iter.hasNext()) {
SingleElementSymbol symbol = (SingleElementSymbol)iter.next();
String name = symbol.getName();
if (symbol instanceof AliasSymbol) {
AliasSymbol alias = (AliasSymbol)symbol;
symbol = alias.getSymbol();
}
if (symbol instanceof ElementSymbol) {
ElementSymbol element = (ElementSymbol)symbol;
GroupSymbol group = element.getGroupSymbol();
Object groupID = null;
if (group != null) {
groupID = group.getMetadataID();
}
infos.add(new ColumnInfo(name, DataTypeManager.getDataTypeName(element.getType()), element.getType(), groupID,
element.getMetadataID()));
} else { // ExpressionSymbol or AggregateSymbol
// Expressions don't map to a single element or group, so don't save that info
infos.add(new ColumnInfo(name, DataTypeManager.getDataTypeName(symbol.getType()), symbol.getType()));
}
}
return infos;
}
// =========================================================================
// O V E R R I D D E N O B J E C T M E T H O D S
// =========================================================================
/** Compares with another result set */
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!this.getClass().isInstance(object)) {
return false;
}
QueryResults other = (QueryResults)object;
// First compare fields
if (!this.getFieldIdents().equals(other.getFieldIdents())) {
return false;
}
List thisRecords = this.getRecords();
List otherRecords = other.getRecords();
if (thisRecords == null) {
if (otherRecords == null) {
return true;
}
return false;
}
if (otherRecords == null) {
return false;
}
return thisRecords.equals(otherRecords);
}
/** Returns a string representation of an instance of this class. */
public String toString() {
StringBuffer buffer = new StringBuffer("Query Results...\n"); //$NON-NLS-1$
buffer.append(printFieldIdentsAndTypes(this.getFieldIdents(), this.columnInfos));
buffer.append("\n"); //$NON-NLS-1$
for (int r = 0; r < this.getRecordCount(); r++) {
buffer.append(r);
buffer.append(": "); //$NON-NLS-1$
List record = this.getRecordValues(r);
for (int c = 0; c < this.getFieldCount(); c++) {
buffer.append(record.get(c));
if (c < this.getFieldCount() - 1) {
buffer.append(", "); //$NON-NLS-1$
}
}
buffer.append("\n"); //$NON-NLS-1$
}
return buffer.toString();
}
private static String printFieldIdentsAndTypes(List fieldIdents,
Map columnInfos) {
StringBuffer buf = new StringBuffer();
Iterator fieldItr = fieldIdents.iterator();
while (fieldItr.hasNext()) {
String aField = (String)fieldItr.next();
if (aField != null) {
buf.append("["); //$NON-NLS-1$
buf.append(aField);
buf.append(" - ["); //$NON-NLS-1$
ColumnInfo colInfo = (ColumnInfo)columnInfos.get(aField);
buf.append(colInfo.getDataType());
buf.append(", "); //$NON-NLS-1$
buf.append(colInfo.getJavaClass());
buf.append("]"); //$NON-NLS-1$
}
buf.append("] "); //$NON-NLS-1$
}
return buf.toString();
}
// =========================================================================
// S E R I A L I Z A T I O N
// =========================================================================
/**
* Implements Externalizable interface to read serialized form
*
* @param s
* Input stream to serialize from
*/
public void readExternal(java.io.ObjectInput s) throws ClassNotFoundException,
IOException {
int numFields = s.readInt();
if (numFields > 0) {
fields = new ArrayList(numFields);
columnInfos = new HashMap();
for (int i = 0; i < numFields; i++) {
String fieldName = s.readUTF();
fields.add(fieldName);
Object colInfo = s.readObject();
columnInfos.put(fieldName, colInfo);
}
}
int numRows = s.readInt();
if (numRows > 0) {
records = new ArrayList(numRows);
for (int row = 0; row < numRows; row++) {
List record = new ArrayList(numFields);
for (int col = 0; col < numFields; col++) {
record.add(s.readObject());
}
records.add(record);
}
}
}
/**
* Implements Externalizable interface to write serialized form
*
* @param s
* Output stream to serialize to
*/
public void writeExternal(java.io.ObjectOutput s) throws IOException {
// Write column names and column information
int numFields = 0;
if (fields == null) {
s.writeInt(0);
} else {
numFields = fields.size();
s.writeInt(numFields);
for (int i = 0; i < numFields; i++) {
String fieldName = (String)fields.get(i);
s.writeUTF(fieldName);
s.writeObject(columnInfos.get(fieldName));
}
}
// Write record data
if (records == null) {
s.writeInt(0);
} else {
int numRows = records.size();
s.writeInt(numRows);
for (int row = 0; row < numRows; row++) {
List record = (List)records.get(row);
for (int col = 0; col < numFields; col++) {
s.writeObject(record.get(col));
}
}
}
}
// =========================================================================
// I N N E R C L A S S E S
// =========================================================================
/**
* Represents all information about a column.
*/
public static class ColumnInfo implements
Serializable {
/**
* Serialization ID - this must be changed if this class is no longer serialization-compatible with old versions.
*/
static final long serialVersionUID = -7131157612965891051L;
private String name;
private String dataType;
private Class javaClass;
private Object groupID; // fully qualified group name
private Object elementID; // short name
public ColumnInfo(String name,
String dataType,
Class javaClass) {
if (name == null) {
throw new IllegalArgumentException("QueryResults column cannot have name==null");
}
this.name = name;
this.dataType = dataType;
this.javaClass = javaClass;
}
public ColumnInfo(String name,
String dataType,
Class javaClass,
Object groupID,
Object elementID) {
this(name, dataType, javaClass);
this.groupID = groupID;
this.elementID = elementID;
}
public String getName() {
return this.name;
}
public String getDataType() {
return this.dataType;
}
public Class getJavaClass() {
return this.javaClass;
}
/**
* May be null
*/
public Object getGroupID() {
return this.groupID;
}
/**
* May be null
*/
public Object getElementID() {
return this.elementID;
}
public String toString() {
StringBuffer str = new StringBuffer("Column["); //$NON-NLS-1$
str.append(this.name);
str.append(", "); //$NON-NLS-1$
str.append(this.dataType);
if (this.groupID != null) {
str.append(", "); //$NON-NLS-1$
str.append(this.groupID);
str.append("."); //$NON-NLS-1$
str.append(this.elementID);
}
str.append("]"); //$NON-NLS-1$
return str.toString();
}
}
} // END CLASS