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-2018 - 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 adql.db.DBType;
import adql.db.DBType.DBDatatype;
import tap.TAPException;
import tap.data.VOTableIterator;
import tap.metadata.TAPTable.TableType;

/**
 * <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>
 *
 * <p><b>WARNING:</b>
 * 	When a vod:TAPType is set to <code>BOOLEAN</code>, this will be interpreted as a SMALLINT.
 * 	Note that this is a convenient hack because BOOLEAN is not a valid TAPType,
 * 	but it is however used a lot of time by TAP implementors.
 * </p>
 *
 * @author Gr&eacute;gory Mantelet (ARI)
 * @version 2.3 (04/2018)
 * @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);
				TAPTable newTable = parseTable(reader, keys);
				newTable.setIndex(tables.size());
				tables.add(newTable);
				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 keys				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")){
				TAPColumn newCol = parseColumn(reader);
				newCol.setIndex(columns.size());
				columns.add(newCol);
			}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{
				if ("BOOLEAN".equalsIgnoreCase(datatype)){
					return new DBType(DBDatatype.SMALLINT, colSize);
				}else
					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);
	}

}
