diff --git a/src/tap/metadata/TableSetParser.java b/src/tap/metadata/TableSetParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..987bb0b88129803fc526385782e688427bad7b98
--- /dev/null
+++ b/src/tap/metadata/TableSetParser.java
@@ -0,0 +1,905 @@
+package tap.metadata;
+
+/*
+ * 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 2015 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.xml.sax.helpers.DefaultHandler;
+
+import tap.TAPException;
+import tap.data.VOTableIterator;
+import tap.metadata.TAPTable.TableType;
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
+
+/**
+ * <p>Let parse an XML document representing a table set, and return the corresponding {@link TAPMetadata} instance.</p>
+ * 
+ * <p><i>Note 1: the table set must follow the syntax specified by the XML Schema http://www.ivoa.net/xml/VODataService.</i></p>
+ * <p><i>Note 2: only tags specified by VODataService are checked. If there is any other tag, they are merely ignored.</i></p>
+ * 
+ * <h3>Exceptions</h3>
+ * 
+ * <p>A {@link TAPException} is thrown in the following cases:</p>
+ * <ul>
+ * 	<li>the root node is not "tableset"</li>
+ * 	<li>table name syntax ([schema.]table) is incorrect</li>
+ * 	<li>a single table name (just "table" without schema prefix) is ambiguous (that's to say, the same name is used for tables of different schemas)</li>
+ * 	<li>"name" node is missing in nodes "schema", "table" and "column"</li>
+ * 	<li>"targetTable" is missing in node "foreignKey"</li>
+ * 	<li>"fromColumn" or "targetColumn" is missing in node "fkColumn"</li>
+ * 	<li>"name" node is duplicated in the same node</li>
+ * 	<li>missing "xsi:type" as attribute in a "dataType" node</li>
+ * 	<li>unknown column datatype</li>
+ * </ul>
+ * 
+ * <p><i>Note: catalog prefixes are not supported in this parser.</i></p>
+ * 
+ * <h3>Datatype</h3>
+ * 
+ * <p>
+ * 	A column datatype may be specified either as a TAP or a VOTable datatype. Thus, the type of specification must be given with the attribute xsi:type of the
+ * 	node "dataType". For instance:
+ * </p>
+ * <ul>
+ * 	<li><code>&lt;dataType xsi:type="vs:VOTableType" arraysize="1"&gt;float&lt;/dataType&gt;</code> for a VOTable datatype</li>
+ * 	<li><code>&lt;dataType xsi:type="vod:TAPType"&gt;VARCHAR&lt;/dataType&gt;</code> for a TAP datatype</li>
+ * </ul>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 2.0 (02/2015)
+ * @since 2.0
+ */
+public class TableSetParser extends DefaultHandler {
+
+	/** XML namespace for the XML schema XMLSchema-instance. */
+	protected final static String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance";
+
+	/** XML namespace for the XML schema VODataService. */
+	protected final static String VODATASERVICE_NAMESPACE = "http://www.ivoa.net/xml/VODataService";
+
+	/**
+	 * <p>Intermediary representation of a Foreign Key.</p>
+	 * 
+	 * <p>
+	 * 	An instance of this class lets save all information provided in the XML document and needed to create the corresponding TAP metadata ({@link TAPForeignKey})
+	 * 	at the end of XML document parsing, once all available tables are listed.
+	 * </p>  
+	 * 
+	 * @author Gr&eacute;gory Mantelet (ARI)
+	 * @version 2.0 (02/2015)
+	 * @since 2.0
+	 * 
+	 * @see TableSetParser#parseFKey(XMLStreamReader)
+	 * @see TableSetParser#parse(InputStream)
+	 */
+	protected static class ForeignKey {
+		/** Foreign key description */
+		public String description = null;
+		/** UType associated with this foreign key. */
+		public String utype = null;
+		/** Source table of the foreign key.
+		 * <i>Note: In the XML document, the foreign key is described inside its table ;
+		 * hence the type of this attribute: TAPTable (it is indeed already known).</i> */
+		public TAPTable fromTable = null;
+		/** Target table of the foreign key. */
+		public String targetTable = null;
+		/** Position of the "targetTable" node inside the XML document.
+		 * <i>Note: this attribute may be used only in case of error.</i> */
+		public String targetTablePosition = "";
+		/** Columns associations.
+		 * Keys are columns of the source table, whereas values are columns of the target table to associate with. */
+		public Map<String,String> keyColumns = new HashMap<String,String>();
+	}
+
+	/**
+	 * Parse the XML TableSet stored in the specified file.
+	 * 
+	 * @param file	The regular file containing the TableSet to parse.
+	 * 
+	 * @return	The corresponding TAP metadata.
+	 * 
+	 * @throws IOException	If any error occurs while reading the given file.
+	 * @throws TAPException	If any error occurs in the XML parsing or in the TAP metadata creation.
+	 * 
+	 * @since {@link #parse(InputStream)}
+	 */
+	public TAPMetadata parse(final File file) throws IOException, TAPException{
+		InputStream input = null;
+		try{
+			input = new BufferedInputStream(new FileInputStream(file));
+			return parse(input);
+		}finally{
+			if (input != null){
+				try{
+					input.close();
+				}catch(IOException ioe2){}
+			}
+		}
+	}
+
+	/**
+	 * Parse the XML TableSet stored in the given stream.
+	 * 
+	 * @param input	The stream containing the TableSet to parse.
+	 * 
+	 * @return	The corresponding TAP metadata.
+	 * 
+	 * @throws IOException	If any error occurs while reading the given stream.
+	 * @throws TAPException	If any error occurs in the XML parsing or in the TAP metadata creation.
+	 * 
+	 * @see #parseSchema(XMLStreamReader, List)
+	 */
+	public TAPMetadata parse(final InputStream input) throws IOException, TAPException{
+		TAPMetadata meta = null;
+
+		XMLInputFactory factory = XMLInputFactory.newInstance();
+		XMLStreamReader reader = null;
+
+		try{
+			// Create the XML streaming reader:
+			reader = factory.createXMLStreamReader(input);
+
+			// Read the first XML tag => MUST BE <tableset> :
+			int event = nextTag(reader);
+			if (event == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equalsIgnoreCase("tableset")){
+
+				// Build the metadata object:
+				meta = new TAPMetadata();
+
+				// Prepare the listing of all foreign keys for a later resolution:
+				ArrayList<ForeignKey> allForeignKeys = new ArrayList<ForeignKey>(20);
+
+				// Read the next XML tag => MUST BE <schema> :
+				while(reader.hasNext() && (event = nextTag(reader)) == XMLStreamConstants.START_ELEMENT){
+					if (reader.getLocalName().equalsIgnoreCase("schema")){
+						// fetch the schema description and content:
+						meta.addSchema(parseSchema(reader, allForeignKeys));
+					}
+				}
+
+				// Read the final XML tag => MUST BE </tableset> :
+				if (event != XMLStreamConstants.END_ELEMENT || !reader.getLocalName().equalsIgnoreCase("tableset")){
+					// throw an error if the tag is not the expected one:
+					throw new TAPException(getPosition(reader) + " XML tag mismatch: <" + (event == XMLStreamConstants.END_ELEMENT ? "/" : "") + reader.getLocalName() + ">! Expected: </tableset>.");
+				}
+
+				// Resolve all ForeignKey objects into TAPForeignKeys and add them into the dedicated TAPTable:
+				long keyId = 0;
+				for(ForeignKey fk : allForeignKeys){
+					// search for the target table:
+					TAPTable targetTable = searchTable(fk.targetTable, meta, fk.targetTablePosition);
+					// build and add the foreign key:
+					fk.fromTable.addForeignKey("" + (++keyId), targetTable, fk.keyColumns, fk.description, fk.utype);
+				}
+
+			}else
+				throw new TAPException(getPosition(reader) + " Missing root tag: \"tableset\"!");
+
+		}catch(XMLStreamException xse){
+			throw new TAPException(getPosition(reader) + " XML ERROR: " + xse.getMessage() + "!", xse);
+		}
+
+		return meta;
+	}
+
+	/* **************************** */
+	/* INDIVIDUAL PARSING FUNCTIONS */
+	/* **************************** */
+
+	/**
+	 * <p>Parse the XML representation of a TAP schema.</p>
+	 * 
+	 * <p><b>Important: This function MUST be called just after the start element "schema" has been read!</b></p>
+	 * 
+	 * <h3>Attributes</h3>
+	 * 
+	 * <p>No attribute is expected in the start element "schema".</p>
+	 * 
+	 * <h3>Children</h3>
+	 * 
+	 * Only the following nodes are taken into account ; the others are ignored:
+	 * <ul>
+	 * 	<li>name <i>REQUIRED</i></li>
+	 * 	<li>description <i>{0..1}</i></li>
+	 * 	<li>title <i>{0..1}</i></li>
+	 * 	<li>utype <i>{0..1}</i></li>
+	 * 	<li>table <i>{*}</i></li>
+	 * </ul>
+	 * 
+	 * @param reader					XML reader.
+	 * @param allForeignKeys			List to fill with all encountered foreign keys.
+	 *                      			<i>note: these keys are not the final TAP meta, but a collection of all information found in the XML document.
+	 *                      			The final TAP meta will be created later, once all available tables and columns are available.</i>
+	 * @throws IllegalStateException	If this function is called while the reader has not just read the START ELEMENT tag of "table".
+	 * 
+	 * @return	The corresponding TAP schema.
+	 * 
+	 * @throws XMLStreamException	If there is an error processing the underlying XML source.
+	 * @throws TAPException			If several "name" nodes are found, or if none such node is found ; exactly one "name" node must be found.
+	 * 
+	 * @see #parseTable(XMLStreamReader, List)
+	 */
+	protected TAPSchema parseSchema(final XMLStreamReader reader, final List<ForeignKey> allForeignKeys) throws XMLStreamException, TAPException{
+		// Ensure the reader has just read the START ELEMENT of schema:
+		if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("schema"))
+			throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseSchema(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"schema\".");
+
+		TAPSchema schema = null;
+		String tag = null, name = null, description = null, title = null, utype = null;
+		ArrayList<TAPTable> tables = new ArrayList<TAPTable>(10);
+
+		while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){
+			// Get the tag name:
+			tag = reader.getLocalName();
+
+			// Identify the current tag:
+			if (tag.equalsIgnoreCase("name")){
+				if (name != null)
+					throw new TAPException(getPosition(reader) + " Only one \"name\" element can exist in a /tableset/schema!");
+				name = getText(reader);
+			}else if (tag.equalsIgnoreCase("description"))
+				description = ((description != null) ? (description + "\n") : "") + getText(reader);
+			else if (tag.equalsIgnoreCase("table")){
+				ArrayList<ForeignKey> keys = new ArrayList<ForeignKey>(2);
+				tables.add(parseTable(reader, keys));
+				allForeignKeys.addAll(keys);
+			}else if (tag.equalsIgnoreCase("title"))
+				title = ((title != null) ? (title + "\n") : "") + getText(reader);
+			else if (tag.equalsIgnoreCase("utype"))
+				utype = getText(reader);
+		}
+
+		// Only one info is required: the schema name!
+		if (name == null)
+			throw new TAPException(getPosition(reader) + " Missing schema \"name\"!");
+
+		// Build the schema:
+		schema = new TAPSchema(name, description, utype);
+		schema.setTitle(title);
+		for(TAPTable t : tables)
+			schema.addTable(t);
+		tables = null;
+
+		return schema;
+	}
+
+	/**
+	 * <p>Parse the XML representation of a TAP table.</p>
+	 * 
+	 * <p><b>Important: This function MUST be called just after the start element "table" has been read!</b></p>
+	 * 
+	 * <h3>Attributes</h3>
+	 * 
+	 * The attribute "type" may be provided in the start element "table". One of the following value is expected:
+	 * <ul>
+	 * 	<li>base_table <i>or table</i></li>
+	 * 	<li>output</li>
+	 * 	<li>view</li>
+	 * </ul>
+	 * 
+	 * <h3>Children</h3>
+	 * 
+	 * Only the following nodes are taken into account ; the others are ignored:
+	 * <ul>
+	 * 	<li>name <i>REQUIRED</i></li>
+	 * 	<li>description <i>{0..1}</i></li>
+	 * 	<li>title <i>{0..1}</i></li>
+	 * 	<li>utype <i>{0..1}</i></li>
+	 * 	<li>column <i>{*}</i></li>
+	 * 	<li>foreignKey <i>{*}</i></li>
+	 * </ul>
+	 * 
+	 * @param reader			XML reader.
+	 * @param allForeignKeys	List to fill with all encountered foreign keys.
+	 *                      	<i>note: these keys are not the final TAP meta, but a collection of all information found in the XML document.
+	 *                      	The final TAP meta will be created later, once all available tables and columns are available.</i>
+	 * 
+	 * @return	The corresponding TAP table.
+	 * 
+	 * @throws XMLStreamException		If there is an error processing the underlying XML source.
+	 * @throws TAPException				If several "name" nodes are found, or if none such node is found ; exactly one "name" node must be found.
+	 * @throws IllegalStateException	If this function is called while the reader has not just read the START ELEMENT tag of "table".
+	 * 
+	 * @see #parseColumn(XMLStreamReader)
+	 * @see #parseFKey(XMLStreamReader)
+	 */
+	protected TAPTable parseTable(final XMLStreamReader reader, final List<ForeignKey> keys) throws XMLStreamException, TAPException{
+		// Ensure the reader has just read the START ELEMENT of table:
+		if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("table"))
+			throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseTable(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"table\".");
+
+		TAPTable table = null;
+		TableType type = TableType.table;
+		String tag = null, name = null, description = null, title = null, utype = null;
+		ArrayList<TAPColumn> columns = new ArrayList<TAPColumn>(10);
+
+		// Get the table type (attribute "type") [OPTIONAL] :
+		if (reader.getAttributeCount() > 0){
+			int indType = 0;
+			while(indType < reader.getAttributeCount() && !reader.getAttributeLocalName(indType).equalsIgnoreCase("type"))
+				indType++;
+			if (indType < reader.getAttributeCount() && reader.getAttributeLocalName(indType).equalsIgnoreCase("type")){
+				String typeTxt = reader.getAttributeValue(indType);
+				if (typeTxt != null && typeTxt.trim().length() > 0){
+					typeTxt = typeTxt.trim().toLowerCase();
+					try{
+						if (typeTxt.equals("base_table"))
+							type = TableType.table;
+						else
+							type = TableType.valueOf(typeTxt);
+					}catch(IllegalArgumentException iae){
+						/* Note: If type unknown, the given value is ignored and the default type - TableType.table - is kept. */
+					}
+				}
+			}
+		}
+
+		// Fetch the other information (tags):
+		while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){
+			// Get the tag name:
+			tag = reader.getLocalName();
+
+			// Identify the current tag:
+			if (tag.equalsIgnoreCase("name")){
+				if (name != null)
+					throw new TAPException(getPosition(reader) + " Only one \"name\" element can exist in a /tableset/schema/table!");
+				name = getText(reader);
+			}else if (tag.equalsIgnoreCase("description"))
+				description = ((description != null) ? (description + "\n") : "") + getText(reader);
+			else if (tag.equalsIgnoreCase("column")){
+				columns.add(parseColumn(reader));
+			}else if (tag.equalsIgnoreCase("foreignKey"))
+				keys.add(parseFKey(reader));
+			else if (tag.equalsIgnoreCase("title"))
+				title = ((title != null) ? (title + "\n") : "") + getText(reader);
+			else if (tag.equalsIgnoreCase("utype"))
+				utype = getText(reader);
+		}
+
+		// Only one info is required: the table name!
+		if (name == null)
+			throw new TAPException(getPosition(reader) + " Missing table \"name\"!");
+
+		// Build the table:
+		table = new TAPTable(name, type, description, utype);
+		table.setTitle(title);
+		for(TAPColumn c : columns)
+			table.addColumn(c);
+		for(ForeignKey k : keys)
+			k.fromTable = table;
+
+		return table;
+	}
+
+	/**
+	 * <p>Parse the XML representation of a TAP column.</p>
+	 * 
+	 * <p><b>Important: This function MUST be called just after the start element "column" has been read!</b></p>
+	 * 
+	 * <h3>Attributes</h3>
+	 * 
+	 * The attribute "std" may be provided in the start element "column". One of the following value is expected:
+	 * <ul>
+	 * 	<li>false <i>(default value if the attribute is omitted)</i></li>
+	 * 	<li>true</li>
+	 * </ul>
+	 * 
+	 * <h3>Children</h3>
+	 * 
+	 * Only the following nodes are taken into account ; the others are ignored:
+	 * <ul>
+	 * 	<li>name <i>REQUIRED</i></li>
+	 * 	<li>description <i>{0..1}</i></li>
+	 * 	<li>ucd <i>{0..1}</i></li>
+	 * 	<li>unit <i>{0..1}</i></li>
+	 * 	<li>utype <i>{0..1}</i></li>
+	 * 	<li>dataType <i>{0..1}</i></li>
+	 * 	<li>flag <i>{*}, but only the values 'nullable', 'indexed' and 'primary' are currently supported by the library)</i></li>
+	 * </ul>
+	 * 
+	 * @param reader	XML reader.
+	 * 
+	 * @return	The corresponding TAP column.
+	 * 
+	 * @throws XMLStreamException		If there is an error processing the underlying XML source.
+	 * @throws TAPException				If several "name" nodes are found, or if none such node is found ; exactly one "name" node must be found.
+	 * @throws IllegalStateException	If this function is called while the reader has not just read the START ELEMENT tag of "column".
+	 * 
+	 * @see #parseDataType(XMLStreamReader)
+	 */
+	protected TAPColumn parseColumn(final XMLStreamReader reader) throws XMLStreamException, TAPException{
+		// Ensure the reader has just read the START ELEMENT of column:
+		if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("column"))
+			throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseColumn(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"column\".");
+
+		TAPColumn column = null;
+		boolean std = false, indexed = false, primary = false, nullable = false;
+		String tag = null, name = null, description = null, unit = null, ucd = null, utype = null;
+		DBType type = null;
+
+		// Get the column STD flag (attribute "std") [OPTIONAL] :
+		if (reader.getAttributeCount() > 0){
+			int indType = 0;
+			while(indType < reader.getAttributeCount() && !reader.getAttributeLocalName(indType).equalsIgnoreCase("std"))
+				indType++;
+			if (indType < reader.getAttributeCount() && reader.getAttributeLocalName(indType).equalsIgnoreCase("std")){
+				String stdTxt = reader.getAttributeValue(indType);
+				if (stdTxt != null)
+					std = Boolean.parseBoolean(stdTxt.trim().toLowerCase());
+			}
+		}
+
+		// Fetch the other information (tags):
+		while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){
+			// Get the tag name:
+			tag = reader.getLocalName();
+
+			// Identify the current tag:
+			if (tag.equalsIgnoreCase("name")){
+				if (name != null)
+					throw new TAPException(getPosition(reader) + " Only one \"name\" element can exist in a /tableset/schema/table/column!");
+				name = getText(reader);
+			}else if (tag.equalsIgnoreCase("description"))
+				description = ((description != null) ? (description + "\n") : "") + getText(reader);
+			else if (tag.equalsIgnoreCase("dataType"))
+				type = parseDataType(reader);
+			else if (tag.equalsIgnoreCase("unit"))
+				unit = getText(reader);
+			else if (tag.equalsIgnoreCase("ucd"))
+				ucd = getText(reader);
+			else if (tag.equalsIgnoreCase("utype"))
+				utype = getText(reader);
+			else if (tag.equalsIgnoreCase("flag")){
+				String txt = getText(reader);
+				if (txt != null){
+					if (txt.equalsIgnoreCase("indexed"))
+						indexed = true;
+					else if (txt.equalsIgnoreCase("primary"))
+						primary = true;
+					else if (txt.equalsIgnoreCase("nullable"))
+						nullable = true;
+				}
+			}
+		}
+
+		// Only one info is required: the table name!
+		if (name == null)
+			throw new TAPException(getPosition(reader) + " Missing column \"name\"!");
+
+		// Build the column:
+		column = new TAPColumn(name, type, description, unit, ucd, utype);
+		column.setStd(std);
+		column.setIndexed(indexed);
+		column.setPrincipal(primary);
+		column.setNullable(nullable);
+
+		return column;
+	}
+
+	/**
+	 * <p>Parse the XML representation of a column datatype.</p>
+	 * 
+	 * <p><b>Important: This function MUST be called just after the start element "dataType" has been read!</b></p>
+	 * 
+	 * <h3>Attributes</h3>
+	 * 
+	 * The attribute "xsi:type" (where xsi = http://www.w3.org/2001/XMLSchema-instance) MUST be provided. Only the following values are supported and accepted
+	 * <i>(below, vs = http://www.ivoa.net/xml/VODataService)</i>:
+	 * <ul>
+	 * 	<li><b>vs:VOTableType</b>, and the following attributes may be also provided:
+	 * 		<ul>
+	 * 			<li>arraysize</li>
+	 * 			<li>xtype</li>
+	 * 		</ul></li>
+	 * 	<li><b>vs:TAPType</b>, and the attribute "size" may be also provided</li>
+	 * </ul>
+	 * 
+	 * <h3>Children</h3>
+	 * 
+	 * No child, but a text MUST be provided. Its value depends of the attribute "xsi:type": a VOTable datatype (e.g. char, float, short) if "xsi:type=vs:VOTableType",
+	 * or a TAP type (e.g. VARCHAR, REAL, SMALLINT) if "xsi:type=vs:TAPType". <i>Any other value will be rejected.</i>
+	 * 
+	 * <p><i>IMPORTANT: All VOTable datatypes will be converted into TAPType automatically by the library.</i></p>
+	 * 
+	 * @param reader	XML reader.
+	 * 
+	 * @return	The corresponding column datatype.
+	 * 
+	 * @throws XMLStreamException		If there is an error processing the underlying XML source.
+	 * @throws TAPException				If the attribute "xsi:type" is missing or incorrect,
+	 *                     				or if the datatype is unknown or not supported.
+	 * @throws IllegalStateException	If this function is called while the reader has not just read the START ELEMENT tag of "dataType".
+	 * 
+	 * @see VOTableIterator#resolveVotType(String, String, String)
+	 * @see DBType#DBType(DBDatatype, int)
+	 */
+	protected DBType parseDataType(final XMLStreamReader reader) throws XMLStreamException, TAPException{
+		// Ensure the reader has just read the START ELEMENT of dataType:
+		if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("dataType"))
+			throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseDataType(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"dataType\".");
+
+		String typeOfType = null, datatype = null, size = null, xtype = null, arraysize = null;
+
+		/* Note:
+		 * The 1st parameter of XMLStreamReader.getAttributeValue(String, String) should be the namespace of the attribute.
+		 * If this value is NULL, the namespace condition is ignored.
+		 * If it is an empty string - "" - an attribute without namespace will be searched. */
+
+		// Get the type of datatype :
+		typeOfType = reader.getAttributeValue(XSI_NAMESPACE, "type");
+
+		// Resolve the datatype:
+		if (typeOfType == null || typeOfType.trim().length() == 0)
+			throw new TAPException(getPosition(reader) + " Missing attribute \"xsi:type\" (where xsi = \"" + XSI_NAMESPACE + "\")! Expected attribute value: vs:VOTableType or vs:TAPType, where vs = " + VODATASERVICE_NAMESPACE + ".");
+
+		// Separate the namespace and type parts:
+		String[] split = typeOfType.split(":");
+
+		// Ensure the number of parts is 2:
+		if (split.length != 2)
+			throw new TAPException(getPosition(reader) + " Unresolved type: \"" + typeOfType + "\"! Missing namespace prefix.");
+		// ...and ensure the namespace is the expected value:
+		else{
+			String datatypeNamespace = reader.getNamespaceURI(split[0]);
+			if (datatypeNamespace == null)
+				throw new TAPException(getPosition(reader) + " Unresolved type: \"" + typeOfType + "\"! Unknown namespace.");
+			else if (!datatypeNamespace.startsWith(VODATASERVICE_NAMESPACE))
+				throw new TAPException(getPosition(reader) + " Unsupported type: \"" + typeOfType + "\"! Expected: vs:VOTableType or vs:TAPType, where vs = " + VODATASERVICE_NAMESPACE + ".");
+		}
+
+		// Get the other attributes:
+		size = reader.getAttributeValue("", "size");
+		xtype = reader.getAttributeValue("", "xtype");
+		arraysize = reader.getAttributeValue("", "arraysize");
+
+		// Get the datatype:
+		datatype = getText(reader);
+		if (datatype == null || datatype.trim().length() == 0)
+			throw new TAPException(getPosition(reader) + " Missing column datatype!");
+		datatype = datatype.trim();
+
+		// Resolve the datatype in function of the value of xsi:type:
+		// CASE: VOTable
+		if (split[1].equalsIgnoreCase("VOTableType"))
+			return VOTableIterator.resolveVotType(datatype, arraysize, xtype).toTAPType();
+
+		// CASE: TAP type
+		else if (split[1].equalsIgnoreCase("TAPType")){
+			// normalize the size attribute:
+			int colSize = -1;
+			if (size != null && size.trim().length() > 0){
+				try{
+					colSize = Integer.parseInt(size);
+				}catch(NumberFormatException nfe){}
+			}
+			// build and return the corresponding type:
+			try{
+				return new DBType(DBDatatype.valueOf(datatype.toUpperCase()), colSize);
+			}catch(IllegalArgumentException iae){
+				throw new TAPException(getPosition(reader) + " Unknown TAPType: \"" + datatype + "\"!");
+			}
+		}
+		// DEFAULT => Throw an exception!
+		else
+			throw new TAPException(getPosition(reader) + " Unsupported type: \"" + typeOfType + "\"! Expected: vs:VOTableType or vs:TAPType, where vs = " + VODATASERVICE_NAMESPACE + ".");
+	}
+
+	/**
+	 * <p>Parse the XML representation of a TAP foreign key.</p>
+	 * 
+	 * <p><b>Important: This function MUST be called just after the start element "foreignKey" has been read!</b></p>
+	 * 
+	 * <h3>Attributes</h3>
+	 * 
+	 * <p>No attribute is expected in the start element "foreignKey".</p>
+	 * 
+	 * <h3>Children</h3>
+	 * 
+	 * Only the following nodes are taken into account ; the others are ignored:
+	 * <ul>
+	 * 	<li>targetTable <i>REQUIRED</i></li>
+	 * 	<li>description <i>{0..1}</i></li>
+	 * 	<li>utype <i>{0..1}</i></li>
+	 * 	<li>fkColumn <i>{1..*}</i>
+	 * 		<ul>
+	 * 			<li>fromColumn <i>REQUIRED</i></li>
+	 * 			<li>targetColumn <i>REQUIRED</i></li>
+	 * 		</ul></li>
+	 * </ul>
+	 * 
+	 * @param reader	XML reader.
+	 * 
+	 * @return	An object containing all information found in the XML node about the foreign key.
+	 * 
+	 * @throws XMLStreamException		If there is an error processing the underlying XML source.
+	 * @throws TAPException				If "targetTable" node is missing,
+	 *                     				or if no "fkColumn" is provided.
+	 * @throws IllegalStateException	If this function is called while the reader has not just read the START ELEMENT tag of "foreignKey".
+	 * 
+	 * @see #parseDataType(XMLStreamReader)
+	 */
+	protected ForeignKey parseFKey(final XMLStreamReader reader) throws XMLStreamException, TAPException{
+		// Ensure the reader has just read the START ELEMENT of foreignKey:
+		if (reader.getEventType() != XMLStreamConstants.START_ELEMENT || reader.getLocalName() == null || !reader.getLocalName().equalsIgnoreCase("foreignKey"))
+			throw new IllegalStateException(getPosition(reader) + " Illegal usage of TableSetParser.parseFKey(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"foreignKey\".");
+
+		String tag;
+		ForeignKey fk = new ForeignKey();
+
+		// Fetch the other information (tags):
+		while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){
+			// Get the tag name:
+			tag = reader.getLocalName();
+
+			// Identify the current tag:
+			if (tag.equalsIgnoreCase("targetTable")){
+				if (fk.targetTable != null)
+					throw new TAPException(getPosition(reader) + " Only one \"targetTable\" element can exist in a /tableset/schema/table/foreignKey!");
+				fk.targetTable = getText(reader);
+				fk.targetTablePosition = getPosition(reader);
+			}else if (tag.equalsIgnoreCase("description"))
+				fk.description = getText(reader);
+			else if (tag.equalsIgnoreCase("utype"))
+				fk.utype = getText(reader);
+			else if (tag.equalsIgnoreCase("fkColumn")){
+				String innerTag, fromCol = null, targetCol = null;
+				while(nextTag(reader) == XMLStreamConstants.START_ELEMENT){
+					innerTag = reader.getLocalName();
+					if (innerTag.equalsIgnoreCase("fromColumn")){
+						if (fromCol != null)
+							throw new TAPException(getPosition(reader) + " Only one \"fromColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !");
+						fromCol = getText(reader);
+					}else if (innerTag.equalsIgnoreCase("targetColumn")){
+						if (targetCol != null)
+							throw new TAPException(getPosition(reader) + " Only one \"targetColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !");
+						targetCol = getText(reader);
+					}else
+						goToEndTag(reader, reader.getLocalName());
+				}
+				// Only two info are required: the source and the target columns!
+				if (fromCol == null)
+					throw new TAPException(getPosition(reader) + " Missing \"fromColumn\"!");
+				else if (targetCol == null)
+					throw new TAPException(getPosition(reader) + " Missing \"targetColumn\"!");
+				else
+					fk.keyColumns.put(fromCol, targetCol);
+			}else
+				goToEndTag(reader, tag);
+		}
+
+		// Check the last read tag is the END ELEMENT of a foreignKey node:
+		if (reader.getEventType() != XMLStreamConstants.END_ELEMENT)
+			throw new TAPException(getPosition(reader) + " Unexpected tag! An END ELEMENT tag for foreignKey was expected.");
+		else if (!reader.getLocalName().equalsIgnoreCase("foreignKey"))
+			throw new TAPException(getPosition(reader) + " Unexpected node end tag: </" + reader.getLocalName() + ">! An END ELEMENT tag for foreignKey was expected.");
+
+		// The target table name is required!
+		if (fk.targetTable == null)
+			throw new TAPException(getPosition(reader) + " Missing \"targetTable\"!");
+		// At least one columns association is expected!
+		else if (fk.keyColumns.size() == 0)
+			throw new TAPException(getPosition(reader) + " Missing at least one \"fkColumn\"!");
+
+		return fk;
+	}
+
+	/* ***************** */
+	/* UTILITY FUNCTIONS */
+	/* ***************** */
+
+	/**
+	 * <p>Get the current position of the given reader.</p>
+	 * 
+	 * <p>
+	 * 	This position is returned as a string having the following syntax: "[l.x,c.y]"
+	 * 	(where x is the line number and y the column number ; x and y start at 1 ; x and y
+	 * 	are both -1 if the end of the XML document has been reached).
+	 * </p>
+	 * 
+	 * <p><i>Note:
+	 * 	The column position is generally just after the read element (node start/end tag, characters).
+	 * 	However, with CHARACTERS items, this column position may be 2 characters after the real end.
+	 * </i></p>
+	 * 
+	 * @param reader	XML reader whose the current position must be returned.
+	 * 
+	 * @return	A string representing the current reader position.
+	 */
+	protected final String getPosition(final XMLStreamReader reader){
+		return "[l." + reader.getLocation().getLineNumber() + ",c." + reader.getLocation().getColumnNumber() + "]";
+	}
+
+	/**
+	 * Skip every elements until a START ELEMENT or an END ELEMENT is reached.
+	 * 
+	 * @param reader	XML reader.
+	 * 
+	 * @return	The event of the last read tag. <i>Here, either {@link XMLStreamConstants#START_ELEMENT} or {@link XMLStreamConstants#END_ELEMENT}.</i>
+	 * 
+	 * @throws XMLStreamException	If there is an error processing the underlying XML source.
+	 */
+	protected final int nextTag(final XMLStreamReader reader) throws XMLStreamException{
+		int event = -1;
+		do{
+			event = reader.next();
+		}while(event != XMLStreamConstants.START_ELEMENT && event != XMLStreamConstants.END_ELEMENT);
+		return event;
+	}
+
+	/**
+	 * <p>Skip all tags from the current position to the end of the specified node.</p>
+	 * 
+	 * <p><b>IMPORTANT:
+	 * 	This function MUST be called ONLY IF the reader is inside the node whose the end tag is searched.
+	 * 	It may be in a child of this node or not, but the most important is to be inside it.
+	 * </b></p>
+	 * 
+	 * <p><i>Note:
+	 * 	No tag will be read if the given startNode is NULL or an empty string.
+	 * </i></p>
+	 * 
+	 * @param reader	XML reader.
+	 * @param startNode	Name of the node whose the end must be reached.
+	 * 
+	 * @throws XMLStreamException	If there is an error processing the underlying XML source.
+	 * @throws TAPException			If the name of the only corresponding end element does not match the given one,
+	 *                     			or if the END ELEMENT can not be found <i>(2 possible reasons for that:
+	 *                     			1/ malformed XML document, 2/ this function has been called before the START ELEMENT has been read)</i>. 
+	 */
+	protected final void goToEndTag(final XMLStreamReader reader, final String startNode) throws XMLStreamException, TAPException{
+		if (startNode == null || startNode.trim().length() <= 0)
+			return;
+		else if (reader.getEventType() == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equalsIgnoreCase(startNode))
+			return;
+
+		int level = 0, event;
+		while(reader.hasNext()){
+			event = reader.next();
+			switch(event){
+				case XMLStreamConstants.START_ELEMENT:
+					level++;
+					break;
+				case XMLStreamConstants.END_ELEMENT:
+					if (level <= 0 && reader.getLocalName().equalsIgnoreCase(startNode)) // "level <= 0" because the reader may be inside a child of the node whose the end is searched.
+						return;
+					else
+						level--;
+			}
+		}
+
+		/* If no matching END ELEMENT, then either the XML document is malformed
+		 * or #goToEndTag(...) has been called before the corresponding START ELEMENT has been read: */
+		throw new TAPException(getPosition(reader) + " Malformed XML document: missing an END TAG </" + startNode + ">!");
+	}
+
+	/**
+	 * <p>Get the text of the current node.</p>
+	 * 
+	 * <p>
+	 * 	This function iterates while the next tags are of type CHARACTERS.
+	 * 	Consequently, the next tag (start or end element) is already read when returning this function.
+	 * </p>
+	 * 
+	 * <p>
+	 * 	All CHARACTERS elements are concatenated.
+	 * 	All leading and trailing space characters (\r \n \t and ' ') of every lines are deleted ; only the last or the first \n or \r are kept.
+	 * </p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is also skipping all COMMENT elements. This is particularly useful if a COMMENT is splitting a node text content ;
+	 * 	in such case, the comment is ignored and both divided text are concatenated.
+	 * </i></p>
+	 * 
+	 * @param reader	XML reader.
+	 * 
+	 * @return	The whole text content of the current node.
+	 * 
+	 * @throws XMLStreamException	If there is an error processing the underlying XML source.
+	 */
+	protected final String getText(final XMLStreamReader reader) throws XMLStreamException{
+		StringBuffer txt = new StringBuffer();
+		while(reader.next() == XMLStreamConstants.CHARACTERS || reader.getEventType() == XMLStreamConstants.COMMENT){
+			if (reader.getEventType() == XMLStreamConstants.CHARACTERS){
+				if (reader.getText() != null)
+					txt.append(reader.getText().replaceAll("[ \\t]+([\\n\\r]+)", "$1").replaceAll("([\\n\\r]+)[ \\t]+", "$1"));
+			}
+		};
+		return txt.toString().trim();
+	}
+
+	/**
+	 * <p>Search for the specified table in the given TAP metadata.</p>
+	 * 
+	 * <p><i>Note: This function is not case sensitive.</i></p>
+	 * 
+	 * @param tableName	Name of the table to search. <i>The table name MAY be prefixed by a schema name (e.g. "mySchema.myTable").</i>
+	 * @param meta		All fetched TAP metadata.
+	 * @param position	Position of the table name in the XML document. <i>This parameter is ONLY used in case of error.</i>
+	 * 
+	 * @return	The corresponding TAP table.
+	 * 
+	 * @throws TAPException	If the table name syntax ([schema.]table) is incorrect,
+	 *                    	or if several tables match to the specified table name (which is not prefixed by a schema name),
+	 *                     	or if no match can be found.
+	 */
+	protected final TAPTable searchTable(final String tableName, final TAPMetadata meta, final String position) throws TAPException{
+		// Extract the schema name and normalize the table name:
+		String schema = null, table = tableName.trim();
+		if (tableName.indexOf('.') >= 0){
+			// get the schema name:
+			schema = tableName.substring(0, tableName.indexOf('.')).trim();
+			// test that the schema name is not null: 
+			if (schema.length() == 0)
+				throw new TAPException(position + " Incorrect full table name - \"" + tableName + "\": empty schema name!");
+			// test that the remaining table name is not null:
+			else if (tableName.substring(schema.length() + 1).trim().length() == 0)
+				throw new TAPException(position + " Incorrect full table name - \"" + tableName + "\": empty table name!");
+			// test there is no more '.' separator in the remaining table name:
+			else if (tableName.indexOf('.', schema.length() + 1) >= 0)
+				throw new TAPException(position + " Incorrect full table name - \"" + tableName + "\": only a schema and a table name can be specified (expected syntax: \"schema.table\")\"!");
+			// get the table name:
+			table = tableName.substring(schema.length() + 1).trim();
+		}
+
+		// Find all matching tables:
+		ArrayList<TAPTable> founds = new ArrayList<TAPTable>(1);
+		StringBuffer foundsAsTxt = new StringBuffer();
+		TAPTable t;
+		Iterator<TAPTable> allTables = meta.getTables();
+		while(allTables.hasNext()){
+			// get the table to test:
+			t = allTables.next();
+			if (t == null)
+				continue;
+			// store it if the schema and table names match:
+			if ((schema == null || t.getADQLSchemaName().equalsIgnoreCase(schema)) && t.getADQLName().equalsIgnoreCase(table)){
+				// update the result array:
+				founds.add(t);
+				// update the text list:
+				if (foundsAsTxt.length() > 0)
+					foundsAsTxt.append(", ");
+				foundsAsTxt.append(t.getADQLSchemaName()).append('.').append(t.getADQLName());
+			}
+		}
+
+		if (founds.size() == 0)
+			throw new TAPException(position + " Unknown table: \"" + tableName + "\"!");
+		else if (founds.size() > 1)
+			throw new TAPException(position + " Unresolved table: \"" + tableName + "\"! Several tables have the same name but in different schemas (here: " + foundsAsTxt.toString() + "). You must prefix this table name by a schema name (expected syntax: \"schema.table\").");
+		else
+			return founds.get(0);
+	}
+
+}
diff --git a/test/tap/metadata/TableSetParserTest.java b/test/tap/metadata/TableSetParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed42f401a4e80b69f6499a071cdb1aaee6070661
--- /dev/null
+++ b/test/tap/metadata/TableSetParserTest.java
@@ -0,0 +1,1162 @@
+package tap.metadata;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.StringBufferInputStream;
+import java.util.ArrayList;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import tap.TAPException;
+import tap.metadata.TableSetParser.ForeignKey;
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
+
+@SuppressWarnings("deprecation")
+public class TableSetParserTest {
+
+	private static TableSetParser parser = null;
+	private static XMLInputFactory factory = null;
+
+	private static final String namespaceDef = "xmlns:vs=\"http://www.ivoa.net/xml/VODataService/v1.1\" xmlns:vtm=\"http://www.ivoa.net/xml/VOSITables/v1.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ivoa.net/xml/VODataService/v1.1 http://vo.ari.uni-heidelberg.de/docs/schemata/VODataService-v1.1.xsd http://www.ivoa.net/xml/VOSITables/v1.0 http://vo.ari.uni-heidelberg.de/docs/schemata/VOSITables-v1.0.xsd\"";
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{
+		// Build an empty parser:
+		parser = new TableSetParser();
+
+		// Build the XML factory:
+		factory = XMLInputFactory.newInstance();
+	}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	private static XMLStreamReader buildReader(final String xmlContent) throws XMLStreamException{
+		return factory.createXMLStreamReader(new StringBufferInputStream(xmlContent));
+	}
+
+	private static void close(final XMLStreamReader reader){
+		if (reader != null){
+			try{
+				reader.close();
+			}catch(Throwable t){}
+		}
+	}
+
+	@Test
+	public void testGetPosition(){
+		XMLStreamReader reader = null;
+		try{
+
+			// Build a reader with an empty XML document:
+			reader = buildReader("");
+			assertEquals("[l.1,c.1]", parser.getPosition(reader));
+			// note: reader.next() is throwing an error on an empty document => no need to test that.
+			close(reader);
+
+			// Build a reader with a simple XML:
+			reader = buildReader("<A anAttr=\"attrValue\">node value</A>");
+
+			// Position before starting reading:
+			assertEquals("[l.1,c.1]", parser.getPosition(reader));
+
+			// Position after getting the node:
+			reader.next(); // START_ELEMENT("A")
+			assertEquals("[l.1,c.23]", parser.getPosition(reader));
+			// The position after getting an attribute should not change:
+			reader.getAttributeLocalName(0); // ATTRIBUTE("attrValue")
+			assertEquals("[l.1,c.23]", parser.getPosition(reader));
+
+			// Position after getting the text:
+			reader.next(); // CHARACTERS("node value")
+			assertEquals("[l.1,c.35]", parser.getPosition(reader));
+
+			// Position after getting the node ending tag:
+			reader.next(); // END_ELEMENT("A")
+			assertEquals("[l.1,c.37]", parser.getPosition(reader));
+
+			// Position once the end reached:
+			reader.next(); // NULL
+			assertEquals("[l.-1,c.-1]", parser.getPosition(reader));
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+		}
+	}
+
+	@Test
+	public void testGoToEndTag(){
+		XMLStreamReader reader = null;
+		try{
+
+			/* Test with a single empty node AND WITH NULL or ""
+			 * => NO TAG SHOULD HAVE BEEN READ: */
+			// CASE: null
+			reader = buildReader("<A></A>");
+			parser.goToEndTag(reader, null);
+			assertEquals("[l.1,c.1]", parser.getPosition(reader));
+			close(reader);
+			// CASE: empty string
+			reader = buildReader("<A></A>");
+			parser.goToEndTag(reader, "");
+			assertEquals("[l.1,c.1]", parser.getPosition(reader));
+			close(reader);
+
+			/* Test BEFORE having read the start element:
+			 * => AN EXCEPTION SHOULD BE THROWN */
+			reader = buildReader("<A></A>");
+			try{
+				parser.goToEndTag(reader, "A");
+				fail("This function should have failed: the START ELEMENT has not yet been read!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.-1,c.-1] Malformed XML document: missing an END TAG </A>!", e.getMessage());
+				else
+					throw e;
+			}
+			close(reader);
+
+			/* Test AFTER having read the start element:
+			 * => NORMAL USAGE */
+			reader = buildReader("<A></A>");
+			reader.next(); // START ELEMENT("A")
+			parser.goToEndTag(reader, "A");
+			assertEquals("[l.1,c.8]", parser.getPosition(reader));
+			close(reader);
+
+			/* Test AFTER having read the start element:
+			 * => NORMAL USAGE with an embedded node */
+			// search for the root node end:
+			reader = buildReader("<A><B></B></A>");
+			reader.next(); // START ELEMENT("A")
+			parser.goToEndTag(reader, "A");
+			assertEquals("[l.1,c.15]", parser.getPosition(reader));
+			close(reader);
+			// variant with some texts:
+			reader = buildReader("<A><B>super blabla</B></A>");
+			reader.next(); // START ELEMENT("A")
+			parser.goToEndTag(reader, "A");
+			assertEquals("[l.1,c.27]", parser.getPosition(reader));
+			close(reader);
+			// variant with some texts + child node:
+			reader = buildReader("<A><B>super<C>blabla</C></B></A>");
+			reader.next(); // START ELEMENT("A")
+			parser.goToEndTag(reader, "A");
+			assertEquals("[l.1,c.33]", parser.getPosition(reader));
+			close(reader);
+			// search for the child node end:
+			reader = buildReader("<A><B></B></A>");
+			reader.next(); // START ELEMENT("A")
+			reader.next(); // START ELEMENT("B")
+			parser.goToEndTag(reader, "B");
+			assertEquals("[l.1,c.11]", parser.getPosition(reader));
+			close(reader);
+			// variant with some texts:
+			reader = buildReader("<A><B>super blabla</B></A>");
+			reader.next(); // START ELEMENT("A")
+			reader.next(); // START ELEMENT("B")
+			parser.goToEndTag(reader, "B");
+			assertEquals("[l.1,c.23]", parser.getPosition(reader));
+			close(reader);
+			// variant with some texts + child node:
+			reader = buildReader("<A><B>super<C>blabla</C></B></A>");
+			reader.next(); // START ELEMENT("A")
+			reader.next(); // START ELEMENT("B")
+			parser.goToEndTag(reader, "B");
+			assertEquals("[l.1,c.29]", parser.getPosition(reader));
+			close(reader);
+
+			// Test: Search the end tag while the reader is inside one of its children:
+			reader = buildReader("<A><B>super<C>blabla</C></B></A>");
+			reader.next(); // START ELEMENT("A")
+			reader.next(); // START ELEMENT("B")
+			parser.goToEndTag(reader, "A");
+			assertEquals("[l.1,c.33]", parser.getPosition(reader));
+			close(reader);
+
+			// Test with a wrong start node name:
+			reader = buildReader("<A></A>");
+			reader.next(); // START ELEMENT("A")
+			try{
+				parser.goToEndTag(reader, "B");
+				fail("This function should have failed: the given node name is wrong (no such node in the XML document)!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.-1,c.-1] Malformed XML document: missing an END TAG </B>!", e.getMessage());
+				else
+					throw e;
+			}
+			close(reader);
+
+			// Test with malformed XML document:
+			// CASE: missing end tag for the root node:
+			reader = buildReader("<A><B></B>");
+			reader.next(); // START ELEMENT("A")
+			try{
+				parser.goToEndTag(reader, "A");
+				fail("This function should have failed: the node A has no END TAG!");
+			}catch(Exception e){
+				if (e instanceof XMLStreamException)
+					assertEquals("ParseError at [row,col]:[1,11]\nMessage: XML document structures must start and end within the same entity.", e.getMessage());
+				else
+					throw e;
+			}
+			close(reader);
+			// CASE: missing end tag for a child:
+			reader = buildReader("<A><B></A>");
+			reader.next(); // START ELEMENT("A")
+			try{
+				parser.goToEndTag(reader, "A");
+				fail("This function should have failed: the node B has no END TAG!");
+			}catch(Exception e){
+				if (e instanceof XMLStreamException)
+					assertEquals("ParseError at [row,col]:[1,9]\nMessage: The element type \"B\" must be terminated by the matching end-tag \"</B>\".", e.getMessage());
+				else
+					throw e;
+			}
+			close(reader);
+			// CASE: missing end tag for the child to search:
+			reader = buildReader("<A><B></A>");
+			reader.next(); // START ELEMENT("A")
+			reader.next(); // START ELEMENT("B")
+			try{
+				parser.goToEndTag(reader, "B");
+				fail("This function should have failed: the node B has no END TAG!");
+			}catch(Exception e){
+				if (e instanceof XMLStreamException)
+					assertEquals("ParseError at [row,col]:[1,9]\nMessage: The element type \"B\" must be terminated by the matching end-tag \"</B>\".", e.getMessage());
+				else
+					throw e;
+			}
+			close(reader);
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+		}
+	}
+
+	@Test
+	public void testGetText(){
+		XMLStreamReader reader = null;
+		String txt;
+		try{
+
+			// Test with a simple XML and an empty text:
+			reader = buildReader("<A></A>");
+			txt = parser.getText(reader);
+			assertEquals(0, txt.length());
+			assertEquals("[l.1,c.4]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.START_ELEMENT, reader.getEventType());
+			close(reader);
+			// variant with spaces and tabs:
+			reader = buildReader("  	 <A></A>");
+			txt = parser.getText(reader);
+			assertEquals(0, txt.length());
+			assertEquals("[l.1,c.8]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.START_ELEMENT, reader.getEventType());
+			close(reader);
+			// variant with line returns:
+			reader = buildReader("  \n <A></A>");
+			txt = parser.getText(reader);
+			assertEquals(0, txt.length());
+			assertEquals("[l.2,c.5]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.START_ELEMENT, reader.getEventType());
+			close(reader);
+
+			// Test with a single line text:
+			reader = buildReader("<A>	Super  blabla     </A>");
+			reader.next(); // START ELEMENT("A")
+			txt = parser.getText(reader);
+			assertEquals("Super  blabla", txt);
+			assertEquals("[l.1,c.27]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType());
+			close(reader);
+			// variant with CDATA:
+			reader = buildReader("<A>	Super  <![CDATA[blabla   ]]>  </A>");
+			reader.next(); // START ELEMENT("A")
+			txt = parser.getText(reader);
+			assertEquals("Super  blabla", txt);
+			assertEquals("[l.1,c.39]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType());
+			close(reader);
+
+			// Test with a text of 2 lines:
+			reader = buildReader("<A>	Super \n	 blabla     </A>");
+			reader.next(); // START ELEMENT("A")
+			txt = parser.getText(reader);
+			assertEquals("Super\nblabla", txt);
+			assertEquals("[l.2,c.18]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType());
+			close(reader);
+			// same test but with an empty line between both:
+			reader = buildReader("<A>	Super \n \n	 blabla     </A>");
+			reader.next(); // START ELEMENT("A")
+			txt = parser.getText(reader);
+			assertEquals("Super\n\nblabla", txt);
+			assertEquals("[l.3,c.18]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType());
+			close(reader);
+			// same test but starting with an empty line:
+			reader = buildReader("<A>\n	 Super \n	 bla  bla     </A>");
+			reader.next(); // START ELEMENT("A")
+			txt = parser.getText(reader);
+			assertEquals("Super\nbla  bla", txt);
+			assertEquals("[l.3,c.20]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType());
+			close(reader);
+			// same test but a comment splitting a text part:
+			reader = buildReader("<A> Super \n	 bla<!-- a super comment -->  bla     </A>");
+			reader.next(); // START ELEMENT("A")
+			txt = parser.getText(reader);
+			assertEquals("Super\nbla  bla", txt);
+			assertEquals("[l.2,c.44]", parser.getPosition(reader));
+			assertEquals(XMLStreamConstants.END_ELEMENT, reader.getEventType());
+			close(reader);
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+		}
+	}
+
+	@Test
+	public void testSearchTable(){
+		try{
+
+			// Create fake metadata:
+			TAPMetadata meta = new TAPMetadata();
+			TAPSchema schema = new TAPSchema("SA");
+			schema.addTable("TA");
+			schema.addTable("TB");
+			meta.addSchema(schema);
+			schema = new TAPSchema("SB");
+			schema.addTable("TB");
+			meta.addSchema(schema);
+
+			// Create a fake position:
+			final String pos = "[l.10,c.1]";
+
+			// Search for an existing table WITHOUT SCHEMA specification:
+			TAPTable t = parser.searchTable("TA", meta, pos);
+			assertEquals("TA", t.getADQLName());
+			assertEquals("SA", t.getADQLSchemaName());
+			// variant with a different case:
+			t = parser.searchTable("ta", meta, pos);
+			assertEquals("TA", t.getADQLName());
+			assertEquals("SA", t.getADQLSchemaName());
+
+			// Search for an existing table WITH SCHEMA specification:
+			t = parser.searchTable("SA.TA", meta, pos);
+			assertEquals("TA", t.getADQLName());
+			assertEquals("SA", t.getADQLSchemaName());
+			// variant with a different case:
+			t = parser.searchTable("sa.ta", meta, pos);
+			assertEquals("TA", t.getADQLName());
+			assertEquals("SA", t.getADQLSchemaName());
+
+			// Search with a wrong table name:
+			try{
+				parser.searchTable("TC", meta, pos);
+				fail("This test should have not failed: there is no table named TC in the given metadata.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Unknown table: \"TC\"!", e.getMessage());
+				else
+					throw e;
+			}
+			// variant with a correct schema name:
+			try{
+				parser.searchTable("SA.TC", meta, pos);
+				fail("This test should have not failed: there is no table named SA.TC in the given metadata.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Unknown table: \"SA.TC\"!", e.getMessage());
+				else
+					throw e;
+			}
+
+			// Search with a wrong schema name:
+			try{
+				parser.searchTable("SC.TB", meta, pos);
+				fail("This test should have not failed: there is no schema named SC in the given metadata.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Unknown table: \"SC.TB\"!", e.getMessage());
+				else
+					throw e;
+			}
+
+			// Search with an ambiguous table name (missing schema name):
+			try{
+				parser.searchTable("TB", meta, pos);
+				fail("This test should have not failed: there are two table named TB ; a schema name is required to choose the table to select.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Unresolved table: \"TB\"! Several tables have the same name but in different schemas (here: SA.TB, SB.TB). You must prefix this table name by a schema name (expected syntax: \"schema.table\").", e.getMessage());
+				else
+					throw e;
+			}
+
+			// Provide a schema + table name with a wrong syntax (missing table name or schema name):
+			try{
+				parser.searchTable(".TB", meta, pos);
+				fail("This test should have not failed: the schema name is missing before the '.'.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Incorrect full table name - \".TB\": empty schema name!", e.getMessage());
+				else
+					throw e;
+			}
+			try{
+				parser.searchTable("SB.", meta, pos);
+				fail("This test should have not failed: the table name is missing after the '.'.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Incorrect full table name - \"SB.\": empty table name!", e.getMessage());
+				else
+					throw e;
+			}
+			try{
+				parser.searchTable("toto.SB.TB", meta, pos);
+				fail("This test should have not failed: the table name is missing after the '.'.");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals(pos + " Incorrect full table name - \"toto.SB.TB\": only a schema and a table name can be specified (expected syntax: \"schema.table\")\"!", e.getMessage());
+				else
+					throw e;
+			}
+
+		}catch(Exception e){
+			e.printStackTrace();
+			fail("Unexpected error: " + e.getMessage());
+		}
+	}
+
+	@Test
+	public void testParseFKey(){
+		XMLStreamReader reader = null;
+		try{
+
+			// Test while search outside from the foreignKey node:
+			reader = buildReader("<table><foreignKey><targetTable>SA.TB</targetTable><utype>truc.chose</utype><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey></table>");
+			reader.next(); // START ELEMENT("table")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: the reader has not just read the \"foreignKey\" START ELEMENT tag.");
+			}catch(Exception e){
+				if (e instanceof IllegalStateException)
+					assertEquals("[l.1,c.8] Illegal usage of TableSetParser.parseFKey(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"foreignKey\".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+
+			// Test with a complete and correct XML foreignKey node:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><utype>truc.chose</utype><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			ForeignKey fk = parser.parseFKey(reader);
+			assertEquals("SA.TB", fk.targetTable);
+			assertEquals("[l.1,c.45]", fk.targetTablePosition);
+			assertEquals("truc.chose", fk.utype);
+			assertEquals("Foreign key\ndescription.", fk.description);
+			assertEquals(1, fk.keyColumns.size());
+			assertEquals("col2", fk.keyColumns.get("col1"));
+			close(reader);
+			// variant with some comments:
+			reader = buildReader("<foreignKey><!-- Here, we are inside! --><targetTable><!-- coucou -->SA.TB</targetTable><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn><!-- Here is the end! --></foreignKey><!-- Nothing more! -->");
+			reader.next(); // START ELEMENT("foreignKey")
+			fk = parser.parseFKey(reader);
+			assertEquals("SA.TB", fk.targetTable);
+			assertEquals("Foreign key\ndescription.", fk.description);
+			assertEquals(1, fk.keyColumns.size());
+			assertEquals("col2", fk.keyColumns.get("col1"));
+			close(reader);
+			// variant with texts at unapropriate places:
+			reader = buildReader("<foreignKey>Here, we are <![CDATA[inside!]]><targetTable>SA.TB</targetTable><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn>Here is the end!</foreignKey>Nothing more!");
+			reader.next(); // START ELEMENT("foreignKey")
+			fk = parser.parseFKey(reader);
+			assertEquals("SA.TB", fk.targetTable);
+			assertEquals("Foreign key\ndescription.", fk.description);
+			assertEquals(1, fk.keyColumns.size());
+			assertEquals("col2", fk.keyColumns.get("col1"));
+			close(reader);
+
+			// Test with a missing targetTable:
+			reader = buildReader("<foreignKey><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: the targetTable node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.2,c.123] Missing \"targetTable\"!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+			// variant with duplicated targetTable:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><targetTable>SA.TA</targetTable><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: the targetTable node is duplicated!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.58] Only one \"targetTable\" element can exist in a /tableset/schema/table/foreignKey!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+
+			// Test with a missing fkColumn:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><description>Foreign key\ndescription.</description></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: at least 1 fkColumn node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.2,c.40] Missing at least one \"fkColumn\"!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+			// variant with several fkColumn:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><description>Foreign key\ndescription.</description><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn><fkColumn><fromColumn>col3</fromColumn><targetColumn>col4</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			fk = parser.parseFKey(reader);
+			assertEquals("SA.TB", fk.targetTable);
+			assertEquals("Foreign key\ndescription.", fk.description);
+			assertEquals(2, fk.keyColumns.size());
+			assertEquals("col2", fk.keyColumns.get("col1"));
+			assertEquals("col4", fk.keyColumns.get("col3"));
+			close(reader);
+
+			// Test with a missing fromColumn:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><fkColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: the fromColumn node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.99] Missing \"fromColumn\"!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+			// variant with several fromColumn:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><fkColumn><fromColumn>col1</fromColumn><fromColumn>col1bis</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: sereval fromColumn are found!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.96] Only one \"fromColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !", e.getMessage());
+			}finally{
+				close(reader);
+			}
+
+			// Test with a missing targetColumn:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><fkColumn><fromColumn>col1</fromColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: the targetColumn node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.95] Missing \"targetColumn\"!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+			// variant with several fromColumn:
+			reader = buildReader("<foreignKey><targetTable>SA.TB</targetTable><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn><targetColumn>col2bis</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			try{
+				parser.parseFKey(reader);
+				fail("This test should have failed: several targetColumn are found!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.131] Only one \"targetColumn\" element can exist in a /tableset/schema/table/foreignKey/fkColumn !", e.getMessage());
+			}finally{
+				close(reader);
+			}
+
+			// Test with a additional node:
+			reader = buildReader("<foreignKey><super>blabla</super><foo>anything</foo><targetTable>SA.TB</targetTable><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey>");
+			reader.next(); // START ELEMENT("foreignKey")
+			fk = parser.parseFKey(reader);
+			assertEquals("SA.TB", fk.targetTable);
+			assertNull(fk.description);
+			assertEquals(1, fk.keyColumns.size());
+			assertEquals("col2", fk.keyColumns.get("col1"));
+			close(reader);
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+		}
+	}
+
+	@Test
+	public void testParseDataType(){
+		XMLStreamReader reader = null;
+		try{
+
+			// Test while search outside from the dataType node:
+			reader = buildReader("<column " + namespaceDef + "><dataType arraysize=\"*\">char</dataType></column>");
+			reader.next(); // START ELEMENT("column")
+			try{
+				parser.parseDataType(reader);
+				fail("This test should have failed: the reader has not just read the \"dataType\" START ELEMENT tag.");
+			}catch(Exception e){
+				if (e instanceof IllegalStateException)
+					assertEquals("[l.1,c.408] Illegal usage of TableSetParser.parseDataType(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"dataType\".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+
+			// Test with a correct TAP type:
+			reader = buildReader("<column " + namespaceDef + "><dataType xsi:type=\"vs:TAPType\">varchar</dataType></column>");
+			reader.next(); // START ELEMENT("column")
+			reader.next(); // START ELEMENT("dataType")
+			DBType dt = parser.parseDataType(reader);
+			assertEquals(DBDatatype.VARCHAR, dt.type);
+			assertEquals(-1, dt.length);
+			close(reader);
+
+			// Test with a correct VOTable type:
+			reader = buildReader("<dataType " + namespaceDef + " xsi:type=\"vs:VOTableType\" arraysize=\"*\">char</dataType>");
+			reader.next(); // START ELEMENT("dataType")
+			dt = parser.parseDataType(reader);
+			assertEquals(DBDatatype.VARCHAR, dt.type);
+			assertEquals(-1, dt.length);
+			close(reader);
+
+			// Test with a missing xsi:type:
+			reader = buildReader("<dataType " + namespaceDef + " arraysize=\"*\">char</dataType>");
+			reader.next(); // START ELEMENT("dataType")
+			try{
+				parser.parseDataType(reader);
+				fail("This test should have failed: the attribute xsi:type is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.424] Missing attribute \"xsi:type\" (where xsi = \"" + TableSetParser.XSI_NAMESPACE + "\")! Expected attribute value: vs:VOTableType or vs:TAPType, where vs = " + TableSetParser.VODATASERVICE_NAMESPACE + ".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+			// variant with a wrong namespace prefix
+			reader = buildReader("<dataType " + namespaceDef + " xsj:type=\"vs:VOTableType\" arraysize=\"*\">char</dataType>");
+			try{
+				reader.next(); // START ELEMENT("dataType")
+				fail("This test should have failed: the prefix of the xsi:type attribute is wrong!");
+			}catch(Exception e){
+				if (e instanceof XMLStreamException)
+					assertEquals("ParseError at [row,col]:[1,450]\nMessage: http://www.w3.org/TR/1999/REC-xml-names-19990114#AttributePrefixUnbound?dataType&xsj:type&xsj", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+			// variant with a missing namespace prefix:
+			reader = buildReader("<dataType xsi:type=\"vs:VOTableType\" arraysize=\"*\">char</dataType>");
+			try{
+				reader.next(); // START ELEMENT("dataType")
+				fail("This test should have failed: the namespace xsi is not defined!");
+			}catch(Exception e){
+				if (e instanceof XMLStreamException)
+					assertEquals("ParseError at [row,col]:[1,51]\nMessage: http://www.w3.org/TR/1999/REC-xml-names-19990114#AttributePrefixUnbound?dataType&xsi:type&xsi", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+
+			// Test with an unsupported xsi:type:
+			reader = buildReader("<dataType " + namespaceDef + " xsi:type=\"vs:foo\" arraysize=\"*\">char</dataType>");
+			reader.next(); // START ELEMENT("dataType")
+			try{
+				parser.parseDataType(reader);
+				fail("This test should have failed: the type foo is not defined in VODataService!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.457] Unsupported type: \"vs:foo\"! Expected: vs:VOTableType or vs:TAPType, where vs = " + TableSetParser.VODATASERVICE_NAMESPACE + ".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+			// variant with no namespace prefix in front of the wrong type:
+			reader = buildReader("<dataType " + namespaceDef + " xsi:type=\"foo\" arraysize=\"*\">char</dataType>");
+			reader.next(); // START ELEMENT("dataType")
+			try{
+				parser.parseDataType(reader);
+				fail("This test should have failed: the namespace prefix is missing in the value of xsi:type!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.439] Unresolved type: \"foo\"! Missing namespace prefix.", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+
+			// Test with a missing datatype:
+			reader = buildReader("<dataType " + namespaceDef + " xsi:type=\"vs:TAPType\"></dataType>");
+			reader.next(); // START ELEMENT("dataType")
+			try{
+				parser.parseDataType(reader);
+				fail("This test should have failed: the datatype value is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.443] Missing column datatype!", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+			// variant with a wrong datatype:
+			reader = buildReader("<dataType " + namespaceDef + " xsi:type=\"vs:TAPType\">foo</dataType>");
+			reader.next(); // START ELEMENT("dataType")
+			try{
+				parser.parseDataType(reader);
+				fail("This test should have failed: the datatype value is unknown!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.446] Unknown TAPType: \"foo\"!", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+		}
+	}
+
+	@Test
+	public void testParseColumn(){
+		XMLStreamReader reader = null;
+		try{
+
+			// Test while search outside from the column node:
+			reader = buildReader("<table " + namespaceDef + "><column std=\"true\"><name>col1</name><description>Column\ndescription.</description><dataType xsi:type=\"vs:TAPType\">SMALLINT</dataType><utype>truc.chose</utype><ucd>t.c</ucd><unit>deg</unit><flag>nullable</flag><flag>primary</flag><flag>indexed</flag></column></table>");
+			reader.next(); // START ELEMENT("table")
+			try{
+				parser.parseColumn(reader);
+				fail("This test should have failed: the reader has not just read the \"column\" START ELEMENT tag.");
+			}catch(Exception e){
+				if (e instanceof IllegalStateException)
+					assertEquals("[l.1,c.407] Illegal usage of TableSetParser.parseColumn(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"column\".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+			}
+
+			// Test with a complete and correct XML column node:
+			reader = buildReader("<table " + namespaceDef + "><column std=\"true\"><name>col1</name><description>Column\ndescription.</description><dataType xsi:type=\"vs:TAPType\">SMALLINT</dataType><utype>truc.chose</utype><ucd>t.c</ucd><unit>deg</unit><flag>nullable</flag><flag>primary</flag><flag>indexed</flag></column></table>");
+			reader.next(); // START ELEMENT("table")
+			reader.next(); // START ELEMENT("column")
+			TAPColumn col = parser.parseColumn(reader);
+			assertEquals("col1", col.getADQLName());
+			assertEquals("Column\ndescription.", col.getDescription());
+			assertEquals(DBDatatype.SMALLINT, col.getDatatype().type);
+			assertEquals(-1, col.getDatatype().length);
+			assertEquals("truc.chose", col.getUtype());
+			assertEquals("t.c", col.getUcd());
+			assertEquals("deg", col.getUnit());
+			assertTrue(col.isIndexed());
+			assertTrue(col.isPrincipal());
+			assertTrue(col.isNullable());
+			assertTrue(col.isStd());
+			close(reader);
+			// variant with entering inside the foreignKey node (as it is done by TableSetParser):
+			reader = buildReader("<column " + namespaceDef + "><name>col1</name><description>Column\ndescription.</description><dataType xsi:type=\"vs:TAPType\">SMALLINT</dataType><utype>truc.chose</utype><ucd>t.c</ucd><unit>deg</unit></column>");
+			reader.next(); // START ELEMENT("column")
+			col = parser.parseColumn(reader);
+			assertEquals("col1", col.getADQLName());
+			assertEquals("Column\ndescription.", col.getDescription());
+			assertEquals(DBDatatype.SMALLINT, col.getDatatype().type);
+			assertEquals(-1, col.getDatatype().length);
+			assertEquals("truc.chose", col.getUtype());
+			assertEquals("t.c", col.getUcd());
+			assertEquals("deg", col.getUnit());
+			assertFalse(col.isIndexed());
+			assertFalse(col.isPrincipal());
+			assertFalse(col.isNullable());
+			assertFalse(col.isStd());
+			close(reader);
+			// variant with some comments:
+			reader = buildReader("<column " + namespaceDef + "><!-- Here we are inside! --><name>col1</name><!-- Here blabla about the column. --><description>Column\ndescription.</description><dataType xsi:type=\"vs:TAPType\">SMALLINT</dataType><utype>truc.chose</utype><ucd>t.c</ucd><unit>deg</unit><!-- Here is the end! --></column><!-- Nothing more! -->");
+			reader.next(); // START ELEMENT("column")
+			col = parser.parseColumn(reader);
+			assertEquals("col1", col.getADQLName());
+			assertEquals("Column\ndescription.", col.getDescription());
+			assertEquals(DBDatatype.SMALLINT, col.getDatatype().type);
+			assertEquals(-1, col.getDatatype().length);
+			assertEquals("truc.chose", col.getUtype());
+			assertEquals("t.c", col.getUcd());
+			assertEquals("deg", col.getUnit());
+			assertFalse(col.isIndexed());
+			assertFalse(col.isPrincipal());
+			assertFalse(col.isNullable());
+			assertFalse(col.isStd());
+			close(reader);
+			// variant with texts at unapropriate places:
+			reader = buildReader("<column " + namespaceDef + ">Here we are <![CDATA[inside!]]><name>col1</name><description>Column\ndescription.</description><dataType xsi:type=\"vs:TAPType\">SMALLINT</dataType><utype>truc.chose</utype><ucd>t.c</ucd><unit>deg</unit></column>Nothing more!");
+			reader.next(); // START ELEMENT("column")
+			col = parser.parseColumn(reader);
+			assertEquals("col1", col.getADQLName());
+			assertEquals("Column\ndescription.", col.getDescription());
+			assertEquals(DBDatatype.SMALLINT, col.getDatatype().type);
+			assertEquals(-1, col.getDatatype().length);
+			assertEquals("truc.chose", col.getUtype());
+			assertEquals("t.c", col.getUcd());
+			assertEquals("deg", col.getUnit());
+			assertFalse(col.isIndexed());
+			assertFalse(col.isPrincipal());
+			assertFalse(col.isNullable());
+			assertFalse(col.isStd());
+			close(reader);
+
+			// Test with a missing "name" node:
+			reader = buildReader("<column></column>");
+			reader.next(); // START ELEMENT("column")
+			try{
+				parser.parseColumn(reader);
+				fail("This test should have failed: the \"name\" node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.18] Missing column \"name\"!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+			// variant with duplicated "name":
+			reader = buildReader("<column><name>col1</name><name>colA</name></column>");
+			reader.next(); // START ELEMENT("column")
+			try{
+				parser.parseColumn(reader);
+				fail("This test should have failed: the \"name\" node is duplicated!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.32] Only one \"name\" element can exist in a /tableset/schema/table/column!", e.getMessage());
+			}finally{
+				close(reader);
+			}
+
+			// Test with a additional node:
+			reader = buildReader("<column><name>col1</name><dbname>colA</dbname><foo>blabla</foo></column>");
+			reader.next(); // START ELEMENT("foreignKey")
+			col = parser.parseColumn(reader);
+			assertEquals("col1", col.getADQLName());
+			assertNull(col.getDescription());
+			assertEquals(DBDatatype.VARCHAR, col.getDatatype().type);
+			assertEquals(-1, col.getDatatype().length);
+			assertNull(col.getUtype());
+			assertNull(col.getUcd());
+			assertNull(col.getUnit());
+			assertFalse(col.isIndexed());
+			assertFalse(col.isPrincipal());
+			assertFalse(col.isNullable());
+			assertFalse(col.isStd());
+			close(reader);
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+		}
+	}
+
+	@Test
+	public void testParseTable(){
+		XMLStreamReader reader = null;
+		ArrayList<ForeignKey> fkeys = new ArrayList<ForeignKey>(0);
+		try{
+
+			// Test while search outside from the table node:
+			reader = buildReader("<schema><table><name>TableA</name></table></schema>");
+			reader.next(); // START ELEMENT("schema")
+			try{
+				parser.parseTable(reader, fkeys);
+				fail("This test should have failed: the reader has not just read the \"table\" START ELEMENT tag.");
+			}catch(Exception e){
+				if (e instanceof IllegalStateException)
+					assertEquals("[l.1,c.9] Illegal usage of TableSetParser.parseTable(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"table\".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+				fkeys.clear();
+			}
+
+			// Test with a complete and correct XML table node:
+			reader = buildReader("<table><name>TableA</name><description>Table \ndescription.</description><utype>truc.chose</utype><title>Table title</title><column><name>col1</name></column><foreignKey><targetTable>TB</targetTable><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey></table>");
+			reader.next(); // START ELEMENT("table")
+			TAPTable t = parser.parseTable(reader, fkeys);
+			assertEquals("TableA", t.getADQLName());
+			assertEquals("Table\ndescription.", t.getDescription());
+			assertEquals("truc.chose", t.getUtype());
+			assertEquals("Table title", t.getTitle());
+			assertEquals(1, t.getNbColumns());
+			assertNotNull(t.getColumn("col1"));
+			assertEquals(0, t.getNbForeignKeys());
+			assertEquals(1, fkeys.size());
+			assertEquals("TB", fkeys.get(0).targetTable);
+			assertEquals(t, fkeys.get(0).fromTable);
+			close(reader);
+			fkeys.clear();
+			// variant with some comments:
+			reader = buildReader("<table><!-- Here we are inside! --><name>TableA</name><description>Table \ndescription.</description><utype><!-- Table UType -->truc.chose</utype><title>Table title</title><column><name>col1</name></column><foreignKey><targetTable>TB</targetTable><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey></table><!-- Nothing more! -->");
+			reader.next(); // START ELEMENT("table")
+			t = parser.parseTable(reader, fkeys);
+			assertEquals("TableA", t.getADQLName());
+			assertEquals("Table\ndescription.", t.getDescription());
+			assertEquals("truc.chose", t.getUtype());
+			assertEquals("Table title", t.getTitle());
+			assertEquals(1, t.getNbColumns());
+			assertNotNull(t.getColumn("col1"));
+			assertEquals(0, t.getNbForeignKeys());
+			assertEquals(1, fkeys.size());
+			assertEquals("TB", fkeys.get(0).targetTable);
+			assertEquals(t, fkeys.get(0).fromTable);
+			close(reader);
+			fkeys.clear();
+			// variant with texts at unapropriate places:
+			reader = buildReader("<table>Here we are <![CDATA[inside!]]><name>TableA</name><description>Table \ndescription.</description><utype><!-- Table UType -->truc.chose</utype><title>Table title</title><column><name>col1</name></column><foreignKey><targetTable>TB</targetTable><fkColumn><fromColumn>col1</fromColumn><targetColumn>col2</targetColumn></fkColumn></foreignKey></table>Nothing more!");
+			reader.next(); // START ELEMENT("table")
+			t = parser.parseTable(reader, fkeys);
+			assertEquals("TableA", t.getADQLName());
+			assertEquals("Table\ndescription.", t.getDescription());
+			assertEquals("truc.chose", t.getUtype());
+			assertEquals("Table title", t.getTitle());
+			assertEquals(1, t.getNbColumns());
+			assertNotNull(t.getColumn("col1"));
+			assertEquals(0, t.getNbForeignKeys());
+			assertEquals(1, fkeys.size());
+			assertEquals("TB", fkeys.get(0).targetTable);
+			assertEquals(t, fkeys.get(0).fromTable);
+			close(reader);
+			fkeys.clear();
+
+			// Test with a missing "name" node:
+			reader = buildReader("<table></table>");
+			reader.next(); // START ELEMENT("table")
+			try{
+				parser.parseTable(reader, fkeys);
+				fail("This test should have failed: the \"name\" node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.16] Missing table \"name\"!", e.getMessage());
+			}finally{
+				close(reader);
+				fkeys.clear();
+			}
+			// variant with duplicated "name":
+			reader = buildReader("<table><name>Table1</name><name>TableA</name></table>");
+			reader.next(); // START ELEMENT("table")
+			try{
+				parser.parseTable(reader, fkeys);
+				fail("This test should have failed: the \"name\" node is duplicated!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.33] Only one \"name\" element can exist in a /tableset/schema/table!", e.getMessage());
+			}finally{
+				close(reader);
+				fkeys.clear();
+			}
+
+			// Test with a additional node:
+			reader = buildReader("<table><name>TableA</name><dbname>SuperTableA</dbname><foo>blabla</foo></table>");
+			reader.next(); // START ELEMENT("table")
+			t = parser.parseTable(reader, fkeys);
+			assertEquals("TableA", t.getADQLName());
+			assertNull(t.getDescription());
+			assertNull(t.getUtype());
+			assertNull(t.getTitle());
+			assertEquals(0, t.getNbColumns());
+			assertEquals(0, t.getNbForeignKeys());
+			assertEquals(0, fkeys.size());
+			close(reader);
+			fkeys.clear();
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+			fkeys.clear();
+		}
+	}
+
+	@Test
+	public void testParseSchema(){
+		XMLStreamReader reader = null;
+		ArrayList<ForeignKey> fkeys = new ArrayList<ForeignKey>(0);
+		try{
+
+			// Test while search outside from the schema node:
+			reader = buildReader("<tableset><schema><name>PublicSchema</name></schema></tableset>");
+			reader.next(); // START ELEMENT("tableset")
+			try{
+				parser.parseSchema(reader, fkeys);
+				fail("This test should have failed: the reader has not just read the \"schema\" START ELEMENT tag.");
+			}catch(Exception e){
+				if (e instanceof IllegalStateException)
+					assertEquals("[l.1,c.11] Illegal usage of TableSetParser.parseSchema(XMLStreamParser)! This function can be called only when the reader has just read the START ELEMENT tag \"schema\".", e.getMessage());
+				else
+					throw e;
+			}finally{
+				close(reader);
+				fkeys.clear();
+			}
+
+			// Test with a complete and correct XML table node:
+			reader = buildReader("<schema><name>PublicSchema</name><description>Schema \ndescription.</description><utype>truc.chose</utype><title>Schema title</title><table><name>TableA</name></table></schema>");
+			reader.next(); // START ELEMENT("schema")
+			TAPSchema s = parser.parseSchema(reader, fkeys);
+			assertEquals("PublicSchema", s.getADQLName());
+			assertEquals("Schema\ndescription.", s.getDescription());
+			assertEquals("truc.chose", s.getUtype());
+			assertEquals("Schema title", s.getTitle());
+			assertEquals(1, s.getNbTables());
+			assertNotNull(s.getTable("TableA"));
+			close(reader);
+			fkeys.clear();
+			// variant with some comments:
+			reader = buildReader("<schema><!-- Here we are inside! --><name>PublicSchema <!-- schema name --></name><description>Schema \ndescription.</description><utype>truc.chose</utype><title>Schema title</title><table><name>TableA</name></table></schema><!-- Nothing more! -->");
+			reader.next(); // START ELEMENT("schema")
+			s = parser.parseSchema(reader, fkeys);
+			assertEquals("PublicSchema", s.getADQLName());
+			assertEquals("Schema\ndescription.", s.getDescription());
+			assertEquals("truc.chose", s.getUtype());
+			assertEquals("Schema title", s.getTitle());
+			assertEquals(1, s.getNbTables());
+			assertNotNull(s.getTable("TableA"));
+			close(reader);
+			fkeys.clear();
+			// variant with texts at unapropriate places:
+			reader = buildReader("<schema>Here we are <![CDATA[inside!]]><name>PublicSchema <!-- schema name --></name><description>Schema \ndescription.</description><utype>truc.chose</utype><title>Schema title</title><table><name>TableA</name></table></schema>Nothing more!");
+			reader.next(); // START ELEMENT("schema")
+			s = parser.parseSchema(reader, fkeys);
+			assertEquals("PublicSchema", s.getADQLName());
+			assertEquals("Schema\ndescription.", s.getDescription());
+			assertEquals("truc.chose", s.getUtype());
+			assertEquals("Schema title", s.getTitle());
+			assertEquals(1, s.getNbTables());
+			assertNotNull(s.getTable("TableA"));
+			close(reader);
+			fkeys.clear();
+
+			// Test with a missing "name" node:
+			reader = buildReader("<schema></schema>");
+			reader.next(); // START ELEMENT("schema")
+			try{
+				parser.parseSchema(reader, fkeys);
+				fail("This test should have failed: the \"name\" node is missing!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.18] Missing schema \"name\"!", e.getMessage());
+			}finally{
+				close(reader);
+				fkeys.clear();
+			}
+			// variant with duplicated "name":
+			reader = buildReader("<schema><name>PublicSchema</name><name>PrivateSchema</name></schema>");
+			reader.next(); // START ELEMENT("schema")
+			try{
+				parser.parseSchema(reader, fkeys);
+				fail("This test should have failed: the \"name\" node is duplicated!");
+			}catch(Exception e){
+				if (e instanceof TAPException)
+					assertEquals("[l.1,c.40] Only one \"name\" element can exist in a /tableset/schema!", e.getMessage());
+			}finally{
+				close(reader);
+				fkeys.clear();
+			}
+
+			// Test with a additional node:
+			reader = buildReader("<schema><name>PublicSchema</name><dbname>public</dbname><foo>blabla</foo></schema>");
+			reader.next(); // START ELEMENT("schema")
+			s = parser.parseSchema(reader, fkeys);
+			assertEquals("PublicSchema", s.getADQLName());
+			assertNull(s.getDescription());
+			assertNull(s.getUtype());
+			assertNull(s.getTitle());
+			assertEquals(0, s.getNbTables());
+			close(reader);
+			fkeys.clear();
+
+		}catch(Exception e){
+			e.printStackTrace();
+			if (e instanceof XMLStreamException)
+				fail("Unexpected error while reading the XML content: " + e.getMessage());
+			else
+				fail("Unexpected error: " + e.getMessage());
+		}finally{
+			close(reader);
+			fkeys.clear();
+		}
+	}
+
+}