Commit 25276fce authored by Grégory Mantelet's avatar Grégory Mantelet
Browse files

[TAP] Fix the VOTable's FITS serialization.

Until now, the generated VOTable file was un-readable even by
STIL/STILTS/TOPCAT. To fix this, it was needed to temporary store the table to
format into FITS so that STIL can read it at least 2 times.

_This commit fixes the GitHub issue #43 ._
parent fea5ccf9
Loading
Loading
Loading
Loading
+71 −61
Original line number Diff line number Diff line
@@ -2,21 +2,21 @@ 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-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 *
 * Copyright 2012-2019 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 *                       Astronomisches Rechen Institut (ARI)
 */

@@ -29,6 +29,9 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import adql.db.DBColumn;
import adql.db.DBType;
import adql.db.DBType.DBDatatype;
import tap.ServiceConnection;
import tap.TAPException;
import tap.TAPExecutionReport;
@@ -44,24 +47,22 @@ import uk.ac.starlink.table.ColumnInfo;
import uk.ac.starlink.table.DescribedValue;
import uk.ac.starlink.table.RowSequence;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.table.StoragePolicy;
import uk.ac.starlink.votable.DataFormat;
import uk.ac.starlink.votable.VOSerializer;
import uk.ac.starlink.votable.VOStarTable;
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>
@@ -71,21 +72,21 @@ import adql.db.DBType.DBDatatype;
 * 	<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&eacute;gory Mantelet (CDS;ARI)
 * @version 2.1 (07/2017)
 * @version 2.2 (03/2019)
 */
public class VOTableFormat implements OutputFormat {

@@ -106,14 +107,14 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * <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{
@@ -122,7 +123,7 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * <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>
@@ -132,10 +133,10 @@ public class VOTableFormat implements OutputFormat {
	 * 	<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{
@@ -144,7 +145,7 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * <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>
@@ -154,11 +155,11 @@ public class VOTableFormat implements OutputFormat {
	 * 	<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{
@@ -202,9 +203,9 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * <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>
	 */
@@ -217,7 +218,7 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * Get the set VOTable data serialization/format (e.g. BINARY, TABLEDATA).
	 * 
	 *
	 * @return	The data format.
	 */
	public final DataFormat getVotSerialization(){
@@ -226,7 +227,7 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * Get the set VOTable version.
	 * 
	 *
	 * @return	The VOTable version.
	 */
	public final VOTableVersion getVotVersion(){
@@ -245,13 +246,13 @@ 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.
	 * </i></p>
	 * 
	 *
	 * <p>Here is the XML format of this VOTable error:</p>
	 * <pre>
	 * 	&lt;VOTABLE version="..." xmlns="..." &gt;
@@ -264,16 +265,16 @@ public class VOTableFormat implements OutputFormat {
	 * 		&lt;/RESOURCE&gt;
	 * 	&lt;/VOTABLE&gt;
	 * </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{
	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:
@@ -298,9 +299,9 @@ public class VOTableFormat implements OutputFormat {

		// Append the ADQL query at the origin of this result:	[OPTIONAL]
		if (otherInfo != null){
			Iterator<Map.Entry<String,String>> it = otherInfo.entrySet().iterator();
			Iterator<Map.Entry<String, String>> it = otherInfo.entrySet().iterator();
			while(it.hasNext()){
				Map.Entry<String,String> entry = it.next();
				Map.Entry<String, String> entry = it.next();
				if (entry.getValue() != null){
					if (entry.getValue().startsWith("\n")){
						int sep = entry.getValue().substring(1).indexOf('\n');
@@ -333,10 +334,18 @@ public class VOTableFormat implements OutputFormat {

		/* Turns the result set into a table. */
		LimitedStarTable table = new LimitedStarTable(queryResult, colInfos, execReport.parameters.getMaxRec(), thread);
		table.setName("result_"+execReport.jobID);
		table.setName("result_" + execReport.jobID);

		/* Prepares the object that will do the serialization work. */
		VOSerializer voser = VOSerializer.makeSerializer(votFormat, votVersion, table);
		VOSerializer voser = null;
		/* 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)
			voser = VOSerializer.makeSerializer(votFormat, votVersion, StoragePolicy.PREFER_DISK.copyTable(table));
		// otherwise, just use the default VOTable serializer:
		else
			voser = VOSerializer.makeSerializer(votFormat, votVersion, table);
		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(output));

		/* Write header. */
@@ -370,11 +379,11 @@ 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>
	 * 
	 *
	 * @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).
	 */
@@ -405,7 +414,7 @@ public class VOTableFormat implements OutputFormat {
			out.write("<INFO name=\"QUERY\"" + VOSerializer.formatAttribute("value", adqlQuery) + "/>");
			out.newLine();
		}
		

		// Insert the definition of all used coordinate systems:
		HashSet<String> insertedCoosys = new HashSet<String>(10);
		for(DBColumn col : execReport.resultingColumns){
@@ -416,7 +425,7 @@ public class VOTableFormat implements OutputFormat {
				// insert the coosys definition ONLY if not already done because of another column:
				if (!insertedCoosys.contains(coosys.getId())){
					// write the VOTable serialization of this coordinate system definition:
					out.write("<COOSYS"+VOSerializer.formatAttribute("ID", coosys.getId()));
					out.write("<COOSYS" + VOSerializer.formatAttribute("ID", coosys.getId()));
					if (coosys.getSystem() != null)
						out.write(VOSerializer.formatAttribute("system", coosys.getSystem()));
					if (coosys.getEquinox() != null)
@@ -436,13 +445,13 @@ 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 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.
@@ -480,10 +489,10 @@ 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.
	 * 
	 *
	 * @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){
@@ -503,9 +512,9 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * 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){
@@ -529,7 +538,7 @@ public class VOTableFormat implements OutputFormat {
		colInfo.setUnitString(tapCol.getUnit());
		colInfo.setUCD(tapCol.getUcd());
		colInfo.setUtype(tapCol.getUtype());
		

		// Set the coosys ref (if any):
		if (tapCol.getCoosys() != null)
			colInfo.setAuxDatum(new DescribedValue(VOStarTable.REF_INFO, tapCol.getCoosys().getId()));
@@ -539,17 +548,17 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * Convert the VOTable datatype string into a corresponding {@link Class} object.
	 * 
	 * @param datatype	Value of the VOTable attribute "datatype". 
	 *
	 * @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): 
		// Guess the corresponding Class object (see section "7.1.4 Data Types" of the STIL documentation):
		switch(datatype){
			case BIT:
				return boolean[].class;
@@ -580,9 +589,9 @@ public class VOTableFormat implements OutputFormat {

	/**
	 * 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){
@@ -597,12 +606,12 @@ public class VOTableFormat implements OutputFormat {

		// '*' or 'n*' => {-1}:
		else if (arraysize.charAt(arraysize.length() - 1) == '*')
			return new int[]{-1};
			return new int[]{ -1 };

		// 'n' => {n}:
		else{
			try{
				return new int[]{Integer.parseInt(arraysize)};
				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];
@@ -615,7 +624,7 @@ public class VOTableFormat implements OutputFormat {
	 * 	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&eacute;gory Mantelet (CDS;ARI)
	 * @version 2.1 (11/2015)
	 * @since 2.0
@@ -649,7 +658,7 @@ public class VOTableFormat implements OutputFormat {

		/**
		 * 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>
@@ -703,7 +712,7 @@ public class VOTableFormat implements OutputFormat {
			overflow = false;
			row = new Object[nbCol];

			return new RowSequence(){
			return new RowSequence() {
				long irow = -1;

				@Override
@@ -743,7 +752,8 @@ public class VOTableFormat implements OutputFormat {
				}

				@Override
				public void close() throws IOException{}
				public void close() throws IOException{
				}
			};
		}
	}