diff --git a/lib/uploadUtilsSrc/cos.jar b/lib/cos.jar
similarity index 100%
rename from lib/uploadUtilsSrc/cos.jar
rename to lib/cos.jar
diff --git a/lib/stil3.0-5.jar b/lib/stil3.0-5.jar
new file mode 100644
index 0000000000000000000000000000000000000000..d9f1a4a798e8f62f884580bc10ab9662eb441c64
Binary files /dev/null and b/lib/stil3.0-5.jar differ
diff --git a/lib/uploadUtils.jar b/lib/uploadUtils.jar
deleted file mode 100644
index 074638e30fbd33bca8d7bf6593d0920dbcc91fdd..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtils.jar and /dev/null differ
diff --git a/lib/uploadUtilsSrc/binarySavot.jar b/lib/uploadUtilsSrc/binarySavot.jar
deleted file mode 100644
index 175bfaf7b60d030f0600a80f826bbf338c8c19c3..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtilsSrc/binarySavot.jar and /dev/null differ
diff --git a/lib/uploadUtilsSrc/buildJar.xml b/lib/uploadUtilsSrc/buildJar.xml
deleted file mode 100644
index de5beb64495e00d108e5252a31d495d2846cb7df..0000000000000000000000000000000000000000
--- a/lib/uploadUtilsSrc/buildJar.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE project>
-<project name="TAPUploaderTest" basedir="." default="jarBuild">
-	<target name="init">
-		<property name="srcRep" value="src" />
-		<property name="buildRep" value="build"/>
-		<property name="jarFile" value="uploadUtils.jar" />
-		<property name="jarDest" value=".." />
-	</target>
-	
-	<target name="jarBuild" depends="init" >
-		<echo>Création du jar (${jarDest}/${jarFile})...</echo>
-		<jar destfile="${jarDest}/${jarFile}">
-			<zipfileset src="binarySavot.jar" excludes="META-INF/*" />
-			<zipfileset src="cds.savot.common.jar" excludes="META-INF/*" />
-			<zipfileset src="cds.savot.model.jar" excludes="META-INF/*" />
-			<zipfileset src="cds.savot.pull.jar" excludes="META-INF/*" />
-			<zipfileset src="cds.savot.writer.jar" excludes="META-INF/*" />
-			<zipfileset src="kxml2-min.jar" excludes="META-INF/*" />
-			<zipfileset src="cos.jar" excludes="META-INF/*" />
-		</jar>
-	</target>
-</project>
\ No newline at end of file
diff --git a/lib/uploadUtilsSrc/cds.savot.common.jar b/lib/uploadUtilsSrc/cds.savot.common.jar
deleted file mode 100644
index 9966915834caa384e344a8668a5f0f2dac02be09..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtilsSrc/cds.savot.common.jar and /dev/null differ
diff --git a/lib/uploadUtilsSrc/cds.savot.model.jar b/lib/uploadUtilsSrc/cds.savot.model.jar
deleted file mode 100644
index 00aac3bb37db258347bec5768027c82a70e90ff3..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtilsSrc/cds.savot.model.jar and /dev/null differ
diff --git a/lib/uploadUtilsSrc/cds.savot.pull.jar b/lib/uploadUtilsSrc/cds.savot.pull.jar
deleted file mode 100644
index fb8dc22d327c808110a07324a3bd8beee9643560..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtilsSrc/cds.savot.pull.jar and /dev/null differ
diff --git a/lib/uploadUtilsSrc/cds.savot.writer.jar b/lib/uploadUtilsSrc/cds.savot.writer.jar
deleted file mode 100644
index 92113cdb854ccfa625a7f70f37975fdc5b5feb73..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtilsSrc/cds.savot.writer.jar and /dev/null differ
diff --git a/lib/uploadUtilsSrc/kxml2-min.jar b/lib/uploadUtilsSrc/kxml2-min.jar
deleted file mode 100644
index a77dd1d0063db3d18e1a81cb5a895c45531eccc5..0000000000000000000000000000000000000000
Binary files a/lib/uploadUtilsSrc/kxml2-min.jar and /dev/null differ
diff --git a/src/tap/data/VOTableIterator.java b/src/tap/data/VOTableIterator.java
index 00349c24714987c12681a093967334cae95d1a18..13628d57be9073f710c101a02d5d99b38fa9006b 100644
--- a/src/tap/data/VOTableIterator.java
+++ b/src/tap/data/VOTableIterator.java
@@ -21,40 +21,45 @@ package tap.data;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Collection;
-import java.util.Iterator;
 import java.util.NoSuchElementException;
 
+import tap.TAPException;
 import tap.metadata.TAPColumn;
 import tap.metadata.TAPType;
 import tap.metadata.VotType;
 import tap.metadata.VotType.VotDatatype;
-import cds.savot.model.DataBinaryReader;
-import cds.savot.model.FieldSet;
-import cds.savot.model.SavotBinary;
-import cds.savot.model.SavotField;
-import cds.savot.model.SavotResource;
-import cds.savot.model.SavotTD;
-import cds.savot.model.SavotTR;
-import cds.savot.model.SavotTableData;
-import cds.savot.pull.SavotPullEngine;
-import cds.savot.pull.SavotPullParser;
+import uk.ac.starlink.table.ColumnInfo;
+import uk.ac.starlink.table.DescribedValue;
+import uk.ac.starlink.table.OnceRowPipe;
+import uk.ac.starlink.table.RowSequence;
+import uk.ac.starlink.table.StarTable;
+import uk.ac.starlink.table.StarTableFactory;
+import uk.ac.starlink.table.TableBuilder;
 
 /**
- * <p>{@link TableIterator} which lets iterate over a VOTable input stream using Savot ({@link SavotPullParser} more exactly).</p>
+ * <p>{@link TableIterator} which lets iterate over a VOTable input stream using STIL.</p>
  * 
  * <p>{@link #getColType()} will return TAP type based on the type declared in the VOTable metadata part.</p>
  * 
- * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
- * @version 2.0 (06/2014)
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 2.0 (07/2014)
  * @since 2.0
  */
 public class VOTableIterator implements TableIterator {
 
 	/** Metadata of all columns identified before the iteration. */
 	private final TAPColumn[] colMeta;
-	/** Inner TableIterator. It lets iterate over a binary or a table data set in a transparent way. */
-	private final TableIterator it;
+	/** Number of columns to read. */
+	private final int nbColumns;
+	/** Sequence of rows over which we must iterate. */
+	private final RowSequence rowSeq;
+
+	/** Indicate whether the row iteration has already started. */
+	private boolean iterationStarted = false;
+	/** Indicate whether the last row has already been reached. */
+	private boolean endReached = false;
+	/** Index of the last read column (=0 just after {@link #nextRow()} and before {@link #nextCol()}, ={@link #nbColumns} after the last column has been read). */
+	private int colIndex;
 
 	/**
 	 * Build a TableIterator able to read rows and columns inside the given VOTable input stream.
@@ -70,51 +75,66 @@ public class VOTableIterator implements TableIterator {
 			throw new NullPointerException("Missing VOTable document input stream over which to iterate!");
 
 		try{
-			// Start parsing the VOTable:
-			SavotPullParser parser = new SavotPullParser(input, SavotPullEngine.SEQUENTIAL, null);
-
-			// Get the first resource:
-			SavotResource resource = parser.getNextResource();
-			if (resource == null)
-				throw new DataReadException("Incorrect VOTable format: missing resource node!");
-
-			// Extract the metadata about all fields:
-			FieldSet fields = resource.getFieldSet(0);
-			colMeta = extractColMeta(fields);
-
-			// Build the iterator over the data:
-			SavotBinary binary = resource.getData(0).getBinary();
-			if (binary != null)
-				it = new BinaryVOTableIterator(binary, fields, colMeta);
-			else
-				it = new DataVOTableIterator(resource.getData(0).getTableData(), colMeta);
+
+			// Set the VOTable builder/interpreter:
+			TableBuilder tb = (new StarTableFactory()).getTableBuilder("votable");
+
+			// Set the TableSink to use in order to stream the data: 
+			OnceRowPipe rowPipe = new OnceRowPipe();
+
+			// Initiate the stream process:
+			tb.streamStarTable(input, rowPipe, null);
+
+			// Start by reading just the metadata:
+			StarTable table = rowPipe.waitForStarTable();
+
+			// Convert columns' information into TAPColumn object:
+			colMeta = extractColMeta(table);
+			nbColumns = colMeta.length;
+
+			// Set the sequence of rows on which this iterator will iterate:
+			rowSeq = table.getRowSequence();
+
+		}catch(TAPException te){
+			throw new DataReadException("Unexpected field datatype: " + te.getMessage(), te);
 		}catch(Exception ex){
 			throw new DataReadException("Unable to parse/read the given VOTable input stream!", ex);
 		}
 	}
 
 	/**
-	 * Extract an array of {@link TAPColumn} objects. Each corresponds to one of the fields given in parameter,
+	 * Extract an array of {@link TAPColumn} objects. Each corresponds to one of the columns listed in the given table,
 	 * and so corresponds to the metadata of a column. 
 	 * 
-	 * @param fields	List of metadata fields provided in a VOTable.
+	 * @param table		{@link StarTable} which contains only the columns' information.
 	 * 
 	 * @return			The corresponding list of {@link TAPColumn} objects.
+	 * 
+	 * @throws TAPException	If there is a problem while resolving the field datatype (for instance: unknown datatype, a multi-dimensional array is provided, a bad number format for the arraysize).
 	 */
-	private static final TAPColumn[] extractColMeta(final FieldSet fields){
+	private static final TAPColumn[] extractColMeta(final StarTable table) throws TAPException{
 		// Count the number columns and initialize the array:
-		TAPColumn[] columns = new TAPColumn[fields.getItemCount()];
+		TAPColumn[] columns = new TAPColumn[table.getColumnCount()];
 
 		// Add all columns meta:
-		for(int i = 0; i < fields.getItemCount(); i++){
+		for(int i = 0; i < columns.length; i++){
 			// get the field:
-			SavotField field = (SavotField)fields.getItemAt(i);
+			ColumnInfo colInfo = table.getColumnInfo(i);
+
+			// get the datatype:
+			String datatype = getAuxDatumValue(colInfo, "Datatype");
+
+			// get the arraysize:
+			String arraysize = ColumnInfo.formatShape(colInfo.getShape());
+
+			// get the xtype:
+			String xtype = getAuxDatumValue(colInfo, "xtype");
 
 			// Resolve the field type:
-			TAPType type = resolveVotType(field.getDataType(), field.getArraySize(), field.getXtype()).toTAPType();
+			TAPType type = resolveVotType(datatype, arraysize, xtype).toTAPType();
 
 			// build the TAPColumn object:
-			TAPColumn col = new TAPColumn(field.getName(), type, field.getDescription(), field.getUnit(), field.getUcd(), field.getUtype());
+			TAPColumn col = new TAPColumn(colInfo.getName(), type, colInfo.getDescription(), colInfo.getUnitString(), colInfo.getUCD(), colInfo.getUtype());
 			col.setPrincipal(false);
 			col.setIndexed(false);
 			col.setStd(false);
@@ -126,6 +146,19 @@ public class VOTableIterator implements TableIterator {
 		return columns;
 	}
 
+	/**
+	 * Extract the specified auxiliary datum value from the given {@link ColumnInfo}.
+	 * 
+	 * @param colInfo			{@link ColumnInfo} from which the auxiliary datum must be extracted.
+	 * @param auxDatumName		The name of the datum to extract.
+	 * 
+	 * @return	The extracted value as String.
+	 */
+	private static final String getAuxDatumValue(final ColumnInfo colInfo, final String auxDatumName){
+		DescribedValue value = colInfo.getAuxDatumByName(auxDatumName);
+		return (value != null) ? value.getValue().toString() : null;
+	}
+
 	/**
 	 * Resolve a VOTable field type by using the datatype, arraysize and xtype strings as specified in a VOTable document.
 	 * 
@@ -134,53 +167,24 @@ public class VOTableIterator implements TableIterator {
 	 * @param xtype			Attribute value of VOTable corresponding to the xtype.
 	 * 
 	 * @return	The resolved VOTable field type, or a CHAR(*) type if the specified type can not be resolved.
+	 * 
+	 * @throws TAPException	If a field datatype is unknown.
 	 */
-	private static VotType resolveVotType(final String datatype, final String arraysize, final String xtype){
+	private static VotType resolveVotType(final String datatype, final String arraysize, final String xtype) throws TAPException{
 		// If no datatype is specified, return immediately a CHAR(*) type:
 		if (datatype == null || datatype.trim().length() == 0)
-			return new VotType(VotDatatype.CHAR, VotType.NO_SIZE, true);
-
-		// 1. IDENTIFY THE DATATYPE:
+			return new VotType(VotDatatype.CHAR, "*");
 
 		// Identify the specified datatype:
 		VotDatatype votdatatype;
 		try{
 			votdatatype = VotDatatype.valueOf(datatype.toUpperCase());
 		}catch(IllegalArgumentException iae){
-			// if it can't be identified, return immediately a CHAR(*) type:
-			return new VotType(VotDatatype.CHAR, VotType.NO_SIZE, true);
+			throw new TAPException("unknown field datatype: \"" + datatype + "\"");
 		}
 
-		// 2. DETERMINE ITS ARRAYSIZE:
-
-		int votarraysize = VotType.NO_SIZE;
-		boolean votunlimitedSize = false;
-
-		// If no arraysize is specified, let's set it to 1 (for an elementary value):
-		if (arraysize == null || arraysize.trim().isEmpty())
-			votarraysize = 1;
-
-		// Otherwise, get it:
-		else{
-			String str = arraysize.trim();
-
-			// Determine whether an "unlimited size" character is specified:
-			votunlimitedSize = str.endsWith("*");
-
-			// If one is specified, remove it from the arraysize string:
-			if (votunlimitedSize)
-				str = str.substring(0, str.length() - 1);
-
-			// If a size is really specified (more characters than "*"), get the arraysize value:
-			if (str.length() > 0){
-				try{
-					votarraysize = Integer.parseInt(str);
-				}catch(NumberFormatException nfe){}
-			}
-		}
-
-		// And finally build the VOTable type:
-		return new VotType(votdatatype, votarraysize, votunlimitedSize, xtype);
+		// Build the VOTable type:
+		return new VotType(votdatatype, arraysize, xtype);
 	}
 
 	/**
@@ -191,11 +195,11 @@ public class VOTableIterator implements TableIterator {
 	 * </ul>
 	 * @throws IllegalStateException
 	 */
-	private static void checkReadState(final boolean iterationStarted, final boolean endReached) throws IllegalStateException{
+	private void checkReadState() throws IllegalStateException{
 		if (!iterationStarted)
 			throw new IllegalStateException("No row has yet been read!");
 		else if (endReached)
-			throw new IllegalStateException("End of ResultSet already reached!");
+			throw new IllegalStateException("End of VOTable file already reached!");
 	}
 
 	@Override
@@ -205,263 +209,55 @@ public class VOTableIterator implements TableIterator {
 
 	@Override
 	public boolean nextRow() throws DataReadException{
-		return it.nextRow();
+		try{
+			// go to the next row:
+			boolean rowFetched = rowSeq.next();
+			endReached = !rowFetched;
+			// prepare the iteration over its columns:
+			colIndex = 0;
+			iterationStarted = true;
+			return rowFetched;
+		}catch(IOException e){
+			throw new DataReadException("Unable to read the next VOTable row!", e);
+		}
 	}
 
 	@Override
 	public boolean hasNextCol() throws IllegalStateException, DataReadException{
-		return it.hasNextCol();
-	}
+		// Check the read state:
+		checkReadState();
 
-	@Override
-	public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
-		return it.nextCol();
+		// Determine whether the last column has been reached or not:
+		return (colIndex < nbColumns);
 	}
 
 	@Override
-	public TAPType getColType() throws IllegalStateException, DataReadException{
-		return it.getColType();
-	}
-
-	/**
-	 * <p>{@link TableIterator} which lets iterate over a VOTable binary data part.</p>
-	 * 
-	 * <p>This {@link TableIterator} is only usable by {@link VOTableIterator}.</p>
-	 * 
-	 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
-	 * @version 2.0 (Jun 27, 2014)
-	 * @since 2.0
-	 */
-	private static class BinaryVOTableIterator implements TableIterator {
-
-		/** Binary data reader which lets read rows and columns, and thus iterate over them. */
-		private final DataBinaryReader reader;
-		/** Metadata of all columns identified before the iteration. <i>(In this TableIterator, they are completely provided by {@link VOTableIterator}).</i> */
-		private final TAPColumn[] colMeta;
-
-		/** The last read row. Each item is a column value. */
-		private Object[] row;
-
-		/** Indicate whether the row iteration has already started. */
-		private boolean iterationStarted = false;
-		/** Indicate whether the last row has already been reached. */
-		private boolean endReached = false;
-		/** Index of the last read column (=0 just after {@link #nextRow()} and before {@link #nextCol()}). */
-		private int colIndex;
-
-		/**
-		 * Build a TableIterator on the given binary data part of a VOTable whose fields are also described in parameter.
-		 * 
-		 * @param binary		Binary data part of a VOTable document.
-		 * @param fields		Description of all the fields that should be read.
-		 * @param columnsMeta	Metadata information extracted from the VOTable metadata part.
-		 * 
-		 * @throws DataReadException	If there is an error while starting reading the given binary data.
-		 */
-		public BinaryVOTableIterator(final SavotBinary binary, final FieldSet fields, final TAPColumn[] columnsMeta) throws DataReadException{
-			try{
-				reader = new DataBinaryReader(binary.getStream(), fields, false);
-				colMeta = columnsMeta;
-			}catch(IOException ioe){
-				throw new DataReadException("Can not open a stream to decode the binary VOTable data!", ioe);
-			}
-		}
-
-		@Override
-		public TAPColumn[] getMetadata(){
-			return null;
-		}
-
-		@Override
-		public boolean nextRow() throws DataReadException{
-			try{
-				// Go to the next row:
-				boolean rowFetched = reader.next();
-				// prepare the iteration over its columns:
-				if (rowFetched){
-					row = reader.getRow();
-					colIndex = -1;
-					iterationStarted = true;
-				}else{
-					row = null;
-					colIndex = -1;
-					endReached = true;
-				}
-				return rowFetched;
-			}catch(IOException e){
-				throw new DataReadException("Unable to read a VOTable row!", e);
-			}
-		}
-
-		@Override
-		public boolean hasNextCol() throws IllegalStateException, DataReadException{
-			// Check the read state:
-			checkReadState(iterationStarted, endReached);
-
-			// Determine whether the last column has been reached or not:
-			return (colIndex + 1 < row.length);
-		}
-
-		@Override
-		public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
-			// Check the read state and ensure there is still at least one column to read:
-			if (!hasNextCol())
-				throw new NoSuchElementException("No more column to read!");
-
-			// Get the column value:
-			return row[++colIndex];
-		}
-
-		@Override
-		public TAPType getColType() throws IllegalStateException, DataReadException{
-			// Basically check the read state (for rows iteration):
-			checkReadState(iterationStarted, endReached);
-
-			// Check deeper the read state (for columns iteration):
-			if (colIndex < 0)
-				throw new IllegalStateException("No column has yet been read!");
-			else if (colIndex >= colMeta.length)
-				return null;
-
-			// Get the column value:
-			return colMeta[colIndex].getDatatype();
-		}
-
-	}
-
-	/**
-	 * <p>{@link TableIterator} which lets iterate over a VOTable table data part.</p>
-	 * 
-	 * <p>This {@link TableIterator} is only usable by {@link VOTableIterator}.</p>
-	 * 
-	 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
-	 * @version 2.0 (Jun 27, 2014)
-	 * @since 2.0
-	 */
-	private static class DataVOTableIterator implements TableIterator {
-
-		/** Iterator over the rows contained in the VOTable data part. */
-		private final Iterator<Object> data;
-		/** Metadata of all columns identified before the iteration. <i>(In this TableIterator, they are completely provided by {@link VOTableIterator}).</i> */
-		private final TAPColumn[] colMeta;
-
-		/** Iterator over the columns contained in the last read row. */
-		private Iterator<Object> colsIt;
-
-		/** Indicate whether the row iteration has already started. */
-		private boolean iterationStarted = false;
-		/** Indicate whether the last row has already been reached. */
-		private boolean endReached = false;
-		/** Index of the last read column (=0 just after {@link #nextRow()} and before {@link #nextCol()}). */
-		private int colIndex;
-
-		/**
-		 * Build a TableIterator on the given table data part of a VOTable.
-		 * 
-		 * @param dataset		Table data part of a VOTable document.
-		 * @param columnsMeta	Metadata information extracted from the VOTable metadata part.
-		 */
-		public DataVOTableIterator(final SavotTableData dataset, final TAPColumn[] columnsMeta){
-			Collection<Object> trset = dataset.getTRs().getItems();
-			if (trset == null){
-				data = new NullIterator();
-				colMeta = columnsMeta;
-				iterationStarted = true;
-				endReached = true;
-			}else{
-				data = trset.iterator();
-				colMeta = columnsMeta;
-			}
-		}
-
-		@Override
-		public TAPColumn[] getMetadata(){
-			return null;
-		}
-
-		@Override
-		public boolean nextRow() throws DataReadException{
-			if (data.hasNext()){
-				// Go to the next row:
-				SavotTR row = (SavotTR)data.next();
-
-				// Prepare the iteration over its columns:
-				Collection<Object> tdset = row.getTDSet().getItems();
-				if (tdset == null)
-					colsIt = new NullIterator();
-				else
-					colsIt = tdset.iterator();
-
-				colIndex = -1;
-				iterationStarted = true;
-
-				return true;
-			}else{
-				// No more row to read => end of VOTable reached:
-				endReached = true;
-				return false;
-			}
-		}
-
-		@Override
-		public boolean hasNextCol() throws IllegalStateException, DataReadException{
-			// Check the read state:
-			checkReadState(iterationStarted, endReached);
-
-			// Determine whether the last column has been reached or not:
-			return colsIt.hasNext();
-		}
-
-		@Override
-		public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
-			// Check the read state and ensure there is still at least one column to read:
-			if (!hasNextCol())
-				throw new NoSuchElementException("No more column to read!");
-
-			// Get the column value:
-			Object value = ((SavotTD)colsIt.next()).getContent();
-			colIndex++;
-			return value;
-		}
-
-		@Override
-		public TAPType getColType() throws IllegalStateException, DataReadException{
-			// Basically check the read state (for rows iteration):
-			checkReadState(iterationStarted, endReached);
-
-			// Check deeper the read state (for columns iteration):
-			if (colIndex < 0)
-				throw new IllegalStateException("No column has yet been read!");
-			else if (colIndex >= colMeta.length)
-				return null;
+	public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
+		// Check the read state and ensure there is still at least one column to read:
+		if (!hasNextCol())
+			throw new NoSuchElementException("No more field to read!");
 
-			// Get the column value:
-			return colMeta[colIndex].getDatatype();
+		// Get the column value:
+		try{
+			return rowSeq.getCell(colIndex++);
+		}catch(IOException se){
+			throw new DataReadException("Can not read the value of the " + colIndex + "-th field!", se);
 		}
-
 	}
 
-	/**
-	 * Iterator over nothing.
-	 * 
-	 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
-	 * @version 2.0 (06/2014)
-	 * @version 2.0
-	 */
-	private final static class NullIterator implements Iterator<Object> {
-		@Override
-		public boolean hasNext(){
-			return false;
-		}
-
-		@Override
-		public Object next(){
-			return null;
-		}
+	@Override
+	public TAPType getColType() throws IllegalStateException, DataReadException{
+		// Basically check the read state (for rows iteration):
+		checkReadState();
 
-		@Override
-		public void remove(){}
+		// Check deeper the read state (for columns iteration):
+		if (colIndex <= 0)
+			throw new IllegalStateException("No field has yet been read!");
+		else if (colIndex > nbColumns)
+			throw new IllegalStateException("All fields have already been read!");
 
+		// Return the column type:
+		return colMeta[colIndex - 1].getDatatype();
 	}
 
 }
diff --git a/src/tap/formatter/JSONFormat.java b/src/tap/formatter/JSONFormat.java
index 974df0546c6108419ef1489066e7d37d842d039f..1e87e5d17689fc0ee97138d49cf791724d4df1ca 100644
--- a/src/tap/formatter/JSONFormat.java
+++ b/src/tap/formatter/JSONFormat.java
@@ -106,7 +106,7 @@ public class JSONFormat implements OutputFormat {
 			// {
 			out.object();
 
-			// "metadata": {...}
+			// "metadata": [...]
 			out.key("metadata");
 
 			// Write metadata part:
@@ -114,7 +114,7 @@ public class JSONFormat implements OutputFormat {
 
 			writer.flush();
 
-			// "data": {...}
+			// "data": [...]
 			out.key("data");
 
 			// Write the data part:
@@ -229,16 +229,11 @@ public class JSONFormat implements OutputFormat {
 			out.key("description").value(tapCol.getDescription());
 
 		// "datatype": "..."
-		VotType votType = tapCol.getVotType();
+		VotType votType = tapCol.getDatatype().toVotType();
 		out.key("datatype").value(votType.datatype);
 
 		// "arraysize": "..." (if any)
-		if (votType.unlimitedArraysize){
-			if (votType.arraysize > 0)
-				out.key("arraysize").value(votType.arraysize + "*");
-			else
-				out.key("arraysize").value("*");
-		}else if (votType.arraysize > 0)
+		if (votType.arraysize != null)
 			out.key("arraysize").value(votType.arraysize);
 
 		// "xtype": "..." (if any)
diff --git a/src/tap/formatter/OutputFormat.java b/src/tap/formatter/OutputFormat.java
index a13383a1900eb7b6fd62f555b4b149c8091fbee8..a881a2acf2e1dd26a3ae9e232df05612901572b6 100644
--- a/src/tap/formatter/OutputFormat.java
+++ b/src/tap/formatter/OutputFormat.java
@@ -64,7 +64,9 @@ public interface OutputFormat {
 	public String getFileExtension();
 
 	/**
-	 * Formats the given query result and writes it in the given output stream.
+	 * <p>Formats the given query result and writes it in the given output stream.</p>
+	 * 
+	 * <p><i>Note: the given output stream should not be closed at the end of this function. It is up to the called to do it.</i></p>
 	 * 
 	 * @param result		The raw (table) result to format.
 	 * @param output		The output stream (a ServletOutputStream or a stream on a file) in which the formatted result must be written.
diff --git a/src/tap/formatter/VOTableFormat.java b/src/tap/formatter/VOTableFormat.java
index 04ca76a0508114add3e6b4d5cabcfe6e4722f78b..16d5e321ad79819e94586ae4a6298a503b6679cb 100644
--- a/src/tap/formatter/VOTableFormat.java
+++ b/src/tap/formatter/VOTableFormat.java
@@ -20,31 +20,49 @@ package tap.formatter;
  *                       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 tap.ServiceConnection;
 import tap.TAPException;
 import tap.TAPExecutionReport;
-import tap.TAPJob;
+import tap.data.DataReadException;
 import tap.data.TableIterator;
 import tap.metadata.TAPColumn;
 import tap.metadata.TAPType;
 import tap.metadata.TAPType.TAPDatatype;
 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 cds.savot.writer.SavotWriter;
 
 /**
  * <p>Format any given query (table) result into VOTable.</p>
+ * 
  * <p>
- * 	Attributes of the VOTable node are by default set by this class but can be overridden if necessary thanks to the corresponding class attributes:
- * 	{@link #votTableVersion}, {@link #xmlnsXsi}, {@link #xsiNoNamespaceSchemaLocation}, {@link #xsiSchemaLocation} and
- *  {@link #xmlns}.
+ * 	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>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>
+ * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
  * @version 2.0 (07/2014)
  */
@@ -56,11 +74,11 @@ 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;
 
-	protected String votTableVersion = "1.2";
-	protected String xmlnsXsi = "http://www.w3.org/2001/XMLSchema-instance";
-	protected String xsiSchemaLocation = "http://www.ivoa.net/xml/VOTable/v1.2";
-	protected String xsiNoNamespaceSchemaLocation = null;
-	protected String xmlns = "http://www.ivoa.net/xml/VOTable/v1.2";
+	/** 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;
 
 	/**
 	 * Creates a VOTable formatter without format report.
@@ -68,13 +86,36 @@ public class VOTableFormat implements OutputFormat {
 	 * @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>.
-	 * 
-	 * @see #VOTableFormat(ServiceConnection, boolean)
 	 */
 	public VOTableFormat(final ServiceConnection service) throws NullPointerException{
 		this(service, false);
 	}
 
+	/**
+	 * Creates a VOTable formatter.
+	 * 
+	 * @param service				The service to use (for the log and to have some information about the service (particularly: name, description).
+	 * @param votFormat				Format 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, false);
+	}
+
+	/**
+	 * Creates a VOTable formatter.
+	 * 
+	 * @param service				The service to use (for the log and to have some information about the service (particularly: name, description).
+	 * @param votFormat				Format 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{
+		this(service, votFormat, votVersion, false);
+	}
+
 	/**
 	 * Creates a VOTable formatter.
 	 * 
@@ -84,10 +125,27 @@ public class VOTableFormat implements OutputFormat {
 	 * @throws NullPointerException	If the given service connection is <code>null</code>.
 	 */
 	public VOTableFormat(final ServiceConnection service, final boolean logFormatReport) throws NullPointerException{
+		this(service, null, null, logFormatReport);
+	}
+
+	/**
+	 * Creates a VOTable formatter.
+	 * 
+	 * @param service				The service to use (for the log and to have some information about the service (particularly: name, description).
+	 * @param votFormat				Format of the VOTable data part. (TABLEDATA, BINARY, BINARY2 or FITS).
+	 * @param votVersion			Version of the resulting VOTable.
+	 * @param logFormatReport		<code>true</code> to append a format report (start and end date/time) in the log output, <code>false</code> otherwise.
+	 * 
+	 * @throws NullPointerException	If the given service connection is <code>null</code>.
+	 */
+	public VOTableFormat(final ServiceConnection service, final DataFormat votFormat, final VOTableVersion votVersion, final boolean logFormatReport) throws NullPointerException{
 		if (service == null)
 			throw new NullPointerException("The given service connection is NULL !");
+
 		this.service = service;
 		this.logFormatReport = logFormatReport;
+		this.votFormat = (votFormat == null) ? DataFormat.BINARY : votFormat;
+		this.votVersion = (votVersion == null) ? VOTableVersion.V13 : votVersion;
 	}
 
 	@Override
@@ -110,87 +168,85 @@ public class VOTableFormat implements OutputFormat {
 		return "xml";
 	}
 
-	/**
-	 * <p>The skeleton of the resulting VOTable is written in this method:</p>
-	 * <ul>
-	 * 	<li>&lt;?xml version="1.0" encoding="UTF-8"&gt;</li>
-	 *	<li><i>{@link #writeHeader(PrintWriter, TAPJob)}</i></li>
-	 *	<li>&lt;TABLE&gt;</li>
-	 *	<li>&lt;DATA&gt;</li>
-	 *	<li><i>{@link #writeData(Object, DBColumn[], OutputStream, TAPJob)}</i></li>
-	 *	<li>&lt;/DATA&gt;</li>
-	 *	<li><i>if (nbRows >= job.getMaxRec()) </i>&lt;INFO name="QUERY_STATUS" value="OVERFLOW" /&gt;</li>
-	 *	<li>&lt;/RESOURCE&gt;</li>
-	 *	<li>&lt;/VOTABLE&gt;</li>
-	 * </ul>
-	 * 
-	 * @see tap.formatter.OutputFormat#writeResult(Object, OutputStream, TAPExecutionReport)
-	 */
 	@Override
 	public final void writeResult(final TableIterator queryResult, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws TAPException, InterruptedException{
 		try{
 			long start = System.currentTimeMillis();
 
-			PrintWriter out = new PrintWriter(output);
-			out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
-			writeHeader(out, execReport);
-			out.println("\t\t<TABLE>");
-			DBColumn[] columns = writeMetadata(queryResult, out, execReport, thread);
-			out.println("\t\t\t<DATA>");
+			ColumnInfo[] colInfos = toColumnInfos(queryResult, execReport, thread);
+
+			/* Turns the result set into a table. */
+			LimitedStarTable table = new LimitedStarTable(queryResult, colInfos, execReport.parameters.getMaxRec());
+
+			/* 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);
+
+			/* Write table element. */
+			voser.writeInlineTableElement(out);
 			out.flush();
-			int nbRows = writeData(queryResult, columns, output, execReport, thread);
-			output.flush();
-			out.println("\t\t\t</DATA>");
-			out.println("\t\t</TABLE>");
-			// OVERFLOW ?
-			if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec())
-				out.println("\t\t<INFO name=\"QUERY_STATUS\" value=\"OVERFLOW\" />");
-			out.println("\t</RESOURCE>");
-			out.println("</VOTABLE>");
+
+			/* 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();
 
 			if (logFormatReport)
-				service.getLogger().info("JOB " + execReport.jobID + " WRITTEN\tResult formatted (in VOTable ; " + nbRows + " rows ; " + columns.length + " columns) in " + (System.currentTimeMillis() - start) + " ms !");
+				service.getLogger().info("JOB " + execReport.jobID + " WRITTEN\tResult formatted (in VOTable ; " + table.getNbReadRows() + " rows ; " + table.getColumnCount() + " columns) in " + (System.currentTimeMillis() - start) + " ms !");
 		}catch(IOException ioe){
 			throw new TAPException("Error while writing a query result in VOTable !", ioe);
 		}
 	}
 
 	/**
-	 * <p>Writes the root node of the VOTable: &lt;VOTABLE&gt;.</p>
-	 * <p>
-	 * 	Attributes of this node are written thanks to their corresponding attributes in this class:
-	 * 	{@link #votTableVersion}, {@link #xmlnsXsi}, {@link #xsiNoNamespaceSchemaLocation}, {@link #xsiSchemaLocation} and {@link #xmlns}.
-	 * 	They are written only if different from <code>null</code>.
-	 * </p>
+	 * <p>Writes the first VOTable nodes/elements preceding the data: VOTABLE, RESOURCE and 3 INFOS (QUERY_STATUS, PROVIDER, QUERY).</p>
 	 * 
-	 * @param output		Writer in which the root node must be written.
+	 * @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 PrintWriter output, final TAPExecutionReport execReport) throws IOException, TAPException{
-		StringBuffer strBuf = new StringBuffer("<VOTABLE");
-		if (votTableVersion != null)
-			strBuf.append(" version=\"").append(SavotWriter.encodeAttribute(votTableVersion)).append('\"');
-		if (xmlnsXsi != null)
-			strBuf.append(" xmlns:xsi=\"").append(SavotWriter.encodeAttribute(xmlnsXsi)).append('\"');
-		if (xsiSchemaLocation != null)
-			strBuf.append(" xsi:schemaLocation=\"").append(SavotWriter.encodeAttribute(xsiSchemaLocation)).append('\"');
-		if (xsiNoNamespaceSchemaLocation != null)
-			strBuf.append(" xsi:noNamespaceSchemaLocation=\"").append(SavotWriter.encodeAttribute(xsiNoNamespaceSchemaLocation)).append('\"');
-		if (xmlns != null)
-			strBuf.append(" xmlns=\"").append(SavotWriter.encodeAttribute(xmlns)).append('\"');
-		strBuf.append('>');
-		output.println(strBuf);
-
-		output.println("\t<RESOURCE type=\"results\">");
-
-		// INFO items:
-		output.println("\t\t<INFO name=\"QUERY_STATUS\" value=\"OK\" />");
-		output.println("\t\t<INFO name=\"PROVIDER\" value=\"" + ((service.getProviderName() == null) ? "" : SavotWriter.encodeAttribute(service.getProviderName())) + "\">" + ((service.getProviderDescription() == null) ? "" : SavotWriter.encodeElement(service.getProviderDescription())) + "</INFO>");
-		output.println("\t\t<INFO name=\"QUERY\"><![CDATA[" + execReport.parameters.getQuery() + "]]></INFO>");
+	protected void writeHeader(final VOTableVersion votVersion, final TAPExecutionReport execReport, final BufferedWriter out) throws IOException, TAPException{
+		// Set the root VOTABLE node:
+		out.write("<VOTABLE" + VOSerializer.formatAttribute("version", votVersion.getVersionNumber()) + VOSerializer.formatAttribute("xmlns", votVersion.getXmlNamespace()) + ">");
+		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.formatText(adqlQuery) + "</INFO>");
+			out.newLine();
+		}
+
+		out.flush();
 	}
 
 	/**
@@ -202,13 +258,13 @@ public class VOTableFormat implements OutputFormat {
 	 * @param execReport	The report of the query execution.
 	 * @param thread		The thread which asked for the result writing.
 	 * 
-	 * @return				Extracted field's metadata.
+	 * @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 in the given Writer.
 	 * @throws TAPException				If there is any other error.
 	 * @throws InterruptedException		If the given thread has been interrupted.
 	 */
-	protected DBColumn[] writeMetadata(final TableIterator result, final PrintWriter output, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException{
+	protected 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;
 
@@ -218,6 +274,9 @@ public class VOTableFormat implements OutputFormat {
 		int indField = 0;
 		if (columnsFromQuery != null){
 
+			// Initialize the resulting array:
+			ColumnInfo[] colInfos = new ColumnInfo[columnsFromQuery.length];
+
 			// For each column:
 			for(DBColumn field : columnsFromQuery){
 
@@ -225,20 +284,18 @@ public class VOTableFormat implements OutputFormat {
 				TAPColumn colFromResult = (columnsFromResult != null && indField < columnsFromResult.length) ? columnsFromResult[indField] : null;
 				TAPColumn tapCol = getValidColMeta(field, colFromResult);
 
-				// Ensure these metadata are well returned at the end of this function:
-				columnsFromQuery[indField] = tapCol;
+				// Build the corresponding ColumnInfo object:
+				colInfos[indField] = getColumnInfo(tapCol);
 
-				// Write the field/column metadata in the JSON output:
-				writeFieldMeta(tapCol, output);
 				indField++;
 
 				if (thread.isInterrupted())
 					throw new InterruptedException();
 			}
-		}else
-			output.println("<INFO name=\"WARNING\" value=\"MISSING_META\">Error while getting field(s) metadata</INFO>");
 
-		return columnsFromQuery;
+			return colInfos;
+		}else
+			return null;
 	}
 
 	/**
@@ -249,7 +306,7 @@ public class VOTableFormat implements OutputFormat {
 	 * 
 	 * @return	The most appropriate metadata.
 	 */
-	protected TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){
+	protected final TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){
 		if (typeFromQuery != null && typeFromQuery instanceof TAPColumn)
 			return (TAPColumn)typeFromQuery;
 		else if (typeFromResult != null){
@@ -262,173 +319,238 @@ public class VOTableFormat implements OutputFormat {
 	}
 
 	/**
-	 * <p>Formats in a VOTable field and writes the given {@link TAPColumn} in the given Writer.</p>
+	 * Convert the given {@link TAPColumn} object into a {@link ColumnInfo} object.
 	 * 
-	 * <p><i><u>Note:</u> If the VOTable datatype is <code>int</code>, <code>short</code> or <code>long</code> a NULL values is set by adding a node VALUES: &lt;VALUES null="..." /&gt;</i></p>
+	 * @param tapCol	{@link TAPColumn} to convert into {@link ColumnInfo}.
 	 * 
-	 * @param col				The column metadata to format into a VOTable field.
-	 * @param out				The stream in which the formatted column metadata must be written.
-	 * 
-	 * @throws IOException		If there is an error while writing the field metadata.
-	 * @throws TAPException		If there is any other error (by default: never happen).
+	 * @return	The corresponding {@link ColumnInfo}.
 	 */
-	protected void writeFieldMeta(TAPColumn col, PrintWriter out) throws IOException, TAPException{
-		StringBuffer fieldline = new StringBuffer("\t\t\t");
-
-		// <FIELD ID="..."
-		fieldline.append("<FIELD ID=").append('"').append(SavotWriter.encodeAttribute(col.getADQLName())).append('"');
-
-		// name="..."
-		fieldline.append(" name=").append('"').append(SavotWriter.encodeAttribute(col.getADQLName())).append('"');
-
-		// datatype="..."
-		VotType type = col.getVotType();
-		String nullVal = getNullValue(type.datatype);
-		fieldline.append(' ').append(type.toString());
-
-		// ucd="..." (if any)
-		if (col.getUcd() != null && col.getUcd().length() > 0)
-			fieldline.append(" ucd=").append('"').append(SavotWriter.encodeAttribute(col.getUcd())).append('"');
-
-		// utype="..." (if any)
-		if (col.getUtype() != null && col.getUtype().length() > 0)
-			fieldline.append(" utype=").append('"').append(SavotWriter.encodeAttribute(col.getUtype())).append('"');
-
-		// unit="..." (if any)
-		if (col.getUnit() != null && col.getUnit().length() > 0)
-			fieldline.append(" unit=").append('"').append(SavotWriter.encodeAttribute(col.getUnit())).append('"');
-
-		// Get the description (or NULL if none is provided):
-		String description = null;
-		if (col.getDescription() != null && !col.getDescription().trim().isEmpty())
-			description = col.getDescription().trim();
-		else
-			description = null;
-
-		// Declares NULL values and write the description:
-		if (nullVal != null || description != null){
-			fieldline.append(">\n");
-
-			// <VALUES null="..." /> (if needed)
-			if (nullVal != null)
-				fieldline.append("<VALUES null=\"" + nullVal + "\" />\n");
-
-			// <DESCRIPTION>...</DESCRIPTION> (if any)
-			if (description != null)
-				fieldline.append("<DESCRIPTION>").append(SavotWriter.encodeElement(description)).append("</DESCRIPTION>\n");
-
-			// </FIELD>
-			fieldline.append("</FIELD>");
-			out.println(fieldline);
-		}else{
-			fieldline.append("/>");
-			out.println(fieldline);
-		}
+	protected ColumnInfo getColumnInfo(final TAPColumn tapCol){
+		// Get the VOTable type:
+		VotType votType = tapCol.getDatatype().toVotType();
+
+		// 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;
 	}
 
 	/**
-	 * Write the whole data part (TABLEDATA) of the VOTable file.
-	 * 
-	 * @param result			The query result which contains the data to write.
-	 * @param selectedColumns	The columns selected by the query.
-	 * @param output			The stream in which the data must be written.
-	 * @param execReport		The report of the query execution (which contains the maximum allowed number of records to output).
-	 * @param thread			The thread which asked for the result writing.
+	 * Convert the VOTable datatype string into a corresponding {@link Class} object.
 	 * 
-	 * @return	The number of written rows. (<i>note: if this number is greater than the value of MAXREC: OVERFLOW</i>)
+	 * @param datatype	Value of the VOTable attribute "datatype". 
+	 * @param arraysize	Value of the VOTable attribute "arraysize".
 	 * 
-	 * @throws IOException				If there is an error while writing the data in the given stream.
-	 * @throws InterruptedException		If the given thread has been interrupted.
-	 * @throws TAPException				If there is any other error.
+	 * @return	The corresponding {@link Class} object.
 	 */
-	protected int writeData(final TableIterator result, final DBColumn[] selectedColumns, final OutputStream output, final TAPExecutionReport execReport, final Thread thread) throws IOException, TAPException, InterruptedException{
-		// <TABLEDATA>
-		output.write("\t\t\t\t<TABLEDATA>\n".getBytes());
-
-		int nbRows = 0;
-		while(result.nextRow()){
-			// Deal with OVERFLOW, if needed:
-			if (execReport.parameters.getMaxRec() > 0 && nbRows >= execReport.parameters.getMaxRec())
-				break;
-
-			// <TR>
-			output.write("\t\t\t\t\t<TR>\n".getBytes());
-			int indCol = 0;
-			while(result.hasNextCol()){
-				// <TD>
-				output.write("\t\t\t\t\t\t<TD>".getBytes());
-				// ...
-				writeFieldValue(result.nextCol(), selectedColumns[indCol++], output);
-				// </TD>
-				output.write("</TD>\n".getBytes());
-
-				if (thread.isInterrupted())
-					throw new InterruptedException();
-			}
+	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"));
 
-			// </TR>
-			output.write("\t\t\t\t\t</TR>\n".getBytes());
-			nbRows++;
-
-			if (thread.isInterrupted())
-				throw new InterruptedException();
+		// 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 DOUBLE_COMPLEX:
+				return double[].class;
+			case FLOAT:
+				return isScalar ? Float.class : float[].class;
+			case FLOAT_COMPLEX:
+				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 UNSIGNED_BYTE:
+				return isScalar ? Short.class : short[].class;
+			case CHAR:
+			case UNICODE_CHAR:
+			default: /* If the type is not know (theoretically, never happens), return char[*] by default. */
+				return isScalar ? Character.class : String.class;
 		}
-
-		// </TABLEDATA>
-		output.write("\t\t\t\t</TABLEDATA>\n".getBytes());
-		return nbRows;
 	}
 
 	/**
-	 * <p>Writes the given field value in the given OutputStream.</p>
+	 * Convert the given VOTable arraysize into a {@link ColumnInfo} shape.
 	 * 
-	 * <p>
-	 * 	The given value will be encoded as an XML element (see {@link SavotWriter#encodeElement(String)}.
-	 * 	Besides, if the given value is <code>null</code> and if the column datatype is <code>int</code>,
-	 * 	<code>short</code> or <code>long</code>, the NULL values declared in the field metadata will be written.
-	 * </p>
-	 * 
-	 * @param value				The value to write.
-	 * @param column			The corresponding column metadata.
-	 * @param output			The stream in which the field value must be written.
+	 * @param arraysize	Value of the VOTable attribute "arraysize".
 	 * 
-	 * @throws IOException		If there is an error while writing the given field value in the given stream.
-	 * @throws TAPException		If there is any other error (by default: never happen).
+	 * @return	The corresponding {@link ColumnInfo} shape.
 	 */
-	protected void writeFieldValue(final Object value, final DBColumn column, final OutputStream output) throws IOException, TAPException{
-		String fieldValue = (value == null) ? null : value.toString();
-		if (fieldValue == null && column instanceof TAPColumn)
-			fieldValue = getNullValue(((TAPColumn)column).getVotType().datatype);
-		if (fieldValue != null)
-			output.write(SavotWriter.encodeElement(fieldValue).getBytes());
+	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>Gets the NULL value corresponding to the given datatype:</p>
-	 * <ul>
-	 * 	<li>for <code>int</code>: {@link Integer#MIN_VALUE}</li>
-	 * 	<li>for <code>short</code>: {@link Short#MIN_VALUE}</li>
-	 * 	<li>for <code>long</code>: {@link Long#MIN_VALUE}</li>
-	 * 	<li>for anything else, <code>null</code> will be returned.</li>
-	 * </ul>
-	 * 
-	 * @param datatype	A VOTable datatype.
+	 * <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>
 	 * 
-	 * @return			The corresponding NULL value, or <code>null</code> if there is none.
+	 * @author Gr&eacute;gory Mantelet (CDS;ARI)
+	 * @version 2.0 (07/2014)
+	 * @since 2.0
 	 */
-	public static final String getNullValue(VotDatatype datatype){
-		if (datatype == null)
-			return null;
+	private 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;
+
+		/** 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>
+		 */
+		LimitedStarTable(final TableIterator tableIt, final ColumnInfo[] colInfos, final long maxrec){
+			this.tableIt = tableIt;
+			nbCol = colInfos.length;
+			columnInfos = colInfos;
+			this.maxrec = maxrec;
+			overflow = false;
+		}
 
-		switch(datatype){
-			case SHORT:
-				return "" + Short.MIN_VALUE;
-			case INT:
-				return "" + Integer.MIN_VALUE;
-			case LONG:
-				return "" + Long.MIN_VALUE;
-			default:
-				return null;
+		/**
+		 * 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 (maxrec <= 0 || irow < maxrec){
+							boolean hasNext = tableIt.nextRow();
+							if (hasNext){
+								for(int i = 0; i < nbCol && tableIt.hasNextCol(); i++)
+									row[i] = tableIt.nextCol();
+							}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{}
+			};
 		}
 	}
 }
diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java
index 2fca36dc6a8001457cf77d0766a95c18ce1843a4..9b91e8593410fb1337ef28dde9a44f7e6e984a2a 100644
--- a/src/tap/metadata/TAPColumn.java
+++ b/src/tap/metadata/TAPColumn.java
@@ -44,8 +44,6 @@ public class TAPColumn implements DBColumn {
 
 	private TAPType datatype = new TAPType(TAPDatatype.VARCHAR);
 
-	private VotType votType = datatype.toVotType();
-
 	private boolean principal = false;
 
 	private boolean indexed = false;
@@ -194,14 +192,6 @@ public class TAPColumn implements DBColumn {
 	 */
 	public final void setDatatype(final TAPType type){
 		datatype = type;
-		votType = datatype.toVotType();
-	}
-
-	/**
-	 * @return The votType.
-	 */
-	public final VotType getVotType(){
-		return votType;
 	}
 
 	/**
diff --git a/src/tap/metadata/TAPType.java b/src/tap/metadata/TAPType.java
index a36c1c3dee169a161810b4d1e34bd1e1866b2443..90643543d9eee044730bb105c6b3984c1f129a33 100644
--- a/src/tap/metadata/TAPType.java
+++ b/src/tap/metadata/TAPType.java
@@ -19,8 +19,6 @@ package tap.metadata;
  * Copyright 2014 - Astronomishes Rechen Institute (ARI)
  */
 
-import tap.metadata.VotType.VotDatatype;
-
 /**
  * 
  * <p>
@@ -86,10 +84,10 @@ public class TAPType {
 	 * 
 	 * @return	The corresponding VOTable field type.
 	 * 
-	 * @see #convertIntoVotType(TAPType)
+	 * @see VotType#VotType(TAPType)
 	 */
 	public VotType toVotType(){
-		return convertIntoVotType(this);
+		return new VotType(this);
 	}
 
 	@Override
@@ -100,158 +98,4 @@ public class TAPType {
 			return type.toString();
 	}
 
-	/**
-	 * Convert the given TAP column type into a VOTable field type.
-	 * 
-	 * @param taptype	The TAP column type to convert.
-	 * 
-	 * @return	The corresponding VOTable field type.
-	 */
-	public static VotType convertIntoVotType(final TAPType taptype){
-		VotType vot = new VotType(VotDatatype.CHAR, VotType.NO_SIZE, false);
-
-		switch(taptype.type){
-			case SMALLINT:
-				vot = new VotType(VotDatatype.SHORT, 1, false);
-				break;
-
-			case INTEGER:
-				vot = new VotType(VotDatatype.INT, 1, false);
-				break;
-
-			case BIGINT:
-				vot = new VotType(VotDatatype.LONG, 1, false);
-				break;
-
-			case REAL:
-				vot = new VotType(VotDatatype.FLOAT, 1, false);
-				break;
-
-			case DOUBLE:
-				vot = new VotType(VotDatatype.DOUBLE, 1, false);
-				break;
-
-			case CHAR:
-				vot = new VotType(VotDatatype.CHAR, (taptype.length > 0 ? taptype.length : 1), false);
-				break;
-
-			case BINARY:
-				vot = new VotType(VotDatatype.UNSIGNED_BYTE, (taptype.length > 0 ? taptype.length : VotType.NO_SIZE), false);
-				break;
-
-			case VARBINARY:
-				vot = new VotType(VotDatatype.UNSIGNED_BYTE, (taptype.length > 0 ? taptype.length : VotType.NO_SIZE), (taptype.length > 0));
-				break;
-
-			case BLOB:
-				vot = new VotType(VotDatatype.UNSIGNED_BYTE, VotType.NO_SIZE, true, VotType.XTYPE_BLOB);
-				break;
-
-			case CLOB:
-				vot = new VotType(VotDatatype.CHAR, VotType.NO_SIZE, true, VotType.XTYPE_CLOB);
-				break;
-
-			case TIMESTAMP:
-				vot = new VotType(VotDatatype.CHAR, VotType.NO_SIZE, true, VotType.XTYPE_TIMESTAMP);
-				break;
-
-			case POINT:
-				vot = new VotType(VotDatatype.CHAR, VotType.NO_SIZE, true, VotType.XTYPE_POINT);
-				break;
-
-			case REGION:
-				vot = new VotType(VotDatatype.CHAR, VotType.NO_SIZE, true, VotType.XTYPE_REGION);
-				break;
-
-			case VARCHAR:
-			default:
-				vot = new VotType(VotDatatype.CHAR, (taptype.length > 0 ? taptype.length : VotType.NO_SIZE), (taptype.length > 0), null);
-				break;
-		}
-
-		return vot;
-	}
-
-	/**
-	 * Convert the given VOTable field type into a TAP column type.
-	 * 
-	 * @param vottype	The VOTable field type to convert.
-	 * 
-	 * @return	The corresponding TAP column type.
-	 */
-	public static TAPType convertFromVotType(final VotType vottype){
-		if (vottype == null)
-			return new TAPType(TAPDatatype.VARCHAR);
-
-		switch(vottype.datatype){
-			case SHORT:
-			case BOOLEAN:
-				if ((vottype.arraysize <= 1 || vottype.arraysize == VotType.NO_SIZE) && !vottype.unlimitedArraysize)
-					return new TAPType(TAPDatatype.SMALLINT);
-				else
-					return new TAPType(TAPDatatype.VARBINARY);
-
-			case INT:
-				if ((vottype.arraysize <= 1 || vottype.arraysize == VotType.NO_SIZE) && !vottype.unlimitedArraysize)
-					return new TAPType(TAPDatatype.INTEGER);
-				else
-					return new TAPType(TAPDatatype.VARBINARY);
-
-			case LONG:
-				if ((vottype.arraysize <= 1 || vottype.arraysize == VotType.NO_SIZE) && !vottype.unlimitedArraysize)
-					return new TAPType(TAPDatatype.BIGINT);
-				else
-					return new TAPType(TAPDatatype.VARBINARY);
-
-			case FLOAT:
-				if ((vottype.arraysize <= 1 || vottype.arraysize == VotType.NO_SIZE) && !vottype.unlimitedArraysize)
-					return new TAPType(TAPDatatype.REAL);
-				else
-					return new TAPType(TAPDatatype.VARBINARY);
-
-			case DOUBLE:
-				if ((vottype.arraysize <= 1 || vottype.arraysize == VotType.NO_SIZE) && !vottype.unlimitedArraysize)
-					return new TAPType(TAPDatatype.DOUBLE);
-				else
-					return new TAPType(TAPDatatype.VARBINARY);
-
-			case UNSIGNED_BYTE:
-				if (vottype.arraysize > 0){
-					if (vottype.unlimitedArraysize)
-						return new TAPType(TAPDatatype.VARBINARY, vottype.arraysize);
-					else
-						return new TAPType(TAPDatatype.BINARY, vottype.arraysize);
-				}else
-					return new TAPType(TAPDatatype.VARBINARY);
-
-			case CHAR:
-			default:
-				TAPType taptype = null;
-				if (vottype.xtype != null && vottype.xtype.trim().length() > 0){
-					if (vottype.xtype.equalsIgnoreCase(VotType.XTYPE_BLOB))
-						taptype = new TAPType(TAPDatatype.BLOB);
-					else if (vottype.xtype.equalsIgnoreCase(VotType.XTYPE_CLOB))
-						taptype = new TAPType(TAPDatatype.CLOB);
-					else if (vottype.xtype.equalsIgnoreCase(VotType.XTYPE_TIMESTAMP))
-						taptype = new TAPType(TAPDatatype.TIMESTAMP);
-					else if (vottype.xtype.equalsIgnoreCase(VotType.XTYPE_POINT))
-						taptype = new TAPType(TAPDatatype.POINT);
-					else if (vottype.xtype.equalsIgnoreCase(VotType.XTYPE_REGION))
-						taptype = new TAPType(TAPDatatype.REGION);
-				}
-				if (taptype == null){
-					if (vottype.unlimitedArraysize)
-						taptype = new TAPType(TAPDatatype.VARCHAR, (vottype.arraysize > 0) ? vottype.arraysize : NO_LENGTH);
-					else{
-						if (vottype.arraysize <= 0 || vottype.arraysize == VotType.NO_SIZE)
-							taptype = new TAPType(TAPDatatype.VARCHAR);
-						else if (vottype.arraysize == 1)
-							taptype = new TAPType(TAPDatatype.CHAR, 1);
-						else
-							taptype = new TAPType(TAPDatatype.CHAR, vottype.arraysize);
-					}
-				}
-				return taptype;
-		}
-	}
 }
diff --git a/src/tap/metadata/VotType.java b/src/tap/metadata/VotType.java
index 000f5f81add035c5f1f6a8acce2c22c719726a30..16332eb9be6938434cbfc8a7012f23271af93eee 100644
--- a/src/tap/metadata/VotType.java
+++ b/src/tap/metadata/VotType.java
@@ -20,7 +20,9 @@ package tap.metadata;
  *                       Astronomishes Rechen Institute (ARI)
  */
 
-import cds.savot.writer.SavotWriter;
+import tap.TAPException;
+import tap.metadata.TAPType.TAPDatatype;
+import uk.ac.starlink.votable.VOSerializer;
 
 /**
  * <p>Describes a full VOTable type. Thus it includes the following field attributes:</p>
@@ -72,18 +74,11 @@ public final class VotType {
 	 * @since 2.0 */
 	public final static String XTYPE_REGION = "adql:REGION";
 
-	/** No array size.
-	 * @since 2.0 */
-	public static final int NO_SIZE = -1;
-
 	/** VOTable datatype
 	 * @since 2.0 */
 	public final VotDatatype datatype;
-	/** A negative or null value means "*" (that's to say: an undetermined arraysize). */
-	public final int arraysize;
-	/** If true, it means either "n*" (where n is the arraysize when > 0) or "*".
-	 * @since 2.0*/
-	public final boolean unlimitedArraysize;
+	/** Arraysize string of a VOTable field element. */
+	public final String arraysize;
 	/** Special type specification (i.e. POINT, TIMESTAMP, ...). */
 	public final String xtype;
 
@@ -91,28 +86,134 @@ public final class VotType {
 	 * Build a VOTable field type.
 	 * 
 	 * @param datatype		A datatype. <b>Null value forbidden</b>
-	 * @param arraysize		A non-null positive integer. (any value &le; 0 will be considered as an undetermined arraysize, that's to say {@link #NO_SIZE}).
-	 * @param unlimitedSize	Indicate whether a * must be appended at the end of the arraysize attribute (so in these 2 cases: "n*" or "*").
+	 * @param arraysize		VOTable arraysize string (<i>may be NULL</i>).
 	 */
-	public VotType(final VotDatatype datatype, final int arraysize, final boolean unlimitedSize){
-		this(datatype, arraysize, unlimitedSize, null);
+	public VotType(final VotDatatype datatype, final String arraysize){
+		this(datatype, arraysize, null);
 	}
 
 	/**
 	 * Build a VOTable field type.
 	 * 
 	 * @param datatype		A datatype. <b>Null value forbidden</b>
-	 * @param arraysize		A non-null positive integer. (any value &le; 0 will be considered as an undetermined arraysize, that's to say {@link #NO_SIZE}).
-	 * @param unlimitedSize	Indicate whether a * must be appended at the end of the arraysize attribute (so in these 2 cases: "n*" or "*").
-	 * @param xtype			A special type (ex: adql:POINT, adql:TIMESTAMP, ...). Null value allowed.
+	 * @param arraysize		VOTable arraysize string (<i>may be NULL</i>).
+	 * @param xtype			A special type (ex: adql:POINT, adql:TIMESTAMP, ...). (<i>may be NULL</i>).
 	 */
-	public VotType(final VotDatatype datatype, final int arraysize, final boolean unlimitedSize, final String xtype){
+	public VotType(final VotDatatype datatype, final String arraysize, final String xtype){
+		// set the datatype:
 		if (datatype == null)
-			throw new NullPointerException("Missing VOTable datatype !");
-		this.datatype = datatype;
-		this.arraysize = (arraysize > 0) ? arraysize : NO_SIZE;
-		this.unlimitedArraysize = unlimitedSize;
-		this.xtype = xtype;
+			throw new NullPointerException("missing VOTable datatype !");
+		else
+			this.datatype = datatype;
+
+		// set the array-size:
+		if (arraysize != null && arraysize.trim().length() > 0)
+			this.arraysize = arraysize.trim();
+		else
+			this.arraysize = null;
+
+		// set the xtype:
+		if (xtype != null && xtype.trim().length() > 0)
+			this.xtype = xtype.trim();
+		else
+			this.xtype = null;
+	}
+
+	/**
+	 * Build a {@link VotType} object by converting the given {@link TAPType}.
+	 * 
+	 * @param tapType	{@link TAPType} to convert.
+	 */
+	public VotType(final TAPType tapType){
+		switch(tapType.type){
+			case SMALLINT:
+				this.datatype = VotDatatype.SHORT;
+				this.arraysize = "1";
+				this.xtype = null;
+				break;
+
+			case INTEGER:
+				this.datatype = VotDatatype.INT;
+				this.arraysize = "1";
+				this.xtype = null;
+				break;
+
+			case BIGINT:
+				this.datatype = VotDatatype.LONG;
+				this.arraysize = "1";
+				this.xtype = null;
+				break;
+
+			case REAL:
+				this.datatype = VotDatatype.FLOAT;
+				this.arraysize = "1";
+				this.xtype = null;
+				break;
+
+			case DOUBLE:
+				this.datatype = VotDatatype.DOUBLE;
+				this.arraysize = "1";
+				this.xtype = null;
+				break;
+
+			case CHAR:
+				this.datatype = VotDatatype.CHAR;
+				this.arraysize = Integer.toString(tapType.length > 0 ? tapType.length : 1);
+				this.xtype = null;
+				break;
+
+			case BINARY:
+				this.datatype = VotDatatype.UNSIGNED_BYTE;
+				this.arraysize = Integer.toString(tapType.length > 0 ? tapType.length : 1);
+				this.xtype = null;
+				break;
+
+			case VARBINARY:
+				/* TODO HOW TO MANAGE VALUES WHICH WHERE ORIGINALLY NUMERIC ARRAYS ?
+				 * (cf the IVOA document TAP#Upload: votable numeric arrays should be converted into VARBINARY...no more array information and particularly the datatype)
+				 */
+				this.datatype = VotDatatype.UNSIGNED_BYTE;
+				this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*");
+				this.xtype = null;
+				break;
+
+			case BLOB:
+				this.datatype = VotDatatype.UNSIGNED_BYTE;
+				this.arraysize = "*";
+				this.xtype = VotType.XTYPE_BLOB;
+				break;
+
+			case CLOB:
+				this.datatype = VotDatatype.CHAR;
+				this.arraysize = "*";
+				this.xtype = VotType.XTYPE_CLOB;
+				break;
+
+			case TIMESTAMP:
+				this.datatype = VotDatatype.CHAR;
+				this.arraysize = "*";
+				this.xtype = VotType.XTYPE_TIMESTAMP;
+				break;
+
+			case POINT:
+				this.datatype = VotDatatype.CHAR;
+				this.arraysize = "*";
+				this.xtype = VotType.XTYPE_POINT;
+				break;
+
+			case REGION:
+				this.datatype = VotDatatype.CHAR;
+				this.arraysize = "*";
+				this.xtype = VotType.XTYPE_REGION;
+				break;
+
+			case VARCHAR:
+			default:
+				this.datatype = VotDatatype.CHAR;
+				this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*");
+				this.xtype = null;
+				break;
+		}
 	}
 
 	@Override
@@ -134,19 +235,14 @@ public final class VotType {
 
 	@Override
 	public String toString(){
-		StringBuffer str = new StringBuffer("datatype=\"");
-		str.append(datatype).append('"');
+		StringBuffer str = new StringBuffer(VOSerializer.formatAttribute("datatype", datatype.toString()));
+		str.deleteCharAt(0);
 
-		if (arraysize > 0){
-			str.append(" arraysize=\"").append(SavotWriter.encodeAttribute("" + arraysize));
-			if (unlimitedArraysize)
-				str.append("*");
-			str.append('"');
-		}else if (unlimitedArraysize)
-			str.append(" arraysize=\"*\"");
+		if (arraysize != null)
+			str.append(VOSerializer.formatAttribute("arraysize", arraysize));
 
 		if (xtype != null)
-			str.append(" xtype=\"").append(SavotWriter.encodeAttribute(xtype)).append('"');
+			str.append(VOSerializer.formatAttribute("xtype", xtype));
 
 		return str.toString();
 	}
@@ -155,9 +251,126 @@ public final class VotType {
 	 * Convert this VOTable type definition into a TAPColumn type.
 	 * 
 	 * @return	The corresponding {@link TAPType}.
+	 * 
+	 * @throws TAPException	If the conversion is impossible (particularly if the array-size refers to a multi-dimensional array ; only 1D arrays are allowed). 
+	 */
+	public TAPType toTAPType() throws TAPException{
+
+		/* Stop immediately if the arraysize refers to a multi-dimensional array:
+		 * (Note: 'x' is the dimension separator of the VOTable attribute 'arraysize') */
+		if (arraysize != null && arraysize.indexOf('x') >= 0)
+			throw new TAPException("failed conversion of a VOTable datatype: multi-dimensional arrays (" + datatype + "[" + arraysize + "]) are not allowed!");
+
+		// Convert the VOTable datatype into TAP datatype:
+		switch(datatype){
+		/* NUMERIC TYPES */
+			case SHORT:
+			case BOOLEAN:
+				return convertNumericType(TAPDatatype.SMALLINT);
+
+			case INT:
+				return convertNumericType(TAPDatatype.INTEGER);
+
+			case LONG:
+				return convertNumericType(TAPDatatype.BIGINT);
+
+			case FLOAT:
+				return convertNumericType(TAPDatatype.REAL);
+
+			case DOUBLE:
+				return convertNumericType(TAPDatatype.DOUBLE);
+
+				/* BINARY TYPES */
+			case UNSIGNED_BYTE:
+				// BLOB exception:
+				if (xtype != null && xtype.equalsIgnoreCase(XTYPE_BLOB))
+					return new TAPType(TAPDatatype.BLOB);
+
+				// Or else, just (var)binary:
+				else
+					return convertVariableLengthType(TAPDatatype.VARBINARY, TAPDatatype.BINARY);
+
+				/* CHARACTER TYPES */
+			case CHAR:
+			default:
+				/* Special type cases: */
+				if (xtype != null){
+					if (xtype.equalsIgnoreCase(VotType.XTYPE_CLOB))
+						return new TAPType(TAPDatatype.CLOB);
+					else if (xtype.equalsIgnoreCase(VotType.XTYPE_TIMESTAMP))
+						return new TAPType(TAPDatatype.TIMESTAMP);
+					else if (xtype.equalsIgnoreCase(VotType.XTYPE_POINT))
+						return new TAPType(TAPDatatype.POINT);
+					else if (xtype.equalsIgnoreCase(VotType.XTYPE_REGION))
+						return new TAPType(TAPDatatype.REGION);
+				}
+
+				// Or if not known or missing, just a (var)char:
+				return convertVariableLengthType(TAPDatatype.VARCHAR, TAPDatatype.CHAR);
+		}
+	}
+
+	/**
+	 * <p>Convert this numeric {@link VotType} object into a corresponding {@link TAPType} whose the datatype is provided in parameter.</p>
+	 * 
+	 * <p>
+	 * 	Thus, just the arraysize must be managed here. If there is no arraysize or if equals to '1', the given datatype will be used.
+	 * 	Otherwise, it is ignored and a {@link TAPType} with VARBINARY is returned.
+	 * </p>
+	 * 
+	 * @param tapDatatype	TAP datatype corresponding to this {@link VotType} (only when arraysize != '*' and 'n').
+	 * 
+	 * @return	The corresponding {@link TAPType}.
+	 */
+	protected TAPType convertNumericType(final TAPDatatype tapDatatype){
+		// If no arraysize:
+		if (arraysize == null || arraysize.equals("1"))
+			return new TAPType(tapDatatype);
+
+		// If only one dimension:
+		else
+			return new TAPType(TAPDatatype.VARBINARY);
+
+		/* Note: The test of multi-dimensional array should have been already done at the beginning of #toTAPType(). */
+	}
+
+	/**
+	 * <p>
+	 * 	Convert this variable length {@link VotType} (unsignedByte and char) object into a corresponding {@link TAPType}
+	 * 	whose the variable length and fixed length versions are given in parameters.
+	 * </p>
+	 * 
+	 * <p>Thus, just the arraysize must be managed here. The following cases are taken into account:</p>
+	 * <ul>
+	 * 	<li><i>No arraysize or '*'</i>: variable length type (i.e. VARCHAR, VARBINARY),</li>
+	 * 	<li><i>'n*'</i>: variable length type with the maximal length (i.e. VARCHAR(n), VARBINARY(n)),</li>
+	 * 	<li><i>'n'</i>: fixed length type with the exact length (i.e. CHAR(n), BINARY(n)).</li>
+	 * </ul>
+	 * 
+	 * @param varType		Variable length type (i.e. VARCHAR, VARBINARY).
+	 * @param fixedType		Fixed length type (i.e. CHAR, BINARY).
+	 * 
+	 * @return	The corresponding {@link TAPType}.
+	 * 
+	 * @throws TAPException	If the arraysize is not valid (that's to say, different from the following syntaxes: NULL, '*', 'n' or 'n*' (where n is a positive and not-null integer)).
 	 */
-	public TAPType toTAPType(){
-		return TAPType.convertFromVotType(this);
+	protected TAPType convertVariableLengthType(final TAPDatatype varType, final TAPDatatype fixedType) throws TAPException{
+		try{
+			// no arraysize or '*' => VARCHAR or VARBINARY
+			if (arraysize == null || arraysize.equals("*"))
+				return new TAPType(varType);
+
+			// 'n*' => VARCHAR(n) or VARBINARY(n)
+			else if (arraysize.charAt(arraysize.length() - 1) == '*')
+				return new TAPType(varType, Integer.parseInt(arraysize.substring(0, arraysize.length() - 1)));
+
+			// 'n' => CHAR(n) or BINARY(n)
+			else
+				return new TAPType(fixedType, Integer.parseInt(arraysize));
+
+		}catch(NumberFormatException nfe){
+			throw new TAPException("failed conversion of a VOTable datatype: non-numeric arraysize (" + arraysize + ")!");
+		}
 	}
 
 }
diff --git a/test/tap/formatter/VOTableFormatTest.java b/test/tap/formatter/VOTableFormatTest.java
index f5f0d174554b2c8cfbad9fa07f4a55b805172663..4cbb8416c078b257d48afa3bdcee25056bc7d9d4 100644
--- a/test/tap/formatter/VOTableFormatTest.java
+++ b/test/tap/formatter/VOTableFormatTest.java
@@ -1,6 +1,6 @@
 package tap.formatter;
 
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
 import java.io.BufferedOutputStream;
@@ -34,6 +34,7 @@ import tap.metadata.TAPType;
 import tap.metadata.TAPType.TAPDatatype;
 import tap.parameters.TAPParameters;
 import testtools.DBTools;
+import uk.ac.starlink.votable.DataFormat;
 import uws.service.UserIdentifier;
 
 /**
@@ -85,17 +86,17 @@ public class VOTableFormatTest {
 
 			TableIterator it = new ResultSetTableIterator(rs);
 
-			VOTableFormat formatter = new VOTableFormat(serviceConn);
+			VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.FITS);
 			OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile));
 			formatter.writeResult(it, output, report, Thread.currentThread());
 			output.close();
 
 			// note: due to the pipe (|), we must call /bin/sh as a command whose the command to execute in is the "grep ... | wc -l":
 			String[] cmd = new String[]{"/bin/sh","-c","grep \"<TR>\" \"" + votableFile.getAbsolutePath() + "\" | wc -l"};
-			assertTrue(executeCommand(cmd).trim().equals("10"));
+			assertEquals("10", executeCommand(cmd).trim());
 
-			cmd = new String[]{"/bin/sh","-c","grep \"<INFO name=\\\"QUERY_STATUS\\\" value=\\\"OVERFLOW\\\" />\" \"" + votableFile.getAbsolutePath() + "\" | wc -l"};
-			assertTrue(executeCommand(cmd).trim().equals("0"));
+			cmd = new String[]{"/bin/sh","-c","grep \"<INFO name=\\\"QUERY_STATUS\\\" value=\\\"OVERFLOW\\\"/>\" \"" + votableFile.getAbsolutePath() + "\" | wc -l"};
+			assertEquals("0", executeCommand(cmd).trim());
 
 		}catch(Exception t){
 			t.printStackTrace();
@@ -117,17 +118,17 @@ public class VOTableFormatTest {
 
 			TableIterator it = new ResultSetTableIterator(rs);
 
-			VOTableFormat formatter = new VOTableFormat(serviceConn);
+			VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.FITS);
 			OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile));
 			formatter.writeResult(it, output, report, Thread.currentThread());
 			output.close();
 
 			// note: due to the pipe (|), we must call /bin/sh as a command whose the command to execute in is the "grep ... | wc -l":
 			String[] cmd = new String[]{"/bin/sh","-c","grep \"<TR>\" \"" + votableFile.getAbsolutePath() + "\" | wc -l"};
-			assertTrue(executeCommand(cmd).trim().equals("5"));
+			assertEquals("5", executeCommand(cmd).trim());
 
-			cmd = new String[]{"/bin/sh","-c","grep \"<INFO name=\\\"QUERY_STATUS\\\" value=\\\"OVERFLOW\\\" />\" \"" + votableFile.getAbsolutePath() + "\" | wc -l"};
-			assertTrue(executeCommand(cmd).trim().equals("1"));
+			cmd = new String[]{"/bin/sh","-c","grep \"<INFO name=\\\"QUERY_STATUS\\\" value=\\\"OVERFLOW\\\"/>\" \"" + votableFile.getAbsolutePath() + "\" | wc -l"};
+			assertEquals("1", executeCommand(cmd).trim());
 
 		}catch(Exception t){
 			t.printStackTrace();