Commit 25a783a8 authored by Grégory Mantelet's avatar Grégory Mantelet
Browse files

[TAP] Fix intermixed VOTable documents in synchronous mode.

Fix #52
parent 30dc11cb
Loading
Loading
Loading
Loading
+37 −20
Original line number Diff line number Diff line
@@ -16,7 +16,8 @@ package tap.formatter;
 * 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 2014-2015 - Astronomisches Rechen Institut (ARI)
 * Copyright 2014-2020 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 *                       Astronomisches Rechen Institut (ARI)
 */

import java.io.IOException;
@@ -35,21 +36,24 @@ import uk.ac.starlink.table.StoragePolicy;
/**
 * Format any given query (table) result into FITS.
 *
 * @author Gr&eacute;gory Mantelet (ARI)
 * @version 2.1 (11/2015)
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 2.4 (08/2020)
 * @since 2.0
 */
public class FITSFormat implements OutputFormat {

	/** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */
	/** The {@link ServiceConnection} to use (for the log and to have some
	 * information about the service (particularly: name, description). */
	protected final ServiceConnection service;

	/**
	 * Creates a FITS formatter.
	 *
	 * @param service	The service to use (for the log and to have some information about the service (particularly: name, description).
	 * @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>.
	 * @throws NullPointerException	If the given service connection is NULL.
	 */
	public FITSFormat(final ServiceConnection service) throws NullPointerException {
		if (service == null)
@@ -87,7 +91,20 @@ public class FITSFormat implements OutputFormat {
		LimitedStarTable table = new LimitedStarTable(result, colInfos, execReport.parameters.getMaxRec(), thread);

		// Copy the table on disk (or in memory if the table is short):
		StarTable copyTable = StoragePolicy.PREFER_DISK.copyTable(table);
		StarTable copyTable;
		try {
			copyTable = StoragePolicy.PREFER_DISK.copyTable(table);
		} catch(IOException ioe) {
			/* In case of time out, LimitedStarTable makes copyTable to stop by
			 * throwing an IOException. In such case, this IOException has to be
			 * interpreted as a normal interruption: */
			if (thread.isInterrupted())
				throw new InterruptedException();
			/* Otherwise, the error has to be managed properly (so, wrap it
			 * inside a TAPException): */
			else
				throw new TAPException("Unexpected error while formatting the result!", ioe);
		}

		if (thread.isInterrupted())
			throw new InterruptedException();
+268 −155
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ package tap.formatter;
 * 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-2019 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 * Copyright 2012-2020 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 *                       Astronomisches Rechen Institut (ARI)
 */

@@ -54,49 +54,70 @@ import uk.ac.starlink.votable.VOStarTable;
import uk.ac.starlink.votable.VOTableVersion;

/**
 * <p>Format any given query (table) result into VOTable.</p>
 * Format any given query (table) result into VOTable.
 *
 * <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.
 * 	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>
 * <p><i><b>Note:</b>
 * 	The MIME type is automatically set in function of the given VOTable
 * 	serialization:
 * </i></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>
 * 	<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>
 * 	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>
 * <p>
 * 	In addition of the INFO elements for QUERY_STATUS="OK",
 * 	QUERY_STATUS="OVERFLOW" and QUERY_STATUS="ERROR", two additional INFO
 * 	elements are written:
 * </p>
 * <ul>
 * 	<li>PROVIDER = {@link ServiceConnection#getProviderName()} and {@link ServiceConnection#getProviderDescription()}</li>
 * 	<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>
 * 	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&eacute;gory Mantelet (CDS;ARI)
 * @version 2.3 (03/2019)
 * @version 2.4 (08/2020)
 */
public class VOTableFormat implements OutputFormat {

	/** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */
	/** 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. */
	/** 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. */
	/** 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. */
@@ -106,61 +127,90 @@ public class VOTableFormat implements OutputFormat {
	protected String shortMimeType;

	/**
	 * <p>Creates a VOTable formatter.</p>
	 * Creates a VOTable formatter.
	 *
	 * <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)}.
	 * <p><i><b>Note:</b>
	 * 	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).
	 * @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>.
	 * @throws NullPointerException	If the given service connection is NULL.
	 */
	public VOTableFormat(final ServiceConnection service) throws NullPointerException {
		this(service, null, null);
	}

	/**
	 * <p>Creates a VOTable formatter.</p>
	 * Creates a VOTable formatter.
	 *
	 * <i>Note: The MIME type is automatically set in function of the given VOTable serialization:</i>
	 * <p><i><b>Note:</b>
	 * 	The MIME type is automatically set in function of the given VOTable
	 * 	serialization:
	 * </i></p>
	 * <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>
	 * 	<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>
	 * <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 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>.
	 * @throws NullPointerException	If the given service connection is NULL.
	 */
	public VOTableFormat(final ServiceConnection service, final DataFormat votFormat) throws NullPointerException {
		this(service, votFormat, null);
	}

	/**
	 * <p>Creates a VOTable formatter.</p>
	 * Creates a VOTable formatter.
	 *
	 * <i>Note: The MIME type is automatically set in function of the given VOTable serialization:</i>
	 * <p><i><b>Note:</b>
	 * 	The MIME type is automatically set in function of the given VOTable
	 * 	serialization:
	 * </i></p>
	 * <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>
	 * 	<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>
	 * <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 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>.
	 * @throws NullPointerException	If the given service connection is NULL.
	 */
	public VOTableFormat(final ServiceConnection service, final DataFormat votFormat, final VOTableVersion votVersion) throws NullPointerException {
		if (service == null)
@@ -202,12 +252,16 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * <p>Set the MIME type associated with this format.</p>
	 * Set the MIME type associated with this format.
	 *
	 * <p><i>Note: NULL means no modification of the current value:</i></p>
	 * <p><i><b>Note:</b>
	 * 	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>
	 * @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)
@@ -245,12 +299,14 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * <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.
	 * Write the given error message as VOTable document.
	 *
	 * <p><i><b>Note:</b>
	 * 	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>
@@ -267,7 +323,8 @@ public class VOTableFormat implements OutputFormat {
	 * </pre>
	 *
	 * @param message	Error message to display to the user.
	 * @param otherInfo	List of other additional information to display. <i>optional</i>
	 * @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.
@@ -341,29 +398,69 @@ public class VOTableFormat implements OutputFormat {
		/* if FITS, copy the table on disk (or in memory if the table is short):
		 * (note: this is needed because STIL needs at least 2 passes on this
		 *        table to format it correctly in FITS format) */
		if (votFormat == DataFormat.FITS)
		if (votFormat == DataFormat.FITS) {
			try {
				voser = VOSerializer.makeSerializer(votFormat, votVersion, StoragePolicy.PREFER_DISK.copyTable(table));
			} catch(IOException ioe) {
				/* As in the class FITSFormat, the caught IOException may be due
				 * to an interruption from LimitedStarTable. In such case,
				 * propagate the interruption: */
				if (thread.isInterrupted())
					throw new InterruptedException();
				/* Any other error should be properly wrapped: */
				else
					throw new TAPException("Unexpected error while formatting the result!", ioe);
			}
		}
		// otherwise, just use the default VOTable serializer:
		else
			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. */
		if (!thread.isInterrupted()) {
			try {
				voser.writeInlineTableElement(out);
				execReport.nbRows = table.getNbReadRows();
				out.flush();
			} catch(Exception ex) {
				/* If synchronous, the partially written VOTable should be
				 * properly closed and an error INFO should be appended: */
				if (execReport.synchronous) {
					if (votFormat != DataFormat.TABLEDATA) {
						out.write("</STREAM>\n</BINARY>\n</DATA>\n</TABLE>");
						out.newLine();
					}
					out.write("<INFO name=\"QUERY_STATUS\" value=\"ERROR\">Result truncated due to an unexpected grave error: " + VOSerializer.formatText(ex.getMessage()) + "</INFO>");
					out.newLine();
				}
				// If asynchronous, just propagate the error:
				else {
					if (ex instanceof TAPException || ex instanceof IOException || ex instanceof InterruptedException)
						throw ex;
					else
						throw new TAPException(ex);
				}
			}
		}

		if (thread.isInterrupted())
		/* If Timed Out... */
		if (thread.isInterrupted()) {
			// ...if synchronous, end properly the VOTable with an error INFO:
			if (execReport != null && execReport.synchronous) {
				out.write("<INFO name=\"QUERY_STATUS\" value=\"ERROR\">Time out! (Hint: Try running this query in asynchronous mode to get the complete result)</INFO>");
				out.newLine();
			}
			// ...if asynchronous, merely propagate the interruption:
			else
				throw new InterruptedException();

		/* Check for overflow and write INFO if required. */
		if (table.lastSequenceOverflowed()){
		}
		/* If Overflow, declare this in an INFO: */
		else if (table.lastSequenceOverflowed()) {
			out.write("<INFO name=\"QUERY_STATUS\" value=\"OVERFLOW\"/>");
			out.newLine();
		}
@@ -378,14 +475,17 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * <p>Writes the first VOTable nodes/elements preceding the data: VOTABLE, RESOURCE and 3 INFOS (QUERY_STATUS, PROVIDER, QUERY).</p>
	 * Writes the first VOTable nodes/elements preceding the data: VOTABLE,
	 * RESOURCE and 3 INFOS (QUERY_STATUS, PROVIDER, QUERY).
	 *
	 * @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).
	 * @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:
@@ -453,15 +553,19 @@ public class VOTableFormat implements OutputFormat {
	/**
	 * Writes fields' metadata of the given query result.
	 *
	 * @param result		The query result from whose fields' metadata must be written.
	 * @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).
	 * @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 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.
	 * @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:
@@ -495,7 +599,8 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * Try to get or otherwise to build appropriate metadata using those extracted from the ADQL query and those extracted from the result.
	 * 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.
@@ -518,7 +623,8 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * Convert the given {@link TAPColumn} object into a {@link ColumnInfo} object.
	 * Convert the given {@link TAPColumn} object into a {@link ColumnInfo}
	 * object.
	 *
	 * @param tapCol	{@link TAPColumn} to convert into {@link ColumnInfo}.
	 *
@@ -554,7 +660,8 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * Convert the VOTable datatype string into a corresponding {@link Class} object.
	 * 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".
@@ -627,10 +734,9 @@ public class VOTableFormat implements OutputFormat {
	}

	/**
	 * <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>
	 * Special {@link StarTable} able to read a fixed maximum number of rows
	 * {@link TableIterator}. However, if no limit is provided, all rows are
	 * read.
	 *
	 * @author Gr&eacute;gory Mantelet (CDS;ARI)
	 * @version 2.1 (11/2015)
@@ -647,17 +753,21 @@ public class VOTableFormat implements OutputFormat {
		/** 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.
		/** 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. */
		/** 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. */
		/** 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. */
		/** 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. */
@@ -666,10 +776,13 @@ public class VOTableFormat implements OutputFormat {
		/**
		 * Build this special {@link StarTable}.
		 *
		 * @param tableIt	Data on which to iterate using 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.
		 * @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;