package tap.formatter;
/*
* This file is part of TAPLibrary.
*
* TAPLibrary 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 3 of the License, or
* (at your option) any later version.
*
* TAPLibrary 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 TAPLibrary. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
* Astronomisches Rechen Institut (ARI)
*/
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Map;
import tap.ServiceConnection;
import tap.TAPException;
import tap.TAPExecutionReport;
import tap.data.DataReadException;
import tap.data.TableIterator;
import tap.error.DefaultTAPErrorWriter;
import tap.metadata.TAPColumn;
import tap.metadata.VotType;
import tap.metadata.VotType.VotDatatype;
import uk.ac.starlink.table.AbstractStarTable;
import uk.ac.starlink.table.ColumnInfo;
import uk.ac.starlink.table.DefaultValueInfo;
import uk.ac.starlink.table.DescribedValue;
import uk.ac.starlink.table.RowSequence;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.votable.DataFormat;
import uk.ac.starlink.votable.VOSerializer;
import uk.ac.starlink.votable.VOTableVersion;
import adql.db.DBColumn;
import adql.db.DBType;
import adql.db.DBType.DBDatatype;
/**
* <p>Format any given query (table) result into VOTable.</p>
*
* <p>
* Format and version of the resulting VOTable can be provided in parameters at the construction time.
* This formatter is using STIL. So all formats and versions managed by STIL are also here.
* Basically, you have the following formats: TABLEDATA, BINARY, BINARY2 (only when using VOTable v1.3) and FITS.
* The versions are: 1.0, 1.1, 1.2 and 1.3.
* </p>
*
* <p>Note: The MIME type is automatically set in function of the given VOTable serialization:</p>
* <ul>
* <li><b>none or unknown</b>: equivalent to BINARY</li>
* <li><b>BINARY</b>: "application/x-votable+xml" = "votable"</li>
* <li><b>BINARY2</b>: "application/x-votable+xml;serialization=BINARY2" = "votable/b2"</li>
* <li><b>TABLEDATA</b>: "application/x-votable+xml;serialization=TABLEDATA" = "votable/td"</li>
* <li><b>FITS</b>: "application/x-votable+xml;serialization=FITS" = "votable/fits"</li>
* </ul>
* <p>It is however possible to change these default values thanks to {@link #setMimeType(String, String)}.</p>
*
* <p>In addition of the INFO elements for QUERY_STATUS="OK" and QUERY_STATUS="OVERFLOW", two additional INFO elements are written:</p>
* <ul>
* <li>PROVIDER = {@link ServiceConnection#getProviderName()} and {@link ServiceConnection#getProviderDescription()}</li>
* <li>QUERY = the ADQL query at the origin of this result.</li>
* </ul>
*
* <p>
* Furthermore, this formatter provides a function to format an error in VOTable: {@link #writeError(String, Map, PrintWriter)}.
* This is useful for TAP which requires to return in VOTable any error that occurs while any operation.
* <i>See {@link DefaultTAPErrorWriter} for more details.</i>
* </p>
*
* @author Grégory Mantelet (CDS;ARI)
* @version 2.1 (11/2015)
*/
public class VOTableFormat implements OutputFormat {
/** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */
protected final ServiceConnection service;
/** Format of the VOTable data part in which data must be formatted. Possible values are: TABLEDATA, BINARY, BINARY2 or FITS. By default, it is set to BINARY. */
protected final DataFormat votFormat;
/** VOTable version in which table data must be formatted. By default, it is set to v13. */
protected final VOTableVersion votVersion;
/** MIME type associated with this format. */
protected String mimeType;
/** Short form of the MIME type associated with this format. */
protected String shortMimeType;
/**
* <p>Creates a VOTable formatter.</p>
*
* <p><i>Note:
* The MIME type is automatically set to "application/x-votable+xml" = "votable".
* It is however possible to change this default value thanks to {@link #setMimeType(String, String)}.
* </i></p>
*
* @param service The service to use (for the log and to have some information about the service (particularly: name, description).
*
* @throws NullPointerException If the given service connection is <code>null</code>.
*/
public VOTableFormat(final ServiceConnection service) throws NullPointerException{
this(service, null, null);
}
/**
* <p>Creates a VOTable formatter.</p>
*
* <i>Note: The MIME type is automatically set in function of the given VOTable serialization:</i>
* <ul>
* <li><i><b>none or unknown</b>: equivalent to BINARY</i></li>
* <li><i><b>BINARY</b>: "application/x-votable+xml" = "votable"</i></li>
* <li><i><b>BINARY2</b>: "application/x-votable+xml;serialization=BINARY2" = "votable/b2"</i></li>
* <li><i><b>TABLEDATA</b>: "application/x-votable+xml;serialization=TABLEDATA" = "votable/td"</i></li>
* <li><i><b>FITS</b>: "application/x-votable+xml;serialization=FITS" = "votable/fits"</i></li>
* </ul>
* <p><i>It is however possible to change these default values thanks to {@link #setMimeType(String, String)}.</i></p>
*
* @param service The service to use (for the log and to have some information about the service (particularly: name, description).
* @param votFormat Serialization of the VOTable data part. (TABLEDATA, BINARY, BINARY2 or FITS).
*
* @throws NullPointerException If the given service connection is <code>null</code>.
*/
public VOTableFormat(final ServiceConnection service, final DataFormat votFormat) throws NullPointerException{
this(service, votFormat, null);
}
/**
* <p>Creates a VOTable formatter.</p>
*
* <i>Note: The MIME type is automatically set in function of the given VOTable serialization:</i>
* <ul>
* <li><i><b>none or unknown</b>: equivalent to BINARY</i></li>
* <li><i><b>BINARY</b>: "application/x-votable+xml" = "votable"</i></li>
* <li><i><b>BINARY2</b>: "application/x-votable+xml;serialization=BINARY2" = "votable/b2"</i></li>
* <li><i><b>TABLEDATA</b>: "application/x-votable+xml;serialization=TABLEDATA" = "votable/td"</i></li>
* <li><i><b>FITS</b>: "application/x-votable+xml;serialization=FITS" = "votable/fits"</i></li>
* </ul>
* <p><i>It is however possible to change these default values thanks to {@link #setMimeType(String, String)}.</i></p>
*
* @param service The service to use (for the log and to have some information about the service (particularly: name, description).
* @param votFormat Serialization of the VOTable data part. (TABLEDATA, BINARY, BINARY2 or FITS).
* @param votVersion Version of the resulting VOTable.
*
* @throws NullPointerException If the given service connection is <code>null</code>.
*/
public VOTableFormat(final ServiceConnection service, final DataFormat votFormat, final VOTableVersion votVersion) throws NullPointerException{
if (service == null)
throw new NullPointerException("The given service connection is NULL!");
this.service = service;
// Set the VOTable serialization and version:
this.votFormat = (votFormat == null) ? DataFormat.BINARY : votFormat;
this.votVersion = (votVersion == null) ? VOTableVersion.V13 : votVersion;
// Deduce automatically the MIME type and its short expression:
if (this.votFormat.equals(DataFormat.BINARY)){
this.mimeType = "application/x-votable+xml";
this.shortMimeType = "votable";
}else if (this.votFormat.equals(DataFormat.BINARY2)){
this.mimeType = "application/x-votable+xml;serialization=BINARY2";
this.shortMimeType = "votable/b2";
}else if (this.votFormat.equals(DataFormat.TABLEDATA)){
this.mimeType = "application/x-votable+xml;serialization=TABLEDATA";
this.shortMimeType = "votable/td";
}else if (this.votFormat.equals(DataFormat.FITS)){
this.mimeType = "application/x-votable+xml;serialization=FITS";
this.shortMimeType = "votable/fits";
}else{
this.mimeType = "application/x-votable+xml";
this.shortMimeType = "votable";
}
}
@Override
public final String getMimeType(){
return mimeType;
}
@Override
public final String getShortMimeType(){
return shortMimeType;
}
/**
* <p>Set the MIME type associated with this format.</p>
*
* <p><i>Note: NULL means no modification of the current value:</i></p>
*
* @param mimeType Full MIME type of this VOTable format. <i>note: if NULL, the MIME type is not modified.</i>
* @param shortForm Short form of this MIME type. <i>note: if NULL, the short MIME type is not modified.</i>
*/
public final void setMimeType(final String mimeType, final String shortForm){
if (mimeType != null)
this.mimeType = mimeType;
if (shortForm != null)
this.shortMimeType = shortForm;
}
/**
* Get the set VOTable data serialization/format (e.g. BINARY, TABLEDATA).
*
* @return The data format.
*/
public final DataFormat getVotSerialization(){
return votFormat;
}
/**
* Get the set VOTable version.
*
* @return The VOTable version.
*/
public final VOTableVersion getVotVersion(){
return votVersion;
}
@Override
public String getDescription(){
return null;
}
@Override
public String getFileExtension(){
return "xml";
}
/**
* <p>Write the given error message as VOTable document.</p>
*
* <p><i>Note:
* In the TAP protocol, all errors must be returned as VOTable. The class {@link DefaultTAPErrorWriter} is in charge of the management
* and reporting of all errors. It is calling this function while the error message to display to the user is ready and
* must be written in the HTTP response.
* </i></p>
*
* <p>Here is the XML format of this VOTable error:</p>
* <pre>
* <VOTABLE version="..." xmlns="..." >
* <RESOURCE type="results">
* <INFO name="QUERY_STATUS" value="ERROR>
* ...
* </INFO>
* <INFO name="PROVIDER" value="...">...</INFO>
* <!-- other optional INFOs (e.g. request parameters) -->
* </RESOURCE>
* </VOTABLE>
* </pre>
*
* @param message Error message to display to the user.
* @param otherInfo List of other additional information to display. <i>optional</i>
* @param writer Stream in which the VOTable error must be written.
*
* @throws IOException If any error occurs while writing in the given output.
*
* @since 2.0
*/
public void writeError(final String message, final Map<String,String> otherInfo, final PrintWriter writer) throws IOException{
BufferedWriter out = new BufferedWriter(writer);
// Set the root VOTABLE node:
out.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
out.newLine();
out.write("<VOTABLE" + VOSerializer.formatAttribute("version", votVersion.getVersionNumber()) + VOSerializer.formatAttribute("xmlns", votVersion.getXmlNamespace()) + VOSerializer.formatAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + VOSerializer.formatAttribute("xsi:schemaLocation", votVersion.getXmlNamespace() + " " + votVersion.getSchemaLocation()) + ">");
out.newLine();
// The RESOURCE note MUST have a type "results": [REQUIRED]
out.write("<RESOURCE type=\"results\">");
out.newLine();
// Indicate that the query has been successfully processed: [REQUIRED]
out.write("<INFO name=\"QUERY_STATUS\" value=\"ERROR\">" + (message == null ? "" : VOSerializer.formatText(message)) + "</INFO>");
out.newLine();
// Append the PROVIDER information (if any): [OPTIONAL]
if (service.getProviderName() != null){
out.write("<INFO name=\"PROVIDER\"" + VOSerializer.formatAttribute("value", service.getProviderName()) + ">" + ((service.getProviderDescription() == null) ? "" : VOSerializer.formatText(service.getProviderDescription())) + "</INFO>");
out.newLine();
}
// Append the ADQL query at the origin of this result: [OPTIONAL]
if (otherInfo != null){
Iterator<Map.Entry<String,String>> it = otherInfo.entrySet().iterator();
while(it.hasNext()){
Map.Entry<String,String> entry = it.next();
if (entry.getValue() != null){
if (entry.getValue().startsWith("\n")){
int sep = entry.getValue().substring(1).indexOf('\n');
if (sep < 0)
sep = 0;
else
sep++;
out.write("<INFO " + VOSerializer.formatAttribute("name", entry.getKey()) + VOSerializer.formatAttribute("value", entry.getValue().substring(1, sep)) + ">\n" + entry.getValue().substring(sep + 1) + "\n</INFO>");
}else
out.write("<INFO " + VOSerializer.formatAttribute("name", entry.getKey()) + VOSerializer.formatAttribute("value", entry.getValue()) + "/>");
out.newLine();
}
}
}
out.flush();
/* Write footer. */
out.write("</RESOURCE>");
out.newLine();
out.write("</VOTABLE>");
out.newLine();
out.flush();
}
@Override
public final void writeResult(final TableIterator queryResult, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws TAPException, IOException, InterruptedException{
ColumnInfo[] colInfos = toColumnInfos(queryResult, execReport, thread);
/* Turns the result set into a table. */
LimitedStarTable table = new LimitedStarTable(queryResult, colInfos, execReport.parameters.getMaxRec(), thread);
/* Prepares the object that will do the serialization work. */
VOSerializer voser = VOSerializer.makeSerializer(votFormat, votVersion, table);
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(output));
/* Write header. */
writeHeader(votVersion, execReport, out);
if (thread.isInterrupted())
throw new InterruptedException();
/* Write table element. */
voser.writeInlineTableElement(out);
execReport.nbRows = table.getNbReadRows();
out.flush();
if (thread.isInterrupted())
throw new InterruptedException();
/* Check for overflow and write INFO if required. */
if (table.lastSequenceOverflowed()){
out.write("<INFO name=\"QUERY_STATUS\" value=\"OVERFLOW\"/>");
out.newLine();
}
/* Write footer. */
out.write("</RESOURCE>");
out.newLine();
out.write("</VOTABLE>");
out.newLine();
out.flush();
}
/**
* <p>Writes the first VOTable nodes/elements preceding the data: VOTABLE, RESOURCE and 3 INFOS (QUERY_STATUS, PROVIDER, QUERY).</p>
*
* @param votVersion Target VOTable version.
* @param execReport The report of the query execution.
* @param out Writer in which the root node must be written.
*
* @throws IOException If there is an error while writing the root node in the given Writer.
* @throws TAPException If there is any other error (by default: never happen).
*/
protected void writeHeader(final VOTableVersion votVersion, final TAPExecutionReport execReport, final BufferedWriter out) throws IOException, TAPException{
// Set the root VOTABLE node:
out.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
out.newLine();
out.write("<VOTABLE" + VOSerializer.formatAttribute("version", votVersion.getVersionNumber()) + VOSerializer.formatAttribute("xmlns", votVersion.getXmlNamespace()) + VOSerializer.formatAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + VOSerializer.formatAttribute("xsi:schemaLocation", votVersion.getXmlNamespace() + " " + votVersion.getSchemaLocation()) + ">");
out.newLine();
// The RESOURCE note MUST have a type "results": [REQUIRED]
out.write("<RESOURCE type=\"results\">");
out.newLine();
// Indicate that the query has been successfully processed: [REQUIRED]
out.write("<INFO name=\"QUERY_STATUS\" value=\"OK\"/>");
out.newLine();
// Append the PROVIDER information (if any): [OPTIONAL]
if (service.getProviderName() != null){
out.write("<INFO name=\"PROVIDER\"" + VOSerializer.formatAttribute("value", service.getProviderName()) + ">" + ((service.getProviderDescription() == null) ? "" : VOSerializer.formatText(service.getProviderDescription())) + "</INFO>");
out.newLine();
}
// Append the ADQL query at the origin of this result: [OPTIONAL]
String adqlQuery = execReport.parameters.getQuery();
if (adqlQuery != null){
out.write("<INFO name=\"QUERY\"" + VOSerializer.formatAttribute("value", adqlQuery) + "/>");
out.newLine();
}
/* TODO Add somewhere in the table header the different Coordinate Systems used in this result!
* 2 ways to do so:
* 1/ COOSYS (deprecated from VOTable 1.2, but soon un-deprecated)
* 2/ a GROUP item with the STC expression of the coordinate system.
*/
out.flush();
}
/**
* Writes fields' metadata of the given query result.
*
* @param result The query result from whose fields' metadata must be written.
* @param execReport The report of the query execution.
* @param thread The thread which asked for the result writing.
*
* @return Extracted field's metadata, or NULL if no metadata have been found (theoretically, it never happens).
*
* @throws IOException If there is an error while writing the metadata.
* @throws TAPException If there is any other error.
* @throws InterruptedException If the given thread has been interrupted.
*/
public static final ColumnInfo[] toColumnInfos(final TableIterator result, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException{
// Get the metadata extracted/guesses from the ADQL query:
DBColumn[] columnsFromQuery = execReport.resultingColumns;
// Get the metadata extracted from the result:
TAPColumn[] columnsFromResult = result.getMetadata();
int indField = 0;
if (columnsFromQuery != null){
// Initialize the resulting array:
ColumnInfo[] colInfos = new ColumnInfo[columnsFromQuery.length];
// For each column:
for(DBColumn field : columnsFromQuery){
// Try to build/get appropriate metadata for this field/column:
TAPColumn colFromResult = (columnsFromResult != null && indField < columnsFromResult.length) ? columnsFromResult[indField] : null;
TAPColumn tapCol = getValidColMeta(field, colFromResult);
// Build the corresponding ColumnInfo object:
colInfos[indField] = getColumnInfo(tapCol);
indField++;
}
return colInfos;
}else
return null;
}
/**
* Try to get or otherwise to build appropriate metadata using those extracted from the ADQL query and those extracted from the result.
*
* @param typeFromQuery Metadata extracted/guessed from the ADQL query.
* @param typeFromResult Metadata extracted/guessed from the result.
*
* @return The most appropriate metadata.
*/
protected static final TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){
if (typeFromQuery != null && typeFromQuery instanceof TAPColumn){
TAPColumn colMeta = (TAPColumn)typeFromQuery;
if (colMeta.getDatatype().isUnknown() && typeFromResult != null && !typeFromResult.getDatatype().isUnknown())
colMeta.setDatatype(typeFromResult.getDatatype());
return colMeta;
}else if (typeFromResult != null){
if (typeFromQuery != null)
return (TAPColumn)typeFromResult.copy(typeFromQuery.getDBName(), typeFromQuery.getADQLName(), null);
else
return (TAPColumn)typeFromResult.copy();
}else
return new TAPColumn((typeFromQuery != null) ? typeFromQuery.getADQLName() : "?", new DBType(DBDatatype.VARCHAR), "?");
}
/**
* Convert the given {@link TAPColumn} object into a {@link ColumnInfo} object.
*
* @param tapCol {@link TAPColumn} to convert into {@link ColumnInfo}.
*
* @return The corresponding {@link ColumnInfo}.
*/
protected static final ColumnInfo getColumnInfo(final TAPColumn tapCol){
// Get the VOTable type:
VotType votType = new VotType(tapCol.getDatatype());
// Build a ColumnInfo with the name, type and description:
ColumnInfo colInfo = new ColumnInfo(tapCol.getADQLName(), getDatatypeClass(votType.datatype, votType.arraysize), tapCol.getDescription());
// Set the shape (VOTable arraysize):
colInfo.setShape(getShape(votType.arraysize));
// Set this value may be NULL (note: it is not really necessary since STIL set this flag to TRUE by default):
colInfo.setNullable(true);
// Set the XType (if any):
if (votType.xtype != null)
colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo("xtype", String.class, "VOTable xtype attribute"), votType.xtype));
// Set the additional information: unit, UCD and UType:
colInfo.setUnitString(tapCol.getUnit());
colInfo.setUCD(tapCol.getUcd());
colInfo.setUtype(tapCol.getUtype());
return colInfo;
}
/**
* Convert the VOTable datatype string into a corresponding {@link Class} object.
*
* @param datatype Value of the VOTable attribute "datatype".
* @param arraysize Value of the VOTable attribute "arraysize".
*
* @return The corresponding {@link Class} object.
*/
protected static final Class<?> getDatatypeClass(final VotDatatype datatype, final String arraysize){
// Determine whether this type is an array or not:
boolean isScalar = arraysize == null || (arraysize.length() == 1 && arraysize.equals("1"));
// Guess the corresponding Class object (see section "7.1.4 Data Types" of the STIL documentation):
switch(datatype){
case BIT:
return boolean[].class;
case BOOLEAN:
return isScalar ? Boolean.class : boolean[].class;
case DOUBLE:
return isScalar ? Double.class : double[].class;
case DOUBLECOMPLEX:
return double[].class;
case FLOAT:
return isScalar ? Float.class : float[].class;
case FLOATCOMPLEX:
return float[].class;
case INT:
return isScalar ? Integer.class : int[].class;
case LONG:
return isScalar ? Long.class : long[].class;
case SHORT:
return isScalar ? Short.class : short[].class;
case UNSIGNEDBYTE:
return isScalar ? Short.class : short[].class;
case CHAR:
case UNICODECHAR:
default: /* If the type is not know (theoretically, never happens), return char[*] by default. */
return isScalar ? Character.class : String.class;
}
}
/**
* Convert the given VOTable arraysize into a {@link ColumnInfo} shape.
*
* @param arraysize Value of the VOTable attribute "arraysize".
*
* @return The corresponding {@link ColumnInfo} shape.
*/
protected static final int[] getShape(final String arraysize){
/*
* Note: multi-dimensional arrays are forbidden in the TAP library,
* so no 'nxm...' is possible.
*/
// No arraysize => empty array:
if (arraysize == null)
return new int[0];
// '*' or 'n*' => {-1}:
else if (arraysize.charAt(arraysize.length() - 1) == '*')
return new int[]{-1};
// 'n' => {n}:
else{
try{
return new int[]{Integer.parseInt(arraysize)};
}catch(NumberFormatException nfe){
// if the given arraysize is incorrect (theoretically, never happens), it is like no arraysize has been provided:
return new int[0];
}
}
}
/**
* <p>
* Special {@link StarTable} able to read a fixed maximum number of rows {@link TableIterator}.
* However, if no limit is provided, all rows are read.
* </p>
*
* @author Grégory Mantelet (CDS;ARI)
* @version 2.1 (11/2015)
* @since 2.0
*/
public static class LimitedStarTable extends AbstractStarTable {
/** Number of columns to read. */
private final int nbCol;
/** Information about all columns to read. */
private final ColumnInfo[] columnInfos;
/** Iterator over the data to read using this special {@link StarTable} */
private final TableIterator tableIt;
/** Thread covering this execution. If it is interrupted, the writing must stop as soon as possible.
* @since 2.1 */
private final Thread threadToWatch;
/** Limit on the number of rows to read. Over this limit, an "overflow" event occurs and {@link #overflow} is set to TRUE. */
private final long maxrec;
/** Indicates whether the maximum allowed number of rows has already been read or not. When true, no more row can be read. */
private boolean overflow;
/** Last read row. If NULL, no row has been read or no more row is available. */
private Object[] row = null;
/** Number of rows read until now. */
private int nbRows;
/**
* Build this special {@link StarTable}.
*
* @param tableIt Data on which to iterate using this special {@link StarTable}.
* @param colInfos Information about all columns.
* @param maxrec Limit on the number of rows to read. <i>(if negative, there will be no limit)</i>
* @param thread Parent thread. When an interruption is detected the writing must stop as soon as possible.
*/
LimitedStarTable(final TableIterator tableIt, final ColumnInfo[] colInfos, final long maxrec, final Thread thread){
this.tableIt = tableIt;
this.threadToWatch = thread;
nbCol = colInfos.length;
columnInfos = colInfos;
this.maxrec = maxrec;
overflow = false;
}
/**
* Indicates whether the last row sequence dispensed by
* this table's getRowSequence method was truncated at maxrec rows.
*
* @return true if the last row sequence overflowed
*/
public boolean lastSequenceOverflowed(){
return overflow;
}
/**
* Get the number of rows that have been successfully read until now.
*
* @return Number of all read rows.
*/
public int getNbReadRows(){
return nbRows;
}
@Override
public int getColumnCount(){
return nbCol;
}
@Override
public ColumnInfo getColumnInfo(final int colInd){
return columnInfos[colInd];
}
@Override
public long getRowCount(){
return -1;
}
@Override
public RowSequence getRowSequence() throws IOException{
overflow = false;
row = new Object[nbCol];
return new RowSequence(){
long irow = -1;
@Override
public boolean next() throws IOException{
irow++;
try{
if (!threadToWatch.isInterrupted() && (maxrec < 0 || irow < maxrec)){
boolean hasNext = tableIt.nextRow();
if (hasNext){
for(int i = 0; i < nbCol && tableIt.hasNextCol(); i++)
row[i] = tableIt.nextCol();
nbRows++;
}else
row = null;
return hasNext;
}else{
overflow = tableIt.nextRow();
row = null;
return false;
}
}catch(DataReadException dre){
if (dre.getCause() != null && dre.getCause() instanceof IOException)
throw (IOException)(dre.getCause());
else
throw new IOException(dre);
}
}
@Override
public Object[] getRow() throws IOException{
return row;
}
@Override
public Object getCell(int cellIndex) throws IOException{
return row[cellIndex];
}
@Override
public void close() throws IOException{}
};
}
}
}