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><dataType xsi:type="vs:VOTableType" arraysize="1">float</dataType></code> for a VOTable datatype</li> + * <li><code><dataType xsi:type="vod:TAPType">VARCHAR</dataType></code> for a TAP datatype</li> + * </ul> + * + * @author Gré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é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(); + } + } + +}