From f90bce45692999ac0c82319e5dedd1ec63713dff Mon Sep 17 00:00:00 2001 From: gmantele <gmantele@ari.uni-heidelberg.de> Date: Tue, 5 Aug 2014 18:15:23 +0200 Subject: [PATCH] [TAP,ADQL] Improve and remake a part of the database connection. Missing javadoc has been added when missing in the modified tables. --- src/adql/translator/ADQLTranslator.java | 6 +- src/adql/translator/JDBCTranslator.java | 803 ++++++ src/adql/translator/PgSphereTranslator.java | 36 +- src/adql/translator/PostgreSQLTranslator.java | 691 +---- src/tap/ADQLExecutor.java | 10 +- src/tap/data/LimitedTableIterator.java | 227 ++ src/tap/data/ResultSetTableIterator.java | 306 +- src/tap/data/TableIterator.java | 14 +- src/tap/data/VOTableIterator.java | 9 + src/tap/db/DBConnection.java | 171 +- src/tap/db/JDBCConnection.java | 2477 +++++++++++++++-- src/tap/db/JDBCTAPFactory.java | 228 -- src/tap/log/TAPLog.java | 2 + src/tap/metadata/TAPColumn.java | 446 ++- src/tap/metadata/TAPDM.java | 52 - src/tap/metadata/TAPMetadata.java | 524 +++- src/tap/metadata/TAPSchema.java | 281 +- src/tap/metadata/TAPTable.java | 583 +++- src/tap/upload/LimitedSizeInputStream.java | 2 +- src/tap/upload/Uploader.java | 7 +- test/adql/IdentifierFieldTest.java | 25 + test/adql/SearchColumnListTest.java | 9 +- test/tap/data/ResultSetTableIteratorTest.java | 21 +- test/tap/data/VOTableIteratorTest.java | 32 +- test/tap/db/JDBCConnectionTest.java | 1015 +++++++ test/tap/db/TestTAPDb.db | Bin 0 -> 23552 bytes test/tap/db/upload_example.vot | 75 + test/tap/formatter/JSONFormatTest.java | 9 +- test/tap/formatter/SVFormatTest.java | 9 +- test/tap/formatter/TextFormatTest.java | 9 +- test/tap/formatter/VOTableFormatTest.java | 13 +- test/testtools/DBTools.java | 2 +- test/testtools/MD5Checksum.java | 46 + 33 files changed, 6794 insertions(+), 1346 deletions(-) create mode 100644 src/adql/translator/JDBCTranslator.java create mode 100644 src/tap/data/LimitedTableIterator.java delete mode 100644 src/tap/db/JDBCTAPFactory.java delete mode 100644 src/tap/metadata/TAPDM.java create mode 100644 test/adql/IdentifierFieldTest.java create mode 100644 test/tap/db/JDBCConnectionTest.java create mode 100644 test/tap/db/TestTAPDb.db create mode 100644 test/tap/db/upload_example.vot create mode 100644 test/testtools/MD5Checksum.java diff --git a/src/adql/translator/ADQLTranslator.java b/src/adql/translator/ADQLTranslator.java index 7ec6bf9..1174f08 100644 --- a/src/adql/translator/ADQLTranslator.java +++ b/src/adql/translator/ADQLTranslator.java @@ -16,7 +16,8 @@ package adql.translator; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.ADQLList; @@ -28,7 +29,6 @@ import adql.query.ClauseSelect; import adql.query.ColumnReference; import adql.query.SelectAllColumns; import adql.query.SelectItem; - import adql.query.constraint.ADQLConstraint; import adql.query.constraint.Between; import adql.query.constraint.Comparison; @@ -60,11 +60,11 @@ import adql.query.operand.function.geometry.DistanceFunction; import adql.query.operand.function.geometry.ExtractCoord; import adql.query.operand.function.geometry.ExtractCoordSys; import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; -import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; /** * Translates ADQL objects into any language (i.e. SQL). diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java new file mode 100644 index 0000000..5f791cf --- /dev/null +++ b/src/adql/translator/JDBCTranslator.java @@ -0,0 +1,803 @@ +package adql.translator; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary 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. + * + * ADQLLibrary 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 ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import adql.db.DBColumn; +import adql.db.DBTable; +import adql.db.exception.UnresolvedJoin; +import adql.query.ADQLList; +import adql.query.ADQLObject; +import adql.query.ADQLOrder; +import adql.query.ADQLQuery; +import adql.query.ClauseConstraints; +import adql.query.ClauseSelect; +import adql.query.ColumnReference; +import adql.query.IdentifierField; +import adql.query.SelectAllColumns; +import adql.query.SelectItem; +import adql.query.constraint.ADQLConstraint; +import adql.query.constraint.Between; +import adql.query.constraint.Comparison; +import adql.query.constraint.ConstraintsGroup; +import adql.query.constraint.Exists; +import adql.query.constraint.In; +import adql.query.constraint.IsNull; +import adql.query.constraint.NotConstraint; +import adql.query.from.ADQLJoin; +import adql.query.from.ADQLTable; +import adql.query.from.FromContent; +import adql.query.operand.ADQLColumn; +import adql.query.operand.ADQLOperand; +import adql.query.operand.Concatenation; +import adql.query.operand.NegativeOperand; +import adql.query.operand.NumericConstant; +import adql.query.operand.Operation; +import adql.query.operand.StringConstant; +import adql.query.operand.WrappedOperand; +import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.MathFunction; +import adql.query.operand.function.SQLFunction; +import adql.query.operand.function.SQLFunctionType; +import adql.query.operand.function.UserDefinedFunction; +import adql.query.operand.function.geometry.AreaFunction; +import adql.query.operand.function.geometry.BoxFunction; +import adql.query.operand.function.geometry.CentroidFunction; +import adql.query.operand.function.geometry.CircleFunction; +import adql.query.operand.function.geometry.ContainsFunction; +import adql.query.operand.function.geometry.DistanceFunction; +import adql.query.operand.function.geometry.ExtractCoord; +import adql.query.operand.function.geometry.ExtractCoordSys; +import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; +import adql.query.operand.function.geometry.IntersectsFunction; +import adql.query.operand.function.geometry.PointFunction; +import adql.query.operand.function.geometry.PolygonFunction; +import adql.query.operand.function.geometry.RegionFunction; + +/** + * <p>Implementation of {@link ADQLTranslator} which translates ADQL queries in SQL queries.</p> + * + * <p> + * It is already able to translate all SQL standard features, but lets abstract the translation of all + * geometrical functions. So, this translator must be extended as {@link PostgreSQLTranslator} and + * {@link PgSphereTranslator} are doing. + * </p> + * + * <p><i>Note: + * Its default implementation of the SQL syntax has been inspired by the PostgreSQL one. + * However, it should work also with SQLite and MySQL, but some translations might be needed + * (as it is has been done for PostgreSQL about the mathematical functions). + * </i></p> + * + * <h3>PostgreSQLTranslator and PgSphereTranslator</h3> + * + * <p> + * {@link PgSphereTranslator} extends {@link PostgreSQLTranslator} and is just translating geometrical + * functions according to the syntax given by PgSphere. + * </p> + * + * <p> + * {@link PostgreSQLTranslator} overwrites the translation of mathematical functions whose some have + * a different name or signature. Besides, it is also implementing the translation of the geometrical + * functions. However, it does not really translate them. It is just returning the ADQL expression + * (by calling {@link #getDefaultADQLFunction(ADQLFunction)}). + * And so, of course, the execution of a SQL query containing geometrical functions and translated + * using this translator will not work. It is just a default implementation in case there is no interest + * of these geometrical functions. + * </p> + * + * <h3>SQL with or without case sensitivity?</h3> + * + * <p> + * In ADQL and in SQL, it is possible to tell the parser to respect the exact case or not of an identifier (schema, table or column name) + * by surrounding it with double quotes. However ADQL identifiers and SQL ones may be different. In that way, the case sensitivity specified + * in ADQL on the different identifiers can not be kept in SQL. That's why this translator lets specify a general rule on which types of + * SQL identifier must be double quoted. This can be done by implementing the abstract function {@link #isCaseSensitive(IdentifierField)}. + * The functions translating column and table names will call this function in order to surround the identifiers by double quotes or not. + * So, <b>be careful if you want to override the functions translating columns and tables!</b> + * </p> + * + * <h3>Translation of "SELECT TOP"</h3> + * + * <p> + * The default behavior of this translator is to translate the ADQL "TOP" into the SQL "LIMIT" at the end of the query. + * This is ok for some DBMS, but not all. So, if your DBMS does not know the "LIMIT" keyword, you should override the function + * translating the whole query: {@link #translate(ADQLQuery)}. Here is its current implementation: + * </p> + * <pre> + * StringBuffer sql = new StringBuffer(translate(query.getSelect())); + * sql.append("\nFROM ").append(translate(query.getFrom())); + * if (!query.getWhere().isEmpty()) + * sql.append('\n').append(translate(query.getWhere())); + * if (!query.getGroupBy().isEmpty()) + * sql.append('\n').append(translate(query.getGroupBy())); + * if (!query.getHaving().isEmpty()) + * sql.append('\n').append(translate(query.getHaving())); + * if (!query.getOrderBy().isEmpty()) + * sql.append('\n').append(translate(query.getOrderBy())); + * if (query.getSelect().hasLimit()) + * sql.append("\nLimit ").append(query.getSelect().getLimit()); + * return sql.toString(); + * </pre> + * + * <h3>Translation of ADQL functions</h3> + * + * <p> + * All ADQL functions are by default not translated. Consequently, the SQL translation is + * actually the ADQL expression. Generally the ADQL expression is generic enough. However some mathematical functions may need + * to be translated differently. For instance {@link PostgreSQLTranslator} is translating differently: LOG, LOG10, RAND and TRUNC. + * </p> + * + * <p><i>Note: + * Geometrical function have not been translated here. They stay abstract because it is obviously impossible to have a generic + * translation ; it totally depends from the database system. + * </i></p> + * + * <h3>Translation of "FROM" with JOINs</h3> + * + * <p> + * The FROM clause is translated into SQL as written in ADQL. There is no differences except the identifiers that are replaced. + * The tables' aliases and their case sensitivity are kept like in ADQL. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (08/2014) + * @since 2.0 + * + * @see PostgreSQLTranslator + * @see PgSphereTranslator + */ +public abstract class JDBCTranslator implements ADQLTranslator { + + /** + * <p>Tell whether the specified identifier MUST be translated so that being interpreted case sensitively or not. + * By default, an identifier that must be translated with case sensitivity will be surrounded by double quotes. + * But, if this function returns FALSE, the SQL name will be written just as given in the metadata, without double quotes.</p> + * + * <p><b>WARNING</b>: + * An {@link IdentifierField} object can be a SCHEMA, TABLE, COLUMN and ALIAS. However, in this translator, + * aliases are translated like in ADQL (so, with the same case sensitivity specification as in ADQL). + * So, this function will never be used to know the case sensitivity to apply to an alias. It is then + * useless to write a special behavior for the ALIAS value. + * </p> + * + * @param field The identifier whose the case sensitive to apply is asked. + * + * @return <i>true</i> if the specified identifier must be translated case sensitivity, <i>false</i> otherwise (included if ALIAS or NULL). + */ + public abstract boolean isCaseSensitive(final IdentifierField field); + + /** + * <p>Get the qualified DB name of the schema containing the given table.</p> + * + * <p><i>Note: + * This function will, by default, add double quotes if the schema name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + * </i></p> + * + * @param table A table of the schema whose the qualified DB name is asked. + * + * @return The qualified (with DB catalog name prefix if any, and with double quotes if needed) DB schema name, + * or an empty string if there is no schema or no DB name. + */ + public String getQualifiedSchemaName(final DBTable table){ + if (table == null || table.getDBSchemaName() == null) + return ""; + + StringBuffer buf = new StringBuffer(); + + if (table.getDBCatalogName() != null) + appendIdentifier(buf, table.getDBCatalogName(), IdentifierField.CATALOG).append('.'); + + appendIdentifier(buf, table.getDBSchemaName(), IdentifierField.SCHEMA); + + return buf.toString(); + } + + /** + * <p>Get the qualified DB name of the given table.</p> + * + * <p><i>Note: + * This function will, by default, add double quotes if the table name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + * </i></p> + * + * @param table The table whose the qualified DB name is asked. + * + * @return The qualified (with DB catalog and schema prefix if any, and with double quotes if needed) DB table name, + * or an empty string if the given table is NULL or if there is no DB name. + */ + public String getQualifiedTableName(final DBTable table){ + if (table == null) + return ""; + + StringBuffer buf = new StringBuffer(getQualifiedSchemaName(table)); + if (buf.length() > 0) + buf.append('.'); + + appendIdentifier(buf, table.getDBName(), IdentifierField.TABLE); + + return buf.toString(); + } + + /** + * <p>Get the DB name of the given column</p> + * + * <p><i>Note: + * This function will, by default, add double quotes if the column name must be case sensitive in the SQL query. + * This information is provided by {@link #isCaseSensitive(IdentifierField)}. + * </i></p> + * + * <p><b>Caution: + * The given column may be NULL and in this case an empty string will be returned. + * But if the given column is not NULL, its DB name MUST NOT BE NULL! + * </b></p> + * + * @param column The column whose the DB name is asked. + * + * @return The DB column name (with double quotes if needed), + * or an empty string if the given column is NULL. + */ + public String getColumnName(final DBColumn column){ + return (column == null) ? "" : appendIdentifier(new StringBuffer(), column.getDBName(), IdentifierField.COLUMN).toString(); + } + + /** + * Appends the given identifier in the given StringBuffer. + * + * @param str The string buffer. + * @param id The identifier to append. + * @param field The type of identifier (column, table, schema, catalog or alias ?). + * + * @return The string buffer + identifier. + */ + public final StringBuffer appendIdentifier(final StringBuffer str, final String id, final IdentifierField field){ + return appendIdentifier(str, id, isCaseSensitive(field)); + } + + /** + * Appends the given identifier to the given StringBuffer. + * + * @param str The string buffer. + * @param id The identifier to append. + * @param caseSensitive <i>true</i> to format the identifier so that preserving the case sensitivity, <i>false</i> otherwise. + * + * @return The string buffer + identifier. + */ + public static final StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive){ + if (caseSensitive) + return str.append('"').append(id).append('"'); + else + return str.append(id); + } + + @Override + @SuppressWarnings({"unchecked","rawtypes"}) + public String translate(ADQLObject obj) throws TranslationException{ + if (obj instanceof ADQLQuery) + return translate((ADQLQuery)obj); + else if (obj instanceof ADQLList) + return translate((ADQLList)obj); + else if (obj instanceof SelectItem) + return translate((SelectItem)obj); + else if (obj instanceof ColumnReference) + return translate((ColumnReference)obj); + else if (obj instanceof ADQLTable) + return translate((ADQLTable)obj); + else if (obj instanceof ADQLJoin) + return translate((ADQLJoin)obj); + else if (obj instanceof ADQLOperand) + return translate((ADQLOperand)obj); + else if (obj instanceof ADQLConstraint) + return translate((ADQLConstraint)obj); + else + return obj.toADQL(); + } + + @Override + public String translate(ADQLQuery query) throws TranslationException{ + StringBuffer sql = new StringBuffer(translate(query.getSelect())); + + sql.append("\nFROM ").append(translate(query.getFrom())); + + if (!query.getWhere().isEmpty()) + sql.append('\n').append(translate(query.getWhere())); + + if (!query.getGroupBy().isEmpty()) + sql.append('\n').append(translate(query.getGroupBy())); + + if (!query.getHaving().isEmpty()) + sql.append('\n').append(translate(query.getHaving())); + + if (!query.getOrderBy().isEmpty()) + sql.append('\n').append(translate(query.getOrderBy())); + + if (query.getSelect().hasLimit()) + sql.append("\nLimit ").append(query.getSelect().getLimit()); + + return sql.toString(); + } + + /* *************************** */ + /* ****** LIST & CLAUSE ****** */ + /* *************************** */ + @Override + public String translate(ADQLList<? extends ADQLObject> list) throws TranslationException{ + if (list instanceof ClauseSelect) + return translate((ClauseSelect)list); + else if (list instanceof ClauseConstraints) + return translate((ClauseConstraints)list); + else + return getDefaultADQLList(list); + } + + /** + * Gets the default SQL output for a list of ADQL objects. + * + * @param list List to format into SQL. + * + * @return The corresponding SQL. + * + * @throws TranslationException If there is an error during the translation. + */ + protected String getDefaultADQLList(ADQLList<? extends ADQLObject> list) throws TranslationException{ + String sql = (list.getName() == null) ? "" : (list.getName() + " "); + + for(int i = 0; i < list.size(); i++) + sql += ((i == 0) ? "" : (" " + list.getSeparator(i) + " ")) + translate(list.get(i)); + + return sql; + } + + @Override + public String translate(ClauseSelect clause) throws TranslationException{ + String sql = null; + + for(int i = 0; i < clause.size(); i++){ + if (i == 0){ + sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : ""); + }else + sql += " " + clause.getSeparator(i); + + sql += " " + translate(clause.get(i)); + } + + return sql; + } + + @Override + public String translate(ClauseConstraints clause) throws TranslationException{ + if (clause instanceof ConstraintsGroup) + return "(" + getDefaultADQLList(clause) + ")"; + else + return getDefaultADQLList(clause); + } + + @Override + public String translate(SelectItem item) throws TranslationException{ + if (item instanceof SelectAllColumns) + return translate((SelectAllColumns)item); + + StringBuffer translation = new StringBuffer(translate(item.getOperand())); + if (item.hasAlias()){ + translation.append(" AS "); + appendIdentifier(translation, item.getAlias(), item.isCaseSensitive()); + }else + translation.append(" AS ").append(item.getName()); + + return translation.toString(); + } + + @Override + public String translate(SelectAllColumns item) throws TranslationException{ + HashMap<String,String> mapAlias = new HashMap<String,String>(); + + // Fetch the full list of columns to display: + Iterable<DBColumn> dbCols = null; + if (item.getAdqlTable() != null && item.getAdqlTable().getDBLink() != null){ + ADQLTable table = item.getAdqlTable(); + dbCols = table.getDBLink(); + if (table.hasAlias()){ + String key = getQualifiedTableName(table.getDBLink()); + mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); + } + }else if (item.getQuery() != null){ + try{ + dbCols = item.getQuery().getFrom().getDBColumns(); + }catch(UnresolvedJoin pe){ + throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe); + } + ArrayList<ADQLTable> tables = item.getQuery().getFrom().getTables(); + for(ADQLTable table : tables){ + if (table.hasAlias()){ + String key = getQualifiedTableName(table.getDBLink()); + mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); + } + } + } + + // Write the DB name of all these columns: + if (dbCols != null){ + StringBuffer cols = new StringBuffer(); + for(DBColumn col : dbCols){ + if (cols.length() > 0) + cols.append(','); + if (col.getTable() != null){ + String fullDbName = getQualifiedTableName(col.getTable()); + if (mapAlias.containsKey(fullDbName)) + appendIdentifier(cols, mapAlias.get(fullDbName), false).append('.'); + else + cols.append(fullDbName).append('.'); + } + appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN); + cols.append(" AS \"").append(col.getADQLName()).append('\"'); + } + return (cols.length() > 0) ? cols.toString() : item.toADQL(); + }else{ + return item.toADQL(); + } + } + + @Override + public String translate(ColumnReference ref) throws TranslationException{ + if (ref instanceof ADQLOrder) + return translate((ADQLOrder)ref); + else + return getDefaultColumnReference(ref); + } + + /** + * Gets the default SQL output for a column reference. + * + * @param ref The column reference to format into SQL. + * + * @return The corresponding SQL. + * + * @throws TranslationException If there is an error during the translation. + */ + protected String getDefaultColumnReference(ColumnReference ref) throws TranslationException{ + if (ref.isIndex()){ + return "" + ref.getColumnIndex(); + }else{ + if (ref.getDBLink() == null){ + return (ref.isCaseSensitive() ? ("\"" + ref.getColumnName() + "\"") : ref.getColumnName()); + }else{ + DBColumn dbCol = ref.getDBLink(); + StringBuffer colName = new StringBuffer(); + // Use the table alias if any: + if (ref.getAdqlTable() != null && ref.getAdqlTable().hasAlias()) + appendIdentifier(colName, ref.getAdqlTable().getAlias(), ref.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); + + // Use the DBTable if any: + else if (dbCol.getTable() != null) + colName.append(getQualifiedTableName(dbCol.getTable())).append('.'); + + appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); + + return colName.toString(); + } + } + } + + @Override + public String translate(ADQLOrder order) throws TranslationException{ + return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC"); + } + + /* ************************** */ + /* ****** TABLE & JOIN ****** */ + /* ************************** */ + @Override + public String translate(FromContent content) throws TranslationException{ + if (content instanceof ADQLTable) + return translate((ADQLTable)content); + else if (content instanceof ADQLJoin) + return translate((ADQLJoin)content); + else + return content.toADQL(); + } + + @Override + public String translate(ADQLTable table) throws TranslationException{ + StringBuffer sql = new StringBuffer(); + + // CASE: SUB-QUERY: + if (table.isSubQuery()) + sql.append('(').append(translate(table.getSubQuery())).append(')'); + + // CASE: TABLE REFERENCE: + else{ + // Use the corresponding DB table, if known: + if (table.getDBLink() != null) + sql.append(getQualifiedTableName(table.getDBLink())); + // Otherwise, use the whole table name given in the ADQL query: + else + sql.append(table.getFullTableName()); + } + + // Add the table alias, if any: + if (table.hasAlias()){ + sql.append(" AS "); + appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS)); + } + + return sql.toString(); + } + + @Override + public String translate(ADQLJoin join) throws TranslationException{ + StringBuffer sql = new StringBuffer(translate(join.getLeftTable())); + + if (join.isNatural()) + sql.append(" NATURAL"); + + sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' '); + + if (!join.isNatural()){ + if (join.getJoinCondition() != null) + sql.append(translate(join.getJoinCondition())); + else if (join.hasJoinedColumns()){ + StringBuffer cols = new StringBuffer(); + Iterator<ADQLColumn> it = join.getJoinedColumns(); + while(it.hasNext()){ + ADQLColumn item = it.next(); + if (cols.length() > 0) + cols.append(", "); + if (item.getDBLink() == null) + appendIdentifier(cols, item.getColumnName(), item.isCaseSensitive(IdentifierField.COLUMN)); + else + appendIdentifier(cols, item.getDBLink().getDBName(), IdentifierField.COLUMN); + } + sql.append("USING (").append(cols).append(')'); + } + } + + return sql.toString(); + } + + /* ********************* */ + /* ****** OPERAND ****** */ + /* ********************* */ + @Override + public String translate(ADQLOperand op) throws TranslationException{ + if (op instanceof ADQLColumn) + return translate((ADQLColumn)op); + else if (op instanceof Concatenation) + return translate((Concatenation)op); + else if (op instanceof NegativeOperand) + return translate((NegativeOperand)op); + else if (op instanceof NumericConstant) + return translate((NumericConstant)op); + else if (op instanceof StringConstant) + return translate((StringConstant)op); + else if (op instanceof WrappedOperand) + return translate((WrappedOperand)op); + else if (op instanceof Operation) + return translate((Operation)op); + else if (op instanceof ADQLFunction) + return translate((ADQLFunction)op); + else + return op.toADQL(); + } + + @Override + public String translate(ADQLColumn column) throws TranslationException{ + // Use its DB name if known: + if (column.getDBLink() != null){ + DBColumn dbCol = column.getDBLink(); + StringBuffer colName = new StringBuffer(); + // Use the table alias if any: + if (column.getAdqlTable() != null && column.getAdqlTable().hasAlias()) + appendIdentifier(colName, column.getAdqlTable().getAlias(), column.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); + + // Use the DBTable if any: + else if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null) + colName.append(getQualifiedTableName(dbCol.getTable())).append('.'); + + // Otherwise, use the prefix of the column given in the ADQL query: + else if (column.getTableName() != null) + colName = column.getFullColumnPrefix().append('.'); + + appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); + + return colName.toString(); + } + // Otherwise, use the whole name given in the ADQL query: + else + return column.getFullColumnName(); + } + + @Override + public String translate(Concatenation concat) throws TranslationException{ + return translate((ADQLList<ADQLOperand>)concat); + } + + @Override + public String translate(NegativeOperand negOp) throws TranslationException{ + return "-" + translate(negOp.getOperand()); + } + + @Override + public String translate(NumericConstant numConst) throws TranslationException{ + return numConst.getValue(); + } + + @Override + public String translate(StringConstant strConst) throws TranslationException{ + return "'" + strConst.getValue() + "'"; + } + + @Override + public String translate(WrappedOperand op) throws TranslationException{ + return "(" + translate(op.getOperand()) + ")"; + } + + @Override + public String translate(Operation op) throws TranslationException{ + return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand()); + } + + /* ************************ */ + /* ****** CONSTRAINT ****** */ + /* ************************ */ + @Override + public String translate(ADQLConstraint cons) throws TranslationException{ + if (cons instanceof Comparison) + return translate((Comparison)cons); + else if (cons instanceof Between) + return translate((Between)cons); + else if (cons instanceof Exists) + return translate((Exists)cons); + else if (cons instanceof In) + return translate((In)cons); + else if (cons instanceof IsNull) + return translate((IsNull)cons); + else if (cons instanceof NotConstraint) + return translate((NotConstraint)cons); + else + return cons.toADQL(); + } + + @Override + public String translate(Comparison comp) throws TranslationException{ + return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand()); + } + + @Override + public String translate(Between comp) throws TranslationException{ + return translate(comp.getLeftOperand()) + " BETWEEN " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand()); + } + + @Override + public String translate(Exists exists) throws TranslationException{ + return "EXISTS(" + translate(exists.getSubQuery()) + ")"; + } + + @Override + public String translate(In in) throws TranslationException{ + return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")"; + } + + @Override + public String translate(IsNull isNull) throws TranslationException{ + return translate(isNull.getColumn()) + " IS " + (isNull.isNotNull() ? "NOT " : "") + "NULL"; + } + + @Override + public String translate(NotConstraint notCons) throws TranslationException{ + return "NOT " + translate(notCons.getConstraint()); + } + + /* *********************** */ + /* ****** FUNCTIONS ****** */ + /* *********************** */ + @Override + public String translate(ADQLFunction fct) throws TranslationException{ + if (fct instanceof GeometryFunction) + return translate((GeometryFunction)fct); + else if (fct instanceof MathFunction) + return translate((MathFunction)fct); + else if (fct instanceof SQLFunction) + return translate((SQLFunction)fct); + else if (fct instanceof UserDefinedFunction) + return translate((UserDefinedFunction)fct); + else + return getDefaultADQLFunction(fct); + } + + /** + * Gets the default SQL output for the given ADQL function. + * + * @param fct The ADQL function to format into SQL. + * + * @return The corresponding SQL. + * + * @throws TranslationException If there is an error during the translation. + */ + protected final String getDefaultADQLFunction(ADQLFunction fct) throws TranslationException{ + String sql = fct.getName() + "("; + + for(int i = 0; i < fct.getNbParameters(); i++) + sql += ((i == 0) ? "" : ", ") + translate(fct.getParameter(i)); + + return sql + ")"; + } + + @Override + public String translate(SQLFunction fct) throws TranslationException{ + if (fct.getType() == SQLFunctionType.COUNT_ALL) + return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)"; + else + return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")"; + } + + @Override + public String translate(MathFunction fct) throws TranslationException{ + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(UserDefinedFunction fct) throws TranslationException{ + return getDefaultADQLFunction(fct); + } + + /* *********************************** */ + /* ****** GEOMETRICAL FUNCTIONS ****** */ + /* *********************************** */ + @Override + public String translate(GeometryFunction fct) throws TranslationException{ + if (fct instanceof AreaFunction) + return translate((AreaFunction)fct); + else if (fct instanceof BoxFunction) + return translate((BoxFunction)fct); + else if (fct instanceof CentroidFunction) + return translate((CentroidFunction)fct); + else if (fct instanceof CircleFunction) + return translate((CircleFunction)fct); + else if (fct instanceof ContainsFunction) + return translate((ContainsFunction)fct); + else if (fct instanceof DistanceFunction) + return translate((DistanceFunction)fct); + else if (fct instanceof ExtractCoord) + return translate((ExtractCoord)fct); + else if (fct instanceof ExtractCoordSys) + return translate((ExtractCoordSys)fct); + else if (fct instanceof IntersectsFunction) + return translate((IntersectsFunction)fct); + else if (fct instanceof PointFunction) + return translate((PointFunction)fct); + else if (fct instanceof PolygonFunction) + return translate((PolygonFunction)fct); + else if (fct instanceof RegionFunction) + return translate((RegionFunction)fct); + else + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(GeometryValue<? extends GeometryFunction> geomValue) throws TranslationException{ + return translate(geomValue.getValue()); + } + +} diff --git a/src/adql/translator/PgSphereTranslator.java b/src/adql/translator/PgSphereTranslator.java index efe3807..fd02dfa 100644 --- a/src/adql/translator/PgSphereTranslator.java +++ b/src/adql/translator/PgSphereTranslator.java @@ -16,12 +16,12 @@ package adql.translator; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import adql.query.constraint.Comparison; import adql.query.constraint.ComparisonOperator; - import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CircleFunction; @@ -32,24 +32,19 @@ import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; -import adql.translator.PostgreSQLTranslator; -import adql.translator.TranslationException; - /** * <p>Translates all ADQL objects into the SQL adaptation of Postgres+PgSphere. * Actually only the geometrical functions are translated in this class. * The other functions are managed by {@link PostgreSQLTranslator}.</p> * - * @author Grégory Mantelet (CDS) - * @version 01/2012 - * - * @see PostgreSQLTranslator + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) */ public class PgSphereTranslator extends PostgreSQLTranslator { /** - * Builds a PgSphereTranslator which takes into account the case sensitivity on column names. - * It means that column names which have been written between double quotes, will be also translated between double quotes. + * Builds a PgSphereTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ; + * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation. * * @see PostgreSQLTranslator#PostgreSQLTranslator() */ @@ -58,23 +53,24 @@ public class PgSphereTranslator extends PostgreSQLTranslator { } /** - * Builds a PgSphereTranslator. + * Builds a PgSphereTranslator which always translates in SQL all identifiers (schema, table and column) in the specified case sensitivity ; + * in other words, schema, table and column names will all be surrounded or not by double quotes in the SQL translation. * - * @param column <i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise. + * @param allCaseSensitive <i>true</i> to translate all identifiers in a case sensitive manner (surrounded by double quotes), <i>false</i> for case insensitivity. * * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean) */ - public PgSphereTranslator(boolean column){ - super(column); + public PgSphereTranslator(boolean allCaseSensitive){ + super(allCaseSensitive); } /** - * Builds a PgSphereTranslator. + * Builds a PgSphereTranslator which will always translate in SQL identifiers with the defined case sensitivity. * - * @param catalog <i>true</i> to take into account the case sensitivity of catalog names, <i>false</i> otherwise. - * @param schema <i>true</i> to take into account the case sensitivity of schema names, <i>false</i> otherwise. - * @param table <i>true</i> to take into account the case sensitivity of table names, <i>false</i> otherwise. - * @param column <i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise. + * @param catalog <i>true</i> to translate catalog names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param schema <i>true</i> to translate schema names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param table <i>true</i> to translate table names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param column <i>true</i> to translate column names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. * * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean, boolean, boolean, boolean) */ diff --git a/src/adql/translator/PostgreSQLTranslator.java b/src/adql/translator/PostgreSQLTranslator.java index 58cdc7a..9698462 100644 --- a/src/adql/translator/PostgreSQLTranslator.java +++ b/src/adql/translator/PostgreSQLTranslator.java @@ -17,50 +17,11 @@ package adql.translator; * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; - -import adql.db.DBColumn; -import adql.db.DBTable; -import adql.db.exception.UnresolvedJoin; -import adql.query.ADQLList; -import adql.query.ADQLObject; -import adql.query.ADQLOrder; -import adql.query.ADQLQuery; -import adql.query.ClauseConstraints; -import adql.query.ClauseSelect; -import adql.query.ColumnReference; import adql.query.IdentifierField; -import adql.query.SelectAllColumns; -import adql.query.SelectItem; -import adql.query.constraint.ADQLConstraint; -import adql.query.constraint.Between; -import adql.query.constraint.Comparison; -import adql.query.constraint.ConstraintsGroup; -import adql.query.constraint.Exists; -import adql.query.constraint.In; -import adql.query.constraint.IsNull; -import adql.query.constraint.NotConstraint; -import adql.query.from.ADQLJoin; -import adql.query.from.ADQLTable; -import adql.query.from.FromContent; -import adql.query.operand.ADQLColumn; -import adql.query.operand.ADQLOperand; -import adql.query.operand.Concatenation; -import adql.query.operand.NegativeOperand; -import adql.query.operand.NumericConstant; -import adql.query.operand.Operation; -import adql.query.operand.StringConstant; -import adql.query.operand.WrappedOperand; -import adql.query.operand.function.ADQLFunction; import adql.query.operand.function.MathFunction; -import adql.query.operand.function.SQLFunction; -import adql.query.operand.function.SQLFunctionType; -import adql.query.operand.function.UserDefinedFunction; import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CentroidFunction; @@ -69,54 +30,61 @@ import adql.query.operand.function.geometry.ContainsFunction; import adql.query.operand.function.geometry.DistanceFunction; import adql.query.operand.function.geometry.ExtractCoord; import adql.query.operand.function.geometry.ExtractCoordSys; -import adql.query.operand.function.geometry.GeometryFunction; -import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; /** - * <p>Translates all ADQL objects into the SQL adaptation of Postgres.</p> + * <p>Translates all ADQL objects into an SQL interrogation query designed for PostgreSQL.</p> * - * <p><b><u>IMPORTANT:</u> The geometrical functions are translated exactly as in ADQL. - * You will probably need to extend this translator to correctly manage the geometrical functions. - * An extension is already available for PgSphere: {@link PgSphereTranslator}.</b></p> + * <p><i><b>Important</b>: + * The geometrical functions are translated exactly as in ADQL. + * You will probably need to extend this translator to correctly manage the geometrical functions. + * An extension is already available for PgSphere: {@link PgSphereTranslator}. + * </i></p> * * @author Grégory Mantelet (CDS;ARI) - * @version 1.2 (03/2014) + * @version 2.0 (08/2014) * * @see PgSphereTranslator */ -public class PostgreSQLTranslator implements ADQLTranslator { +public class PostgreSQLTranslator extends JDBCTranslator { - protected boolean inSelect = false; + /** <p>Indicate the case sensitivity to apply to each SQL identifier (only SCHEMA, TABLE and COLUMN).</p> + * + * <p><i>Note: + * In this implementation, this field is set by the constructor and never modified elsewhere. + * It would be better to never modify it after the construction in order to keep a certain consistency. + * </i></p> + */ protected byte caseSensitivity = 0x00; /** - * Builds a PostgreSQLTranslator which takes into account the case sensitivity on column names. - * It means that column names which have been written between double quotes, will be also translated between double quotes. + * Builds a PostgreSQLTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ; + * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation. */ public PostgreSQLTranslator(){ - this(true); + caseSensitivity = 0x0F; } /** - * Builds a PostgreSQLTranslator. + * Builds a PostgreSQLTranslator which always translates in SQL all identifiers (schema, table and column) in the specified case sensitivity ; + * in other words, schema, table and column names will all be surrounded or not by double quotes in the SQL translation. * - * @param column <i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise. + * @param allCaseSensitive <i>true</i> to translate all identifiers in a case sensitive manner (surrounded by double quotes), <i>false</i> for case insensitivity. */ - public PostgreSQLTranslator(final boolean column){ - caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column); + public PostgreSQLTranslator(final boolean allCaseSensitive){ + caseSensitivity = allCaseSensitive ? (byte)0x0F : (byte)0x00; } /** - * Builds a PostgreSQLTranslator. + * Builds a PostgreSQLTranslator which will always translate in SQL identifiers with the defined case sensitivity. * - * @param catalog <i>true</i> to take into account the case sensitivity of catalog names, <i>false</i> otherwise. - * @param schema <i>true</i> to take into account the case sensitivity of schema names, <i>false</i> otherwise. - * @param table <i>true</i> to take into account the case sensitivity of table names, <i>false</i> otherwise. - * @param column <i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise. + * @param catalog <i>true</i> to translate catalog names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param schema <i>true</i> to translate schema names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param table <i>true</i> to translate table names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param column <i>true</i> to translate column names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. */ public PostgreSQLTranslator(final boolean catalog, final boolean schema, final boolean table, final boolean column){ caseSensitivity = IdentifierField.CATALOG.setCaseSensitive(caseSensitivity, catalog); @@ -125,521 +93,9 @@ public class PostgreSQLTranslator implements ADQLTranslator { caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column); } - /** - * Appends the full name of the given table to the given StringBuffer. - * - * @param str The string buffer. - * @param dbTable The table whose the full name must be appended. - * - * @return The string buffer + full table name. - */ - public final StringBuffer appendFullDBName(final StringBuffer str, final DBTable dbTable){ - if (dbTable != null){ - if (dbTable.getDBCatalogName() != null) - appendIdentifier(str, dbTable.getDBCatalogName(), IdentifierField.CATALOG).append('.'); - - if (dbTable.getDBSchemaName() != null) - appendIdentifier(str, dbTable.getDBSchemaName(), IdentifierField.SCHEMA).append('.'); - - appendIdentifier(str, dbTable.getDBName(), IdentifierField.TABLE); - } - return str; - } - - /** - * Appends the given identifier in the given StringBuffer. - * - * @param str The string buffer. - * @param id The identifier to append. - * @param field The type of identifier (column, table, schema, catalog or alias ?). - * - * @return The string buffer + identifier. - */ - public final StringBuffer appendIdentifier(final StringBuffer str, final String id, final IdentifierField field){ - return appendIdentifier(str, id, field.isCaseSensitive(caseSensitivity)); - } - - /** - * Appends the given identifier to the given StringBuffer. - * - * @param str The string buffer. - * @param id The identifier to append. - * @param caseSensitive <i>true</i> to format the identifier so that preserving the case sensitivity, <i>false</i> otherwise. - * - * @return The string buffer + identifier. - */ - public static final StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive){ - if (caseSensitive) - return str.append('\"').append(id).append('\"'); - else - return str.append(id); - } - - @Override - @SuppressWarnings("unchecked") - public String translate(ADQLObject obj) throws TranslationException{ - if (obj instanceof ADQLQuery) - return translate((ADQLQuery)obj); - else if (obj instanceof ADQLList) - return translate((ADQLList)obj); - else if (obj instanceof SelectItem) - return translate((SelectItem)obj); - else if (obj instanceof ColumnReference) - return translate((ColumnReference)obj); - else if (obj instanceof ADQLTable) - return translate((ADQLTable)obj); - else if (obj instanceof ADQLJoin) - return translate((ADQLJoin)obj); - else if (obj instanceof ADQLOperand) - return translate((ADQLOperand)obj); - else if (obj instanceof ADQLConstraint) - return translate((ADQLConstraint)obj); - else - return obj.toADQL(); - } - - @Override - public String translate(ADQLQuery query) throws TranslationException{ - StringBuffer sql = new StringBuffer(translate(query.getSelect())); - - sql.append("\nFROM ").append(translate(query.getFrom())); - - if (!query.getWhere().isEmpty()) - sql.append('\n').append(translate(query.getWhere())); - - if (!query.getGroupBy().isEmpty()) - sql.append('\n').append(translate(query.getGroupBy())); - - if (!query.getHaving().isEmpty()) - sql.append('\n').append(translate(query.getHaving())); - - if (!query.getOrderBy().isEmpty()) - sql.append('\n').append(translate(query.getOrderBy())); - - if (query.getSelect().hasLimit()) - sql.append("\nLimit ").append(query.getSelect().getLimit()); - - return sql.toString(); - } - - /* *************************** */ - /* ****** LIST & CLAUSE ****** */ - /* *************************** */ - @Override - public String translate(ADQLList<? extends ADQLObject> list) throws TranslationException{ - if (list instanceof ClauseSelect) - return translate((ClauseSelect)list); - else if (list instanceof ClauseConstraints) - return translate((ClauseConstraints)list); - else - return getDefaultADQLList(list); - } - - /** - * Gets the default SQL output for a list of ADQL objects. - * - * @param list List to format into SQL. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultADQLList(ADQLList<? extends ADQLObject> list) throws TranslationException{ - String sql = (list.getName() == null) ? "" : (list.getName() + " "); - - boolean oldInSelect = inSelect; - inSelect = (list.getName() != null) && list.getName().equalsIgnoreCase("select"); - - try{ - for(int i = 0; i < list.size(); i++) - sql += ((i == 0) ? "" : (" " + list.getSeparator(i) + " ")) + translate(list.get(i)); - }finally{ - inSelect = oldInSelect; - } - - return sql; - } - - @Override - public String translate(ClauseSelect clause) throws TranslationException{ - String sql = null; - - for(int i = 0; i < clause.size(); i++){ - if (i == 0){ - sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : ""); - }else - sql += " " + clause.getSeparator(i); - - sql += " " + translate(clause.get(i)); - } - - return sql; - } - - @Override - public String translate(ClauseConstraints clause) throws TranslationException{ - if (clause instanceof ConstraintsGroup) - return "(" + getDefaultADQLList(clause) + ")"; - else - return getDefaultADQLList(clause); - } - - @Override - public String translate(SelectItem item) throws TranslationException{ - if (item instanceof SelectAllColumns) - return translate((SelectAllColumns)item); - - StringBuffer translation = new StringBuffer(translate(item.getOperand())); - if (item.hasAlias()){ - translation.append(" AS "); - appendIdentifier(translation, item.getAlias(), item.isCaseSensitive()); - }else - translation.append(" AS ").append(item.getName()); - - return translation.toString(); - } - - @Override - public String translate(SelectAllColumns item) throws TranslationException{ - HashMap<String,String> mapAlias = new HashMap<String,String>(); - - // Fetch the full list of columns to display: - Iterable<DBColumn> dbCols = null; - if (item.getAdqlTable() != null && item.getAdqlTable().getDBLink() != null){ - ADQLTable table = item.getAdqlTable(); - dbCols = table.getDBLink(); - if (table.hasAlias()){ - String key = appendFullDBName(new StringBuffer(), table.getDBLink()).toString(); - mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); - } - }else if (item.getQuery() != null){ - try{ - dbCols = item.getQuery().getFrom().getDBColumns(); - }catch(UnresolvedJoin pe){ - throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe); - } - ArrayList<ADQLTable> tables = item.getQuery().getFrom().getTables(); - for(ADQLTable table : tables){ - if (table.hasAlias()){ - String key = appendFullDBName(new StringBuffer(), table.getDBLink()).toString(); - mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); - } - } - } - - // Write the DB name of all these columns: - if (dbCols != null){ - StringBuffer cols = new StringBuffer(); - for(DBColumn col : dbCols){ - if (cols.length() > 0) - cols.append(','); - if (col.getTable() != null){ - String fullDbName = appendFullDBName(new StringBuffer(), col.getTable()).toString(); - if (mapAlias.containsKey(fullDbName)) - appendIdentifier(cols, mapAlias.get(fullDbName), false).append('.'); - else - cols.append(fullDbName).append('.'); - } - appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN); - cols.append(" AS \"").append(col.getADQLName()).append('\"'); - } - return (cols.length() > 0) ? cols.toString() : item.toADQL(); - }else{ - return item.toADQL(); - } - } - - @Override - public String translate(ColumnReference ref) throws TranslationException{ - if (ref instanceof ADQLOrder) - return translate((ADQLOrder)ref); - else - return getDefaultColumnReference(ref); - } - - /** - * Gets the default SQL output for a column reference. - * - * @param ref The column reference to format into SQL. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultColumnReference(ColumnReference ref) throws TranslationException{ - if (ref.isIndex()){ - return "" + ref.getColumnIndex(); - }else{ - if (ref.getDBLink() == null){ - return (ref.isCaseSensitive() ? ("\"" + ref.getColumnName() + "\"") : ref.getColumnName()); - }else{ - DBColumn dbCol = ref.getDBLink(); - StringBuffer colName = new StringBuffer(); - // Use the table alias if any: - if (ref.getAdqlTable() != null && ref.getAdqlTable().hasAlias()) - appendIdentifier(colName, ref.getAdqlTable().getAlias(), ref.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); - - // Use the DBTable if any: - else if (dbCol.getTable() != null) - appendFullDBName(colName, dbCol.getTable()).append('.'); - - appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); - - return colName.toString(); - } - } - } - - @Override - public String translate(ADQLOrder order) throws TranslationException{ - return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC"); - } - - /* ************************** */ - /* ****** TABLE & JOIN ****** */ - /* ************************** */ - @Override - public String translate(FromContent content) throws TranslationException{ - if (content instanceof ADQLTable) - return translate((ADQLTable)content); - else if (content instanceof ADQLJoin) - return translate((ADQLJoin)content); - else - return content.toADQL(); - } - - @Override - public String translate(ADQLTable table) throws TranslationException{ - StringBuffer sql = new StringBuffer(); - - // CASE: SUB-QUERY: - if (table.isSubQuery()) - sql.append('(').append(translate(table.getSubQuery())).append(')'); - - // CASE: TABLE REFERENCE: - else{ - // Use the corresponding DB table, if known: - if (table.getDBLink() != null) - appendFullDBName(sql, table.getDBLink()); - // Otherwise, use the whole table name given in the ADQL query: - else - sql.append(table.getFullTableName()); - } - - // Add the table alias, if any: - if (table.hasAlias()){ - sql.append(" AS "); - appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS)); - } - - return sql.toString(); - } - - @Override - public String translate(ADQLJoin join) throws TranslationException{ - StringBuffer sql = new StringBuffer(translate(join.getLeftTable())); - - if (join.isNatural()) - sql.append(" NATURAL"); - - sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' '); - - if (!join.isNatural()){ - if (join.getJoinCondition() != null) - sql.append(translate(join.getJoinCondition())); - else if (join.hasJoinedColumns()){ - StringBuffer cols = new StringBuffer(); - Iterator<ADQLColumn> it = join.getJoinedColumns(); - while(it.hasNext()){ - ADQLColumn item = it.next(); - if (cols.length() > 0) - cols.append(", "); - if (item.getDBLink() == null) - appendIdentifier(cols, item.getColumnName(), item.isCaseSensitive(IdentifierField.COLUMN)); - else - appendIdentifier(cols, item.getDBLink().getDBName(), IdentifierField.COLUMN); - } - sql.append("USING (").append(cols).append(')'); - } - } - - return sql.toString(); - } - - /* ********************* */ - /* ****** OPERAND ****** */ - /* ********************* */ - @Override - public String translate(ADQLOperand op) throws TranslationException{ - if (op instanceof ADQLColumn) - return translate((ADQLColumn)op); - else if (op instanceof Concatenation) - return translate((Concatenation)op); - else if (op instanceof NegativeOperand) - return translate((NegativeOperand)op); - else if (op instanceof NumericConstant) - return translate((NumericConstant)op); - else if (op instanceof StringConstant) - return translate((StringConstant)op); - else if (op instanceof WrappedOperand) - return translate((WrappedOperand)op); - else if (op instanceof Operation) - return translate((Operation)op); - else if (op instanceof ADQLFunction) - return translate((ADQLFunction)op); - else - return op.toADQL(); - } - - @Override - public String translate(ADQLColumn column) throws TranslationException{ - // Use its DB name if known: - if (column.getDBLink() != null){ - DBColumn dbCol = column.getDBLink(); - StringBuffer colName = new StringBuffer(); - // Use the table alias if any: - if (column.getAdqlTable() != null && column.getAdqlTable().hasAlias()) - appendIdentifier(colName, column.getAdqlTable().getAlias(), column.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.'); - - // Use the DBTable if any: - else if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null) - appendFullDBName(colName, dbCol.getTable()).append('.'); - - // Otherwise, use the prefix of the column given in the ADQL query: - else if (column.getTableName() != null) - colName = column.getFullColumnPrefix().append('.'); - - appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN); - - return colName.toString(); - } - // Otherwise, use the whole name given in the ADQL query: - else - return column.getFullColumnName(); - } - - @Override - public String translate(Concatenation concat) throws TranslationException{ - return translate((ADQLList<ADQLOperand>)concat); - } - - @Override - public String translate(NegativeOperand negOp) throws TranslationException{ - return "-" + translate(negOp.getOperand()); - } - - @Override - public String translate(NumericConstant numConst) throws TranslationException{ - return numConst.getValue(); - } - - @Override - public String translate(StringConstant strConst) throws TranslationException{ - return "'" + strConst.getValue() + "'"; - } - - @Override - public String translate(WrappedOperand op) throws TranslationException{ - return "(" + translate(op.getOperand()) + ")"; - } - - @Override - public String translate(Operation op) throws TranslationException{ - return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand()); - } - - /* ************************ */ - /* ****** CONSTRAINT ****** */ - /* ************************ */ - @Override - public String translate(ADQLConstraint cons) throws TranslationException{ - if (cons instanceof Comparison) - return translate((Comparison)cons); - else if (cons instanceof Between) - return translate((Between)cons); - else if (cons instanceof Exists) - return translate((Exists)cons); - else if (cons instanceof In) - return translate((In)cons); - else if (cons instanceof IsNull) - return translate((IsNull)cons); - else if (cons instanceof NotConstraint) - return translate((NotConstraint)cons); - else - return cons.toADQL(); - } - - @Override - public String translate(Comparison comp) throws TranslationException{ - return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand()); - } - @Override - public String translate(Between comp) throws TranslationException{ - return translate(comp.getLeftOperand()) + " BETWEEN " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand()); - } - - @Override - public String translate(Exists exists) throws TranslationException{ - return "EXISTS(" + translate(exists.getSubQuery()) + ")"; - } - - @Override - public String translate(In in) throws TranslationException{ - return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")"; - } - - @Override - public String translate(IsNull isNull) throws TranslationException{ - return translate(isNull.getColumn()) + " IS " + (isNull.isNotNull() ? "NOT " : "") + "NULL"; - } - - @Override - public String translate(NotConstraint notCons) throws TranslationException{ - return "NOT " + translate(notCons.getConstraint()); - } - - /* *********************** */ - /* ****** FUNCTIONS ****** */ - /* *********************** */ - @Override - public String translate(ADQLFunction fct) throws TranslationException{ - if (fct instanceof GeometryFunction) - return translate((GeometryFunction)fct); - else if (fct instanceof MathFunction) - return translate((MathFunction)fct); - else if (fct instanceof SQLFunction) - return translate((SQLFunction)fct); - else if (fct instanceof UserDefinedFunction) - return translate((UserDefinedFunction)fct); - else - return getDefaultADQLFunction(fct); - } - - /** - * Gets the default SQL output for the given ADQL function. - * - * @param fct The ADQL function to format into SQL. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultADQLFunction(ADQLFunction fct) throws TranslationException{ - String sql = fct.getName() + "("; - - for(int i = 0; i < fct.getNbParameters(); i++) - sql += ((i == 0) ? "" : ", ") + translate(fct.getParameter(i)); - - return sql + ")"; - } - - @Override - public String translate(SQLFunction fct) throws TranslationException{ - if (fct.getType() == SQLFunctionType.COUNT_ALL) - return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)"; - else - return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")"; + public boolean isCaseSensitive(final IdentifierField field){ + return field == null ? false : field.isCaseSensitive(caseSensitivity); } @Override @@ -658,125 +114,64 @@ public class PostgreSQLTranslator implements ADQLTranslator { } } - @Override - public String translate(UserDefinedFunction fct) throws TranslationException{ - return getDefaultADQLFunction(fct); - } - - /* *********************************** */ - /* ****** GEOMETRICAL FUNCTIONS ****** */ - /* *********************************** */ - @Override - public String translate(GeometryFunction fct) throws TranslationException{ - if (fct instanceof AreaFunction) - return translate((AreaFunction)fct); - else if (fct instanceof BoxFunction) - return translate((BoxFunction)fct); - else if (fct instanceof CentroidFunction) - return translate((CentroidFunction)fct); - else if (fct instanceof CircleFunction) - return translate((CircleFunction)fct); - else if (fct instanceof ContainsFunction) - return translate((ContainsFunction)fct); - else if (fct instanceof DistanceFunction) - return translate((DistanceFunction)fct); - else if (fct instanceof ExtractCoord) - return translate((ExtractCoord)fct); - else if (fct instanceof ExtractCoordSys) - return translate((ExtractCoordSys)fct); - else if (fct instanceof IntersectsFunction) - return translate((IntersectsFunction)fct); - else if (fct instanceof PointFunction) - return translate((PointFunction)fct); - else if (fct instanceof PolygonFunction) - return translate((PolygonFunction)fct); - else if (fct instanceof RegionFunction) - return translate((RegionFunction)fct); - else - return getDefaultGeometryFunction(fct); - } - - /** - * <p>Gets the default SQL output for the given geometrical function.</p> - * - * <p><i><u>Note:</u> By default, only the ADQL serialization is returned.</i></p> - * - * @param fct The geometrical function to translate. - * - * @return The corresponding SQL. - * - * @throws TranslationException If there is an error during the translation. - */ - protected String getDefaultGeometryFunction(GeometryFunction fct) throws TranslationException{ - if (inSelect) - return "'" + fct.toADQL().replaceAll("'", "''") + "'"; - else - return getDefaultADQLFunction(fct); - } - - @Override - public String translate(GeometryValue<? extends GeometryFunction> geomValue) throws TranslationException{ - return translate(geomValue.getValue()); - } - @Override public String translate(ExtractCoord extractCoord) throws TranslationException{ - return getDefaultGeometryFunction(extractCoord); + return getDefaultADQLFunction(extractCoord); } @Override public String translate(ExtractCoordSys extractCoordSys) throws TranslationException{ - return getDefaultGeometryFunction(extractCoordSys); + return getDefaultADQLFunction(extractCoordSys); } @Override public String translate(AreaFunction areaFunction) throws TranslationException{ - return getDefaultGeometryFunction(areaFunction); + return getDefaultADQLFunction(areaFunction); } @Override public String translate(CentroidFunction centroidFunction) throws TranslationException{ - return getDefaultGeometryFunction(centroidFunction); + return getDefaultADQLFunction(centroidFunction); } @Override public String translate(DistanceFunction fct) throws TranslationException{ - return getDefaultGeometryFunction(fct); + return getDefaultADQLFunction(fct); } @Override public String translate(ContainsFunction fct) throws TranslationException{ - return getDefaultGeometryFunction(fct); + return getDefaultADQLFunction(fct); } @Override public String translate(IntersectsFunction fct) throws TranslationException{ - return getDefaultGeometryFunction(fct); + return getDefaultADQLFunction(fct); } @Override public String translate(BoxFunction box) throws TranslationException{ - return getDefaultGeometryFunction(box); + return getDefaultADQLFunction(box); } @Override public String translate(CircleFunction circle) throws TranslationException{ - return getDefaultGeometryFunction(circle); + return getDefaultADQLFunction(circle); } @Override public String translate(PointFunction point) throws TranslationException{ - return getDefaultGeometryFunction(point); + return getDefaultADQLFunction(point); } @Override public String translate(PolygonFunction polygon) throws TranslationException{ - return getDefaultGeometryFunction(polygon); + return getDefaultADQLFunction(polygon); } @Override public String translate(RegionFunction region) throws TranslationException{ - return getDefaultGeometryFunction(region); + return getDefaultADQLFunction(region); } } diff --git a/src/tap/ADQLExecutor.java b/src/tap/ADQLExecutor.java index a7aee6f..93a8dc3 100644 --- a/src/tap/ADQLExecutor.java +++ b/src/tap/ADQLExecutor.java @@ -17,7 +17,7 @@ package tap; * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -261,8 +261,12 @@ public class ADQLExecutor { } protected void writeResult(TableIterator queryResult, OutputFormat formatter, OutputStream output) throws InterruptedException, TAPException{ - //logger.info("Job "+report.jobID+" - 5/5 Writing result file..."); - formatter.writeResult(queryResult, output, report, thread); + try{ + //logger.info("Job "+report.jobID+" - 5/5 Writing result file..."); + formatter.writeResult(queryResult, output, report, thread); + }finally{ + queryResult.close(); + } } protected void dropUploadedTables() throws TAPException{ diff --git a/src/tap/data/LimitedTableIterator.java b/src/tap/data/LimitedTableIterator.java new file mode 100644 index 0000000..96c7c35 --- /dev/null +++ b/src/tap/data/LimitedTableIterator.java @@ -0,0 +1,227 @@ +package tap.data; + +/* + * This file is part of TAPLibrary. + * + * ADQLLibrary 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. + * + * ADQLLibrary 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 ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2014 - Astronomisches Rechen Institut (ARI) + */ + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.NoSuchElementException; + +import tap.ServiceConnection.LimitUnit; +import tap.metadata.TAPColumn; +import tap.metadata.TAPType; +import tap.upload.LimitedSizeInputStream; + +import com.oreilly.servlet.multipart.ExceededSizeException; + +/** + * <p>Wrap a {@link TableIterator} in order to limit its reading to a fixed number of rows.</p> + * + * <p> + * This wrapper can be "mixed" with a {@link LimitedSizeInputStream}, by wrapping the original input stream by a {@link LimitedSizeInputStream} + * and then by wrapping the {@link TableIterator} based on this wrapped input stream by {@link LimitedTableIterator}. + * Thus, this wrapper will be able to detect embedded {@link ExceededSizeException} thrown by a {@link LimitedSizeInputStream} through another {@link TableIterator}. + * If a such exception is detected, it will declare this wrapper as overflowed as it would be if a rows limit is reached. + * </p> + * + * <p><b>Warning:</b> + * To work together with a {@link LimitedSizeInputStream}, this wrapper relies on the hypothesis that any {@link IOException} (including {@link ExceededSizeException}) + * will be embedded in a {@link DataReadException} as cause of this exception (using {@link DataReadException#DataReadException(Throwable)} + * or {@link DataReadException#DataReadException(String, Throwable)}). If it is not the case, no overflow detection could be done and the exception will just be forwarded. + * </p> + * + * <p> + * If a limit - either of rows or of bytes - is reached, a flag "overflow" is set to true. This flag can be got with {@link #isOverflow()}. + * Thus, when a {@link DataReadException} is caught, it will be easy to detect whether the error occurred because of an overflow + * or of another problem. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (07/2014) + * @since 2.0 + */ +public class LimitedTableIterator implements TableIterator { + + /** The wrapped {@link TableIterator}. */ + private final TableIterator innerIt; + + /** Limit on the number of rows to read. <i>note: a negative value means "no limit".</i> */ + private final int maxNbRows; + + /** The number of rows already read. */ + private int countRow = 0; + + /** Indicate whether a limit (rows or bytes) has been reached or not. */ + private boolean overflow = false; + + /** + * Wrap the given {@link TableIterator} so that limiting the number of rows to read to the given value. + * + * @param it The iterator to wrap. <i>MUST NOT be NULL</i> + * @param maxNbRows Maximum number of rows that can be read. There is overflow if more than this number of rows is asked. <i>A negative value means "no limit".</i> + */ + public LimitedTableIterator(final TableIterator it, final int nbMaxRows) throws DataReadException{ + if (it == null) + throw new NullPointerException("Missing TableIterator to wrap!"); + innerIt = it; + this.maxNbRows = nbMaxRows; + } + + /** + * Wrap the given {@link TableIterator} so that limiting the number of rows to read to the given value. + * + * @param it The iterator to wrap. <i>MUST NOT be NULL</i> + * @param maxNbRows Maximum number of rows that can be read. There is overflow if more than this number of rows is asked. <i>A negative value means "no limit".</i> + */ + public < T extends TableIterator > LimitedTableIterator(final Class<T> classIt, final InputStream input, final LimitUnit type, final int limit) throws DataReadException{ + try{ + Constructor<T> construct = classIt.getConstructor(InputStream.class); + if (type == LimitUnit.bytes){ + maxNbRows = -1; + innerIt = construct.newInstance(new LimitedSizeInputStream(input, limit)); + }else{ + innerIt = construct.newInstance(input); + maxNbRows = (type == null) ? -1 : limit; + } + }catch(InvocationTargetException ite){ + Throwable t = ite.getCause(); + if (t != null && t instanceof DataReadException){ + ExceededSizeException exceedEx = getExceededSizeException(t); + // if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception: + if (exceedEx != null) + throw new DataReadException(exceedEx.getMessage(), exceedEx); + else + throw (DataReadException)t; + }else + throw new DataReadException("Can not create a LimitedTableIterator!", ite); + }catch(Exception ex){ + throw new DataReadException("Can not create a LimitedTableIterator!", ex); + } + } + + /** + * Get the iterator wrapped by this {@link TableIterator} instance. + * + * @return The wrapped iterator. + */ + public final TableIterator getWrappedIterator(){ + return innerIt; + } + + /** + * <p>Tell whether a limit (in rows or bytes) has been reached.</p> + * + * <p><i>Note: + * If <i>true</i> is returned (that's to say, if a limit has been reached) no more rows or column values + * can be read ; an {@link IllegalStateException} would then be thrown. + * </i></p> + * + * @return <i>true</i> if a limit has been reached, <i>false</i> otherwise. + */ + public final boolean isOverflow(){ + return overflow; + } + + @Override + public void close() throws DataReadException{ + innerIt.close(); + } + + @Override + public TAPColumn[] getMetadata(){ + return innerIt.getMetadata(); + } + + @Override + public boolean nextRow() throws DataReadException{ + // Test the overflow flag and proceed only if not overflowed: + if (overflow) + throw new DataReadException("Data read overflow: the limit has been reached! No more data can be read."); + + // Read the next row: + boolean nextRow; + try{ + nextRow = innerIt.nextRow(); + countRow++; + }catch(DataReadException ex){ + ExceededSizeException exceedEx = getExceededSizeException(ex); + // if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception: + if (exceedEx != null){ + overflow = true; + throw new DataReadException(exceedEx.getMessage()); + }else + throw ex; + } + + // If, counting this one, the number of rows exceeds the limit, set this iterator as overflowed and throw an exception: + if (nextRow && maxNbRows >= 0 && countRow > maxNbRows){ + overflow = true; + throw new DataReadException("Data read overflow: the limit of " + maxNbRows + " rows has been reached!"); + } + + // Send back the value returned by the inner iterator: + return nextRow; + } + + @Override + public boolean hasNextCol() throws IllegalStateException, DataReadException{ + testOverflow(); + return innerIt.hasNextCol(); + } + + @Override + public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{ + testOverflow(); + return innerIt.nextCol(); + } + + @Override + public TAPType getColType() throws IllegalStateException, DataReadException{ + testOverflow(); + return innerIt.getColType(); + } + + /** + * Test the overflow flag and throw an {@link IllegalStateException} if <i>true</i>. + * + * @throws IllegalStateException If this iterator is overflowed (because of either a bytes limit or a rows limit). + */ + private void testOverflow() throws IllegalStateException{ + if (overflow) + throw new IllegalStateException("Data read overflow: the limit has been reached! No more data can be read."); + } + + /** + * Get the first {@link ExceededSizeException} found in the given {@link Throwable} trace. + * + * @param ex A {@link Throwable} + * + * @return The first {@link ExceededSizeException} encountered, or NULL if none has been found. + */ + private ExceededSizeException getExceededSizeException(Throwable ex){ + if (ex == null) + return null; + while(!(ex instanceof ExceededSizeException) && ex.getCause() != null) + ex = ex.getCause(); + return (ex instanceof ExceededSizeException) ? (ExceededSizeException)ex : null; + } + +} diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java index ae2fd1d..d632e72 100644 --- a/src/tap/data/ResultSetTableIterator.java +++ b/src/tap/data/ResultSetTableIterator.java @@ -24,17 +24,20 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.NoSuchElementException; -import tap.db.JDBCTAPFactory; import tap.metadata.TAPColumn; import tap.metadata.TAPType; +import tap.metadata.TAPType.TAPDatatype; +import adql.db.DBColumn; /** * <p>{@link TableIterator} which lets iterate over a SQL {@link ResultSet}.</p> * - * <p>{@link #getColType()} will return a TAP type base on the one declared in the {@link ResultSetMetaData} object.</p> + * <p><i>Note: + * {@link #getColType()} will return a TAP type based on the one declared in the {@link ResultSetMetaData} object. + * </i></p> * - * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de - * @version 2.0 (06/2014) + * @author Grégory Mantelet (ARI) + * @version 2.0 (08/2014) * @since 2.0 */ public class ResultSetTableIterator implements TableIterator { @@ -55,27 +58,128 @@ public class ResultSetTableIterator implements TableIterator { private int colIndex; /** - * Build a TableIterator able to read rows and columns of the given ResultSet. + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> * - * @param dataSet Dataset over which this iterator must iterate. + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} + * which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * </p> + * + * <p><i><b>Important</b>: + * To guess the TAP type from a DBMS type, {@link #convertType(String, String)} may not need to know the DBMS, + * except for SQLite. Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know + * it is the DBMS from which the ResultSet is coming. Without this information, type guessing will be unpredictable! + * + * <b>So, if your ResultSet is coming from a SQLite connection, you SHOULD really use one of the 2 other constructors</b> + * and provide "sqlite" as value for the second parameter. + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. * * @throws NullPointerException If NULL is given in parameter. - * @throws DataReadException If the given ResultSet is closed - * or if the metadata (columns count and types) can not be fetched. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet) throws NullPointerException, DataReadException{ + this(dataSet, null, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} + * which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * </p> + * + * <p><i><b>Important</b>: + * The second parameter of this constructor is given as second parameter of {@link #convertType(String, String)}. + * <b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> Indeed, SQLite has so many datatype + * restrictions that it is absolutely needed to know it is the DBMS from which the ResultSet is coming. Without this + * information, type guessing will be unpredictable! + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, String) + */ + public ResultSetTableIterator(final ResultSet dataSet, final String dbms) throws NullPointerException, DataReadException{ + this(dataSet, dbms, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is reading first the given metadata (if any), + * and then, try to guess the datatype from the DBMS column datatype (using {@link #convertType(String, String)}). + * </p> + * + * <h3>Provided metadata</h3> + * + * <p>The third parameter of this constructor aims to provide the metadata expected for each column of the ResultSet.</p> + * + * <p> + * For that, it is expected that all these metadata are {@link TAPColumn} objects. Indeed, simple {@link DBColumn} + * instances do not have the type information. If just {@link DBColumn}s are provided, the ADQL name it provides will be kept + * but the type will be guessed from the type provide by the ResultSetMetadata. + * </p> + * + * <p><i>Note: + * If this parameter is incomplete (array length less than the column count returned by the ResultSet or some array items are NULL), + * column metadata will be associated in the same order as the ResultSet columns. Missing metadata will be built from the + * {@link ResultSetMetaData} and so the types will be guessed. + * </i></p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} + * which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * </p> + * + * <p><i><b>Important</b>: + * The second parameter of this constructor is given as second parameter of {@link #convertType(String, String)}. + * <b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> Indeed, SQLite has so many datatype + * restrictions that it is absolutely needed to know it is the DBMS from which the ResultSet is coming. Without this + * information, type guessing will be unpredictable! + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> + * @param resultMeta List of expected columns. <i>note: these metadata are expected to be really {@link TAPColumn} objects ; MAY be NULL.</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the metadata (columns count and types) can not be fetched. + * + * @see #convertType(String, String) + */ + public ResultSetTableIterator(final ResultSet dataSet, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{ // A dataset MUST BE provided: if (dataSet == null) throw new NullPointerException("Missing ResultSet object over which to iterate!"); - // It MUST also BE OPEN: - try{ - if (dataSet.isClosed()) - throw new DataReadException("Closed ResultSet: impossible to iterate over it!"); - }catch(SQLException se){ - throw new DataReadException("Can not know whether the ResultSet is open!", se); - } - // Keep a reference to the ResultSet: data = dataSet; @@ -88,14 +192,32 @@ public class ResultSetTableIterator implements TableIterator { // determine their type: colMeta = new TAPColumn[nbColumns]; for(int i = 1; i <= nbColumns; i++){ - TAPType datatype = JDBCTAPFactory.toTAPType(metadata.getColumnTypeName(i)); - colMeta[i - 1] = new TAPColumn(metadata.getColumnLabel(i), datatype); + if (resultMeta != null && (i - 1) < resultMeta.length && resultMeta[i - 1] != null){ + try{ + colMeta[i - 1] = (TAPColumn)resultMeta[i - 1]; + }catch(ClassCastException cce){ + TAPType datatype = convertType(metadata.getColumnTypeName(i), dbms); + colMeta[i - 1] = new TAPColumn(resultMeta[i - 1].getADQLName(), datatype); + } + }else{ + TAPType datatype = convertType(metadata.getColumnTypeName(i), dbms); + colMeta[i - 1] = new TAPColumn(metadata.getColumnLabel(i), datatype); + } } }catch(SQLException se){ throw new DataReadException("Can not get the column types of the given ResultSet!", se); } } + @Override + public void close() throws DataReadException{ + try{ + data.close(); + }catch(SQLException se){ + throw new DataReadException("Can not close the iterated ResultSet!", se); + } + } + @Override public TAPColumn[] getMetadata(){ return colMeta; @@ -122,6 +244,7 @@ public class ResultSetTableIterator implements TableIterator { * <li>the row iteration has started = the first row has been read = a first call of {@link #nextRow()} has been done</li> * <li>AND the row iteration is not finished = the last row has been read.</li> * </ul> + * * @throws IllegalStateException */ private void checkReadState() throws IllegalStateException{ @@ -169,4 +292,151 @@ public class ResultSetTableIterator implements TableIterator { return colMeta[colIndex - 1].getDatatype(); } + /** + * <p>Convert the given DBMS type into the better matching {@link TAPType} instance. + * This function is used to guess the TAP type of a column when it is not provided in the constructor. + * It aims not to be exhaustive, but just to provide a type when the given TAP metadata are incomplete.</p> + * + * <p><i>Note: + * Any unknown DBMS datatype will be considered and translated as a VARCHAR. + * The same type will be returned if the given parameter is an empty string or NULL. + * </i></p> + * + * <p><i>Note: + * This type conversion function has been designed to work with all standard datatypes of the following DBMS: + * PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby. + * </i></p> + * + * <p><i><b>Important</b>: + * <b>The second parameter is REALLY NEEDED when the DBMS is SQLite ("sqlite")!</b> + * Indeed, SQLite has a so restrictive list of datatypes that this function can reliably convert its types + * only if it knows the DBMS is SQLite. Otherwise, the conversion result would be unpredictable. + * </i>In this default implementation of this function, all other DBMS values are ignored.<i> + * </i></p> + * + * <p><b>Warning</b>: + * This function is not translating the geometrical datatypes. If a such datatype is encountered, + * it will considered as unknown and so, a VARCHAR TAP type will be returned. + * </p> + * + * @param dbmsType DBMS column datatype name. + * @param dbms Lower-case string which indicates which DBMS the ResultSet is coming from. <i>note: MAY be NULL.</i> + * + * @return The best suited {@link TAPType} object. + */ + protected TAPType convertType(String dbmsType, final String dbms){ + // If no type is provided return VARCHAR: + if (dbmsType == null || dbmsType.trim().length() == 0) + return new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH); + + // Extract the type prefix and lower-case it: + dbmsType = dbmsType.toLowerCase(); + int paramIndex = dbmsType.indexOf('('); + String dbmsTypePrefix = (paramIndex <= 0) ? dbmsType : dbmsType.substring(0, paramIndex); + int firstParam = getLengthParam(dbmsTypePrefix, paramIndex); + + // CASE: SQLITE + if (dbms != null && dbms.equals("sqlite")){ + // INTEGER -> SMALLINT, INTEGER, BIGINT + if (dbmsTypePrefix.equals("integer")) + return new TAPType(TAPDatatype.BIGINT); + // REAL -> REAL, DOUBLE + else if (dbmsTypePrefix.equals("real")) + return new TAPType(TAPDatatype.DOUBLE); + // TEXT -> CHAR, VARCHAR, CLOB, TIMESTAMP + else if (dbmsTypePrefix.equals("text")) + return new TAPType(TAPDatatype.VARCHAR); + // BLOB -> BINARY, VARBINARY, BLOB + else if (dbmsTypePrefix.equals("blob")) + return new TAPType(TAPDatatype.BLOB); + // Default: + else + return new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH); + } + // CASE: OTHER DBMS + else{ + // SMALLINT + if (dbmsTypePrefix.equals("smallint") || dbmsTypePrefix.equals("int2")) + return new TAPType(TAPDatatype.SMALLINT); + // INTEGER + else if (dbmsTypePrefix.equals("integer") || dbmsTypePrefix.equals("int") || dbmsTypePrefix.equals("int4")) + return new TAPType(TAPDatatype.INTEGER); + // BIGINT + else if (dbmsTypePrefix.equals("bigint") || dbmsTypePrefix.equals("int8") || dbmsTypePrefix.equals("int4") || dbmsTypePrefix.equals("number")) + return new TAPType(TAPDatatype.BIGINT); + // REAL + else if (dbmsTypePrefix.equals("float4") || (dbmsTypePrefix.equals("float") && firstParam <= 63)) + return new TAPType(TAPDatatype.REAL); + // DOUBLE + else if (dbmsTypePrefix.equals("double") || dbmsTypePrefix.equals("double precision") || dbmsTypePrefix.equals("float8") || (dbmsTypePrefix.equals("float") && firstParam > 63)) + return new TAPType(TAPDatatype.DOUBLE); + // BINARY + else if (dbmsTypePrefix.equals("binary") || dbmsTypePrefix.equals("raw") || ((dbmsTypePrefix.equals("char") || dbmsTypePrefix.equals("character")) && dbmsType.endsWith(" for bit data"))) + return new TAPType(TAPDatatype.BINARY, firstParam); + // VARBINARY + else if (dbmsTypePrefix.equals("varbinary") || dbmsTypePrefix.equals("long raw") || ((dbmsTypePrefix.equals("varchar") || dbmsTypePrefix.equals("character varying")) && dbmsType.endsWith(" for bit data"))) + return new TAPType(TAPDatatype.VARBINARY, firstParam); + // CHAR + else if (dbmsTypePrefix.equals("char") || dbmsTypePrefix.equals("character")) + return new TAPType(TAPDatatype.CHAR, firstParam); + // VARCHAR + else if (dbmsTypePrefix.equals("varchar") || dbmsTypePrefix.equals("varchar2") || dbmsTypePrefix.equals("character varying")) + return new TAPType(TAPDatatype.VARBINARY, firstParam); + // BLOB + else if (dbmsTypePrefix.equals("bytea") || dbmsTypePrefix.equals("blob") || dbmsTypePrefix.equals("binary large object")) + return new TAPType(TAPDatatype.BLOB); + // CLOB + else if (dbmsTypePrefix.equals("text") || dbmsTypePrefix.equals("clob") || dbmsTypePrefix.equals("character large object")) + return new TAPType(TAPDatatype.CLOB); + // TIMESTAMP + else if (dbmsTypePrefix.equals("timestamp")) + return new TAPType(TAPDatatype.TIMESTAMP); + // Default: + else + return new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH); + } + } + + /** + * <p>Extract the 'length' parameter of a DBMS type string.</p> + * + * <p> + * If the given type string does not contain any parameter + * OR if the first parameter can not be casted into an integer, + * {@link TAPType#NO_LENGTH} will be returned. + * </p> + * + * @param dbmsType DBMS type string (containing the datatype and the 'length' parameter). + * @param paramIndex Index of the open bracket. + * + * @return The 'length' parameter value if found, {@link TAPType#NO_LENGTH} otherwise. + */ + protected final int getLengthParam(final String dbmsType, final int paramIndex){ + // If no parameter has been previously detected, no length parameter: + if (paramIndex <= 0) + return TAPType.NO_LENGTH; + + // If there is one and that at least ONE parameter is provided.... + else{ + int lengthParam = TAPType.NO_LENGTH; + String paramsStr = dbmsType.substring(paramIndex + 1); + + // ...extract the 'length' parameter: + /* note: we suppose here that no other parameter is possible ; + * but if there are, they are ignored and we try to consider the first parameter + * as the length */ + int paramEndIndex = paramsStr.indexOf(','); + if (paramEndIndex <= 0) + paramEndIndex = paramsStr.indexOf(')'); + + // ...cast it into an integer: + try{ + lengthParam = Integer.parseInt(paramsStr.substring(0, paramEndIndex)); + }catch(Exception ex){} + + // ...and finally return it: + return lengthParam; + } + } + } diff --git a/src/tap/data/TableIterator.java b/src/tap/data/TableIterator.java index 3051b3a..0aed73c 100644 --- a/src/tap/data/TableIterator.java +++ b/src/tap/data/TableIterator.java @@ -43,11 +43,15 @@ import tap.metadata.TAPType; * } * }catch(DataReadException dre){ * ... + * }finally{ + * try{ + * it.close(); + * }catch(DataReadException dre){ ... } * } * </pre> * * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de - * @version 2.0 (06/2014) + * @version 2.0 (08/2014) * @since 2.0 */ public interface TableIterator { @@ -123,4 +127,12 @@ public interface TableIterator { * @throws DataReadException If an error occurs while reading the table dataset. */ public TAPType getColType() throws IllegalStateException, DataReadException; + + /** + * Close the stream or input over which this class iterates. + * + * @throws DataReadException If any error occurs while closing it. + */ + public void close() throws DataReadException; + } diff --git a/src/tap/data/VOTableIterator.java b/src/tap/data/VOTableIterator.java index 13628d5..861ac52 100644 --- a/src/tap/data/VOTableIterator.java +++ b/src/tap/data/VOTableIterator.java @@ -202,6 +202,15 @@ public class VOTableIterator implements TableIterator { throw new IllegalStateException("End of VOTable file already reached!"); } + @Override + public void close() throws DataReadException{ + try{ + rowSeq.close(); + }catch(IOException ioe){ + throw new DataReadException("Can not close the iterated VOTable!", ioe); + } + } + @Override public TAPColumn[] getMetadata(){ return colMeta; diff --git a/src/tap/db/DBConnection.java b/src/tap/db/DBConnection.java index 00a5dba..c3e92e3 100644 --- a/src/tap/db/DBConnection.java +++ b/src/tap/db/DBConnection.java @@ -17,15 +17,17 @@ package tap.db; * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import tap.data.DataReadException; import tap.data.TableIterator; -import tap.metadata.TAPDM; +import tap.metadata.TAPColumn; +import tap.metadata.TAPMetadata; import tap.metadata.TAPTable; import uws.service.log.UWSLogType; import adql.query.ADQLQuery; +import adql.translator.ADQLTranslator; /** * <p>Connection to the "database" (whatever is the type or whether it is linked to a true DBMS connection).</p> @@ -52,37 +54,125 @@ public interface DBConnection { public String getID(); /** - * <p>Let executing the given ADQL query.</p> + * <p>Fetch the whole content of TAP_SCHEMA.</p> * - * <p>The result of this query must be formatted as a table, and so must be iterable using a {@link TableIterator}.</p> + * <p> + * This function SHOULD be used only once: at the starting of the TAP service. It is an alternative way + * to get the published schemas, tables and columns. The other way is to build a {@link TAPMetadata} object + * yourself in function of the schemas/tables/columns you want to publish (i.e. which can be done by reading + * metadata from a XML document - following the same schema - XSD- as for the TAP resource <i>tables</i>) + * and then to load them in the DB (see {@link #setTAPSchema(TAPMetadata, boolean)} for more details). + * </p> * - * <p><i>note: the interpretation of the ADQL query is up to the implementation. In most of the case, it is just needed - * to translate this ADQL query into an SQL query (understandable by the chosen DBMS).</i></p> + * <p><b>CAUTION: + * This function MUST NOT be used if the tables to publish or the standard TAP_SCHEMA tables have names in DB different from the + * ones defined by the TAP standard. So, if DB names are different from the ADQL names, you have to write yourself a way to get + * the metadata from the DB. + * </b></p> * - * @param adqlQuery ADQL query to execute. + * <p><i><b>Important note:</b> + * If the schema or some standard tables or columns are missing, TAP_SCHEMA will be considered as incomplete + * and an exception will be thrown. + * </i></p> * - * @return The table result. + * <p><i>Note: + * This function MUST be able to read the standard tables and columns described by the IVOA. All other tables/columns + * will be merely ignored. + * </i></p> * - * @throws DBException If any errors occurs while executing the query. + * @return Content of TAP_SCHEMA inside the DB. + * + * @throws DBException If TAP_SCHEMA can not be found, is incomplete or if some important metadata can not be retrieved. + * + * @since 2.0 */ - public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException; + public TAPMetadata getTAPSchema() throws DBException; /** - * <p>Add or update the specified TAP_SCHEMA table with the given data.</p> - * - * <p><i><b>Warning:</b> It is expected that the given data SHOULD be the only ones inside the specified table. - * So, the table SHOULD probably be cleared before the insertion of the given data. However, this behavior MAY depend of the - * implementation and more particularly of the way the TAP_SCHEMA is updated.</i></p> - * - * @param tapTableName Name of the TAP_SCHEMA table to add/update. - * @param data Data to use in order to fill the specified table. - * - * @return <i>true</i> if the specified table has been successfully added/updated, <i>false</i> otherwise. + * <p>Empty and then fill all the TAP_SCHEMA tables with the given list of metadata.</p> + * + * <p> + * This function SHOULD be used only once: at the starting of the TAP service, + * when metadata are loaded from a XML document (following the same schema - XSD- + * as for the TAP resource <i>tables</i>). + * </p> + * + * <p> + * <i>THIS FUNCTION IS MANIPULATING THE SCHEMAS AND TABLES OF YOUR DATABASE. + * SO IT SHOULD HAVE A SPECIFIC BEHAVIOR DESCRIBED BELOW. + * <b>SO PLEASE READ THE FOLLOWINGS AND TRY TO RESPECT IT AS MUCH AS POSSIBLE IN THE IMPLEMENTATIONS</b> + * </i></p> + * + * <h3>TAP_SCHEMA CREATION</h3> + * <p> + * This function is MAY drop and then re-create the schema TAP_SCHEMA and all + * its tables listed in the TAP standard (TAP_SCHEMA.schemas, .tables, .columns, .keys and .key_columns). + * <i>All other tables inside TAP_SCHEMA SHOULD NOT be modified!</i> + * </p> + * + * <p> + * The schema and the tables MUST be created using either the <b>standard definition</b> or the + * <b>definition provided in the {@link TAPMetadata} object</b> (if provided). Indeed, if your definition of these TAP tables + * is different from the standard (the standard + new elements), you MUST provide your modifications in parameter + * through the {@link TAPMetadata} object so that they can be applied and taken into account in TAP_SCHEMA. + * </p> + * + * <p><i>Note: + * DB names provided in the given TAPMetadata (see {@link TAPTable#getDBSchemaName()}, {@link TAPTable#getDBName()} and {@link TAPColumn#getDBName()}) + * are used for the creation and filling of the tables. + * + * Whether these requests must be case sensitive or not SHOULD be managed by ADQLTranslator (see {@link ADQLTranslator#getQualifiedSchemaName(adql.db.DBTable)}, + * {@link ADQLTranslator#getQualifiedTableName(adql.db.DBTable)} and {@link ADQLTranslator#getColumnName(adql.db.DBColumn)}). + * </i></p> + * + * <h3>TAPMetadata PARAMETER</h3> + * <p> + * This object MUST contain all schemas, tables and columns that MUST be published. All its content will be + * used in order to fill the TAP_SCHEMA tables. + * </p> + * <p> + * Of course, TAP_SCHEMA and its tables MAY be provided in this object. But: + * </p> + * <ul> + * <li><b>(a) if TAP_SCHEMA tables are NOT provided</b>: + * this function SHOULD consider their definition as exactly the one provided by + * the TAP standard/protocol. If so, the standard definition MUST be automatically added + * into the {@link TAPMetadata} object AND into TAP_SCHEMA. + * </li> + * <li><b>(b) if TAP_SCHEMA tables ARE provided</b>: + * the definition of all given elements will be taken into account while updating the TAP_SCHEMA. + * Each element definition not provided MUST be considered as exactly the same as the standard one + * and MUST be added into the {@link TAPMetadata} object AND into TAP_SCHEMA. + * </li> + * </ul> + * + * <p><i>Note: By default, all implementations of this interface in the TAP library will fill only standard columns and tables of TAP_SCHEMA. + * To fill your own, you MUST implement yourself this interface or to extend an existing implementation.</i></p> + * + * <p><i><b>WARNING</b>: + * (b) lets consider a TAP_SCHEMA different from the standard one. BUT, these differences MUST be only additions, + * NOT modifications or deletion of the standard definition! This function MUST be able to work AT LEAST on a + * standard definition of TAP_SCHEMA. + * </p> + * + * <h3>FILLING BEHAVIOUR</h3> + * <p> + * The TAP_SCHEMA tables SHOULD be completely emptied (in SQL: "DELETE FROM <table_name>;" or merely "DROP TABLE <table_name>") before insertions can be processed. + * </p> + * + * <h3>ERRORS MANAGEMENT</h3> + * <p> + * If any error occurs while executing any "DB" queries (in SQL: DROP, DELETE, INSERT, CREATE, ...), all queries executed + * before in this function MUST be canceled (in SQL: ROLLBACK). + * </p> + * + * @param metadata List of all schemas, tables, columns and foreign keys to insert in the TAP_SCHEMA. * * @throws DBException If any error occurs while updating the database. - * @throws DataReadException If any error occurs while reading the given data. + * + * @since 2.0 */ - public boolean updateTAPTable(final TAPDM tapTableName, final TableIterator data) throws DBException, DataReadException; + public void setTAPSchema(final TAPMetadata metadata) throws DBException; /** * Add the defined and given table inside the TAP_UPLOAD schema. @@ -93,27 +183,52 @@ public interface DBConnection { * * @param tableDef Definition of the table to upload (list of all columns and of their type). * @param data Rows and columns of the table to upload. - * @param maxNbRows Maximum number of rows allowed to be inserted. Beyond this limit, a - * {@link DataReadException} MUST be sent. <i>A negative or a NULL value means "no limit".</i> * * @return <i>true</i> if the given table has been successfully added, <i>false</i> otherwise. * * @throws DBException If any error occurs while adding the table. - * @throws DataReadException If any error occurs while reading the given data. + * @throws DataReadException If any error occurs while reading the given data (particularly if any limit - in byte or row - set in the TableIterator is reached). + * + * @since 2.0 */ - public boolean addUploadedTable(final TAPTable tableDef, final TableIterator data, final int maxNbRows) throws DBException, DataReadException; + public boolean addUploadedTable(final TAPTable tableDef, final TableIterator data) throws DBException, DataReadException; /** * <p>Drop the specified uploaded table from the database. * More precisely, it means dropping a table from the TAP_UPLOAD schema.</p> * - * @param tableName Name (in the database) of the uploaded table to drop. + * <p><i>Note: + * This function SHOULD drop only one table. So, if more than one table match in the "database" to the given one, an exception MAY be thrown. + * This behavior is implementation-dependent. + * </i></p> + * + * @param tableDef Definition of the uploaded table to drop (the whole object is needed in order to get the DB schema and tables names). * * @return <i>true</i> if the specified table has been successfully dropped, <i>false</i> otherwise. * * @throws DBException If any error occurs while dropping the specified uploaded table. + * + * @since 2.0 */ - public boolean dropUploadedTable(final String tableName) throws DBException; + public boolean dropUploadedTable(final TAPTable tableDef) throws DBException; + + /** + * <p>Let executing the given ADQL query.</p> + * + * <p>The result of this query must be formatted as a table, and so must be iterable using a {@link TableIterator}.</p> + * + * <p><i>note: the interpretation of the ADQL query is up to the implementation. In most of the case, it is just needed + * to translate this ADQL query into an SQL query (understandable by the chosen DBMS).</i></p> + * + * @param adqlQuery ADQL query to execute. + * + * @return The table result. + * + * @throws DBException If any errors occurs while executing the query. + * + * @since 2.0 + */ + public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException; /** * <p>Close the connection (if needed).</p> diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index 94be3f6..d68a948 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -17,79 +17,292 @@ package tap.db; * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomishes Rechen Institute (ARI) + * Astronomishes Rechen Institut (ARI) */ +import java.io.PrintStream; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Map; +import tap.data.DataReadException; +import tap.data.ResultSetTableIterator; +import tap.data.TableIterator; import tap.log.TAPLog; import tap.metadata.TAPColumn; +import tap.metadata.TAPForeignKey; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPMetadata.STDSchema; +import tap.metadata.TAPMetadata.STDTable; +import tap.metadata.TAPSchema; import tap.metadata.TAPTable; +import tap.metadata.TAPTable.TableType; +import tap.metadata.TAPType; +import tap.metadata.TAPType.TAPDatatype; import adql.query.ADQLQuery; -import cds.savot.model.SavotTR; -import cds.savot.model.TDSet; +import adql.query.IdentifierField; +import adql.translator.ADQLTranslator; +import adql.translator.JDBCTranslator; +import adql.translator.TranslationException; /** - * Simple implementation of the {@link DBConnection} interface. - * It creates and manages a JDBC connection to a specified database. - * Thus results of any executed SQL query will be a {@link ResultSet}. + * <p>This {@link DBConnection} implementation is theoretically able to deal with any DBMS connection.</p> + * + * <p><i>Note: + * "Theoretically", because its design has been done using information about Postgres, SQLite, Oracle, MySQL and Java DB (Derby). + * Then it has been really tested successfully with Postgres and SQLite. + * </i></p> + * + * <h3>Deal with different DBMS features</h3> + * + * <p>Update queries are taking into account whether the following features are supported by the DBMS:</p> + * <ul> + * <li><b>data definition</b>: when not supported, no update operation will be possible. + * All corresponding functions will then throw a {@link DBException} ; + * only {@link #executeQuery(ADQLQuery)} will be possibly called.</li> + * + * <li><b>transactions</b>: when not supported, no transaction is started or merely used. + * It means that in case of update failure, no rollback will be possible + * and that already done modification will remain in the database.</li> + * + * <li><b>schemas</b>: when the DBMS does not have the notion of schema (like SQLite), no schema creation or dropping will be obviously processed. + * Besides, if not already done, database name of all tables will be prefixed by the schema name. + * The prefix to apply is returned by {@link #getTablePrefix(String)}.</li> + * + * <li><b>batch updates</b>: when not supported, updates will just be done, "normally, one by one. + * In one word, there will be merely no optimization. + * Anyway, this feature concerns only the insertions into tables.</li> + * + * <li><b>case sensitivity of identifiers</b>: the case sensitivity of quoted identifier varies from the used DBMS. This {@link DBConnection} + * implementation is able to adapt itself in function of the way identifiers are stored and + * researched in the database. How the case sensitivity is managed by the DBMS is the problem + * of only one function (which can be overwritten if needed): {@link #equals(String, String, boolean)}.</li> + * </ul> + * + * <p><i><b>Warning</b>: + * All these features have no impact at all on ADQL query executions ({@link #executeQuery(ADQLQuery)}). + * </i></p> + * + * <h3>Datatypes</h3> + * + * <p>Column types are converted from DBMS to TAP types with {@link #getTAPType(String)} and from TAP to DBMS types with {@link #getDBMSDatatype(TAPType)}.</p> + * + * <p> + * All typical DBMS datatypes are taken into account, <b>EXCEPT the geometrical types</b> (POINT and REGION). For these types, the only object having this + * information is the translator thanks to {@link JDBCTranslator#isPointType(String)}, {@link JDBCTranslator#isRegionType(String)}, + * {@link JDBCTranslator#getPointType()} and {@link JDBCTranslator#getRegionType()}. The two first functions are used to identify a DBMS type as a point or + * a region (note: several DBMS datatypes may be identified as a geometry type). The two others provide the DBMS type corresponding the best to the TAP types + * POINT and REGION. + * </p> + * + * <p><i><b>Warning:</b> + * The TAP type REGION can be either a circle, a box or a polygon. Since several DBMS types correspond to one TAP type, {@link JDBCTranslator#getRegionType()} + * MUST return a type covering all these region datatypes. Generally, it will be a VARCHAR whose the values would be STC-S expressions. + * Note that this function is used ONLY WHEN tables with a geometrical value is uploaded. On the contrary, {@link JDBCTranslator#isRegionType(String)} + * is used much more often: in order to write the metadata part of a query result. + * </i></p> * * @author Grégory Mantelet (CDS;ARI) - * @version 1.1 (04/2014) + * @version 2.0 (07/2014) + * @since 2.0 */ public class JDBCConnection implements DBConnection { - /** JDBC prefix of any database URL (for instance: jdbc:postgresql://127.0.0.1/myDB or jdbc:postgresql:myDB). */ - public final static String JDBC_PREFIX = "jdbc"; + /** DBMS name of PostgreSQL used in the database URL. */ + protected final static String DBMS_POSTGRES = "postgresql"; + + /** DBMS name of SQLite used in the database URL. */ + protected final static String DBMS_SQLITE = "sqlite"; + + /** DBMS name of MySQL used in the database URL. */ + protected final static String DBMS_MYSQL = "mysql"; - /** Connection ID (typically, the job ID). */ + /** DBMS name of Oracle used in the database URL. */ + protected final static String DBMS_ORACLE = "oracle"; + + /** Connection ID (typically, the job ID). It lets identify the DB errors linked to the Job execution in the logs. */ protected final String ID; /** JDBC connection (created and initialized at the creation of this {@link JDBCConnection} instance). */ protected final Connection connection; - /** Logger to use if any message needs to be printed to the server manager. */ + /** The translator this connection must use to translate ADQL into SQL. It is also used to get information about the case sensitivity of all types of identifier (schema, table, column). */ + protected final JDBCTranslator translator; + + /** Object to use if any message needs to be logged. <i>note: this logger may be NULL. If NULL, messages will never be printed.</i> */ protected final TAPLog logger; + /* JDBC URL MANAGEMENT */ + + /** JDBC prefix of any database URL (for instance: jdbc:postgresql://127.0.0.1/myDB or jdbc:postgresql:myDB). */ + public final static String JDBC_PREFIX = "jdbc"; + + /** Name (in lower-case) of the DBMS with which the connection is linked. */ + protected final String dbms; + + /* DBMS SUPPORTED FEATURES */ + + /** Indicate whether the DBMS supports transactions (start, commit, rollback and end). <i>note: If no transaction is possible, no transaction will be used, but then, it will never possible to cancel modifications in case of error.</i> */ + protected boolean supportsTransaction; + + /** Indicate whether the DBMS supports the definition of data (create, update, drop, insert into schemas and tables). <i>note: If not supported, it will never possible to create TAP_SCHEMA from given metadata (see {@link #setTAPSchema(TAPMetadata)}) and to upload/drop tables (see {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #dropUploadedTable(TAPTable)}).</i> */ + protected boolean supportsDataDefinition; + + /** Indicate whether the DBMS supports several updates in once (using {@link Statement#addBatch(String)} and {@link Statement#executeBatch()}). <i>note: If not supported, every updates will be done one by one. So it is not really a problem, but just a loss of optimization.</i> */ + protected boolean supportsBatchUpdates; + + /** Indicate whether the DBMS has the notion of SCHEMA. Most of the DBMS has it, but not SQLite for instance. <i>note: If not supported, the DB table name will be prefixed by the DB schema name followed by the character "_". Nevertheless, if the DB schema name is NULL, the DB table name will never be prefixed.</i> */ + protected boolean supportsSchema; + + /* CASE SENSITIVITY SUPPORT */ + + /** Indicate whether UNquoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. <i>note: If FALSE, unquoted identifiers will still be considered as case insensitive for the researches, but will be stored in lower or upper case (in function of {@link #lowerCaseUnquoted} and {@link #upperCaseUnquoted}). If none of these two flags is TRUE, the storage case will be though considered as mixed.</i> */ + protected boolean supportsMixedCaseUnquotedIdentifier; + /** Indicate whether the unquoted identifiers are stored in lower case in the DBMS. */ + protected boolean lowerCaseUnquoted; + /** Indicate whether the unquoted identifiers are stored in upper case in the DBMS. */ + protected boolean upperCaseUnquoted; + + /** Indicate whether quoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. <i>note: If FALSE, quoted identifiers will be considered as case sensitive and will be stored either in lower, upper or in mixed case (in function of {@link #lowerCaseQuoted}, {@link #upperCaseQuoted} and {@link #mixedCaseQuoted}). If none of these three flags is TRUE, the storage case will be mixed case.</i> */ + protected boolean supportsMixedCaseQuotedIdentifier; + /** Indicate whether the quoted identifiers are stored in lower case in the DBMS. */ + protected boolean lowerCaseQuoted; + /** Indicate whether the quoted identifiers are stored in mixed case in the DBMS. */ + protected boolean mixedCaseQuoted; + /** Indicate whether the quoted identifiers are stored in upper case in the DBMS. */ + protected boolean upperCaseQuoted; + /** - * <p> - * Creates a JDBC connection to the specified database and with the specified JDBC driver. - * This connection is established using the given user name and password. - * <p> - * <p><i><u>note:</u> the JDBC driver is loaded using <pre>Class.forName(driverPath)</pre>.</i></p> + * <p>Creates a JDBC connection to the specified database and with the specified JDBC driver. + * This connection is established using the given user name and password.<p> + * + * <p><i><u>note:</u> the JDBC driver is loaded using <pre>Class.forName(driverPath)</pre> and the connection is created with <pre>DriverManager.getConnection(dbUrl, dbUser, dbPassword)</pre>.</i></p> + * + * <p><i><b>Warning:</b> + * This constructor really creates a new SQL connection. Creating a SQL connection is time consuming! + * That's why it is recommended to use a pool of connections. When doing so, you should use the other constructor of this class + * ({@link #JDBCConnection(Connection, String, TAPLog)}). + * </i></p> * * @param driverPath Full class name of the JDBC driver. * @param dbUrl URL to the database. <i><u>note</u> This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added.</i> - * @param dbUser Name of the database user (supposed to be the database owner). + * @param dbUser Name of the database user. * @param dbPassword Password of the given database user. - * @param logger Logger to use if any message needs to be printed to the server admin. + * @param translator {@link ADQLTranslator} to use in order to get SQL from an ADQL query and to get qualified DB table names. + * @param connID ID of this connection. <i>note: may be NULL ; but in this case, logs concerning this connection will be more difficult to localize.</i> + * @param logger Logger to use in case of need. <i>note: may be NULL ; in this case, error will never be logged, but sometimes DBException may be raised.</i> + * + * @throws DBException If the driver can not be found or if the connection can not merely be created (usually because DB parameters are wrong). + */ + public JDBCConnection(final String driverPath, final String dbUrl, final String dbUser, final String dbPassword, final JDBCTranslator translator, final String connID, final TAPLog logger) throws DBException{ + this(createConnection(driverPath, dbUrl, dbUser, dbPassword), translator, connID, logger); + } + + /** + * <p>Create a JDBC connection by wrapping the given connection.</p> * - * @throws DBException If the specified driver can not be found, or if the database URL or user is incorrect. + * <p><i><b>Warning:</b> + * Calling {@link #close()} will call the function close() of the given connection. + * So, if this connection is coming from a pool, it is here supposed that a call to this function will not close the connection but gives it back to the pool. + * If it's not the case, {@link #close()} must be overwritten in order to apply the good "close" behavior. + * </i></p> + * + * @param conn Connection to wrap. + * @param translator {@link ADQLTranslator} to use in order to get SQL from an ADQL query and to get qualified DB table names. + * @param connID ID of this connection. <i>note: may be NULL ; but in this case, logs concerning this connection will be more difficult to localize.</i> + * @param logger Logger to use in case of need. <i>note: may be NULL ; in this case, error will never be logged, but sometimes DBException may be raised.</i> */ - public JDBCConnection(final String ID, final String driverPath, final String dbUrl, final String dbUser, final String dbPassword, final TAPLog logger) throws DBException{ + public JDBCConnection(final Connection conn, final JDBCTranslator translator, final String connID, final TAPLog logger) throws DBException{ + if (conn == null) + throw new NullPointerException("Missing SQL connection! => can not create a JDBCConnection object."); + if (translator == null) + throw new NullPointerException("Missing ADQL translator! => can not create a JDBCConnection object."); + + this.connection = conn; + this.translator = translator; + this.ID = connID; this.logger = logger; - this.ID = ID; + // Set the supporting features' flags + DBMS type: + try{ + DatabaseMetaData dbMeta = connection.getMetaData(); + dbms = getDBMSName(dbMeta.getURL()); + supportsTransaction = dbMeta.supportsTransactions(); + supportsBatchUpdates = dbMeta.supportsBatchUpdates(); + supportsDataDefinition = dbMeta.supportsDataDefinitionAndDataManipulationTransactions(); + supportsSchema = dbMeta.supportsSchemasInTableDefinitions(); + lowerCaseUnquoted = dbMeta.storesLowerCaseIdentifiers(); + upperCaseUnquoted = dbMeta.storesUpperCaseIdentifiers(); + supportsMixedCaseUnquotedIdentifier = dbMeta.supportsMixedCaseIdentifiers(); + lowerCaseQuoted = dbMeta.storesLowerCaseQuotedIdentifiers(); + mixedCaseQuoted = dbMeta.storesMixedCaseQuotedIdentifiers(); + upperCaseQuoted = dbMeta.storesUpperCaseQuotedIdentifiers(); + supportsMixedCaseQuotedIdentifier = dbMeta.supportsMixedCaseQuotedIdentifiers(); + }catch(SQLException se){ + throw new DBException("Unable to access to one or several DB metadata (url, supportsTransaction, supportsBatchUpdates, supportsDataDefinitionAndDataManipulationTransactions, supportsSchemasInTableDefinitions, storesLowerCaseIdentifiers, storesUpperCaseIdentifiers, supportsMixedCaseIdentifiers, storesLowerCaseQuotedIdentifiers, storesMixedCaseQuotedIdentifiers, storesUpperCaseQuotedIdentifiers and supportsMixedCaseQuotedIdentifiers) from the given Connection!"); + } + } + + /** + * Extract the DBMS name from the given database URL. + * + * @param dbUrl JDBC URL to access the database. <b>This URL must start with "jdbc:" ; otherwise an exception will be thrown.</b> + * + * @return The DBMS name as found in the given URL. + * + * @throws DBException If NULL has been given, if the URL is not a JDBC one (starting with "jdbc:") or if the DBMS name is missing. + */ + protected static final String getDBMSName(String dbUrl) throws DBException{ + if (dbUrl == null) + throw new DBException("Missing database URL!"); + + if (!dbUrl.startsWith(JDBC_PREFIX + ":")) + throw new DBException("This DBConnection implementation is only able to deal with JDBC connection! (the DB URL must start with \"" + JDBC_PREFIX + ":\" ; given url: " + dbUrl + ")"); + + dbUrl = dbUrl.substring(5); + int indSep = dbUrl.indexOf(':'); + if (indSep <= 0) + throw new DBException("Incorrect database URL: " + dbUrl); + + return dbUrl.substring(0, indSep).toLowerCase(); + } + + /** + * Create a {@link Connection} instance using the specified JDBC Driver and the given database parameters. + * + * @param driverPath Path to the JDBC driver. + * @param dbUrl JDBC URL to connect to the database. <i><u>note</u> This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added.</i> + * @param dbUser Name of the user to use to connect to the database. + * @param dbPassword Password of the user to use to connect to the database. + * + * @return A new DB connection. + * + * @throws DBException If the driver can not be found or if the connection can not merely be created (usually because DB parameters are wrong). + * + * @see DriverManager#getConnection(String, String, String) + */ + private final static Connection createConnection(final String driverPath, final String dbUrl, final String dbUser, final String dbPassword) throws DBException{ // Load the specified JDBC driver: try{ Class.forName(driverPath); }catch(ClassNotFoundException cnfe){ - logger.dbError("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe); throw new DBException("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe); } // Build a connection to the specified database: String url = dbUrl.startsWith(JDBC_PREFIX) ? dbUrl : (JDBC_PREFIX + dbUrl); try{ - connection = DriverManager.getConnection(url, dbUser, dbPassword); - logger.connectionOpened(this, (dbUrl.lastIndexOf('/') > 0 ? dbUrl.substring(dbUrl.lastIndexOf('/')) : dbUrl.substring(dbUrl.lastIndexOf(':')))); + return DriverManager.getConnection(url, dbUser, dbPassword); }catch(SQLException se){ - logger.dbError("Impossible to establish a connection to the database \"" + url + "\" !", se); throw new DBException("Impossible to establish a connection to the database \"" + url + "\" !", se); } } @@ -100,213 +313,2141 @@ public class JDBCConnection implements DBConnection { } @Override - public void startTransaction() throws DBException{ + public void close() throws DBException{ try{ - Statement st = connection.createStatement(); - st.execute("begin"); - logger.transactionStarted(this); + connection.close(); + log(0, "Connection CLOSED.", null); }catch(SQLException se){ - logger.dbError("Impossible to begin a transaction !", se); - throw new DBException("Impossible to begin a transaction !", se); + log(1, "CLOSING connection impossible!", se); + throw new DBException("Impossible to close the database connection !", se); } } + /* ********************* */ + /* INTERROGATION METHODS */ + /* ********************* */ @Override - public void cancelTransaction() throws DBException{ + public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException{ + String sql = null; + ResultSet result = null; try{ - connection.rollback(); - logger.transactionCancelled(this); + // 1. Translate the ADQL query into SQL: + log(0, "Translating ADQL : " + adqlQuery.toADQL(), null); + sql = translator.translate(adqlQuery); + + // 2. Execute the SQL query: + Statement stmt = connection.createStatement(); + log(0, "Executing SQL : " + sql, null); + result = stmt.executeQuery(sql); + + // 3. Return the result through a TableIterator object: + log(0, "Returning result...", null); + return new ResultSetTableIterator(result, dbms, adqlQuery.getResultingColumns()); + }catch(SQLException se){ - logger.dbError("Impossible to cancel/rollback a transaction !", se); - throw new DBException("Impossible to cancel (rollback) the transaction !", se); + close(result); + log(2, "Unexpected error while EXECUTING SQL query!", se); + throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se); + }catch(TranslationException te){ + close(result); + log(2, "Unexpected error while TRANSLATING ADQL into SQL!", te); + throw new DBException("Unexpected error while translating ADQL into SQL: " + te.getMessage(), te); + }catch(DataReadException dre){ + close(result); + throw new DBException("Impossible to read the query result, because: " + dre.getMessage(), dre); + } + } + + /* *********************** */ + /* TAP_SCHEMA MANIPULATION */ + /* *********************** */ + + /** + * Tell when, compared to the other TAP standard tables, a given standard TAP table should be created. + * + * @param table Standard TAP table. + * + * @return An index between 0 and 4 (included) - 0 meaning the first table to create whereas 4 is the last one. + * -1 is returned if NULL is given in parameter of if the standard table is not taken into account here. + */ + protected int getCreationOrder(final STDTable table){ + if (table == null) + return -1; + + switch(table){ + case SCHEMAS: + return 0; + case TABLES: + return 1; + case COLUMNS: + return 2; + case KEYS: + return 3; + case KEY_COLUMNS: + return 4; + default: + return -1; } } + /* ************************************ */ + /* GETTING TAP_SCHEMA FROM THE DATABASE */ + /* ************************************ */ + + /** + * <p>In this implementation, this function is first creating a virgin {@link TAPMetadata} object + * that will be filled progressively by calling the following functions:</p> + * <ol> + * <li>{@link #loadSchemas(TAPTable, TAPMetadata, Statement)}</li> + * <li>{@link #loadTables(TAPTable, TAPMetadata, Statement)}</li> + * <li>{@link #loadColumns(TAPTable, List, Statement)}</li> + * <li>{@link #loadKeys(TAPTable, TAPTable, List, Statement)}</li> + * </ol> + * + * <p><i>Note: + * If schemas are not supported by this DBMS connection, the DB name of all tables will be set to NULL + * and the DB name of all tables will be prefixed by the ADQL name of their respective schema (using {@link #getTablePrefix(String)}). + * </i></p> + * + * @see tap.db.DBConnection#getTAPSchema() + */ @Override - public void endTransaction() throws DBException{ + public TAPMetadata getTAPSchema() throws DBException{ + // Build a virgin TAP metadata: + TAPMetadata metadata = new TAPMetadata(); + + // Get the definition of the standard TAP_SCHEMA tables: + TAPSchema tap_schema = TAPMetadata.getStdSchema(); + + // If schemas are not supported by the DBMS connection, the schema must not be translated in the DB: + if (!supportsSchema){ + String namePrefix = getTablePrefix(tap_schema.getADQLName()); + tap_schema.setDBName(null); + for(TAPTable t : tap_schema) + t.setDBName(namePrefix + t.getDBName()); + } + + // LOAD ALL METADATA FROM THE STANDARD TAP TABLES: + Statement stmt = null; try{ - connection.commit(); - logger.transactionEnded(this); + // create a common statement for all loading functions: + stmt = connection.createStatement(); + + // load all schemas from TAP_SCHEMA.schemas: + log(0, "Loading TAP_SCHEMA.schemas.", null); + loadSchemas(tap_schema.getTable(STDTable.SCHEMAS.label), metadata, stmt); + + // load all tables from TAP_SCHEMA.tables: + log(0, "Loading TAP_SCHEMA.tables.", null); + List<TAPTable> lstTables = loadTables(tap_schema.getTable(STDTable.TABLES.label), metadata, stmt); + + // load all columns from TAP_SCHEMA.columns: + log(0, "Loading TAP_SCHEMA.columns.", null); + loadColumns(tap_schema.getTable(STDTable.COLUMNS.label), lstTables, stmt); + + // load all foreign keys from TAP_SCHEMA.keys and TAP_SCHEMA.key_columns: + log(0, "Loading TAP_SCHEMA.keys and TAP_SCHEMA.key_columns.", null); + loadKeys(tap_schema.getTable(STDTable.KEYS.label), tap_schema.getTable(STDTable.KEY_COLUMNS.label), lstTables, stmt); + }catch(SQLException se){ - logger.dbError("Impossible to end/commit a transaction !", se); - throw new DBException("Impossible to end/commit the transaction !", se); + log(2, "Impossible to create a Statement!", se); + throw new DBException("Can not create a Statement!", se); + }finally{ + close(stmt); } + + return metadata; } - @Override - public void close() throws DBException{ + /** + * <p>Load into the given metadata all schemas listed in TAP_SCHEMA.schemas.</p> + * + * <p><i>Note: + * If schemas are not supported by this DBMS connection, the DB name of the loaded schemas is set to NULL. + * </i></p> + * + * @param tablesDef Definition of the table TAP_SCHEMA.schemas. + * @param metadata Metadata to fill with all found schemas. + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If any error occurs while interacting with the database. + */ + protected void loadSchemas(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{ + ResultSet rs = null; try{ - connection.close(); - logger.connectionClosed(this); + // Build the SQL query: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype"))); + sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(tableDef)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all schemas: + while(rs.next()){ + String schemaName = rs.getString(1), description = rs.getString(2), utype = rs.getString(3); + + // create the new schema: + TAPSchema newSchema = new TAPSchema(schemaName, nullifyIfNeeded(description), nullifyIfNeeded(utype)); + + // If schemas are not supported by the DBMS connection, the schema must not be translated in the DB: + if (!supportsSchema) + newSchema.setDBName(null); + + // add the new schema inside the given metadata: + metadata.addSchema(newSchema); + } }catch(SQLException se){ - logger.dbError("Impossible to close a database transaction !", se); - throw new DBException("Impossible to close the database transaction !", se); + log(2, "Impossible to load schemas from TAP_SCHEMA.schemas!", se); + throw new DBException("Impossible to load schemas from TAP_SCHEMA.schemas!", se); + }finally{ + close(rs); } } - /* ********************* */ - /* INTERROGATION METHODS */ - /* ********************* */ - @Override - public ResultSet executeQuery(final String sqlQuery, final ADQLQuery adqlQuery) throws DBException{ + /** + * <p>Load into the corresponding metadata all tables listed in TAP_SCHEMA.tables.</p> + * + * <p><i>Note: + * Schemas are searched in the given metadata by their ADQL name and case sensitively. + * If they can not be found a {@link DBException} is thrown. + * </i></p> + * + * <p><i>Note: + * If schemas are not supported by this DBMS connection, the DB name of the loaded {@link TAPTable}s is prefixed by the ADQL name of their respective schema. + * The table prefix is built by {@link #getTablePrefix(String)}. + * </i></p> + * + * @param tablesDef Definition of the table TAP_SCHEMA.tables. + * @param metadata Metadata (containing already all schemas listed in TAP_SCHEMA.schemas). + * @param stmt Statement to use in order to interact with the database. + * + * @return The complete list of all loaded tables. <i>note: this list is required by {@link #loadColumns(TAPTable, List, Statement)}.</i> + * + * @throws DBException If a schema can not be found, or if any other error occurs while interacting with the database. + * + * @see #getTablePrefix(String) + */ + protected List<TAPTable> loadTables(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{ + ResultSet rs = null; try{ - Statement stmt = connection.createStatement(); - logger.sqlQueryExecuting(this, sqlQuery); - ResultSet result = stmt.executeQuery(sqlQuery); - logger.sqlQueryExecuted(this, sqlQuery); - return result; + // Build the SQL query: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("table_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("table_type"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype"))); + sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(tableDef)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all tables: + ArrayList<TAPTable> lstTables = new ArrayList<TAPTable>(); + while(rs.next()){ + String schemaName = rs.getString(1), tableName = rs.getString(2), typeStr = rs.getString(3), description = rs.getString(4), utype = rs.getString(5); + + // get the schema: + TAPSchema schema = metadata.getSchema(schemaName); + if (schema == null){ + log(2, "Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!", null); + throw new DBException("Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!"); + } + + // resolve the table type (if any) ; by default, it will be "table": + TableType type = TableType.table; + if (typeStr != null){ + try{ + type = TableType.valueOf(typeStr.toLowerCase()); + }catch(IllegalArgumentException iae){} + } + + // create the new table: + TAPTable newTable = new TAPTable(tableName, type, nullifyIfNeeded(description), nullifyIfNeeded(utype)); + + // If schemas are not supported by the DBMS connection, the DB table name must be prefixed by the schema name: + if (!supportsSchema) + newTable.setDBName(getTablePrefix(schema.getADQLName()) + newTable.getDBName()); + + // add the new table inside its corresponding schema: + schema.addTable(newTable); + lstTables.add(newTable); + } + + return lstTables; }catch(SQLException se){ - logger.sqlQueryError(this, sqlQuery, se); - throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se); + log(2, "Impossible to load tables from TAP_SCHEMA.tables!", se); + throw new DBException("Impossible to load tables from TAP_SCHEMA.tables!", se); + }finally{ + close(rs); } } - /* ************** */ - /* UPLOAD METHODS */ - /* ************** */ - @Override - public void createSchema(final String schemaName) throws DBException{ - String sql = "CREATE SCHEMA " + schemaName + ";"; + /** + * <p>Load into the corresponding tables all columns listed in TAP_SCHEMA.columns.</p> + * + * <p><i>Note: + * Tables are searched in the given list by their ADQL name and case sensitively. + * If they can not be found a {@link DBException} is thrown. + * </i></p> + * + * @param columnsDef Definition of the table TAP_SCHEMA.columns. + * @param lstTables List of all published tables (= all tables listed in TAP_SCHEMA.tables). + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If a table can not be found, or if any other error occurs while interacting with the database. + */ + protected void loadColumns(final TAPTable tableDef, final List<TAPTable> lstTables, final Statement stmt) throws DBException{ + ResultSet rs = null; try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.schemaCreated(this, schemaName); + // Build the SQL query: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(tableDef.getColumn("table_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("column_name"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("unit"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("ucd"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("datatype"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("size"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("principal"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("indexed"))); + sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("std"))); + sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(tableDef)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all tables: + while(rs.next()){ + String tableName = rs.getString(1), columnName = rs.getString(2), description = rs.getString(3), unit = rs.getString(4), ucd = rs.getString(5), utype = rs.getString(6), datatype = rs.getString(7); + int size = rs.getInt(8); + boolean principal = toBoolean(rs.getObject(9)), indexed = toBoolean(rs.getObject(10)), std = toBoolean(rs.getObject(11)); + + // get the table: + TAPTable table = searchTable(tableName, lstTables.iterator()); + if (table == null){ + log(2, "Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!", null); + throw new DBException("Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!"); + } + + // resolve the column type (if any) ; by default, it will be "VARCHAR" if unknown or missing: + TAPDatatype tapDatatype = null; + // ...try to resolve the datatype in function of all datatypes declared by the TAP standard. + if (datatype != null){ + try{ + tapDatatype = TAPDatatype.valueOf(datatype.toUpperCase()); + }catch(IllegalArgumentException iae){} + } + // ...build the column type: + TAPType type; + if (tapDatatype == null) + type = new TAPType(TAPDatatype.VARCHAR); + else + type = new TAPType(tapDatatype, size); + + // create the new column: + TAPColumn newColumn = new TAPColumn(columnName, type, nullifyIfNeeded(description), nullifyIfNeeded(unit), nullifyIfNeeded(ucd), nullifyIfNeeded(utype)); + newColumn.setPrincipal(principal); + newColumn.setIndexed(indexed); + newColumn.setStd(std); + + // add the new column inside its corresponding table: + table.addColumn(newColumn); + } }catch(SQLException se){ - logger.dbError("Impossible to create the schema \"" + schemaName + "\" !", se); - throw new DBException("Impossible to create the schema \"" + schemaName + "\" !", se); + log(2, "Impossible to load columns from TAP_SCHEMA.columns!", se); + throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); + }finally{ + close(rs); } } - @Override - public void dropSchema(final String schemaName) throws DBException{ - String sql = "DROP SCHEMA IF EXISTS " + schemaName + " CASCADE;"; + /** + * <p>Load into the corresponding tables all keys listed in TAP_SCHEMA.keys and detailed in TAP_SCHEMA.key_columns.</p> + * + * <p><i>Note: + * Tables and columns are searched in the given list by their ADQL name and case sensitively. + * If they can not be found a {@link DBException} is thrown. + * </i></p> + * + * @param keysDef Definition of the table TAP_SCHEMA.keys. + * @param keyColumnsDef Definition of the table TAP_SCHEMA.key_columns. + * @param lstTables List of all published tables (= all tables listed in TAP_SCHEMA.tables). + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If a table or a column can not be found, or if any other error occurs while interacting with the database. + */ + protected void loadKeys(final TAPTable keysDef, final TAPTable keyColumnsDef, final List<TAPTable> lstTables, final Statement stmt) throws DBException{ + ResultSet rs = null; + PreparedStatement keyColumnsStmt = null; try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.schemaDropped(this, schemaName); + // Prepare the query to get the columns of each key: + StringBuffer sqlBuf = new StringBuffer("SELECT "); + sqlBuf.append(translator.getColumnName(keyColumnsDef.getColumn("key_id"))); + sqlBuf.append(", ").append(translator.getColumnName(keyColumnsDef.getColumn("from_column"))); + sqlBuf.append(", ").append(translator.getColumnName(keyColumnsDef.getColumn("target_column"))); + sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(keyColumnsDef)); + sqlBuf.append(" WHERE ").append(translator.getColumnName(keyColumnsDef.getColumn("key_id"))).append(" = ?").append(';'); + keyColumnsStmt = connection.prepareStatement(sqlBuf.toString()); + + // Build the SQL query to get the keys: + sqlBuf.delete(0, sqlBuf.length()); + sqlBuf.append("SELECT ").append(translator.getColumnName(keysDef.getColumn("key_id"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("from_table"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("target_table"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("description"))); + sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("utype"))); + sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(keysDef)).append(';'); + + // Execute the query: + rs = stmt.executeQuery(sqlBuf.toString()); + + // Create all foreign keys: + while(rs.next()){ + String key_id = rs.getString(1), from_table = rs.getString(2), target_table = rs.getString(3), description = rs.getString(4), utype = rs.getString(5); + + // get the two tables (source and target): + TAPTable sourceTable = searchTable(from_table, lstTables.iterator()); + if (sourceTable == null){ + log(2, "Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!", null); + throw new DBException("Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!"); + } + TAPTable targetTable = searchTable(target_table, lstTables.iterator()); + if (targetTable == null){ + log(2, "Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!", null); + throw new DBException("Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!"); + } + + // get the list of columns joining the two tables of the foreign key: + HashMap<String,String> columns = new HashMap<String,String>(); + ResultSet rsKeyCols = null; + try{ + keyColumnsStmt.setString(1, key_id); + rsKeyCols = keyColumnsStmt.executeQuery(); + while(rsKeyCols.next()) + columns.put(rsKeyCols.getString(1), rsKeyCols.getString(2)); + }catch(SQLException se){ + log(2, "Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); + throw new DBException("Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); + }finally{ + close(rsKeyCols); + } + + // create and add the new foreign key inside the source table: + try{ + sourceTable.addForeignKey(key_id, targetTable, columns, nullifyIfNeeded(description), nullifyIfNeeded(utype)); + }catch(Exception ex){ + log(2, "Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); + throw new DBException("Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); + } + } }catch(SQLException se){ - logger.dbError("Impossible to drop the schema \"" + schemaName + "\" !", se); - throw new DBException("Impossible to drop the schema \"" + schemaName + "\" !", se); + log(2, "Impossible to load columns from TAP_SCHEMA.columns!", se); + throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); + }finally{ + close(rs); + close(keyColumnsStmt); } } + /* ********************************** */ + /* SETTING TAP_SCHEMA IN THE DATABASE */ + /* ********************************** */ + + /** + * <p>This function is just calling the following functions:</p> + * <ol> + * <li>{@link #mergeTAPSchemaDefs(TAPMetadata)}</li> + * <li>{@link #startTransaction()}</li> + * <li>{@link #resetTAPSchema(Statement, TAPTable[])}</li> + * <li>{@link #createTAPSchemaTable(TAPTable, Statement)} for each standard TAP_SCHEMA table</li> + * <li>{@link #fillTAPSchema(TAPMetadata)}</li> + * <li>{@link #createTAPTableIndexes(TAPTable, Statement)} for each standard TA_SCHEMA table</li> + * <li>{@link #commit()} or {@link #rollback()}</li> + * <li>{@link #endTransaction()}</li> + * </ol> + * + * <p><i><b>Important note: + * If the connection does not support transactions, then there will be merely no transaction. + * Consequently, any failure (exception/error) will not clean the partial modifications done by this function. + * </i></p> + * + * @see tap.db.DBConnection#setTAPSchema(tap.metadata.TAPMetadata) + */ @Override - public void createTable(final TAPTable table) throws DBException{ - // Build the SQL query: - StringBuffer sqlBuf = new StringBuffer(); - sqlBuf.append("CREATE TABLE ").append(table.getDBSchemaName()).append('.').append(table.getDBName()).append("("); - Iterator<TAPColumn> it = table.getColumns(); - while(it.hasNext()){ - TAPColumn col = it.next(); - sqlBuf.append('"').append(col.getDBName()).append("\" ").append(' ').append(getDBType(col.getDatatype(), col.getArraySize(), logger)); - if (it.hasNext()) - sqlBuf.append(','); - } - sqlBuf.append(");"); + public void setTAPSchema(final TAPMetadata metadata) throws DBException{ + Statement stmt = null; - // Execute the creation query: - String sql = sqlBuf.toString(); try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.tableCreated(this, table); + // A. GET THE DEFINITION OF ALL STANDARD TAP TABLES: + TAPTable[] stdTables = mergeTAPSchemaDefs(metadata); + + startTransaction(); + + // B. RE-CREATE THE STANDARD TAP_SCHEMA TABLES: + stmt = connection.createStatement(); + + // 1. Ensure TAP_SCHEMA exists and drop all its standard TAP tables: + log(0, "Cleaning TAP_SCHEMA.", null); + resetTAPSchema(stmt, stdTables); + + // 2. Create all standard TAP tables: + log(0, "Creating TAP_SCHEMA tables.", null); + for(TAPTable table : stdTables) + createTAPSchemaTable(table, stmt); + + // C. FILL THE NEW TABLE USING THE GIVEN DATA ITERATOR: + log(0, "Filling TAP_SCHEMA tables.", null); + fillTAPSchema(metadata); + + // D. CREATE THE INDEXES OF ALL STANDARD TAP TABLES: + log(0, "Creating TAP_SCHEMA tables' indexes.", null); + for(TAPTable table : stdTables) + createTAPTableIndexes(table, stmt); + + commit(); }catch(SQLException se){ - logger.dbError("Impossible to create the table \"" + table.getFullName() + "\" !", se); - throw new DBException("Impossible to create the table \"" + table.getFullName() + "\" !", se); + rollback(); + throw new DBException("Impossible to SET TAP_SCHEMA in DB!", se); + }finally{ + close(stmt); + endTransaction(); } } /** - * Gets the database type corresponding to the given {@link TAPColumn} type. + * <p>Merge the definition of TAP_SCHEMA tables given in parameter with the definition provided in the TAP standard.</p> + * + * <p> + * The goal is to get in output the list of all standard TAP_SCHEMA tables. But it must take into account the customized + * definition given in parameter if there is one. Indeed, if a part of TAP_SCHEMA is not provided, it will be completed here by the + * definition provided in the TAP standard. And so, if the whole TAP_SCHEMA is not provided at all, the returned tables will be those + * of the IVOA standard. + * </p> + * + * <p><i><b>Important note:</b> + * If the TAP_SCHEMA definition is missing or incomplete in the given metadata, it will be added or completed automatically + * by this function with the definition provided in the IVOA TAP standard. + * </i></p> + * + * <p><i>Note: + * Only the standard tables of TAP_SCHEMA are considered. The others are skipped (that's to say: never returned by this function ; + * however, they will stay in the given metadata). + * </i></p> + * + * <p><i>Note: + * If schemas are not supported by this DBMS connection, the DB name of schemas is set to NULL and + * the DB name of tables is prefixed by the schema name (using {@link #getTablePrefix(String)}). + * </i></p> * - * @param datatype Column datatype (short, int, long, float, double, boolea, char or unsignedByte). - * @param arraysize Size of the array type (1 if not an array, a value > 1 for an array). - * @param logger Object to use to print warnings (for instance, if a given datatype is unknown). + * @param metadata Metadata (with or without TAP_SCHEMA schema or some of its table). <i>Must not be NULL</i> * - * @return The corresponding database type or the given datatype if unknown. + * @return The list of all standard TAP_SCHEMA tables, ordered by creation order (see {@link #getCreationOrder(STDTable)}). + * + * @see TAPMetadata#resolveStdTable(String) + * @see TAPMetadata#getStdSchema() + * @see TAPMetadata#getStdTable(STDTable) */ - public static String getDBType(String datatype, final int arraysize, final TAPLog logger){ - datatype = (datatype == null) ? null : datatype.trim().toLowerCase(); + protected TAPTable[] mergeTAPSchemaDefs(final TAPMetadata metadata){ + // 1. Get the TAP_SCHEMA schema from the given metadata: + TAPSchema tapSchema = null; + Iterator<TAPSchema> itSchema = metadata.iterator(); + while(tapSchema == null && itSchema.hasNext()){ + TAPSchema schema = itSchema.next(); + if (schema.getADQLName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label)) + tapSchema = schema; + } + + // 2. Get the provided definition of the standard TAP tables: + TAPTable[] customStdTables = new TAPTable[5]; + if (tapSchema != null){ + + /* if the schemas are not supported with this DBMS, + * remove its DB name: */ + if (!supportsSchema) + tapSchema.setDBName(null); - if (datatype == null || datatype.isEmpty()){ - if (logger != null) - logger.warning("undefined datatype => considered as VARCHAR !"); - return "VARCHAR"; + // retrieve only the standard TAP tables: + Iterator<TAPTable> itTable = tapSchema.iterator(); + while(itTable.hasNext()){ + TAPTable table = itTable.next(); + int indStdTable = getCreationOrder(TAPMetadata.resolveStdTable(table.getADQLName())); + if (indStdTable > -1) + customStdTables[indStdTable] = table; + } } - if (datatype.equals("short")) - return (arraysize == 1) ? "INT2" : "BYTEA"; - else if (datatype.equals("int")) - return (arraysize == 1) ? "INT4" : "BYTEA"; - else if (datatype.equals("long")) - return (arraysize == 1) ? "INT8" : "BYTEA"; - else if (datatype.equals("float")) - return (arraysize == 1) ? "FLOAT4" : "BYTEA"; - else if (datatype.equals("double")) - return (arraysize == 1) ? "FLOAT8" : "BYTEA"; - else if (datatype.equals("boolean")) - return (arraysize == 1) ? "BOOL" : "BYTEA"; - else if (datatype.equals("char")) - return (arraysize == 1) ? "CHAR(1)" : ((arraysize <= 0) ? "VARCHAR" : ("VARCHAR(" + arraysize + ")")); - else if (datatype.equals("unsignedbyte")) - return "BYTEA"; - else{ - if (logger != null) - logger.dbInfo("Warning: unknown datatype: \"" + datatype + "\" => considered as \"" + datatype + "\" !"); - return datatype; + // 3. Build a common TAPSchema, if needed: + if (tapSchema == null){ + + // build a new TAP_SCHEMA definition based on the standard definition: + tapSchema = TAPMetadata.getStdSchema(); + + /* if the schemas are not supported with this DBMS, + * remove its DB name: */ + if (!supportsSchema) + tapSchema.setDBName(null); + + // add the new TAP_SCHEMA definition in the given metadata object: + metadata.addSchema(tapSchema); + } + + // 4. Finally, build the join between the standard tables and the custom ones: + TAPTable[] stdTables = new TAPTable[]{TAPMetadata.getStdTable(STDTable.SCHEMAS),TAPMetadata.getStdTable(STDTable.TABLES),TAPMetadata.getStdTable(STDTable.COLUMNS),TAPMetadata.getStdTable(STDTable.KEYS),TAPMetadata.getStdTable(STDTable.KEY_COLUMNS)}; + for(int i = 0; i < stdTables.length; i++){ + + // CASE: no custom definition: + if (customStdTables[i] == null){ + /* if the schemas are not supported with this DBMS, + * prefix the DB name with "tap_schema_": */ + if (!supportsSchema) + stdTables[i].setDBName(getTablePrefix(tapSchema.getADQLName()) + stdTables[i].getDBName()); + // add the table to the fetched or built-in schema: + tapSchema.addTable(stdTables[i]); + } + // CASE: custom definition + else + stdTables[i] = customStdTables[i]; } + + return stdTables; } - @Override - public void dropTable(final TAPTable table) throws DBException{ - String sql = "DROP TABLE " + table.getDBSchemaName() + "." + table.getDBName() + ";"; + /** + * <p>Ensure the TAP_SCHEMA schema exists in the database AND it must especially drop all of its standard tables + * (schemas, tables, columns, keys and key_columns), if they exist.</p> + * + * <p><i><b>Important note</b>: + * If TAP_SCHEMA already exists and contains other tables than the standard ones, they will not be dropped and they will stay in place. + * </i></p> + * + * @param stmt The statement to use in order to interact with the database. + * @param stdTables List of all standard tables that must be (re-)created. + * They will be used just to know the name of the standard tables that should be dropped here. + * + * @throws SQLException If any error occurs while querying or updating the database. + */ + protected void resetTAPSchema(final Statement stmt, final TAPTable[] stdTables) throws SQLException{ + DatabaseMetaData dbMeta = connection.getMetaData(); + + // 1. Get the qualified DB schema name: + String dbSchemaName = stdTables[0].getDBSchemaName(); + + /* 2. Test whether the schema TAP_SCHEMA exists + * and if it does not, create it: */ + if (dbSchemaName != null){ + // test whether the schema TAP_SCHEMA exists: + boolean hasTAPSchema = isSchemaExisting(dbSchemaName, dbMeta); + + // create TAP_SCHEMA if it does not exist: + if (!hasTAPSchema) + stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(stdTables[0]) + ";"); + } + + // 2-bis. Drop all its standard tables: + dropTAPSchemaTables(stdTables, stmt, dbMeta); + } + + /** + * <p>Remove/Drop all standard TAP_SCHEMA tables given in parameter.</p> + * + * <p><i>Note: + * To test the existence of tables to drop, {@link DatabaseMetaData#getTables(String, String, String, String[])} is called. + * Then the schema and table names are compared with the case sensitivity defined by the translator. + * Only tables matching with these comparisons will be dropped. + * </i></p> + * + * @param stdTables Tables to drop. (they should be provided ordered by their creation order (see {@link #getCreationOrder(STDTable)})). + * @param stmt Statement to use in order to interact with the database. + * @param dbMeta Database metadata. Used to list all existing tables. + * + * @throws SQLException If any error occurs while querying or updating the database. + * + * @see ADQLTranslator#isCaseSensitive(IdentifierField) + */ + private void dropTAPSchemaTables(final TAPTable[] stdTables, final Statement stmt, final DatabaseMetaData dbMeta) throws SQLException{ + String[] stdTablesToDrop = new String[]{null,null,null,null,null}; + + ResultSet rs = null; try{ - Statement stmt = connection.createStatement(); - stmt.executeUpdate(sql); - logger.tableDropped(this, table); - }catch(SQLException se){ - logger.dbError("Impossible to drop the table \"" + table.getFullName() + "\" !", se); - throw new DBException("Impossible to drop the table \"" + table.getFullName() + "\" !", se); + // Retrieve only the schema name and determine whether the search should be case sensitive: + String tapSchemaName = stdTables[0].getDBSchemaName(); + boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE); + + // Identify which standard TAP tables must be dropped: + rs = dbMeta.getTables(null, null, null, null); + while(rs.next()){ + String rsSchema = nullifyIfNeeded(rs.getString(2)), rsTable = rs.getString(3); + if (!supportsSchema || (tapSchemaName == null && rsSchema == null) || equals(rsSchema, tapSchemaName, schemaCaseSensitive)){ + int indStdTable; + indStdTable = getCreationOrder(isStdTable(rsTable, stdTables, tableCaseSensitive)); + if (indStdTable > -1){ + stdTablesToDrop[indStdTable] = (rsSchema != null ? "\"" + rsSchema + "\"." : "") + "\"" + rsTable + "\""; + } + } + } + }finally{ + close(rs); + } + + // Drop the existing tables (in the reverse order of creation): + for(int i = stdTablesToDrop.length - 1; i >= 0; i--){ + if (stdTablesToDrop[i] != null) + stmt.executeUpdate("DROP TABLE " + stdTablesToDrop[i] + ";"); } } - @Override - public void insertRow(final SavotTR row, final TAPTable table) throws DBException{ - StringBuffer sql = new StringBuffer("INSERT INTO "); - sql.append(table.getDBSchemaName()).append('.').append(table.getDBName()).append(" VALUES ("); + /** + * <p>Create the specified standard TAP_SCHEMA tables into the database.</p> + * + * <p><i><b>Important note:</b> + * Only standard TAP_SCHEMA tables (schemas, tables, columns, keys and key_columns) can be created here. + * If the given table is not part of the schema TAP_SCHEMA (comparison done on the ADQL name case-sensitively) + * and is not a standard TAP_SCHEMA table (comparison done on the ADQL name case-sensitively), + * this function will do nothing and will throw an exception. + * </i></p> + * + * @param table Table to create. + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If the given table is not a standard TAP_SCHEMA table. + * @throws SQLException If any error occurs while querying or updating the database. + */ + protected void createTAPSchemaTable(final TAPTable table, final Statement stmt) throws DBException, SQLException{ + // 1. ENSURE THE GIVEN TABLE IS REALLY A TAP_SCHEMA TABLE (according to the ADQL names): + if (!table.getADQLSchemaName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label) || TAPMetadata.resolveStdTable(table.getADQLName()) == null) + throw new DBException("Forbidden table creation: " + table + " is not a standard table of TAP_SCHEMA!"); + + // 2. BUILD THE SQL QUERY TO CREATE THE TABLE: + StringBuffer sql = new StringBuffer("CREATE TABLE "); - TDSet cells = row.getTDs(); + // a. Write the fully qualified table name: + sql.append(translator.getQualifiedTableName(table)); + + // b. List all the columns: + sql.append('('); Iterator<TAPColumn> it = table.getColumns(); - String datatype, value; - TAPColumn col; - int i = 0; while(it.hasNext()){ - col = it.next(); - if (i > 0) + TAPColumn col = it.next(); + + // column name: + sql.append(translator.getColumnName(col)); + + // column type: + sql.append(' ').append(getDBMSDatatype(col.getDatatype())); + + // last column ? + if (it.hasNext()) sql.append(','); - datatype = col.getDatatype(); - value = cells.getContent(i); - if (value == null || value.isEmpty()) - sql.append("NULL"); - else if (datatype.equalsIgnoreCase("char") || datatype.equalsIgnoreCase("varchar") || datatype.equalsIgnoreCase("unsignedByte")) - sql.append('\'').append(value.replaceAll("'", "''").replaceAll("\0", "")).append('\''); - else{ - if (value.equalsIgnoreCase("nan")) - sql.append("'NaN'"); - else - sql.append(value.replaceAll("\0", "")); - } - i++; } - sql.append(");"); - try{ - Statement stmt = connection.createStatement(); - int nbInsertedRows = stmt.executeUpdate(sql.toString()); - logger.rowsInserted(this, table, nbInsertedRows); - }catch(SQLException se){ - logger.dbError("Impossible to insert a row into the table \"" + table.getFullName() + "\" !", se); - throw new DBException("Impossible to insert a row in the table \"" + table.getFullName() + "\" !", se); + // c. Append the primary key definition, if needed: + String primaryKey = getPrimaryKeyDef(table.getADQLName()); + if (primaryKey != null) + sql.append(',').append(primaryKey); + + // d. End the query: + sql.append(')').append(';'); + + // 3. FINALLY CREATE THE TABLE: + stmt.executeUpdate(sql.toString()); + } + + /** + * <p>Get the primary key corresponding to the specified table.</p> + * + * <p>If the specified table is not a standard TAP_SCHEMA table, NULL will be returned.</p> + * + * @param tableName ADQL table name. + * + * @return The primary key definition (prefixed by a space) corresponding to the specified table (ex: " PRIMARY KEY(schema_name)"), + * or NULL if the specified table is not a standard TAP_SCHEMA table. + */ + private String getPrimaryKeyDef(final String tableName){ + STDTable stdTable = TAPMetadata.resolveStdTable(tableName); + if (stdTable == null) + return null; + + boolean caseSensitive = translator.isCaseSensitive(IdentifierField.COLUMN); + switch(stdTable){ + case SCHEMAS: + return " PRIMARY KEY(" + (caseSensitive ? "\"schema_name\"" : "schema_name") + ")"; + case TABLES: + return " PRIMARY KEY(" + (caseSensitive ? "\"schema_name\"" : "schema_name") + ", " + (caseSensitive ? "\"table_name\"" : "table_name") + ")"; + case COLUMNS: + return " PRIMARY KEY(" + (caseSensitive ? "\"table_name\"" : "table_name") + ", " + (caseSensitive ? "\"column_name\"" : "column_name") + ")"; + case KEYS: + case KEY_COLUMNS: + return " PRIMARY KEY(" + (caseSensitive ? "\"key_id\"" : "key_id") + ")"; + default: + return null; + } + } + + /** + * <p>Create the DB indexes corresponding to the given TAP_SCHEMA table.</p> + * + * <p><i><b>Important note:</b> + * Only standard TAP_SCHEMA tables (schemas, tables, columns, keys and key_columns) can be created here. + * If the given table is not part of the schema TAP_SCHEMA (comparison done on the ADQL name case-sensitively) + * and is not a standard TAP_SCHEMA table (comparison done on the ADQL name case-sensitively), + * this function will do nothing and will throw an exception. + * </i></p> + * + * @param table Table whose indexes must be created here. + * @param stmt Statement to use in order to interact with the database. + * + * @throws DBException If the given table is not a standard TAP_SCHEMA table. + * @throws SQLException If any error occurs while querying or updating the database. + */ + protected void createTAPTableIndexes(final TAPTable table, final Statement stmt) throws DBException, SQLException{ + // 1. Ensure the given table is really a TAP_SCHEMA table (according to the ADQL names): + if (!table.getADQLSchemaName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label) || TAPMetadata.resolveStdTable(table.getADQLName()) == null) + throw new DBException("Forbidden index creation: " + table + " is not a standard table of TAP_SCHEMA!"); + + // Build the fully qualified DB name of the table: + final String dbTableName = translator.getQualifiedTableName(table); + + // Build the name prefix of all the indexes to create: + final String indexNamePrefix = "INDEX_" + ((table.getADQLSchemaName() != null) ? (table.getADQLSchemaName() + "_") : "") + table.getADQLName() + "_"; + + Iterator<TAPColumn> it = table.getColumns(); + while(it.hasNext()){ + TAPColumn col = it.next(); + // Create an index only for columns that have the 'indexed' flag: + if (col.isIndexed() && !isPartOfPrimaryKey(col.getADQLName())) + stmt.executeUpdate("CREATE INDEX " + indexNamePrefix + col.getADQLName() + " ON " + dbTableName + "(" + translator.getColumnName(col) + ");"); + } + } + + /** + * Tell whether the specified column is part of the primary key of its table. + * + * @param adqlName ADQL name of a column. + * + * @return <i>true</i> if the specified column is part of the primary key, + * <i>false</i> otherwise. + */ + private boolean isPartOfPrimaryKey(final String adqlName){ + if (adqlName == null) + return false; + else + return (adqlName.equalsIgnoreCase("schema_name") || adqlName.equalsIgnoreCase("table_name") || adqlName.equalsIgnoreCase("column_name") || adqlName.equalsIgnoreCase("key_id")); + } + + /** + * <p>Fill all the standard tables of TAP_SCHEMA (schemas, tables, columns, keys and key_columns).</p> + * + * <p>This function just call the following functions:</p> + * <ol> + * <li>{@link #fillSchemas(TAPTable, Iterator)}</li> + * <li>{@link #fillTables(TAPTable, Iterator)}</li> + * <li>{@link #fillColumns(TAPTable, Iterator)}</li> + * <li>{@link #fillKeys(TAPTable, TAPTable, Iterator)}</li> + * </ol> + * + * @param meta All schemas and tables to list inside the TAP_SCHEMA tables. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + protected void fillTAPSchema(final TAPMetadata meta) throws SQLException, DBException{ + TAPTable metaTable; + + // 1. Fill SCHEMAS: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label); + Iterator<TAPTable> allTables = fillSchemas(metaTable, meta.iterator()); + + // 2. Fill TABLES: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label); + Iterator<TAPColumn> allColumns = fillTables(metaTable, allTables); + allTables = null; + + // Fill COLUMNS: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label); + Iterator<TAPForeignKey> allKeys = fillColumns(metaTable, allColumns); + allColumns = null; + + // Fill KEYS and KEY_COLUMNS: + metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label); + TAPTable metaTable2 = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label); + fillKeys(metaTable, metaTable2, allKeys); + } + + /** + * <p>Fill the standard table TAP_SCHEMA.schemas with the list of all published schemas.</p> + * + * <p><i>Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + * </i></p> + * + * @param metaTable Description of TAP_SCHEMA.schemas. + * @param itSchemas Iterator over the list of schemas. + * + * @return Iterator over the full list of all tables (whatever is their schema). + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private Iterator<TAPTable> fillSchemas(final TAPTable metaTable, final Iterator<TAPSchema> itSchemas) throws SQLException, DBException{ + List<TAPTable> allTables = new ArrayList<TAPTable>(); + + // Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + sql.append(translator.getQualifiedTableName(metaTable)).append(" ("); + sql.append(translator.getColumnName(metaTable.getColumn("schema_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype"))); + sql.append(") VALUES (?, ?, ?);"); + + // Prepare the statement: + PreparedStatement stmt = null; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // Execute the query for each schema: + int nbRows = 0; + while(itSchemas.hasNext()){ + TAPSchema schema = itSchemas.next(); + nbRows++; + + // list all tables of this schema: + appendAllInto(allTables, schema.iterator()); + + // add the schema entry into the DB: + stmt.setString(1, schema.getADQLName()); + stmt.setString(2, schema.getDescription()); + stmt.setString(3, schema.getUtype()); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + }finally{ + close(stmt); + } + + return allTables.iterator(); + } + + /** + * <p>Fill the standard table TAP_SCHEMA.tables with the list of all published tables.</p> + * + * <p><i>Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + * </i></p> + * + * @param metaTable Description of TAP_SCHEMA.tables. + * @param itTables Iterator over the list of tables. + * + * @return Iterator over the full list of all columns (whatever is their table). + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private Iterator<TAPColumn> fillTables(final TAPTable metaTable, final Iterator<TAPTable> itTables) throws SQLException, DBException{ + List<TAPColumn> allColumns = new ArrayList<TAPColumn>(); + + // Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + sql.append(translator.getQualifiedTableName(metaTable)).append(" ("); + sql.append(translator.getColumnName(metaTable.getColumn("schema_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("table_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("table_type"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype"))); + sql.append(") VALUES (?, ?, ?, ?, ?);"); + + // Prepare the statement: + PreparedStatement stmt = null; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // Execute the query for each table: + int nbRows = 0; + while(itTables.hasNext()){ + TAPTable table = itTables.next(); + nbRows++; + + // list all columns of this table: + appendAllInto(allColumns, table.getColumns()); + + // add the table entry into the DB: + stmt.setString(1, table.getADQLSchemaName()); + stmt.setString(2, table.getADQLName()); + stmt.setString(3, table.getType().toString()); + stmt.setString(4, table.getDescription()); + stmt.setString(5, table.getUtype()); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + }finally{ + close(stmt); + } + + return allColumns.iterator(); + } + + /** + * <p>Fill the standard table TAP_SCHEMA.columns with the list of all published columns.</p> + * + * <p><i>Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + * </i></p> + * + * @param metaTable Description of TAP_SCHEMA.columns. + * @param itColumns Iterator over the list of columns. + * + * @return Iterator over the full list of all foreign keys. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private Iterator<TAPForeignKey> fillColumns(final TAPTable metaTable, final Iterator<TAPColumn> itColumns) throws SQLException, DBException{ + List<TAPForeignKey> allKeys = new ArrayList<TAPForeignKey>(); + + // Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + sql.append(translator.getQualifiedTableName(metaTable)).append(" ("); + sql.append(translator.getColumnName(metaTable.getColumn("table_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("column_name"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("unit"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("ucd"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("datatype"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("size"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("principal"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("indexed"))); + sql.append(", ").append(translator.getColumnName(metaTable.getColumn("std"))); + sql.append(") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); + + // Prepare the statement: + PreparedStatement stmt = null; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // Execute the query for each column: + int nbRows = 0; + while(itColumns.hasNext()){ + TAPColumn col = itColumns.next(); + nbRows++; + + // list all foreign keys of this column: + appendAllInto(allKeys, col.getTargets()); + + // add the column entry into the DB: + stmt.setString(1, col.getTable().getADQLName()); + stmt.setString(2, col.getADQLName()); + stmt.setString(3, col.getDescription()); + stmt.setString(4, col.getUnit()); + stmt.setString(5, col.getUcd()); + stmt.setString(6, col.getUtype()); + stmt.setString(7, col.getDatatype().type.toString()); + stmt.setInt(8, col.getDatatype().length); + stmt.setInt(9, col.isPrincipal() ? 1 : 0); + stmt.setInt(10, col.isIndexed() ? 1 : 0); + stmt.setInt(11, col.isStd() ? 1 : 0); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + }finally{ + close(stmt); + } + + return allKeys.iterator(); + } + + /** + * <p>Fill the standard tables TAP_SCHEMA.keys and TAP_SCHEMA.key_columns with the list of all published foreign keys.</p> + * + * <p><i>Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + * </i></p> + * + * @param metaKeys Description of TAP_SCHEMA.keys. + * @param metaKeyColumns Description of TAP_SCHEMA.key_columns. + * @param itKeys Iterator over the list of foreign keys. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + */ + private void fillKeys(final TAPTable metaKeys, final TAPTable metaKeyColumns, final Iterator<TAPForeignKey> itKeys) throws SQLException, DBException{ + // Build the SQL update query for KEYS: + StringBuffer sqlKeys = new StringBuffer("INSERT INTO "); + sqlKeys.append(translator.getQualifiedTableName(metaKeys)).append(" ("); + sqlKeys.append(translator.getColumnName(metaKeys.getColumn("key_id"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("from_table"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("target_table"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("description"))); + sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("utype"))); + sqlKeys.append(") VALUES (?, ?, ?, ?, ?);"); + + PreparedStatement stmtKeys = null, stmtKeyCols = null; + try{ + // Prepare the statement for KEYS: + stmtKeys = connection.prepareStatement(sqlKeys.toString()); + + // Build the SQL update query for KEY_COLUMNS: + StringBuffer sqlKeyCols = new StringBuffer("INSERT INTO "); + sqlKeyCols.append(translator.getQualifiedTableName(metaKeyColumns)).append(" ("); + sqlKeyCols.append(translator.getColumnName(metaKeyColumns.getColumn("key_id"))); + sqlKeyCols.append(", ").append(translator.getColumnName(metaKeyColumns.getColumn("from_column"))); + sqlKeyCols.append(", ").append(translator.getColumnName(metaKeyColumns.getColumn("target_column"))); + sqlKeyCols.append(") VALUES (?, ?, ?);"); + + // Prepare the statement for KEY_COLUMNS: + stmtKeyCols = connection.prepareStatement(sqlKeyCols.toString()); + + // Execute the query for each column: + int nbKeys = 0, nbKeyColumns = 0; + while(itKeys.hasNext()){ + TAPForeignKey key = itKeys.next(); + nbKeys++; + + // add the key entry into KEYS: + stmtKeys.setString(1, key.getKeyId()); + stmtKeys.setString(2, key.getFromTable().getFullName()); + stmtKeys.setString(3, key.getTargetTable().getFullName()); + stmtKeys.setString(4, key.getDescription()); + stmtKeys.setString(5, key.getUtype()); + executeUpdate(stmtKeys, nbKeys); + + // add the key columns into KEY_COLUMNS: + Iterator<Map.Entry<String,String>> itAssoc = key.iterator(); + while(itAssoc.hasNext()){ + nbKeyColumns++; + Map.Entry<String,String> assoc = itAssoc.next(); + stmtKeyCols.setString(1, key.getKeyId()); + stmtKeyCols.setString(2, assoc.getKey()); + stmtKeyCols.setString(3, assoc.getValue()); + executeUpdate(stmtKeyCols, nbKeyColumns); + } + } + + executeBatchUpdates(stmtKeys, nbKeys); + executeBatchUpdates(stmtKeyCols, nbKeyColumns); + }finally{ + close(stmtKeys); + close(stmtKeyCols); + } + } + + /* ***************** */ + /* UPLOAD MANAGEMENT */ + /* ***************** */ + + /** + * <p><i><b>Important note:</b> + * Only tables uploaded by users can be created in the database. To ensure that, the schema name of this table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL. + * If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different. + * </i></p> + * + * <p><i><b>Important note:</b> + * This function may modify the given {@link TAPTable} object in the following cases:. + * </i></p> + * <ul> + * <li><i> + * If no schema is set to the given {@link TAPTable} object, one will be set automatically by this function. + * This schema will have the same ADQL and DB name: {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD"). + * </i></li> + * <li><i> + * If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly + * inside the given {@link TAPTable} object (building the prefix with {@link #getTablePrefix(String)}). Then the DB name + * of the schema will be set to NULL. + * </i></li> + * </ul> + * + * <p><i>Note: + * If the upload schema does not already exist in the database, it will be created. + * </i></p> + * + * @see tap.db.DBConnection#addUploadedTable(tap.metadata.TAPTable, tap.data.TableIterator) + * @see #checkUploadedTableDef(TAPTable) + */ + @Override + public boolean addUploadedTable(TAPTable tableDef, TableIterator data) throws DBException, DataReadException{ + // If no table to upload, consider it has been dropped and return TRUE: + if (tableDef == null) + return true; + + // Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD): + checkUploadedTableDef(tableDef); + + Statement stmt = null; + try{ + + // Start a transaction: + startTransaction(); + // ...create a statement: + stmt = connection.createStatement(); + + DatabaseMetaData dbMeta = connection.getMetaData(); + + // 1. Create the upload schema, if it does not already exist: + if (!isSchemaExisting(tableDef.getDBSchemaName(), dbMeta)) + stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(tableDef) + ";"); + + // 1bis. Ensure the table does not already exist and if it is the case, throw an understandable exception: + else if (isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), dbMeta)) + throw new DBException("Impossible to create the user uploaded table in the database: " + translator.getQualifiedTableName(tableDef) + "! This table already exists."); + + // 2. Create the table: + // ...build the SQL query: + StringBuffer sqlBuf = new StringBuffer("CREATE TABLE "); + sqlBuf.append(translator.getQualifiedTableName(tableDef)).append(" ("); + Iterator<TAPColumn> it = tableDef.getColumns(); + while(it.hasNext()){ + TAPColumn col = it.next(); + // column name: + sqlBuf.append(translator.getColumnName(col)); + // column type: + sqlBuf.append(' ').append(getDBMSDatatype(col.getDatatype())); + // last column ? + if (it.hasNext()) + sqlBuf.append(','); + } + sqlBuf.append(");"); + // ...execute the update query: + stmt.executeUpdate(sqlBuf.toString()); + + // 3. Fill the table: + fillUploadedTable(tableDef, data); + + // Commit the transaction: + commit(); + + return true; + + }catch(SQLException se){ + rollback(); + log(1, "Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); + throw new DBException("Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); + }catch(DBException de){ + rollback(); + throw de; + }catch(DataReadException dre){ + rollback(); + throw dre; + }finally{ + close(stmt); + endTransaction(); + } + } + + /** + * <p>Fill the table uploaded by the user with the given data.</p> + * + * <p><i>Note: + * Batch updates may be done here if its supported by the DBMS connection. + * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. + * </i></p> + * + * @param metaTable Description of the updated table. + * @param data Iterator over the rows to insert. + * + * @return Number of inserted rows. + * + * @throws DBException If rows can not be inserted because the SQL update query has failed. + * @throws SQLException If any other SQL exception occurs. + * @throws DataReadException If there is any error while reading the data from the given {@link TableIterator} (and particularly if a limit - in byte or row - has been reached). + */ + protected int fillUploadedTable(final TAPTable metaTable, final TableIterator data) throws SQLException, DBException, DataReadException{ + // 1. Build the SQL update query: + StringBuffer sql = new StringBuffer("INSERT INTO "); + StringBuffer varParam = new StringBuffer(); + // ...table name: + sql.append(translator.getQualifiedTableName(metaTable)).append(" ("); + // ...list of columns: + TAPColumn[] cols = data.getMetadata(); + for(int c = 0; c < cols.length; c++){ + if (c > 0){ + sql.append(", "); + varParam.append(", "); + } + sql.append(translator.getColumnName(cols[c])); + varParam.append('?'); + } + // ...values pattern: + sql.append(") VALUES (").append(varParam).append(");"); + + // 2. Prepare the statement: + PreparedStatement stmt = null; + int nbRows = 0; + try{ + stmt = connection.prepareStatement(sql.toString()); + + // 3. Execute the query for each given row: + while(data.nextRow()){ + nbRows++; + int c = 1; + while(data.hasNextCol()) + stmt.setObject(c++, data.nextCol()); + executeUpdate(stmt, nbRows); + } + executeBatchUpdates(stmt, nbRows); + + return nbRows; + + }finally{ + close(stmt); + } + } + + /** + * <p><i><b>Important note:</b> + * Only tables uploaded by users can be dropped from the database. To ensure that, the schema name of this table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL. + * If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different. + * </i></p> + * + * <p><i><b>Important note:</b> + * This function may modify the given {@link TAPTable} object in the following cases:. + * </i></p> + * <ul> + * <li><i> + * If no schema is set to the given {@link TAPTable} object, one will be set automatically by this function. + * This schema will have the same ADQL and DB name: {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD"). + * </i></li> + * <li><i> + * If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly + * inside the given {@link TAPTable} object (building the prefix with {@link #getTablePrefix(String)}). Then the DB name + * of the schema will be set to NULL. + * </i></li> + * </ul> + * + * <p><i>Note: + * This implementation is able to drop only one uploaded table. So if this function finds more than one table matching to the given one, + * an exception will be thrown and no table will be dropped. + * </i></p> + * + * @see tap.db.DBConnection#dropUploadedTable(tap.metadata.TAPTable) + * @see #checkUploadedTableDef(TAPTable) + */ + @Override + public boolean dropUploadedTable(final TAPTable tableDef) throws DBException{ + // If no table to upload, consider it has been dropped and return TRUE: + if (tableDef == null) + return true; + + // Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD): + checkUploadedTableDef(tableDef); + + Statement stmt = null; + try{ + + // Check the existence of the table to drop: + if (!isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), connection.getMetaData())) + return true; + + // Execute the update: + stmt = connection.createStatement(); + int cnt = stmt.executeUpdate("DROP TABLE " + translator.getQualifiedTableName(tableDef) + ";"); + + // Ensure the update is successful: + return (cnt == 0); + + }catch(SQLException se){ + log(1, "Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); + throw new DBException("Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); + }finally{ + close(stmt); + } + } + + /** + * <p>Ensures that the given table MUST be inside the upload schema in ADQL.</p> + * + * <p>Thus, the following cases are taken into account:</p> + * <ul> + * <li> + * If no schema is set to the given {@link TAPTable} object, one will be set automatically by this function. + * This schema will have the same ADQL and DB name: {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD"). + * </li> + * <li> + * The schema name of the given table , if provided, MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL. + * If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different. + * </li> + * <li> + * If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly + * inside the given {@link TAPTable} object (building the prefix with {@link #getTablePrefix(String)}). Then the DB name + * of the schema will be set to NULL. + * </li> + * </ul> + * + * @param tableDef Definition of the table to create/drop. + * + * @throws DBException If the ADQL schema name of the given table is not {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD"). + */ + protected void checkUploadedTableDef(final TAPTable tableDef) throws DBException{ + // If no schema is provided, consider it is the default and standard upload schema - TAP_UPLOAD: + if (tableDef.getSchema() == null){ + TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.label, "Schema for tables uploaded by users."); + uploadSchema.addTable(tableDef); + } + // But if the ADQL name of the provided schema is not TAP_UPLOAD, throw an exception: + else if (!tableDef.getSchema().getADQLName().equals(STDSchema.UPLOADSCHEMA.label)) + throw new DBException("Missing upload schema! An uploaded table must be inside a schema whose the ADQL name is strictly equals to \"" + STDSchema.UPLOADSCHEMA.label + "\" (but the DB name may be different)."); + + // If schemas are not supported, prefix the table name and set to NULL the DB schema name: + if (!supportsSchema && tableDef.getDBSchemaName() != null){ + tableDef.setDBName(getTablePrefix(tableDef.getDBSchemaName()) + tableDef.getDBName()); + tableDef.getSchema().setDBName(null); + } + } + + /* ************** */ + /* TOOL FUNCTIONS */ + /* ************** */ + + /** + * <p>Log the given message and/or exception with the given level.</p> + * + * <p><i>Note: + * If no logger has been provided, only the WARNING and ERROR messages are printed in the standard error output stream. + * </i></p> + * + * @param level <=0: INFO, 1: WARNING, >=2: ERROR + * @param message Message to log. <i>may be NULL</i> + * @param ex Exception to log. <i>may be NULL</i> + */ + protected void log(final int level, String message, final Exception ex){ + //if (logger != null){ + if (message != null) + message = message.replaceAll("(\t|\r?\n)+", " "); + else + message = (ex != null ? ex.getClass().getName() : ""); + message = "{" + getID() + "} " + message; + PrintStream out = (level <= 0) ? System.out : System.err; + out.println((level <= 0 ? "[INFO] " : (level == 1 ? "[WARNING] " : "[ERROR] ")) + message + (ex != null ? " CAUSED BY: " + ex.getMessage() : "")); // TODO rmb debug msg + if (ex != null) + ex.printStackTrace(out); + // TODO JDBCConnection.log(int, String, Exception) + //} + } + + /** + * <p>Get the DBMS compatible datatype corresponding to the given column TAPType.</p> + * + * <p><i>Note 1: + * This function is able to generate a DB datatype compatible with the currently used DBMS. + * In this current implementation, only Postgresql, Oracle, SQLite, MySQL and Java DB/Derby have been considered. + * Most of the TAP types have been tested only with Postgresql and SQLite without any problem. + * If the DBMS you are using has not been considered, note that this function will return the TAP type expression by default. + * </i></p> + * + * <p><i>Note 2: + * In case the given datatype is NULL or not managed here, the DBMS type corresponding to "VARCHAR" will be returned. + * </i></p> + * + * <p><i>Note 3: + * The special TAP types POINT and REGION are converted into the DBMS type corresponding to "VARCHAR". + * </i></p> + * + * @param datatype Column TAP type. + * + * @return The corresponding DB type, or NULL if the given type is not managed or is NULL. + */ + protected String getDBMSDatatype(TAPType datatype){ + if (datatype == null) + datatype = new TAPType(TAPDatatype.VARCHAR); + + switch(datatype.type){ + + case SMALLINT: + return dbms.equals("sqlite") ? "INTEGER" : "SMALLINT"; + + case INTEGER: + case REAL: + return datatype.type.toString(); + + case BIGINT: + if (dbms.equals("oracle")) + return "NUMBER(19,0)"; + else if (dbms.equals("sqlite")) + return "INTEGER"; + else + return "BIGINT"; + + case DOUBLE: + if (dbms.equals("postgresql") || dbms.equals("oracle")) + return "DOUBLE PRECISION"; + else if (dbms.equals("sqlite")) + return "REAL"; + else + return "DOUBLE"; + + case BINARY: + if (dbms.equals("postgresql")) + return "bytea"; + else if (dbms.equals("sqlite")) + return "BLOB"; + else if (dbms.equals("oracle")) + return "RAW" + (datatype.length > 0 ? "(" + datatype.length + ")" : ""); + else if (dbms.equals("derby")) + return "CHAR" + (datatype.length > 0 ? "(" + datatype.length + ")" : "") + " FOR BIT DATA"; + else + return datatype.type.toString(); + + case VARBINARY: + if (dbms.equals("postgresql")) + return "bytea"; + else if (dbms.equals("sqlite")) + return "BLOB"; + else if (dbms.equals("oracle")) + return "LONG RAW" + (datatype.length > 0 ? "(" + datatype.length + ")" : ""); + else if (dbms.equals("derby")) + return "VARCHAR" + (datatype.length > 0 ? "(" + datatype.length + ")" : "") + " FOR BIT DATA"; + else + return datatype.type.toString(); + + case CHAR: + if (dbms.equals("sqlite")) + return "TEXT"; + else + return "CHAR"; + + case BLOB: + if (dbms.equals("postgresql")) + return "bytea"; + else + return "BLOB"; + + case CLOB: + if (dbms.equals("postgresql") || dbms.equals("mysql") || dbms.equals("sqlite")) + return "TEXT"; + else + return "CLOB"; + + case TIMESTAMP: + if (dbms.equals("sqlite")) + return "TEXT"; + else + return "TIMESTAMP"; + + case POINT: + case REGION: + case VARCHAR: + default: + if (dbms.equals("sqlite")) + return "TEXT"; + else + return "VARCHAR"; + } + } + + /** + * <p>Start a transaction.</p> + * + * <p> + * Basically, if transactions are supported by this connection, the flag AutoCommit is just turned off. + * It will be turned on again when {@link #endTransaction()} is called. + * </p> + * + * <p>If transactions are not supported by this connection, nothing is done.</p> + * + * <p><b><i>Important note:</b> + * If any error interrupts the START TRANSACTION operation, transactions will be afterwards considered as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + * </i></p> + * + * @throws DBException If it is impossible to start a transaction though transactions are supported by this connection. + * If these are not supported, this error can never be thrown. + */ + protected void startTransaction() throws DBException{ + try{ + if (supportsTransaction){ + connection.setAutoCommit(false); + log(0, "Transaction STARTED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + log(2, "Transaction STARTing impossible!", se); + throw new DBException("Transaction STARTing impossible!", se); + } + } + + /** + * <p>Commit the current transaction.</p> + * + * <p> + * {@link #startTransaction()} must have been called before. If it's not the case the connection + * may throw a {@link SQLException} which will be transformed into a {@link DBException} here. + * </p> + * + * <p>If transactions are not supported by this connection, nothing is done.</p> + * + * <p><b><i>Important note:</b> + * If any error interrupts the COMMIT operation, transactions will be afterwards considered as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + * </i></p> + * + * @throws DBException If it is impossible to commit a transaction though transactions are supported by this connection.. + * If these are not supported, this error can never be thrown. + */ + protected void commit() throws DBException{ + try{ + if (supportsTransaction){ + connection.commit(); + log(0, "Transaction COMMITED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + log(2, "Transaction COMMIT impossible!", se); + throw new DBException("Transaction COMMIT impossible!", se); + } + } + + /** + * <p>Rollback the current transaction.</p> + * + * <p> + * {@link #startTransaction()} must have been called before. If it's not the case the connection + * may throw a {@link SQLException} which will be transformed into a {@link DBException} here. + * </p> + * + * <p>If transactions are not supported by this connection, nothing is done.</p> + * + * <p><b><i>Important note:</b> + * If any error interrupts the ROLLBACK operation, transactions will considered afterwards as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + * </i></p> + * + * @throws DBException If it is impossible to rollback a transaction though transactions are supported by this connection.. + * If these are not supported, this error can never be thrown. + */ + protected void rollback(){ + try{ + if (supportsTransaction){ + connection.rollback(); + log(0, "Transaction ROLLBACKED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + log(2, "Transaction ROLLBACK impossible!", se); + } + } + + /** + * <p>End the current transaction.</p> + * + * <p> + * Basically, if transactions are supported by this connection, the flag AutoCommit is just turned on. + * </p> + * + * <p>If transactions are not supported by this connection, nothing is done.</p> + * + * <p><b><i>Important note:</b> + * If any error interrupts the END TRANSACTION operation, transactions will be afterwards considered as not supported by this connection. + * So, subsequent call to this function (and any other transaction related function) will never do anything. + * </i></p> + * + * @throws DBException If it is impossible to end a transaction though transactions are supported by this connection. + * If these are not supported, this error can never be thrown. + */ + protected void endTransaction(){ + try{ + if (supportsTransaction){ + connection.setAutoCommit(true); + log(0, "Transaction ENDED.", null); + } + }catch(SQLException se){ + supportsTransaction = false; + log(2, "Transaction ENDing impossible!", se); + } + } + + /** + * <p>Close silently the given {@link ResultSet}.</p> + * + * <p>If the given {@link ResultSet} is NULL, nothing (even exception/error) happens.</p> + * + * <p> + * If any {@link SQLException} occurs during this operation, it is caught and just logged (see {@link #log(int, String, Exception)}). + * No error is thrown and nothing else is done. + * </p> + * + * @param rs {@link ResultSet} to close. + */ + protected final void close(final ResultSet rs){ + try{ + if (rs != null) + rs.close(); + }catch(SQLException se){ + log(1, "Can not close a ResultSet!", null); + } + } + + /** + * <p>Close silently the given {@link Statement}.</p> + * + * <p>If the given {@link Statement} is NULL, nothing (even exception/error) happens.</p> + * + * <p> + * If any {@link SQLException} occurs during this operation, it is caught and just logged (see {@link #log(int, String, Exception)}). + * No error is thrown and nothing else is done. + * </p> + * + * @param rs {@link Statement} to close. + */ + protected final void close(final Statement stmt){ + try{ + if (stmt != null) + stmt.close(); + }catch(SQLException se){ + log(1, "Can not close a Statement!", null); + } + } + + /** + * <p>Transform the given column value in a boolean value.</p> + * + * <p>The following cases are taken into account in function of the given value's type:</p> + * <ul> + * <li><b>NULL</b>: <i>false</i> is always returned.</li> + * + * <li><b>{@link Boolean}</b>: the boolean value is returned as provided (but casted in boolean).</li> + * + * <li><b>{@link Integer}</b>: <i>true</i> is returned only if the integer value is strictly greater than 0, otherwise <i>false</i> is returned.</li> + * + * <li><b>Other</b>: toString().trim() is first called on this object. Then, an integer value is tried to be extracted from it. + * If it succeeds, the previous rule is applied. If it fails, <i>true</i> will be returned only if the string is "t" or "true" (case insensitively).</li> + * </ul> + * + * @param colValue The column value to transform in boolean. + * + * @return Its corresponding boolean value. + */ + protected final boolean toBoolean(final Object colValue){ + // NULL => false: + if (colValue == null) + return false; + + // Boolean value => cast in boolean and return this value: + else if (colValue instanceof Boolean) + return ((Boolean)colValue).booleanValue(); + + // Integer value => cast in integer and return true only if the value is positive and not null: + else if (colValue instanceof Integer){ + int intFlag = ((Integer)colValue).intValue(); + return (intFlag > 0); + } + // Otherwise => get the string representation and: + // 1/ try to cast it into an integer and apply the same test as before + // 2/ if the cast fails, return true only if the value is "t" or "true" (case insensitively): + else{ + String strFlag = colValue.toString().trim(); + try{ + int intFlag = Integer.parseInt(strFlag); + return (intFlag > 0); + }catch(NumberFormatException nfe){ + return strFlag.equalsIgnoreCase("t") || strFlag.equalsIgnoreCase("true"); + } + } + } + + /** + * Return NULL if the given column value is an empty string (or it just contains space characters) or NULL. + * Otherwise the given given is returned as provided. + * + * @param dbValue Value to nullify if needed. + * + * @return NULL if the given string is NULL or empty, otherwise the given value. + */ + protected final String nullifyIfNeeded(final String dbValue){ + return (dbValue != null && dbValue.trim().length() <= 0) ? null : dbValue; + } + + /** + * Search a {@link TAPTable} instance whose the ADQL name matches (case sensitively) to the given one. + * + * @param tableName ADQL name of the table to search. + * @param itTables Iterator over the set of tables in which the research must be done. + * + * @return The found table, or NULL if not found. + */ + private TAPTable searchTable(String tableName, final Iterator<TAPTable> itTables){ + // Get the schema name, if any prefix the given table name: + String schemaName = null; + int indSep = tableName.indexOf('.'); + if (indSep > 0){ + schemaName = tableName.substring(0, indSep); + tableName = tableName.substring(indSep + 1); + } + + // Search by schema name (if any) and then by table name: + while(itTables.hasNext()){ + // get the table: + TAPTable table = itTables.next(); + // test the schema name (if one was prefixing the table name) (case sensitively): + if (schemaName != null){ + if (table.getADQLSchemaName() == null || !schemaName.equals(table.getADQLSchemaName())) + continue; + } + // test the table name (case sensitively): + if (tableName.equals(table.getADQLName())) + return table; + } + + // NULL if no table matches: + return null; + } + + /** + * <p>Tell whether the specified schema exists in the database. + * To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing schemas.</p> + * + * <p><i>Note: + * This function is completely useless if the connection is not supporting schemas. + * </i></p> + * + * <p><i>Note: + * Test on the schema name is done considering the case sensitivity indicated by the translator + * (see {@link ADQLTranslator#isCaseSensitive(IdentifierField)}). + * </i></p> + * + * <p><i>Note: + * This functions is used by {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #resetTAPSchema(Statement, TAPTable[])}. + * </i></p> + * + * @param schemaName DB name of the schema whose the existence must be checked. + * @param dbMeta Metadata about the database, and mainly the list of all existing schemas. + * + * @return <i>true</i> if the specified schema exists, <i>false</i> otherwise. + * + * @throws SQLException If any error occurs while interrogating the database about existing schema. + */ + protected boolean isSchemaExisting(String schemaName, final DatabaseMetaData dbMeta) throws SQLException{ + if (schemaName == null || schemaName.length() == 0) + return true; + + // Determine the case sensitivity to use for the equality test: + boolean caseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + + ResultSet rs = null; + try{ + // List all schemas available and stop when a schema name matches ignoring the case: + rs = dbMeta.getSchemas(); + boolean hasSchema = false; + while(!hasSchema && rs.next()) + hasSchema = equals(rs.getString(1), schemaName, caseSensitive); + return hasSchema; + }finally{ + close(rs); + } + } + + /** + * <p>Tell whether the specified table exists in the database. + * To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing tables.</p> + * + * <p><i><b>Important note:</b> + * If schemas are not supported by this connection but a schema name is even though provided in parameter, + * the table name will be prefixed by the schema name using {@link #getTablePrefix(String)}. + * The research will then be done with NULL as schema name and this prefixed table name. + * </i></p> + * + * <p><i>Note: + * Test on the schema name is done considering the case sensitivity indicated by the translator + * (see {@link ADQLTranslator#isCaseSensitive(IdentifierField)}). + * </i></p> + * + * <p><i>Note: + * This function is used by {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #dropUploadedTable(TAPTable)}. + * </i></p> + * + * @param schemaName DB name of the schema in which the table to search is. <i>If NULL, the table is expected in any schema but ONLY one MUST exist.</i> + * @param tableName DB name of the table to search. + * @param dbMeta Metadata about the database, and mainly the list of all existing tables. + * + * @return <i>true</i> if the specified table exists, <i>false</i> otherwise. + * + * @throws SQLException If any error occurs while interrogating the database about existing tables. + */ + protected boolean isTableExisting(String schemaName, String tableName, final DatabaseMetaData dbMeta) throws DBException, SQLException{ + if (tableName == null || tableName.length() == 0) + return true; + + // Determine the case sensitivity to use for the equality test: + boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA); + boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE); + + ResultSet rs = null; + try{ + // Prefix the table name by the schema name if needed (if schemas are not supported by this connection): + if (!supportsSchema) + tableName = getTablePrefix(schemaName) + tableName; + + // List all matching tables: + if (supportsSchema){ + String schemaPattern = schemaCaseSensitive ? schemaName : null; + String tablePattern = tableCaseSensitive ? tableName : null; + rs = dbMeta.getTables(null, schemaPattern, tablePattern, null); + }else{ + String tablePattern = tableCaseSensitive ? tableName : null; + rs = dbMeta.getTables(null, null, tablePattern, null); + } + + // Stop on the first table which match completely (schema name + table name in function of their respective case sensitivity): + int cnt = 0; + while(rs.next()){ + String rsSchema = nullifyIfNeeded(rs.getString(2)); + String rsTable = rs.getString(3); + if (!supportsSchema || schemaName == null || equals(rsSchema, schemaName, schemaCaseSensitive)){ + if (equals(rsTable, tableName, tableCaseSensitive)) + cnt++; + } + } + + if (cnt > 1){ + log(2, "More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!", null); + throw new DBException("More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!"); + } + + return cnt == 1; + + }finally{ + close(rs); + } + } + + /** + * <p>Build a table prefix with the given schema name.</p> + * + * <p>By default, this function returns: schemaName + "_".</p> + * + * <p><b>CAUTION: + * This function is used only when schemas are not supported by the DBMS connection. + * It aims to propose an alternative of the schema notion by prefixing the table name by the schema name. + * </b></p> + * + * <p><i>Note: + * If the given schema is NULL or is an empty string, an empty string will be returned. + * Thus, no prefix will be set....which is very useful when the table name has already been prefixed + * (in such case, the DB name of its schema has theoretically set to NULL). + * </i></p> + * + * @param schemaName (DB) Schema name. + * + * @return The corresponding table prefix, or "" if the given schema name is an empty string or NULL. + */ + protected String getTablePrefix(final String schemaName){ + if (schemaName != null && schemaName.trim().length() > 0) + return schemaName + "_"; + else + return ""; + } + + /** + * Tell whether the specified table (using its DB name only) is a standard one or not. + * + * @param dbTableName DB (unqualified) table name. + * @param stdTables List of all tables to consider as the standard ones. + * @param caseSensitive Indicate whether the equality test must be done case sensitively or not. + * + * @return The corresponding {@link STDTable} if the specified table is a standard one, + * NULL otherwise. + * + * @see TAPMetadata#resolveStdTable(String) + */ + protected final STDTable isStdTable(final String dbTableName, final TAPTable[] stdTables, final boolean caseSensitive){ + if (dbTableName != null){ + for(TAPTable t : stdTables){ + if (equals(dbTableName, t.getDBName(), caseSensitive)) + return TAPMetadata.resolveStdTable(t.getADQLName()); + } + } + return null; + } + + /** + * <p>"Execute" the query update. <i>This update must concern ONLY ONE ROW.</i></p> + * + * <p> + * Note that the "execute" action will be different in function of whether batch update queries are supported or not by this connection: + * </p> + * <ul> + * <li> + * If <b>batch update queries are supported</b>, just {@link PreparedStatement#addBatch()} will be called. + * It means, the query will be appended in a list and will be executed only if + * {@link #executeBatchUpdates(PreparedStatement, int)} is then called. + * </li> + * <li> + * If <b>they are NOT supported</b>, {@link PreparedStatement#executeUpdate()} will merely be called. + * </li> + * </ul> + * + * <p> + * Before returning, and only if batch update queries are not supported, this function is ensuring that exactly one row has been updated. + * If it is not the case, a {@link DBException} is thrown. + * </p> + * + * <p><i><b>Important note:</b> + * If the function {@link PreparedStatement#addBatch()} fails by throwing an {@link SQLException}, batch updates + * will be afterwards considered as not supported by this connection. Besides, if this row is the first one in a batch update (parameter indRow=1), + * then, the error will just be logged and an {@link PreparedStatement#executeUpdate()} will be tried. However, if the row is not the first one, + * the error will be logged but also thrown as a {@link DBException}. In both cases, a subsequent call to + * {@link #executeBatchUpdates(PreparedStatement, int)} will have obviously no effect. + * </i></p> + * + * @param stmt {@link PreparedStatement} in which the update query has been prepared. + * @param indRow Index of the row in the whole update process. It is used only for error management purpose. + * + * @throws SQLException If {@link PreparedStatement#executeUpdate()} fails.</i> + * @throws DBException If {@link PreparedStatement#addBatch()} fails and this update does not concern the first row, or if the number of updated rows is different from 1. + */ + protected final void executeUpdate(final PreparedStatement stmt, int indRow) throws SQLException, DBException{ + // BATCH INSERTION: (the query is queued and will be executed later) + if (supportsBatchUpdates){ + // Add the prepared query in the batch queue of the statement: + try{ + stmt.addBatch(); + }catch(SQLException se){ + supportsBatchUpdates = false; + /* + * If the error happens for the first row, it is still possible to insert all rows + * with the non-batch function - executeUpdate(). + * + * Otherwise, it is impossible to insert the previous batched rows ; an exception must be thrown + * and must stop the whole TAP_SCHEMA initialization. + */ + if (indRow == 1) + log(1, "BATCH query impossible => TRYING AGAIN IN A NORMAL EXECUTION (executeUpdate())!", se); + else{ + log(2, "BATCH query impossible!", se); + throw new DBException("BATCH query impossible!", se); + } + } + } + + // NORMAL INSERTION: (immediate insertion) + if (!supportsBatchUpdates){ + + // Insert the row prepared in the given statement: + int nbRowsWritten = stmt.executeUpdate(); + + // Check the row has been inserted with success: + if (nbRowsWritten != 1){ + log(2, "ROW " + indRow + " not inserted!", null); + throw new DBException("ROW " + indRow + " not inserted!"); + } + } + } + + /** + * <p>Execute all batched queries.</p> + * + * <p>To do so, {@link PreparedStatement#executeBatch()} and then, if the first was successful, {@link PreparedStatement#clearBatch()} is called.</p> + * + * <p> + * Before returning, this function is ensuring that exactly the given number of rows has been updated. + * If it is not the case, a {@link DBException} is thrown. + * </p> + * + * <p><i>Note: + * This function has no effect if batch queries are not supported. + * </i></p> + * + * <p><i><b>Important note:</b> + * In case {@link PreparedStatement#executeBatch()} fails by throwing an {@link SQLException}, + * batch update queries will be afterwards considered as not supported by this connection. + * </i></p> + * + * @param stmt {@link PreparedStatement} in which the update query has been prepared. + * @param nbRows Number of rows that should be updated. + * + * @throws DBException If {@link PreparedStatement#executeBatch()} fails, or if the number of updated rows is different from the given one. + */ + protected final void executeBatchUpdates(final PreparedStatement stmt, int nbRows) throws DBException{ + if (supportsBatchUpdates){ + // Execute all the batch queries: + int[] rows; + try{ + rows = stmt.executeBatch(); + }catch(SQLException se){ + supportsBatchUpdates = false; + log(2, "BATCH execution impossible!", se); + throw new DBException("BATCH execution impossible!", se); + } + + // Remove executed queries from the statement: + try{ + stmt.clearBatch(); + }catch(SQLException se){ + log(1, "CLEAR BATCH impossible!", se); + } + + // Count the updated rows: + int nbRowsUpdated = 0; + for(int i = 0; i < rows.length; i++) + nbRowsUpdated += rows[i]; + + // Check all given rows have been inserted with success: + if (nbRowsUpdated != nbRows){ + log(2, "ROWS not all update (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!", null); + throw new DBException("ROWS not all updated (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!"); + } + } + } + + /** + * Append all items of the iterator inside the given list. + * + * @param lst List to update. + * @param it All items to append inside the list. + */ + private < T > void appendAllInto(final List<T> lst, final Iterator<T> it){ + while(it.hasNext()) + lst.add(it.next()); + } + + /** + * <p>Tell whether the given DB name is equals (case sensitively or not, in function of the given parameter) + * to the given name coming from a {@link TAPMetadata} object.</p> + * + * <p>If at least one of the given name is NULL, <i>false</i> is returned.</p> + * + * <p><i>Note: + * The comparison will be done in function of the specified case sensitivity BUT ALSO of the case supported and stored by the DBMS. + * For instance, if it has been specified a case insensitivity and that mixed case is not supported by unquoted identifier, + * the comparison must be done, surprisingly, by considering the case if unquoted identifiers are stored in lower or upper case. + * Thus, this special way to evaluate equality should be as closed as possible to the identifier storage and research policies of the used DBMS. + * </i></p> + * + * @param dbName Name provided by the database. + * @param metaName Name provided by a {@link TAPMetadata} object. + * @param caseSensitive <i>true</i> if the equality test must be done case sensitively, <i>false</i> otherwise. + * + * @return <i>true</i> if both names are equal, <i>false</i> otherwise. + */ + protected final boolean equals(final String dbName, final String metaName, final boolean caseSensitive){ + if (dbName == null || metaName == null) + return false; + + if (caseSensitive){ + if (supportsMixedCaseQuotedIdentifier || mixedCaseQuoted) + return dbName.equals(metaName); + else if (lowerCaseQuoted) + return dbName.equals(metaName.toLowerCase()); + else if (upperCaseQuoted) + return dbName.equals(metaName.toUpperCase()); + else + return dbName.equalsIgnoreCase(metaName); + }else{ + if (supportsMixedCaseUnquotedIdentifier) + return dbName.equalsIgnoreCase(metaName); + else if (lowerCaseUnquoted) + return dbName.equals(metaName.toLowerCase()); + else if (upperCaseUnquoted) + return dbName.equals(metaName.toUpperCase()); + else + return dbName.equalsIgnoreCase(metaName); } } } diff --git a/src/tap/db/JDBCTAPFactory.java b/src/tap/db/JDBCTAPFactory.java deleted file mode 100644 index 8ef12bf..0000000 --- a/src/tap/db/JDBCTAPFactory.java +++ /dev/null @@ -1,228 +0,0 @@ -package tap.db; - -import java.util.HashMap; -import java.util.Map; - -import tap.metadata.TAPType; -import tap.metadata.TAPType.TAPDatatype; - -public class JDBCTAPFactory { - - public static enum DBMS{ - POSTGRES; - } - - public static interface DbmsTypeConverter< T, C > { - public C convert(final T typeToConvert); - } - - public static Map<String,TAPDatatype> mapTypeAliases; - public static Map<String,DbmsTypeConverter<String,TAPType>> mapDbmsToTap; - public static Map<DBMS,Map<TAPDatatype,DbmsTypeConverter<TAPType,String>>> mapTapToDbms; - - static{ - /* DECLARE DBMS TYPE ALIASES */ - mapTypeAliases = new HashMap<String,TAPType.TAPDatatype>(); - mapTypeAliases.put("int8", TAPDatatype.BIGINT); - mapTypeAliases.put("bigserial", TAPDatatype.BIGINT); - mapTypeAliases.put("bit", TAPDatatype.VARCHAR); - mapTypeAliases.put("bit varying", TAPDatatype.VARCHAR); - mapTypeAliases.put("varbit", TAPDatatype.VARCHAR); - mapTypeAliases.put("boolean", TAPDatatype.SMALLINT); - mapTypeAliases.put("bytea", TAPDatatype.VARBINARY); - mapTypeAliases.put("character varying", TAPDatatype.VARCHAR); - mapTypeAliases.put("character", TAPDatatype.CHAR); - mapTypeAliases.put("double precision", TAPDatatype.DOUBLE); - mapTypeAliases.put("float8", TAPDatatype.DOUBLE); - mapTypeAliases.put("integer", TAPDatatype.INTEGER); - mapTypeAliases.put("int4", TAPDatatype.INTEGER); - mapTypeAliases.put("float4", TAPDatatype.REAL); - mapTypeAliases.put("int2", TAPDatatype.SMALLINT); - mapTypeAliases.put("serial", TAPDatatype.INTEGER); - mapTypeAliases.put("serial4", TAPDatatype.INTEGER); - mapTypeAliases.put("text", TAPDatatype.VARCHAR); - - /* DECLARE SPECIAL DBMS->TAP CONVERSIONS */ - mapDbmsToTap = new HashMap<String,JDBCTAPFactory.DbmsTypeConverter<String,TAPType>>(); - mapDbmsToTap.put("numeric", new DbmsTypeConverter<String,TAPType>(){ - @Override - public TAPType convert(String typeToConvert){ - return new TAPType(TAPDatatype.DOUBLE); - } - }); - mapDbmsToTap.put("decimal", new DbmsTypeConverter<String,TAPType>(){ - @Override - public TAPType convert(String typeToConvert){ - return new TAPType(TAPDatatype.DOUBLE); - } - }); - - /* DECLARE SPECIAL TAP->DBMS CONVERSIONS */ - mapTapToDbms = new HashMap<DBMS,Map<TAPDatatype,DbmsTypeConverter<TAPType,String>>>(); - // POSTGRES - HashMap<TAPDatatype,DbmsTypeConverter<TAPType,String>> postgresConverters = new HashMap<TAPDatatype,JDBCTAPFactory.DbmsTypeConverter<TAPType,String>>(); - postgresConverters.put(TAPDatatype.DOUBLE, new DbmsTypeConverter<TAPType,String>(){ - @Override - public String convert(TAPType typeToConvert){ - return "double precision"; - } - }); - DbmsTypeConverter<TAPType,String> binaryConverter = new DbmsTypeConverter<TAPType,String>(){ - @Override - public String convert(TAPType typeToConvert){ - return "bytea"; - } - }; - postgresConverters.put(TAPDatatype.VARBINARY, binaryConverter); - postgresConverters.put(TAPDatatype.BINARY, binaryConverter); - postgresConverters.put(TAPDatatype.BLOB, binaryConverter); - postgresConverters.put(TAPDatatype.CLOB, binaryConverter); - mapTapToDbms.put(DBMS.POSTGRES, postgresConverters); - } - - public JDBCTAPFactory(){ - // TODO Auto-generated constructor stub - } - - /** - * <p>Convert the given TAP column type into a column type compatible with the specified DBMS.</p> - * - * <p><i>Note 1: if no {@link TAPType} is provided, the returned DBMS type will correspond to a - * VARCHAR.</i></p> - * - * <p><i>Note 2: if no DBMS is specified or if the conversion has failed, the given TAP type will be - * just "stringified" (by calling {@link TAPType#toString()})</i></p> - * - * @param tapType A TAP column type. - * @param dbms DBMS target in which the given TAP column type must be converted. - * - * @return The corresponding DBMS column type. - */ - public static String toDbmsType(TAPType tapType, final DBMS dbms){ - // If no TAP type is specified, consider it by default as a VARCHAR type: - if (tapType == null) - tapType = new TAPType(TAPDatatype.VARCHAR); - - // By default, just "stringify" the given TAP type: - String dbmsType = tapType.toString(); - - // If some converters are defined for the specified DBMS... - if (dbms != null && mapTapToDbms.containsKey(dbms)){ - Map<TAPDatatype,DbmsTypeConverter<TAPType,String>> dbmsMap = mapTapToDbms.get(dbms); - // ...and if a converter exists for the given TAP datatype... - DbmsTypeConverter<TAPType,String> converter = dbmsMap.get(tapType.type); - if (converter != null){ - // ...convert the given TAP type: - String conversion = converter.convert(tapType); - // ...and set the DBMS conversion if NOT NULL: - if (conversion != null) - dbmsType = conversion; - } - } - - return dbmsType; - } - - /** - * <p>Convert the given DBMS column type into a compatible TAP datatype.</p> - * - * <p><i>Note: If no DBMS type is specified or if the DBMS type can not be identified, - * it will be converted as a VARCHAR.</i></p> - * - * @param dbmsType DBMS column datatype. - * - * @return The corresponding TAP column datatype. - */ - public static TAPType toTAPType(final String dbmsType){ - // If no type is provided return VARCHAR: - if (dbmsType == null || dbmsType.trim().length() == 0) - return new TAPType(TAPDatatype.VARCHAR); - - // Extract the type prefix and lower-case it: - int paramIndex = dbmsType.indexOf('('); - String dbmsTypePrefix = (paramIndex <= 0) ? dbmsType : dbmsType.substring(0, paramIndex); - dbmsTypePrefix = dbmsTypePrefix.toLowerCase(); - - // Use this type prefix as key to determine if it's a DBMS type alias and get its corresponding TAP datatype: - TAPDatatype datatype = mapTypeAliases.get(dbmsTypePrefix); - - // If it's an alias, build the corresponding TAP type: - if (datatype != null) - return new TAPType(datatype, getLengthParam(dbmsType, paramIndex)); - - // If it's not an alias, use the type prefix as key to get a corresponding converter: - DbmsTypeConverter<String,TAPType> converter = mapDbmsToTap.get(dbmsTypePrefix); - - // Try the type conversion using this converter: - TAPType taptype = null; - if (converter != null) - taptype = converter.convert(dbmsType); - - /* - * If no converter was found OR if the type conversion has failed, - * consider the given type as equivalent to a declared TAP type. - * - * /!\ But if no equivalent exists, the given type will be ignore and - * VARCHAR will be returned! - */ - if (taptype == null){ - try{ - - // Try to find an equivalent TAPType: - datatype = TAPDatatype.valueOf(dbmsTypePrefix.toUpperCase()); - - // If there is one return directly the TAPType: - taptype = new TAPType(datatype, getLengthParam(dbmsType, paramIndex)); - - }catch(IllegalArgumentException iae){ - // If none exists, return VARCHAR: - taptype = new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH); - } - } - - return taptype; - } - - /** - * <p>Extract the 'length' parameter of a DBMS type string.</p> - * - * <p> - * If the given type string does not contain any parameter - * OR if the first parameter can not be casted into an integer, - * {@link TAPType#NO_LENGTH} will be returned. - * </p> - * - * @param dbmsType DBMS type string (containing the datatype and the 'length' parameter). - * @param paramIndex Index of the open bracket. - * - * @return The 'length' parameter value if found, {@link TAPType#NO_LENGTH} otherwise. - */ - private static int getLengthParam(final String dbmsType, final int paramIndex){ - // If no parameter has been previously detected, no length parameter: - if (paramIndex <= 0) - return TAPType.NO_LENGTH; - - // If there is one and that at least ONE parameter is provided.... - else{ - int lengthParam = TAPType.NO_LENGTH; - String paramsStr = dbmsType.substring(paramIndex + 1); - - // ...extract the 'length' parameter: - /* note: we suppose here that no other parameter is possible ; - * but if there are, they are ignored and we try to consider the first parameter - * as the length */ - int paramEndIndex = paramsStr.indexOf(','); - if (paramEndIndex <= 0) - paramEndIndex = paramsStr.indexOf(')'); - - // ...cast it into an integer: - try{ - lengthParam = Integer.parseInt(paramsStr.substring(0, paramEndIndex)); - }catch(Exception ex){} - - // ...and finally return it: - return lengthParam; - } - } - -} diff --git a/src/tap/log/TAPLog.java b/src/tap/log/TAPLog.java index 6d15c8f..33e6a62 100644 --- a/src/tap/log/TAPLog.java +++ b/src/tap/log/TAPLog.java @@ -43,6 +43,8 @@ public interface TAPLog extends UWSLog { public void tapMetadataLoaded(final TAPMetadata metadata); + public void connectionOpened(final DBConnection connection); + public void connectionClosed(final DBConnection connection); public void sqlQueryExecuting(final DBConnection connection, final String sql); diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java index 9b91e85..4c2bbb5 100644 --- a/src/tap/metadata/TAPColumn.java +++ b/src/tap/metadata/TAPColumn.java @@ -16,46 +16,136 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.awt.List; import java.util.ArrayList; import java.util.Iterator; +import java.util.Map; import tap.metadata.TAPType.TAPDatatype; import adql.db.DBColumn; import adql.db.DBTable; +/** + * <p>Represent a column as described by the IVOA standard in the TAP protocol definition.</p> + * + * <p> + * This object representation has exactly the same fields as the column of the table TAP_SCHEMA.columns. + * But it also provides a way to add other data. For instance, if information not listed in the standard + * may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be + * a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc... + * </p> + * + * <p><i><b>Important note:</b> + * A {@link TAPColumn} object MUST always have a DB name. That's why by default, at the creation + * the DB name is the ADQL name. Once created, it is possible to set the DB name with {@link #setDBName(String)}. + * This DB name MUST be UNqualified and without double quotes. If a NULL or empty value is provided, + * nothing is done and the object keeps its former DB name. + * </i></p> + * + * <h3>Set a table</h3> + * + * <p> + * By default a column is detached (not part of a table). To specify the table in which this column is, + * you must use {@link TAPTable#addColumn(TAPColumn)}. By doing this, the table link inside this column + * will be set automatically and you will be able to get the table with {@link #getTable()}. + * </p> + * + * <h3>Foreign keys</h3> + * + * <p> + * In case this column is linked to one or several of other tables, it will be possible to list all + * foreign keys where the target columns is with {@link #getTargets()}. In the same way, it will be + * possible to list all foreign keys in which this column is a target with {@link #getSources()}. + * However, in order to ensure the consistency between all metadata, these foreign key's links are + * set at the table level by the table itself using {@link #addSource(TAPForeignKey)} and + * {@link #addTarget(TAPForeignKey)}. + * </p> + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ public class TAPColumn implements DBColumn { + /** Name that this column MUST have in ADQL queries. */ private final String adqlName; + /** Name that this column have in the database. + * <i>Note: It CAN NOT be NULL. By default, it is the ADQL name.</i> */ private String dbName = null; + /** Table which owns this column. + * <i>Note: It should be NULL only at the construction or for a quick representation of a column. + * Then, this attribute is automatically set by a {@link TAPTable} when adding this column inside it + * with {@link TAPTable#addColumn(TAPColumn)}.</i> */ private DBTable table = null; + /** Description of this column. + * <i>Note: Standard TAP column field ; MAY be NULL.</i> */ private String description = null; + /** Unit of this column's values. + * <i>Note: Standard TAP column field ; MAY be NULL.</i> */ private String unit = null; + /** UCD describing the scientific content of this column. + * <i>Note: Standard TAP column field ; MAY be NULL.</i> */ private String ucd = null; + /** UType associating this column with a data-model. + * <i>Note: Standard TAP column field ; MAY be NULL.</i> */ private String utype = null; + /** Type of this column. + * <i>Note: Standard TAP column field ; CAN'T be NULL.</i> */ private TAPType datatype = new TAPType(TAPDatatype.VARCHAR); + /** Flag indicating whether this column is one of those that should be returned by default. + * <i>Note: Standard TAP column field ; FALSE by default.</i> */ private boolean principal = false; + /** Flag indicating whether this column is indexed in the database. + * <i>Note: Standard TAP column field ; FALSE by default.</i> */ private boolean indexed = false; + /** Flag indicating whether this column is defined by a standard. + * <i>Note: Standard TAP column field ; FALSE by default.</i> */ private boolean std = false; + /** Let add some information in addition of the ones of the TAP protocol. + * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ... + * Its content is totally free and never used or checked.</i> */ protected Object otherData = null; + /** List all foreign keys in which this column is a source. + * <p><b>CAUTION: For consistency consideration, this attribute SHOULD never be modified! + * It is set by the constructor and filled ONLY by the table.</b></p> */ protected final ArrayList<TAPForeignKey> lstTargets; + /** List all foreign keys in which this column is a target. + * <p><b>CAUTION: For consistency consideration, this attribute SHOULD never be modified! + * It is set by the constructor and filled ONLY by the table.</b></p> */ protected final ArrayList<TAPForeignKey> lstSources; + /** + * <p>Build a {@link TAPColumn} instance with the given ADQL name.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * The datatype is set by default to VARCHAR. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * @param columnName Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + */ public TAPColumn(String columnName){ if (columnName == null || columnName.trim().length() == 0) throw new NullPointerException("Missing column name !"); @@ -66,21 +156,114 @@ public class TAPColumn implements DBColumn { lstSources = new ArrayList<TAPForeignKey>(1); } + /** + * <p>Build a {@link TAPColumn} instance with the given ADQL name and datatype.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * <p><i>Note: + * The datatype is set by calling the function {@link #setDatatype(TAPType)} which does not do + * anything if the given datatype is NULL. + * </i></p> + * + * @param columnName Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param type Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i> + * + * @see #setDatatype(TAPType) + */ public TAPColumn(String columnName, TAPType type){ this(columnName); setDatatype(type); } + /** + * <p>Build a {@link TAPColumn} instance with the given ADQL name, datatype and description.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * <p><i>Note: + * The datatype is set by calling the function {@link #setDatatype(TAPType)} which does do + * anything if the given datatype is NULL. + * </i></p> + * + * @param columnName Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param type Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i> + * @param description Description of the column's content. <i>May be NULL</i> + */ public TAPColumn(String columnName, TAPType type, String description){ this(columnName, type); this.description = description; } + /** + * <p>Build a {@link TAPColumn} instance with the given field.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * <p><i>Note: + * The datatype is set by calling the function {@link #setDatatype(TAPType)} which does do + * anything if the given datatype is NULL. + * </i></p> + * + * @param columnName Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param type Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i> + * @param description Description of the column's content. <i>May be NULL</i> + * @param unit Unit of the column's values. <i>May be NULL</i> + */ public TAPColumn(String columnName, TAPType type, String description, String unit){ this(columnName, type, description); this.unit = unit; } + /** + * <p>Build a {@link TAPColumn} instance with the given field.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * <p><i>Note: + * The datatype is set by calling the function {@link #setDatatype(TAPType)} which does do + * anything if the given datatype is NULL. + * </i></p> + * + * @param columnName Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param type Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i> + * @param description Description of the column's content. <i>May be NULL</i> + * @param unit Unit of the column's values. <i>May be NULL</i> + * @param ucd UCD describing the scientific content of this column. + * @param utype UType associating this column with a data-model. + */ public TAPColumn(String columnName, TAPType type, String description, String unit, String ucd, String utype){ this(columnName, type, description, unit); this.ucd = ucd; @@ -88,8 +271,13 @@ public class TAPColumn implements DBColumn { } /** - * @return The name. + * Get the ADQL name (the name this column MUST have in ADQL queries). + * + * @return Its ADQL name. + * @see #getADQLName() + * @deprecated Does not do anything special: just call {@link #getADQLName()}. */ + @Deprecated public final String getName(){ return getADQLName(); } @@ -104,188 +292,372 @@ public class TAPColumn implements DBColumn { return dbName; } + /** + * <p>Change the name that this column MUST have in the database (i.e. in SQL queries).</p> + * + * <p><i>Note: + * If the given value is NULL or an empty string, nothing is done ; the DB name keeps is former value. + * </i></p> + * + * @param name The new database name of this column. + */ public final void setDBName(String name){ name = (name != null) ? name.trim() : name; - dbName = (name == null || name.length() == 0) ? adqlName : name; + if (name != null && name.length() > 0) + dbName = name; } - /** - * @return The table. - */ @Override public final DBTable getTable(){ return table; } /** - * @param table The table to set. + * <p>Set the table in which this column is.</p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column. + * </i></p> + * + * <p><i><b>Important note:</b> + * If this column was already linked with another {@link TAPTable} object, the previous link is removed + * here, but also in the table (by calling {@link TAPTable#removeColumn(String)}). + * </i></p> + * + * @param table The table that owns this column. */ - public final void setTable(DBTable table){ + protected final void setTable(final DBTable table){ + if (this.table != null && this.table instanceof TAPTable && (table == null || !table.equals(this.table))) + ((TAPTable)this.table).removeColumn(adqlName); this.table = table; } /** - * @return The description. + * Get the description of this column. + * + * @return Its description. <i>MAY be NULL</i> */ public final String getDescription(){ return description; } /** - * @param description The description to set. + * Set the description of this column. + * + * @param description Its new description. <i>MAY be NULL</i> */ public final void setDescription(String description){ this.description = description; } /** - * @return The unit. + * Get the unit of the column's values. + * + * @return Its unit. <i>MAY be NULL</i> */ public final String getUnit(){ return unit; } /** - * @param unit The unit to set. + * Set the unit of the column's values. + * + * @param unit Its new unit. <i>MAY be NULL</i> */ public final void setUnit(String unit){ this.unit = unit; } /** - * @return The ucd. + * Get the UCD describing the scientific content of this column. + * + * @return Its UCD. <i>MAY be NULL</i> */ public final String getUcd(){ return ucd; } /** - * @param ucd The ucd to set. + * Set the UCD describing the scientific content of this column. + * + * @param ucd Its new UCD. <i>MAY be NULL</i> */ public final void setUcd(String ucd){ this.ucd = ucd; } /** - * @return The utype. + * Get the UType associating this column with a data-model. + * + * @return Its UType. <i>MAY be NULL</i> */ public final String getUtype(){ return utype; } /** - * @param utype The utype to set. + * Set the UType associating this column with a data-model. + * + * @param utype Its new UType. <i>MAY be NULL</i> */ public final void setUtype(String utype){ this.utype = utype; } /** - * @return The datatype. + * Get the type of the column's values. + * + * @return Its datatype. <i>CAN'T be NULL</i> */ public final TAPType getDatatype(){ return datatype; } /** - * @param type The new column datatype. + * <p>Set the type of the column's values.</p> + * + * <p><i>Note: + * The datatype won't be changed, if the given type is NULL. + * </i></p> + * + * @param type Its new datatype. */ public final void setDatatype(final TAPType type){ - datatype = type; + if (type != null) + datatype = type; } /** - * @return The principal. + * Tell whether this column is one of those returned by default. + * + * @return <i>true</i> if this column should be returned by default, <i>false</i> otherwise. */ public final boolean isPrincipal(){ return principal; } /** - * @param principal The principal to set. + * Set whether this column should be one of those returned by default. + * + * @param principal <i>true</i> if this column should be returned by default, <i>false</i> otherwise. */ public final void setPrincipal(boolean principal){ this.principal = principal; } /** - * @return The indexed. + * Tell whether this column is indexed. + * + * @return <i>true</i> if this column is indexed, <i>false</i> otherwise. */ public final boolean isIndexed(){ return indexed; } /** - * @param indexed The indexed to set. + * Set whether this column is indexed or not. + * + * @param indexed <i>true</i> if this column is indexed, <i>false</i> otherwise. */ public final void setIndexed(boolean indexed){ this.indexed = indexed; } /** - * @return The std. + * Tell whether this column is defined by a standard. + * + * @return <i>true</i> if this column is defined by a standard, <i>false</i> otherwise. */ public final boolean isStd(){ return std; } /** - * @param std The std to set. + * Set whether this column is defined by a standard. + * + * @param std <i>true</i> if this column is defined by a standard, <i>false</i> otherwise. */ public final void setStd(boolean std){ this.std = std; } + /** + * <p>Get the other (piece of) information associated with this column.</p> + * + * <p><i>Note: + * By default, NULL is returned, but it may be any kind of value ({@link Integer}, + * {@link String}, {@link Map}, {@link List}, ...). + * </i></p> + * + * @return The other (piece of) information. <i>MAY be NULL</i> + */ public Object getOtherData(){ return otherData; } + /** + * Set the other (piece of) information associated with this column. + * + * @param data Another information about this column. <i>MAY be NULL</i> + */ public void setOtherData(Object data){ otherData = data; } + /** + * <p>Let add a foreign key in which this column is a source (= which is targeting another column).</p> + * + * <p><i>Note: + * Nothing is done if the given value is NULL. + * </i></p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + * </i></p> + * + * @param key A foreign key. + */ protected void addTarget(TAPForeignKey key){ if (key != null) lstTargets.add(key); } - protected int getNbTargets(){ + /** + * Get the number of times this column is targeting another column. + * + * @return How many this column is source in a foreign key. + */ + public int getNbTargets(){ return lstTargets.size(); } - protected Iterator<TAPForeignKey> getTargets(){ + /** + * Get the list of foreign keys in which this column is a source (= is targeting another column). + * + * @return List of foreign keys in which this column is a source. + */ + public Iterator<TAPForeignKey> getTargets(){ return lstTargets.iterator(); } + /** + * <p>Remove the fact that this column is a source (= is targeting another column) + * in the given foreign key.</p> + * + * <p><i>Note: + * Nothing is done if the given value is NULL. + * </i></p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + * </i></p> + * + * @param key Foreign key in which this column was targeting another column. + */ protected void removeTarget(TAPForeignKey key){ - lstTargets.remove(key); + if (key != null) + lstTargets.remove(key); } + /** + * <p>Remove the fact that this column is a source (= is targeting another column) + * in any foreign key in which it was.</p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + * </i></p> + * + * @param key Foreign key in which this column was targeting another column. + */ protected void removeAllTargets(){ lstTargets.clear(); } + /** + * <p>Let add a foreign key in which this column is a target (= which is targeted by another column).</p> + * + * <p><i>Note: + * Nothing is done if the given value is NULL. + * </i></p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + * </i></p> + * + * @param key A foreign key. + */ protected void addSource(TAPForeignKey key){ if (key != null) lstSources.add(key); } - protected int getNbSources(){ + /** + * Get the number of times this column is targeted by another column. + * + * @return How many this column is target in a foreign key. + */ + public int getNbSources(){ return lstSources.size(); } - protected Iterator<TAPForeignKey> getSources(){ + /** + * Get the list of foreign keys in which this column is a target (= is targeted another column). + * + * @return List of foreign keys in which this column is a target. + */ + public Iterator<TAPForeignKey> getSources(){ return lstSources.iterator(); } + /** + * <p>Remove the fact that this column is a target (= is targeted by another column) + * in the given foreign key.</p> + * + * <p><i>Note: + * Nothing is done if the given value is NULL. + * </i></p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + * </i></p> + * + * @param key Foreign key in which this column was targeted by another column. + */ protected void removeSource(TAPForeignKey key){ lstSources.remove(key); } + /** + * <p>Remove the fact that this column is a target (= is targeted by another column) + * in any foreign key in which it was.</p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPTable} + * that owns this column or that is part of the foreign key. + * </i></p> + * + * @param key Foreign key in which this column was targeted by another column. + */ protected void removeAllSources(){ lstSources.clear(); } + /** + * <p><i><b>Warning:</b> + * Since the type of the other data is not known, the copy of its value + * can not be done properly. So, this column and its copy will share the same other data object. + * If it is also needed to make a deep copy of this other data object, this function MUST be + * overridden. + * </i></b> + * + * @see adql.db.DBColumn#copy(java.lang.String, java.lang.String, adql.db.DBTable) + */ @Override public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable){ TAPColumn copy = new TAPColumn((adqlName == null) ? this.adqlName : adqlName, datatype, description, unit, ucd, utype); @@ -300,6 +672,18 @@ public class TAPColumn implements DBColumn { return copy; } + /** + * <p>Provide a deep copy (included the other data) of this column.</p> + * + * <p><i><b>Warning:</b> + * Since the type of the other data is not known, the copy of its value + * can not be done properly. So, this column and its copy will share the same other data object. + * If it is also needed to make a deep copy of this other data object, this function MUST be + * overridden. + * </i></b> + * + * @return The deep copy of this column. + */ public DBColumn copy(){ TAPColumn copy = new TAPColumn(adqlName, datatype, description, unit, ucd, utype); copy.setDBName(dbName); @@ -317,7 +701,7 @@ public class TAPColumn implements DBColumn { return false; TAPColumn col = (TAPColumn)obj; - return col.getTable().equals(table) && col.getName().equals(adqlName); + return col.getTable().equals(table) && col.getADQLName().equals(adqlName); } @Override diff --git a/src/tap/metadata/TAPDM.java b/src/tap/metadata/TAPDM.java deleted file mode 100644 index 5a0548f..0000000 --- a/src/tap/metadata/TAPDM.java +++ /dev/null @@ -1,52 +0,0 @@ -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 2014 - Astronomisches Rechen Institute (ARI) - */ - -/** - * Enumeration of all schemas and tables of the TAP datamodel (and particularly of TAP_SCHEMA). - * - * @author Grégory Mantelet (ARI) - * @version 2.0 (07/2014) - * @since 2.0 - */ -public enum TAPDM{ - TAPSCHEMA("TAP_SCHEMA"), SCHEMAS("schemas"), TABLES("tables"), COLUMNS("columns"), FOREIGN_KEYS("foreign_keys"), UPLOADSCHEMA("TAP_UPLOAD"); - - /** Real name of the schema/table. */ - private final String label; - - private TAPDM(final String name){ - this.label = name; - } - - /** - * Get the real name of the schema/table of the TAP datamodel. - * - * @return Real name of the schema/table. - */ - public String getLabel(){ - return label; - } - - @Override - public String toString(){ - return label; - } -} diff --git a/src/tap/metadata/TAPMetadata.java b/src/tap/metadata/TAPMetadata.java index fde3103..3f1c38f 100644 --- a/src/tap/metadata/TAPMetadata.java +++ b/src/tap/metadata/TAPMetadata.java @@ -16,7 +16,8 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -32,29 +33,106 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import tap.metadata.TAPTable.TableType; +import tap.metadata.TAPType.TAPDatatype; import tap.resource.Capabilities; import tap.resource.TAPResource; import tap.resource.VOSIResource; import adql.db.DBTable; +/** + * <p>Let listing all schemas, tables and columns available in a TAP service. + * This list also corresponds to the TAP resource "/tables".</p> + * + * <p> + * Only schemas are stored in this object. So that's why only schemas can be added and removed + * from this class. However, {@link TAPSchema} objects are listing tables, whose the object + * representation is listing columns. So to add tables, you must first embed them in a schema. + * </p> + * + * <p> + * All metadata have two names: one to use in ADQL queries and the other to use when really querying + * the database. This is very useful to hide the real complexity of the database and propose + * a simpler view of the query-able data. It is particularly useful if a schema does not exist in the + * database but has been added in the TAP schema for more logical separation on the user point of view. + * In a such case, the schema would have an ADQL name but no DB name (NULL value ; which is possible only + * with {@link TAPSchema} objects). + * </p> + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResource { + /** Resource name of the TAP metadata. This name is also used - in this class - in the TAP URL to identify this resource. + * Here it corresponds to the following URI: ".../tables". */ public static final String RESOURCE_NAME = "tables"; + /** List of all schemas available through the TAP service. */ protected final Map<String,TAPSchema> schemas; + + /** Part of the TAP URI which identify this TAP resource. + * By default, it is the resource name ; so here, the corresponding TAP URI would be: "/tables". */ protected String accessURL = getName(); + /** + * <p>Build an empty list of metadata.</p> + * + * <p><i>Note: + * By default, a TAP service must have at least a TAP_SCHEMA schema which contains a set of 5 tables + * (schemas, tables, columns, keys and key_columns). This schema is not created here by default + * because it can be customized by the service implementor. Besides, the DB name may be different. + * However, you can easily get this schema thanks to the function {@link #getStdSchema()} + * which returns the standard definition of this schema (including all tables and columns described + * by the standard). For a standard definition of this schema, you can then write the following: + * </i></p> + * <pre> + * TAPMetadata meta = new TAPMetadata(); + * meta.addSchema(TAPMetadata.getStdSchema()); + * </pre> + * <p><i> + * Of course, this schema (and its tables and their columns) can be customized after if needed. + * Otherwise, if you want customize just some part of this schema, you can also use the function + * {@link #getStdTable(STDTable)} to get just the standard definition of some of its tables, either + * to customize them or to merely get them and keep them like they are. + * </i></p> + */ public TAPMetadata(){ schemas = new HashMap<String,TAPSchema>(); } + /** + * <p>Add the given schema inside this TAP metadata set.</p> + * + * <p><i>Note: + * If the given schema is NULL, nothing will be done. + * </i></p> + * + * @param s The schema to add. + */ public final void addSchema(TAPSchema s){ - if (s != null && s.getName() != null) - schemas.put(s.getName(), s); + if (s != null && s.getADQLName() != null) + schemas.put(s.getADQLName(), s); } + /** + * <p>Build a new {@link TAPSchema} object with the given ADQL name. + * Then, add it inside this TAP metadata set.</p> + * + * <p><i>Note: + * The built {@link TAPSchema} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param schemaName ADQL name of the schema to create and add inside this TAP metadata set. + * + * @return The created and added schema, + * or NULL if the given schema is NULL or an empty string. + * + * @see TAPSchema#TAPSchema(String) + * @see #addSchema(TAPSchema) + */ public TAPSchema addSchema(String schemaName){ - if (schemaName == null) + if (schemaName == null || schemaName.trim().length() <= 0) return null; TAPSchema s = new TAPSchema(schemaName); @@ -62,6 +140,24 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return s; } + /** + * <p>Build a new {@link TAPSchema} object with the given ADQL name. + * Then, add it inside this TAP metadata set.</p> + * + * <p><i>Note: + * The built {@link TAPSchema} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param schemaName ADQL name of the schema to create and add inside this TAP metadata set. + * @param description Description of the new schema. <i>MAY be NULL</i> + * @param utype UType associating the new schema with a data-model. <i>MAY be NULL</i> + * + * @return The created and added schema, + * or NULL if the given schema is NULL or an empty string. + * + * @see TAPSchema#TAPSchema(String, String, String) + * @see #addSchema(TAPSchema) + */ public TAPSchema addSchema(String schemaName, String description, String utype){ if (schemaName == null) return null; @@ -71,6 +167,17 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return s; } + /** + * <p>Tell whether there is a schema with the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * @param schemaName ADQL name of the schema whose the existence must be checked. + * + * @return <i>true</i> if a schema with the given ADQL name exists, <i>false</i> otherwise. + */ public final boolean hasSchema(String schemaName){ if (schemaName == null) return false; @@ -78,6 +185,18 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return schemas.containsKey(schemaName); } + /** + * <p>Search for a schema having the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * @param schemaName ADQL name of the schema to search. + * + * @return The schema having the given ADQL name, + * or NULL if no such schema can be found. + */ public final TAPSchema getSchema(String schemaName){ if (schemaName == null) return null; @@ -85,14 +204,44 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return schemas.get(schemaName); } + /** + * Get the number of schemas contained in this TAP metadata set. + * + * @return Number of all schemas. + */ public final int getNbSchemas(){ return schemas.size(); } + /** + * Tell whether this TAP metadata set contains no schema. + * + * @return <i>true</i> if this TAP metadata set has no schema, + * <i>false</i> if it contains at least one schema. + */ public final boolean isEmpty(){ return schemas.isEmpty(); } + /** + * <p>Remove the schema having the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * <p><i><b>WARNING:</b> + * If the goal of this function's call is to delete definitely the specified schema + * from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()} on the + * removed table. Indeed, foreign keys of this table would still link the removed table + * with other tables AND columns of the whole metadata set. + * </i></p> + * + * @param schemaName ADQL name of the schema to remove from this TAP metadata set. + * + * @return The removed schema, + * or NULL if no such schema can be found. + */ public final TAPSchema removeSchema(String schemaName){ if (schemaName == null) return null; @@ -100,6 +249,9 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return schemas.remove(schemaName); } + /** + * Remove all schemas of this metadata set. + */ public final void removeAllSchemas(){ schemas.clear(); } @@ -109,10 +261,27 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return schemas.values().iterator(); } + /** + * Get the list of all tables available in this TAP metadata set. + * + * @return An iterator over the list of all tables contained in this TAP metadata set. + */ public Iterator<TAPTable> getTables(){ return new TAPTableIterator(this); } + /** + * <p>Tell whether this TAP metadata set contains the specified table.</p> + * + * <p><i>Note: + * This function is case sensitive! + * </i></p> + * + * @param schemaName ADQL name of the schema owning the table to search. + * @param tableName ADQL name of the table to search. + * + * @return <i>true</i> if the specified table exists, <i>false</i> otherwise. + */ public boolean hasTable(String schemaName, String tableName){ TAPSchema s = getSchema(schemaName); if (s != null) @@ -121,6 +290,17 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return false; } + /** + * <p>Tell whether this TAP metadata set contains a table with the given ADQL name, whatever is its schema.</p> + * + * <p><i>Note: + * This function is case sensitive! + * </i></p> + * + * @param tableName ADQL name of the table to search. + * + * @return <i>true</i> if the specified table exists, <i>false</i> otherwise. + */ public boolean hasTable(String tableName){ for(TAPSchema s : this) if (s.hasTable(tableName)) @@ -128,7 +308,19 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return false; } - // @Override + /** + * <p>Search for the specified table in this TAP metadata set.</p> + * + * <p><i>Note: + * This function is case sensitive! + * </i></p> + * + * @param schemaName ADQL name of the schema owning the table to search. + * @param tableName ADQL name of the table to search. + * + * @return The table which has the given ADQL name and which is inside the specified schema, + * or NULL if no such table can be found. + */ public TAPTable getTable(String schemaName, String tableName){ TAPSchema s = getSchema(schemaName); if (s != null) @@ -137,7 +329,19 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return null; } - // @Override + /** + * <p>Search in this TAP metadata set for all tables whose the ADQL name matches the given one, + * whatever is their schema.</p> + * + * <p><i>Note: + * This function is case sensitive! + * </i></p> + * + * @param tableName ADQL name of the tables to search. + * + * @return A list of all the tables which have the given ADQL name, + * or an empty list if no such table can be found. + */ public ArrayList<DBTable> getTable(String tableName){ ArrayList<DBTable> tables = new ArrayList<DBTable>(); for(TAPSchema s : this) @@ -146,6 +350,11 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return tables; } + /** + * Get the number of all tables contained in this TAP metadata set. + * + * @return Number of all its tables. + */ public int getNbTables(){ int nbTables = 0; for(TAPSchema s : this) @@ -153,7 +362,13 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return nbTables; } - public static class TAPTableIterator implements Iterator<TAPTable> { + /** + * Let iterating over the list of all tables contained in a given {@link TAPMetadata} object. + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ + protected static class TAPTableIterator implements Iterator<TAPTable> { private Iterator<TAPSchema> it; private Iterator<TAPTable> itTables; @@ -229,14 +444,10 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour } @Override - public void init(ServletConfig config) throws ServletException{ - ; - } + public void init(ServletConfig config) throws ServletException{} @Override - public void destroy(){ - ; - } + public void destroy(){} @Override public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ @@ -260,11 +471,35 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour return false; } + /** + * <p>Format in XML the given schema and then write it in the given writer.</p> + * + * <p>Written lines:</p> + * <pre> + * <schema> + * <name>...</name> + * <description>...</description> + * <utype>...</utype> + * // call #writeTable(TAPTable, PrintWriter) for each table + * </schema> + * </pre> + * + * <p><i>Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description and utype. + * </i></p> + * + * @param s The schema to format and to write in XML. + * @param writer Output in which the XML serialization of the given schema must be written. + * + * @throws IOException If there is any error while writing the XML in the given writer. + * + * @see #writeTable(TAPTable, PrintWriter) + */ private void writeSchema(TAPSchema s, PrintWriter writer) throws IOException{ final String prefix = "\t\t"; writer.println("\t<schema>"); - writeAtt(prefix, "name", s.getName(), writer); + writeAtt(prefix, "name", s.getADQLName(), writer); writeAtt(prefix, "description", s.getDescription(), writer); writeAtt(prefix, "utype", s.getUtype(), writer); @@ -274,11 +509,34 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour writer.println("\t</schema>"); } + /** + * <p>Format in XML the given table and then write it in the given writer.</p> + * + * <p>Written lines:</p> + * <pre> + * <table type="..."> + * <name>...</name> + * <description>...</description> + * <utype>...</utype> + * // call #writeColumn(TAPColumn, PrintWriter) for each column + * // call #writeForeignKey(TAPForeignKey, PrintWriter) for each foreign key + * </table> + * </pre> + * + * <p><i>Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description and utype. + * </i></p> + * + * @param t The table to format and to write in XML. + * @param writer Output in which the XML serialization of the given table must be written. + * + * @throws IOException If there is any error while writing the XML in the given writer. + */ private void writeTable(TAPTable t, PrintWriter writer) throws IOException{ final String prefix = "\t\t\t"; writer.print("\t\t<table type=\""); - writer.print(t.getType().equalsIgnoreCase("table") ? "base_table" : t.getType()); + writer.print(t.getType().toString()); writer.println("\">"); writeAtt(prefix, "name", t.getFullName(), writer); @@ -296,6 +554,32 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour writer.println("\t\t</table>"); } + /** + * <p>Format in XML the given column and then write it in the given writer.</p> + * + * <p>Written lines:</p> + * <pre> + * <column std="true|false"> // the value of this field is TAPColumn#isStd() + * <name>...</name> + * <description>...</description> + * <unit>...</unit> + * <utype>...</utype> + * <ucd>...</ucd> + * <dataType xsi:type="vod:TAPType" size="...">...</dataType> + * <flag>indexed</flag> // if TAPColumn#isIndexed() + * <flag>primary</flag> // if TAPColumn#isPrincipal() + * </column> + * </pre> + * + * <p><i>Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description, unit, utype, ucd and flags. + * </i></p> + * + * @param c The column to format and to write in XML. + * @param writer Output in which the XML serialization of the given column must be written. + * + * @throws IOException If there is any error while writing the XML in the given writer. + */ private void writeColumn(TAPColumn c, PrintWriter writer) throws IOException{ final String prefix = "\t\t\t\t"; @@ -303,7 +587,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour writer.print(c.isStd()); writer.println("\">"); - writeAtt(prefix, "name", c.getName(), writer); + writeAtt(prefix, "name", c.getADQLName(), writer); writeAtt(prefix, "description", c.getDescription(), writer); writeAtt(prefix, "unit", c.getUnit(), writer); writeAtt(prefix, "utype", c.getUtype(), writer); @@ -330,6 +614,32 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour writer.println("\t\t\t</column>"); } + /** + * <p>Format in XML the given foreign key and then write it in the given writer.</p> + * + * <p>Written lines:</p> + * <pre> + * <foreignKey> + * <targetTable>...</targetTable> + * <description>...</description> + * <utype>...</utype> + * <fkColumn> + * <fromColumn>...</fromColumn> + * <targetColumn>...</targetColumn> + * </fkColumn> + * ... + * </foreignKey> + * </pre> + * + * <p><i>Note: + * When NULL an attribute or a field is not written. Here this rule concerns: description and utype. + * </i></p> + * + * @param fk The foreign key to format and to write in XML. + * @param writer Output in which the XML serialization of the given foreign key must be written. + * + * @throws IOException If there is any error while writing the XML in the given writer. + */ private void writeForeignKey(TAPForeignKey fk, PrintWriter writer) throws IOException{ final String prefix = "\t\t\t\t"; @@ -352,6 +662,16 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour writer.println("\t\t\t</foreignKey>"); } + /** + * Write the specified metadata attribute as a simple XML node. + * + * @param prefix Prefix of the XML node. (generally, space characters) + * @param attributeName Name of the metadata attribute to write (= Name of the XML node). + * @param attributeValue Value of the metadata attribute (= Value of the XML node). + * @param writer Output in which the XML node must be written. + * + * @throws IOException If there is a problem while writing the XML node inside the given writer. + */ private void writeAtt(String prefix, String attributeName, String attributeValue, PrintWriter writer) throws IOException{ if (attributeValue != null){ StringBuffer xml = new StringBuffer(prefix); @@ -360,4 +680,176 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour } } + /** + * <p> + * Get the definition of the whole standard TAP_SCHEMA. Thus, all standard TAP_SCHEMA tables + * (with all their columns) are also included in this object. + * </p> + * + * <p><i>Note: + * This function create the {@link TAPSchema} and all its {@link TAPTable}s objects on the fly. + * </p> + * + * @return The whole TAP_SCHEMA definition. + * + * @see STDSchema#TAPSCHEMA + * @see STDTable + * @see #getStdTable(STDTable) + * + * @since 2.0 + */ + public static final TAPSchema getStdSchema(){ + TAPSchema tap_schema = new TAPSchema(STDSchema.TAPSCHEMA.toString(), "Set of tables listing and describing the schemas, tables and columns published in this TAP service.", null); + for(STDTable t : STDTable.values()){ + TAPTable table = getStdTable(t); + tap_schema.addTable(table); + } + return tap_schema; + } + + /** + * <p>Get the definition of the specified standard TAP table.</p> + * + * <p><i><b>Important note:</b> + * The returned table is not linked at all with a schema, on the contrary of {@link #getStdSchema()} which returns tables linked with the returned schema. + * So, you may have to linked this table to schema (by using {@link TAPSchema#addTable(TAPTable)}) whose the ADQL name is TAP_SCHEMA after calling this function. + * </i></p> + * + * <p><i>Note: + * This function create the {@link TAPTable} object on the fly. + * </p> + * + * @param tableId ID of the TAP table to return. + * + * @return The corresponding table definition (with no schema). + * + * @since 2.0 + */ + public static final TAPTable getStdTable(final STDTable tableId){ + switch(tableId){ + + case SCHEMAS: + TAPTable schemas = new TAPTable(STDTable.SCHEMAS.toString(), TableType.table, "List of schemas published in this TAP service.", null); + schemas.addColumn("schema_name", new TAPType(TAPDatatype.VARCHAR), "schema name, possibly qualified", null, null, null, true, true, true); + schemas.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "brief description of schema", null, null, null, false, false, true); + schemas.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "UTYPE if schema corresponds to a data model", null, null, null, false, false, true); + return schemas; + + case TABLES: + TAPTable tables = new TAPTable(STDTable.TABLES.toString(), TableType.table, "List of tables published in this TAP service.", null); + tables.addColumn("schema_name", new TAPType(TAPDatatype.VARCHAR), "the schema name from TAP_SCHEMA.schemas", null, null, null, true, true, true); + tables.addColumn("table_name", new TAPType(TAPDatatype.VARCHAR), "table name as it should be used in queries", null, null, null, true, true, true); + tables.addColumn("table_type", new TAPType(TAPDatatype.VARCHAR), "one of: table, view", null, null, null, false, false, true); + tables.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "brief description of table", null, null, null, false, false, true); + tables.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "UTYPE if table corresponds to a data model", null, null, null, false, false, true); + return tables; + + case COLUMNS: + TAPTable columns = new TAPTable(STDTable.COLUMNS.toString(), TableType.table, "List of columns of all tables listed in TAP_SCHEMA.TABLES and published in this TAP service.", null); + columns.addColumn("table_name", new TAPType(TAPDatatype.VARCHAR), "table name from TAP_SCHEMA.tables", null, null, null, true, true, true); + columns.addColumn("column_name", new TAPType(TAPDatatype.VARCHAR), "column name", null, null, null, true, true, true); + columns.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "brief description of column", null, null, null, false, false, true); + columns.addColumn("unit", new TAPType(TAPDatatype.VARCHAR), "unit in VO standard format", null, null, null, false, false, true); + columns.addColumn("ucd", new TAPType(TAPDatatype.VARCHAR), "UCD of column if any", null, null, null, false, false, true); + columns.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "UTYPE of column if any", null, null, null, false, false, true); + columns.addColumn("datatype", new TAPType(TAPDatatype.VARCHAR), "ADQL datatype as in section 2.5", null, null, null, false, false, true); + columns.addColumn("size", new TAPType(TAPDatatype.INTEGER), "length of variable length datatypes", null, null, null, false, false, true); + columns.addColumn("principal", new TAPType(TAPDatatype.INTEGER), "a principal column; 1 means true, 0 means false", null, null, null, false, false, true); + columns.addColumn("indexed", new TAPType(TAPDatatype.INTEGER), "an indexed column; 1 means true, 0 means false", null, null, null, false, false, true); + columns.addColumn("std", new TAPType(TAPDatatype.INTEGER), "a standard column; 1 means true, 0 means false", null, null, null, false, false, true); + return columns; + + case KEYS: + TAPTable keys = new TAPTable(STDTable.KEYS.toString(), TableType.table, "List all foreign keys but provides just the tables linked by the foreign key. To know which columns of these tables are linked, see in TAP_SCHEMA.key_columns using the key_id.", null); + keys.addColumn("key_id", new TAPType(TAPDatatype.VARCHAR), "unique key identifier", null, null, null, true, true, true); + keys.addColumn("from_table", new TAPType(TAPDatatype.VARCHAR), "fully qualified table name", null, null, null, false, false, true); + keys.addColumn("target_table", new TAPType(TAPDatatype.VARCHAR), "fully qualified table name", null, null, null, false, false, true); + keys.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "description of this key", null, null, null, false, false, true); + keys.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "utype of this key", null, null, null, false, false, true); + return keys; + + case KEY_COLUMNS: + TAPTable key_columns = new TAPTable(STDTable.KEY_COLUMNS.toString(), TableType.table, "List all foreign keys but provides just the columns linked by the foreign key. To know the table of these columns, see in TAP_SCHEMA.keys using the key_id.", null); + key_columns.addColumn("key_id", new TAPType(TAPDatatype.VARCHAR), "unique key identifier", null, null, null, true, true, true); + key_columns.addColumn("from_column", new TAPType(TAPDatatype.VARCHAR), "key column name in the from_table", null, null, null, false, false, true); + key_columns.addColumn("target_column", new TAPType(TAPDatatype.VARCHAR), "key column name in the target_table", null, null, null, false, false, true); + return key_columns; + + default: + return null; + } + } + + /** + * <p>Tell whether the given table name is a standard TAP table.</p> + * + * <p><i>Note: + * This function is case sensitive. Indeed TAP_SCHEMA tables are defined by the TAP standard by a given case. + * Thus, this case is expected here. + * </i></p> + * + * @param tableName Unqualified table name. + * + * @return The corresponding {@link STDTable} or NULL if the given table is not part of the TAP standard. + * + * @since 2.0 + */ + public static final STDTable resolveStdTable(String tableName){ + if (tableName == null || tableName.trim().length() == 0) + return null; + + for(STDTable t : STDTable.values()){ + if (t.label.equals(tableName)) + return t; + } + + return null; + } + + /** + * Enumeration of all schemas defined in the TAP standard. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (07/2014) + * @since 2.0 + */ + public enum STDSchema{ + TAPSCHEMA("TAP_SCHEMA"), UPLOADSCHEMA("TAP_UPLOAD"); + + /** Real name of the schema. */ + public final String label; + + private STDSchema(final String name){ + this.label = name; + } + + @Override + public String toString(){ + return label; + } + } + + /** + * Enumeration of all tables of TAP_SCHEMA. + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (07/2014) + * @since 2.0 + */ + public enum STDTable{ + SCHEMAS("schemas"), TABLES("tables"), COLUMNS("columns"), KEYS("keys"), KEY_COLUMNS("key_columns"); + + /** Real name of the table. */ + public final String label; + + private STDTable(final String name){ + this.label = name; + } + + @Override + public String toString(){ + return label; + } + } + } diff --git a/src/tap/metadata/TAPSchema.java b/src/tap/metadata/TAPSchema.java index faced37..984d087 100644 --- a/src/tap/metadata/TAPSchema.java +++ b/src/tap/metadata/TAPSchema.java @@ -16,37 +16,122 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ +import java.awt.List; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import tap.metadata.TAPTable.TableType; + +/** + * <p>Represent a schema as described by the IVOA standard in the TAP protocol definition.</p> + * + * <p> + * This object representation has exactly the same fields as the column of the table TAP_SCHEMA.schemas. + * But it also provides a way to add other data. For instance, if information not listed in the standard + * may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be + * a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc... + * </p> + * + * <p><i>Note: + * On the contrary to {@link TAPColumn} and {@link TAPTable}, a {@link TAPSchema} object MAY have no DB name. + * But by default, at the creation the DB name is the ADQL name. Once created, it is possible to set the DB + * name with {@link #setDBName(String)}. This DB name MAY be qualified, BUT MUST BE without double quotes. + * </i></p> + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ public class TAPSchema implements Iterable<TAPTable> { + /** Name that this schema MUST have in ADQL queries. */ private final String adqlName; + /** Name that this schema have in the database. + * <i>Note: It MAY be NULL. By default, it is the ADQL name.</i> */ private String dbName = null; + /** Description of this schema. + * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */ private String description = null; + /** UType describing the scientific content of this schema. + * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */ private String utype = null; + /** Let add some information in addition of the ones of the TAP protocol. + * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ... + * Its content is totally free and never used or checked.</i> */ protected Object otherData = null; + /** List all tables contained inside this schema. */ protected final Map<String,TAPTable> tables; + /** + * <p>Build a {@link TAPSchema} instance with the given ADQL name.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * @param schemaName Name that this schema MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + */ public TAPSchema(String schemaName){ - adqlName = schemaName; + if (schemaName == null || schemaName.trim().length() == 0) + throw new NullPointerException("Missing schema name !"); + int indPrefix = schemaName.lastIndexOf('.'); + adqlName = (indPrefix >= 0) ? schemaName.substring(indPrefix + 1).trim() : schemaName.trim(); dbName = adqlName; tables = new HashMap<String,TAPTable>(); } + /** + * <p>Build a {@link TAPSchema} instance with the given ADQL name and description.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * @param schemaName Name that this schema MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param description Description of this schema. <i>MAY be NULL</i> + */ public TAPSchema(String schemaName, String description){ this(schemaName, description, null); } + /** + * <p>Build a {@link TAPSchema} instance with the given ADQL name, description and UType.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * @param schemaName Name that this schema MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param description Description of this schema. <i>MAY be NULL</i> + * @param utype UType associating this schema with a data-model. <i>MAY be NULL</i> + */ public TAPSchema(String schemaName, String description, String utype){ this(schemaName); this.description = description; @@ -54,68 +139,143 @@ public class TAPSchema implements Iterable<TAPTable> { } /** - * @return The name. + * Get the ADQL name (the name this schema MUST have in ADQL queries). + * + * @return Its ADQL name. + * @see #getADQLName() + * @deprecated Does not do anything special: just call {@link #getADQLName()}. */ + @Deprecated public final String getName(){ return getADQLName(); } + /** + * Get the name this schema MUST have in ADQL queries. + * + * @return Its ADQL name. <i>CAN'T be NULL</i> + */ public final String getADQLName(){ return adqlName; } + /** + * Get the name this schema MUST have in the database. + * + * @return Its DB name. <i>MAY be NULL</i> + */ public final String getDBName(){ return dbName; } + /** + * Set the name this schema MUST have in the database. + * + * @param name Its new DB name. <i>MAY be NULL</i> + */ public final void setDBName(String name){ name = (name != null) ? name.trim() : name; - dbName = (name == null || name.length() == 0) ? adqlName : name; + dbName = name; } /** - * @return The description. + * Get the description of this schema. + * + * @return Its description. <i>MAY be NULL</i> */ public final String getDescription(){ return description; } /** - * @param description The description to set. + * Set the description of this schema. + * + * @param description Its new description. <i>MAY be NULL</i> */ public final void setDescription(String description){ this.description = description; } /** - * @return The utype. + * Get the UType associating this schema with a data-model. + * + * @return Its UType. <i>MAY be NULL</i> */ public final String getUtype(){ return utype; } /** - * @param utype The utype to set. + * Set the UType associating this schema with a data-model. + * + * @param utype Its new UType. <i>MAY be NULL</i> */ public final void setUtype(String utype){ this.utype = utype; } + /** + * <p>Get the other (piece of) information associated with this schema.</p> + * + * <p><i>Note: + * By default, NULL is returned, but it may be any kind of value ({@link Integer}, + * {@link String}, {@link Map}, {@link List}, ...). + * </i></p> + * + * @return The other (piece of) information. <i>MAY be NULL</i> + */ public Object getOtherData(){ return otherData; } + /** + * Set the other (piece of) information associated with this schema. + * + * @param data Another information about this schema. <i>MAY be NULL</i> + */ public void setOtherData(Object data){ otherData = data; } + /** + * <p>Add the given table inside this schema.</p> + * + * <p><i>Note: + * If the given table is NULL, nothing will be done. + * </i></p> + * + * <p><i><b>Important note:</b> + * By adding the given table inside this schema, it + * will be linked with this schema using {@link TAPTable#setSchema(TAPSchema)}. + * In this function, if the table was already linked with another {@link TAPSchema}, + * the former link is removed using {@link TAPSchema#removeTable(String)}. + * </i></p> + * + * @param newTable Table to add inside this schema. + */ public final void addTable(TAPTable newTable){ - if (newTable != null && newTable.getName() != null){ - tables.put(newTable.getName(), newTable); + if (newTable != null && newTable.getADQLName() != null){ + tables.put(newTable.getADQLName(), newTable); newTable.setSchema(this); } } + /** + * <p>Build a {@link TAPTable} object whose the ADQL and DB name will the given one. + * Then, add this table inside this schema.</p> + * + * <p><i>Note: + * The built {@link TAPTable} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param tableName ADQL name (and indirectly also the DB name) of the table to create and add. + * + * @return The created and added {@link TAPTable} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPTable#TAPTable(String) + * @see #addTable(TAPTable) + */ public TAPTable addTable(String tableName){ if (tableName == null) return null; @@ -125,15 +285,48 @@ public class TAPSchema implements Iterable<TAPTable> { return t; } - public TAPTable addTable(String tableName, String tableType, String description, String utype){ + /** + * <p>Build a {@link TAPTable} object whose the ADQL and DB name will the given one. + * Then, add this table inside this schema.</p> + * + * <p><i>Note: + * The built {@link TAPTable} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param tableName ADQL name (and indirectly also the DB name) of the table to create and add. + * @param tableType Type of the new table. <i>If NULL, "table" will be the type of the created table.</i> + * @param description Description of the new table. <i>MAY be NULL</i> + * @param unit Unit of the new table's values. <i>MAY be NULL</i> + * @param ucd UCD describing the scientific content of the new column. <i>MAY be NULL</i> + * @param utype UType associating the new column with a data-model. <i>MAY be NULL</i> + * + * @return The created and added {@link TAPTable} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPTable#TAPTable(String, TableType, String, String, String, String) + * @see #addTable(TAPSchema) + */ + public TAPTable addTable(String tableName, TableType tableType, String description, String utype){ if (tableName == null) return null; TAPTable t = new TAPTable(tableName, tableType, description, utype); addTable(t); + return t; } + /** + * <p>Tell whether this schema contains a table having the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * @param tableName Name of the table whose the existence in this schema must be checked. + * + * @return <i>true</i> if a table with the given ADQL name exists, <i>false</i> otherwise. + */ public final boolean hasTable(String tableName){ if (tableName == null) return false; @@ -141,6 +334,18 @@ public class TAPSchema implements Iterable<TAPTable> { return tables.containsKey(tableName); } + /** + * <p>Search for a table having the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * @param tableName ADQL name of the table to search. + * + * @return The table having the given ADQL name, + * or NULL if no such table can be found. + */ public final TAPTable getTable(String tableName){ if (tableName == null) return null; @@ -148,33 +353,79 @@ public class TAPSchema implements Iterable<TAPTable> { return tables.get(tableName); } + /** + * Get the number of all tables contained inside this schema. + * + * @return Number of its tables. + */ public final int getNbTables(){ return tables.size(); } + /** + * Tell whether this schema contains no table. + * + * @return <i>true</i> if this schema contains no table, + * <i>false</i> if it has at least one table. + */ public final boolean isEmpty(){ return tables.isEmpty(); } + /** + * <p>Remove the table having the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * <p><i>Note: + * If the specified table is removed, its schema link is also deleted. + * </i></p> + * + * <p><i><b>WARNING:</b> + * If the goal of this function's call is to delete definitely the specified table + * from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()}. + * Indeed, foreign keys of the table would still link the removed table with other tables + * AND columns of the whole metadata set. + * </i></p> + * + * @param tableName ADQL name of the table to remove from this schema. + * + * @return The removed table, + * or NULL if no table with the given ADQL name can be found. + */ public final TAPTable removeTable(String tableName){ if (tableName == null) return null; TAPTable removedTable = tables.remove(tableName); - if (removedTable != null){ + if (removedTable != null) removedTable.setSchema(null); - removedTable.removeAllForeignKeys(); - } return removedTable; } + /** + * <p>Remove all the tables contained inside this schema.</p> + * + * <p><i>Note: + * When a table is removed, its schema link is also deleted. + * </i></p> + * + * <p><b>CAUTION: + * If the goal of this function's call is to delete definitely all the tables of this schema + * from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()} + * on all tables before calling this function. + * Indeed, foreign keys of the tables would still link the removed tables with other tables + * AND columns of the whole metadata set. + * </b></p> + */ public final void removeAllTables(){ Iterator<Map.Entry<String,TAPTable>> it = tables.entrySet().iterator(); while(it.hasNext()){ Map.Entry<String,TAPTable> entry = it.next(); it.remove(); entry.getValue().setSchema(null); - entry.getValue().removeAllForeignKeys(); } } diff --git a/src/tap/metadata/TAPTable.java b/src/tap/metadata/TAPTable.java index 5486377..770e868 100644 --- a/src/tap/metadata/TAPTable.java +++ b/src/tap/metadata/TAPTable.java @@ -16,39 +16,109 @@ package tap.metadata; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ +import java.awt.List; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import tap.TAPException; import adql.db.DBColumn; import adql.db.DBTable; +/** + * <p>Represent a table as described by the IVOA standard in the TAP protocol definition.</p> + * + * <p> + * This object representation has exactly the same fields as the column of the table TAP_SCHEMA.tables. + * But it also provides a way to add other data. For instance, if information not listed in the standard + * may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be + * a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc... + * </p> + * + * <p><i><b>Important note:</b> + * A {@link TAPTable} object MUST always have a DB name. That's why by default, at the creation + * the DB name is the ADQL name. Once created, it is possible to set the DB name with {@link #setDBName(String)}. + * This DB name MUST be UNqualified and without double quotes. If a NULL or empty value is provided, + * nothing is done and the object keeps its former DB name. + * </i></p> + * + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (08/2014) + */ public class TAPTable implements DBTable { + /** + * Different types of table according to the TAP protocol. + * The default one should be "table". + * + * @author Grégory Mantelet (ARI) + * @version 2.0 (08/2014) + * + * @since 2.0 + */ + public enum TableType{ + output, table, view; + } + + /** Name that this table MUST have in ADQL queries. */ private final String adqlName; + /** Name that this table have in the database. + * <i>Note: It CAN'T be NULL. By default, it is the ADQL name.</i> */ private String dbName = null; + /** The schema which owns this table. + * <i>Note: It is NULL only at the construction. + * Then, this attribute is automatically set by a {@link TAPSchema} when adding this table inside it + * with {@link TAPSchema#addTable(TAPTable)}.</i> */ private TAPSchema schema = null; - private String type = "table"; + /** Type of this table. + * <i>Note: Standard TAP table field ; CAN NOT be NULL ; by default, it is "table".</i> */ + private TableType type = TableType.table; + /** Description of this table. + * <i>Note: Standard TAP table field ; MAY be NULL.</i> */ private String description = null; + /** UType associating this table with a data-model. + * <i>Note: Standard TAP table field ; MAY be NULL.</i> */ private String utype = null; + /** List of columns composing this table. + * <i>Note: all columns of this list are linked to this table from the moment they are added inside it.</i> */ protected final Map<String,TAPColumn> columns; + /** List of all foreign keys linking this table to others. */ protected final ArrayList<TAPForeignKey> foreignKeys; + /** Let add some information in addition of the ones of the TAP protocol. + * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ... + * Its content is totally free and never used or checked.</i> */ protected Object otherData = null; + /** + * <p>Build a {@link TAPTable} instance with the given ADQL name.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * The table type is set by default to "table". + * </i></p> + * + * <p><i>Note: + * If the given ADQL name is prefixed (= it has some text separated by a '.' before the table name), + * this prefix will be removed. Only the part after the '.' character will be kept. + * </i></p> + * + * @param tableName Name that this table MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + */ public TAPTable(String tableName){ if (tableName == null || tableName.trim().length() == 0) throw new NullPointerException("Missing table name !"); @@ -59,27 +129,84 @@ public class TAPTable implements DBTable { foreignKeys = new ArrayList<TAPForeignKey>(); } - public TAPTable(String tableName, String tableType){ + /** + * <p>Build a {@link TAPTable} instance with the given ADQL name and table type.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * The table type is set by calling the function {@link #setType(TableType)} which does not do + * anything if the given table type is NULL. + * </i></p> + * + * @param tableName Name that this table MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param tableType Type of this table. <i>If NULL, "table" will be the type of this table.</i> + * + * @see #setType(TableType) + */ + public TAPTable(String tableName, TableType tableType){ this(tableName); - type = tableType; + setType(tableType); } - public TAPTable(String tableName, String tableType, String description, String utype){ + /** + * <p>Build a {@link TAPTable} instance with the given ADQL name, table type, description and UType.</p> + * + * <p><i>Note: + * The DB name is set by default with the ADQL name. To set the DB name, + * you MUST call then {@link #setDBName(String)}. + * </i></p> + * + * <p><i>Note: + * The table type is set by calling the function {@link #setType(TableType)} which does not do + * anything if the given table type is NULL. + * </i></p> + * + * @param tableName Name that this table MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i> + * @param tableType Type of this table. <i>If NULL, "table" will be the type of this table.</i> + * @param description Description of this table. <i>MAY be NULL.</i> + * @param utype UType associating this table with a data-model. <i>MAY be NULL</i> + * + * @see #setType(TableType) + */ + public TAPTable(String tableName, TableType tableType, String description, String utype){ this(tableName, tableType); this.description = description; this.utype = utype; } + /** + * <p>Get the qualified name of this table.</p> + * + * <p><i><b>Warning:</b> + * The part of the returned full name won't be double quoted! + * </i></p> + * + * <p><i>Note: + * If this table is not attached to a schema, this function will just return + * the ADQL name of this table. + * </i></p> + * + * @return Qualified ADQL name of this table. + */ public final String getFullName(){ if (schema != null) - return schema.getName() + "." + adqlName; + return schema.getADQLName() + "." + adqlName; else return adqlName; } /** - * @return The name. + * Get the ADQL name (the name this table MUST have in ADQL queries). + * + * @return Its ADQL name. + * @see #getADQLName() + * @deprecated Does not do anything special: just call {@link #getADQLName()}. */ + @Deprecated public final String getName(){ return getADQLName(); } @@ -94,6 +221,15 @@ public class TAPTable implements DBTable { return dbName; } + /** + * <p>Change the name that this table MUST have in the database (i.e. in SQL queries).</p> + * + * <p><i>Note: + * If the given value is NULL or an empty string, nothing is done ; the DB name keeps is former value. + * </i></p> + * + * @param name The new database name of this table. + */ public final void setDBName(String name){ name = (name != null) ? name.trim() : name; dbName = (name == null || name.length() == 0) ? adqlName : name; @@ -111,87 +247,167 @@ public class TAPTable implements DBTable { @Override public final String getADQLSchemaName(){ - return schema.getADQLName(); + return schema == null ? null : schema.getADQLName(); } @Override public final String getDBSchemaName(){ - return schema.getDBName(); + return schema == null ? null : schema.getDBName(); } /** - * @return The schema. + * Get the schema that owns this table. + * + * @return Its schema. <i>MAY be NULL</i> */ public final TAPSchema getSchema(){ return schema; } /** - * @param schema The schema to set. + * <p>Set the schema in which this schema is.</p> + * + * <p><i><b>Warning:</b> + * For consistency reasons, this function SHOULD be called only by the {@link TAPSchema} + * that owns this table. + * </i></p> + * + * <p><i><b>Important note:</b> + * If this table was already linked with another {@link TAPSchema} object, the previous link is removed + * here, but also in the schema (by calling {@link TAPSchema#removeTable(String)}). + * </i></p> + * + * @param schema The schema that owns this table. */ - protected final void setSchema(TAPSchema schema){ + protected final void setSchema(final TAPSchema schema){ + if (this.schema != null && (schema == null || !schema.equals(this.schema))) + this.schema.removeTable(adqlName); this.schema = schema; } /** - * @return The type. + * Get the type of this table. + * + * @return Its type. */ - public final String getType(){ + public final TableType getType(){ return type; } /** - * @param type The type to set. + * <p>Set the type of this table.</p> + * + * <p><i>Note: + * If the given type is NULL, nothing will be done ; the type of this table won't be changed. + * </i></p> + * + * @param type Its new type. */ - public final void setType(String type){ - this.type = type; + public final void setType(TableType type){ + if (type != null) + this.type = type; } /** - * @return The description. + * Get the description of this table. + * + * @return Its description. <i>MAY be NULL</i> */ public final String getDescription(){ return description; } /** - * @param description The description to set. + * Set the description of this table. + * + * @param description Its new description. <i>MAY be NULL</i> */ public final void setDescription(String description){ this.description = description; } /** - * @return The utype. + * Get the UType associating this table with a data-model. + * + * @return Its UType. <i>MAY be NULL</i> */ public final String getUtype(){ return utype; } /** - * @param utype The utype to set. + * Set the UType associating this table with a data-model. + * + * @param utype Its new UType. <i>MAY be NULL</i> */ public final void setUtype(String utype){ this.utype = utype; } + /** + * <p>Get the other (piece of) information associated with this table.</p> + * + * <p><i>Note: + * By default, NULL is returned, but it may be any kind of value ({@link Integer}, + * {@link String}, {@link Map}, {@link List}, ...). + * </i></p> + * + * @return The other (piece of) information. <i>MAY be NULL</i> + */ public Object getOtherData(){ return otherData; } + /** + * Set the other (piece of) information associated with this table. + * + * @param data Another information about this table. <i>MAY be NULL</i> + */ public void setOtherData(Object data){ otherData = data; } - public final void addColumn(TAPColumn newColumn){ - if (newColumn != null && newColumn.getName() != null){ - columns.put(newColumn.getName(), newColumn); + /** + * <p>Add a column to this table.</p> + * + * <p><i>Note: + * If the given column is NULL, nothing will be done. + * </i></p> + * + * <p><i><b>Important note:</b> + * By adding the given column inside this table, it + * will be linked with this table using {@link TAPColumn#setTable(DBTable)}. + * In this function, if the column was already linked with another {@link TAPTable}, + * the former link is removed using {@link TAPTable#removeColumn(String)}. + * </i></p> + * + * @param newColumn Column to add inside this table. + */ + public final void addColumn(final TAPColumn newColumn){ + if (newColumn != null && newColumn.getADQLName() != null){ + columns.put(newColumn.getADQLName(), newColumn); newColumn.setTable(this); } } + /** + * <p>Build a {@link TAPColumn} object whose the ADQL and DB name will the given one. + * Then, add this column inside this table.</p> + * + * <p><i>Note: + * The built {@link TAPColumn} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param columnName ADQL name (and indirectly also the DB name) of the column to create and add. + * + * @return The created and added {@link TAPColumn} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPColumn#TAPColumn(String) + * @see #addColumn(TAPColumn) + */ public final TAPColumn addColumn(String columnName){ - if (columnName == null) + if (columnName == null || columnName.trim().length() <= 0) return null; TAPColumn c = new TAPColumn(columnName); @@ -199,8 +415,29 @@ public class TAPTable implements DBTable { return c; } + /** + * <p>Build a {@link TAPColumn} object whose the ADQL and DB name will the given one. + * Then, add this column inside this table.</p> + * + * <p><i>Note: + * The built {@link TAPColumn} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param columnName ADQL name (and indirectly also the DB name) of the column to create and add. + * @param datatype Type of the new column's values. <i>If NULL, VARCHAR will be the type of the created column.</i> + * @param description Description of the new column. <i>MAY be NULL</i> + * @param unit Unit of the new column's values. <i>MAY be NULL</i> + * @param ucd UCD describing the scientific content of the new column. <i>MAY be NULL</i> + * @param utype UType associating the new column with a data-model. <i>MAY be NULL</i> + * + * @return The created and added {@link TAPColumn} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPColumn#TAPColumn(String, TAPType, String, String, String, String) + * @see #addColumn(TAPColumn) + */ public TAPColumn addColumn(String columnName, TAPType datatype, String description, String unit, String ucd, String utype){ - if (columnName == null) + if (columnName == null || columnName.trim().length() <= 0) return null; TAPColumn c = new TAPColumn(columnName, datatype, description, unit, ucd, utype); @@ -208,8 +445,35 @@ public class TAPTable implements DBTable { return c; } + /** + * <p>Build a {@link TAPColumn} object whose the ADQL and DB name will the given one. + * Then, add this column inside this table.</p> + * + * <p><i>Note: + * The built {@link TAPColumn} object is returned, so that being modified afterwards if needed. + * </i></p> + * + * @param columnName ADQL name (and indirectly also the DB name) of the column to create and add. + * @param datatype Type of the new column's values. <i>If NULL, VARCHAR will be the type of the created column.</i> + * @param description Description of the new column. <i>MAY be NULL</i> + * @param unit Unit of the new column's values. <i>MAY be NULL</i> + * @param ucd UCD describing the scientific content of the new column. <i>MAY be NULL</i> + * @param utype UType associating the new column with a data-model. <i>MAY be NULL</i> + * @param principal <i>true</i> if the new column should be returned by default, <i>false</i> otherwise. + * @param indexed <i>true</i> if the new column is indexed, <i>false</i> otherwise. + * @param std <i>true</i> if the new column is defined by a standard, <i>false</i> otherwise. + * + * @return The created and added {@link TAPColumn} object, + * or NULL if the given name is NULL or an empty string. + * + * @see TAPColumn#TAPColumn(String, TAPType, String, String, String, String) + * @see TAPColumn#setPrincipal(boolean) + * @see TAPColumn#setIndexed(boolean) + * @see TAPColumn#setStd(boolean) + * @see #addColumn(TAPColumn) + */ public TAPColumn addColumn(String columnName, TAPType datatype, String description, String unit, String ucd, String utype, boolean principal, boolean indexed, boolean std){ - if (columnName == null) + if (columnName == null || columnName.trim().length() <= 0) return null; TAPColumn c = new TAPColumn(columnName, datatype, description, unit, ucd, utype); @@ -220,6 +484,17 @@ public class TAPTable implements DBTable { return c; } + /** + * <p>Tell whether this table contains a column with the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive. + * </i></p> + * + * @param columnName ADQL name (case sensitive) of the column whose the existence must be checked. + * + * @return <i>true</i> if a column having the given ADQL name exists in this table, <i>false</i> otherwise. + */ public final boolean hasColumn(String columnName){ if (columnName == null) return false; @@ -227,6 +502,11 @@ public class TAPTable implements DBTable { return columns.containsKey(columnName); } + /** + * Get the list of all columns contained in this table. + * + * @return An iterator over the list of this table's columns. + */ public Iterator<TAPColumn> getColumns(){ return columns.values().iterator(); } @@ -239,7 +519,7 @@ public class TAPTable implements DBTable { if (colName != null && colName.length() > 0){ Collection<TAPColumn> collColumns = columns.values(); for(TAPColumn column : collColumns){ - if (column.getDBName().equalsIgnoreCase(colName)) + if (column.getDBName().equals(colName)) return column; } } @@ -247,6 +527,18 @@ public class TAPTable implements DBTable { } } + /** + * <p>Search a column inside this table having the given ADQL name.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive. + * </i></p> + * + * @param columnName ADQL name of the column to search. + * + * @return The matching column, + * or NULL if no column with this ADQL name has been found. + */ public final TAPColumn getColumn(String columnName){ if (columnName == null) return null; @@ -254,18 +546,64 @@ public class TAPTable implements DBTable { return columns.get(columnName); } + /** + * <p>Tell whether this table contains a column with the given ADQL or DB name.</p> + * + * <p><i>Note: + * This functions is just calling {@link #getColumn(String, boolean)} and compare its result + * with NULL in order to check the existence of the specified column. + * </i></p> + * + * @param colName ADQL or DB name that the column to search must have. + * @param byAdqlName <i>true</i> to search the column by ADQL name, <i>false</i> to search by DB name. + * + * @return <i>true</i> if a column has been found inside this table with the given ADQL or DB name, + * <i>false</i> otherwise. + * + * @see #getColumn(String, boolean) + */ public boolean hasColumn(String colName, boolean byAdqlName){ return (getColumn(colName, byAdqlName) != null); } + /** + * Get the number of columns composing this table. + * + * @return Number of its columns. + */ public final int getNbColumns(){ return columns.size(); } + /** + * Tell whether this table contains no column. + * + * @return <i>true</i> if this table is empty (no column), + * <i>false</i> if it contains at least one column. + */ public final boolean isEmpty(){ return columns.isEmpty(); } + /** + * <p>Remove the specified column.</p> + * + * <p><i><b>Important note:</b> + * This function is case sensitive! + * </i></p> + * + * <p><i>Note: + * If some foreign keys were associating the column to remove, + * they will be also deleted. + * </i></p> + * + * @param columnName ADQL name of the column to remove. + * + * @return The removed column, + * or NULL if no column with the given ADQL name has been found. + * + * @see #deleteColumnRelations(TAPColumn) + */ public final TAPColumn removeColumn(String columnName){ if (columnName == null) return null; @@ -273,9 +611,15 @@ public class TAPTable implements DBTable { TAPColumn removedColumn = columns.remove(columnName); if (removedColumn != null) deleteColumnRelations(removedColumn); + return removedColumn; } + /** + * Delete all foreign keys having the given column in the sources or the targets list. + * + * @param col A column. + */ protected final void deleteColumnRelations(TAPColumn col){ // Remove the relation between the column and this table: col.setTable(null); @@ -292,6 +636,10 @@ public class TAPTable implements DBTable { } } + /** + * Remove all columns composing this table. + * Foreign keys will also be deleted. + */ public final void removeAllColumns(){ Iterator<Map.Entry<String,TAPColumn>> it = columns.entrySet().iterator(); while(it.hasNext()){ @@ -301,7 +649,31 @@ public class TAPTable implements DBTable { } } - public final void addForeignKey(TAPForeignKey key) throws Exception{ + /** + * <p>Add the given foreign key to this table.</p> + * + * <p><i>Note: + * This function will do nothing if the given foreign key is NULL. + * </i></p> + * + * <p><i><b>WARNING:</b> + * The source table ({@link TAPForeignKey#getFromTable()}) of the given foreign key MUST be this table + * and the foreign key MUST be completely defined. + * If not, an exception will be thrown and the key won't be added. + * </i></p> + * + * <p><i>Note: + * If the given foreign key is added to this table, all the columns of this key will be + * linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or + * {@link TAPColumn#addTarget(TAPForeignKey)}. + * </i></p> + * + * @param key Foreign key (whose the FROM table is this table) to add inside this table. + * + * @throws TAPException If the source table of the given foreign key is not this table + * or if the given key is not completely defined. + */ + public final void addForeignKey(TAPForeignKey key) throws TAPException{ if (key == null) return; @@ -309,57 +681,120 @@ public class TAPTable implements DBTable { final String errorMsgPrefix = "Impossible to add the foreign key \"" + keyId + "\" because "; if (key.getFromTable() == null) - throw new Exception(errorMsgPrefix + "no source table is specified !"); + throw new TAPException(errorMsgPrefix + "no source table is specified !"); if (!this.equals(key.getFromTable())) - throw new Exception(errorMsgPrefix + "the source table is not \"" + getName() + "\""); + throw new TAPException(errorMsgPrefix + "the source table is not \"" + getADQLName() + "\""); if (key.getTargetTable() == null) - throw new Exception(errorMsgPrefix + "no target table is specified !"); + throw new TAPException(errorMsgPrefix + "no target table is specified !"); if (key.isEmpty()) - throw new Exception(errorMsgPrefix + "it defines no relation !"); + throw new TAPException(errorMsgPrefix + "it defines no relation !"); if (foreignKeys.add(key)){ try{ TAPTable targetTable = key.getTargetTable(); for(Map.Entry<String,String> relation : key){ if (!hasColumn(relation.getKey())) - throw new Exception(errorMsgPrefix + "the source column \"" + relation.getKey() + "\" doesn't exist in \"" + getName() + "\" !"); + throw new TAPException(errorMsgPrefix + "the source column \"" + relation.getKey() + "\" doesn't exist in \"" + getName() + "\" !"); else if (!targetTable.hasColumn(relation.getValue())) - throw new Exception(errorMsgPrefix + "the target column \"" + relation.getValue() + "\" doesn't exist in \"" + targetTable.getName() + "\" !"); + throw new TAPException(errorMsgPrefix + "the target column \"" + relation.getValue() + "\" doesn't exist in \"" + targetTable.getName() + "\" !"); else{ getColumn(relation.getKey()).addTarget(key); targetTable.getColumn(relation.getValue()).addSource(key); } } - }catch(Exception ex){ + }catch(TAPException ex){ foreignKeys.remove(key); throw ex; } } } - public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns) throws Exception{ + /** + * <p>Build a foreign key using the ID, the target table and the given list of columns. + * Then, add the created foreign key to this table.</p> + * + * <p><i>Note: + * The source table of the created foreign key ({@link TAPForeignKey#getFromTable()}) will be this table. + * </i></p> + * + * <p><i>Note: + * If the given foreign key is added to this table, all the columns of this key will be + * linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or + * {@link TAPColumn#addTarget(TAPForeignKey)}. + * </i></p> + * + * @return The created and added foreign key. + * + * @throws TAPException If the specified key is not completely or correctly defined. + * + * @see TAPForeignKey#TAPForeignKey(String, TAPTable, TAPTable, Map) + */ + public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns) throws TAPException{ TAPForeignKey key = new TAPForeignKey(keyId, this, targetTable, columns); addForeignKey(key); return key; } - public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns, String description, String utype) throws Exception{ + /** + * <p>Build a foreign key using the ID, the target table, the given list of columns, the given description and the given UType. + * Then, add the created foreign key to this table.</p> + * + * <p><i>Note: + * The source table of the created foreign key ({@link TAPForeignKey#getFromTable()}) will be this table. + * </i></p> + * + * <p><i>Note: + * If the given foreign key is added to this table, all the columns of this key will be + * linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or + * {@link TAPColumn#addTarget(TAPForeignKey)}. + * </i></p> + * + * @return The created and added foreign key. + * + * @throws TAPException If the specified key is not completely or correctly defined. + * + * @see TAPForeignKey#TAPForeignKey(String, TAPTable, TAPTable, Map, String, String) + */ + public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns, String description, String utype) throws TAPException{ TAPForeignKey key = new TAPForeignKey(keyId, this, targetTable, columns, description, utype); addForeignKey(key); return key; } + /** + * Get the list of all foreign keys associated whose the source is this table. + * + * @return An iterator over all its foreign keys. + */ public final Iterator<TAPForeignKey> getForeignKeys(){ return foreignKeys.iterator(); } + /** + * Get the number of all foreign keys whose the source is this table + * + * @return Number of all its foreign keys. + */ public final int getNbForeignKeys(){ return foreignKeys.size(); } + /** + * <p>Remove the given foreign key from this table.</p> + * + * <p><i>Note: + * This function will also delete the link between the columns of the foreign key + * and the foreign key, using {@link #deleteRelations(TAPForeignKey)}. + * </i></p> + * + * @param keyToRemove Foreign key to removed from this table. + * + * @return <i>true</i> if the key has been successfully removed, + * <i>false</i> otherwise. + */ public final boolean removeForeignKey(TAPForeignKey keyToRemove){ if (foreignKeys.remove(keyToRemove)){ deleteRelations(keyToRemove); @@ -368,6 +803,14 @@ public class TAPTable implements DBTable { return false; } + /** + * <p>Remove all the foreign keys whose the source is this table.</p> + * + * <p><i>Note: + * This function will also delete the link between the columns of all the removed foreign keys + * and the foreign keys, using {@link #deleteRelations(TAPForeignKey)}. + * </i></p> + */ public final void removeAllForeignKeys(){ Iterator<TAPForeignKey> it = foreignKeys.iterator(); while(it.hasNext()){ @@ -376,6 +819,13 @@ public class TAPTable implements DBTable { } } + /** + * Delete the link between all columns of the given foreign key + * and this foreign key. Thus, these columns won't be anymore source or target + * of this foreign key. + * + * @param key A foreign key whose links with its columns must be deleted. + */ protected final void deleteRelations(TAPForeignKey key){ for(Map.Entry<String,String> relation : key){ TAPColumn col = key.getFromTable().getColumn(relation.getKey()); @@ -412,60 +862,7 @@ public class TAPTable implements DBTable { @Override public String toString(){ - return ((schema != null) ? (schema.getName() + ".") : "") + adqlName; - } - - public static void main(String[] args) throws Exception{ - TAPSchema schema1 = new TAPSchema("monSchema1"); - TAPSchema schema2 = new TAPSchema("monSchema2"); - - TAPTable tRef = schema1.addTable("ToRef"); - tRef.addColumn("monMachin"); - - TAPTable t = schema2.addTable("Test"); - t.addColumn("machin"); - t.addColumn("truc"); - HashMap<String,String> mapCols = new HashMap<String,String>(); - mapCols.put("machin", "monMachin"); - TAPForeignKey key = new TAPForeignKey("KeyID", t, tRef, mapCols); - t.addForeignKey(key); - mapCols = new HashMap<String,String>(); - mapCols.put("truc", "monMachin"); - key = new TAPForeignKey("2ndKey", t, tRef, mapCols); - t.addForeignKey(key); - - printSchema(schema1); - printSchema(schema2); - - System.out.println(); - - schema2.removeTable("Test"); - printSchema(schema1); - printSchema(schema2); - } - - public static void printSchema(TAPSchema schema){ - System.out.println("*** SCHEMA \"" + schema.getName() + "\" ***"); - for(TAPTable t : schema) - printTable(t); - } - - public static void printTable(TAPTable t){ - System.out.println("TABLE: " + t + "\nNb Columns: " + t.getNbColumns() + "\nNb Relations: " + t.getNbForeignKeys()); - Iterator<TAPColumn> it = t.getColumns(); - while(it.hasNext()){ - TAPColumn col = it.next(); - System.out.print("\t- " + col + "( "); - Iterator<TAPForeignKey> keys = col.getTargets(); - while(keys.hasNext()) - for(Map.Entry<String,String> relation : keys.next()) - System.out.print(">" + relation.getKey() + "/" + relation.getValue() + " "); - keys = col.getSources(); - while(keys.hasNext()) - for(Map.Entry<String,String> relation : keys.next()) - System.out.print("<" + relation.getKey() + "/" + relation.getValue() + " "); - System.out.println(")"); - } + return ((schema != null) ? (schema.getADQLName() + ".") : "") + adqlName; } @Override diff --git a/src/tap/upload/LimitedSizeInputStream.java b/src/tap/upload/LimitedSizeInputStream.java index dd95527..2836545 100644 --- a/src/tap/upload/LimitedSizeInputStream.java +++ b/src/tap/upload/LimitedSizeInputStream.java @@ -49,7 +49,7 @@ public final class LimitedSizeInputStream extends InputStream { counter += nbReads; if (counter > sizeLimit){ exceed = true; - throw new ExceededSizeException(); + throw new ExceededSizeException("Data read overflow: the limit of " + sizeLimit + " bytes has been reached!"); } } } diff --git a/src/tap/upload/Uploader.java b/src/tap/upload/Uploader.java index 76bbab4..8cef0db 100644 --- a/src/tap/upload/Uploader.java +++ b/src/tap/upload/Uploader.java @@ -17,7 +17,7 @@ package tap.upload; * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institute (ARI) + * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -30,7 +30,7 @@ import tap.data.DataReadException; import tap.data.VOTableIterator; import tap.db.DBConnection; import tap.metadata.TAPColumn; -import tap.metadata.TAPDM; +import tap.metadata.TAPMetadata.STDSchema; import tap.metadata.TAPSchema; import tap.metadata.TAPTable; @@ -105,7 +105,7 @@ public class Uploader { * @see DBConnection#addUploadedTable(TAPTable, tap.data.TableIterator) */ public TAPSchema upload(final TableLoader[] loaders) throws TAPException{ - TAPSchema uploadSchema = new TAPSchema(TAPDM.UPLOADSCHEMA.getLabel()); + TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.getLabel()); InputStream votable = null; String tableName = null; try{ @@ -137,6 +137,7 @@ public class Uploader { // Close the VOTable stream: votable.close(); + votable = null; } }catch(DataReadException dre){ if (dre.getCause() instanceof ExceededSizeException) diff --git a/test/adql/IdentifierFieldTest.java b/test/adql/IdentifierFieldTest.java new file mode 100644 index 0000000..8caeceb --- /dev/null +++ b/test/adql/IdentifierFieldTest.java @@ -0,0 +1,25 @@ +package adql; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import adql.query.IdentifierField; + +public class IdentifierFieldTest { + + @Test + public void testIsCaseSensitive(){ + byte b = 0x00; + assertFalse(IdentifierField.SCHEMA.isCaseSensitive(b)); + b = IdentifierField.SCHEMA.setCaseSensitive(b, true); + assertTrue(IdentifierField.SCHEMA.isCaseSensitive(b)); + } + + /*@Test + public void testSetCaseSensitive(){ + fail("Not yet implemented"); + }*/ + +} diff --git a/test/adql/SearchColumnListTest.java b/test/adql/SearchColumnListTest.java index 0a3b48d..b2e5988 100644 --- a/test/adql/SearchColumnListTest.java +++ b/test/adql/SearchColumnListTest.java @@ -8,6 +8,7 @@ import java.util.Map; import tap.metadata.TAPColumn; import tap.metadata.TAPSchema; import tap.metadata.TAPTable; +import tap.metadata.TAPTable.TableType; import tap.metadata.TAPType; import tap.metadata.TAPType.TAPDatatype; import adql.db.DBColumn; @@ -25,10 +26,10 @@ public class SearchColumnListTest { /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */ // Describe the available table: - TAPTable tableA = new TAPTable("A", "TABLE", "NATURAL JOIN Test table", null); - TAPTable tableB = new TAPTable("B", "TABLE", "NATURAL JOIN Test table", null); - TAPTable tableC = new TAPTable("C", "TABLE", "NATURAL JOIN Test table", null); - TAPTable tableD = new TAPTable("D", "TABLE", "NATURAL JOIN Test table", null); + TAPTable tableA = new TAPTable("A", TableType.table, "NATURAL JOIN Test table", null); + TAPTable tableB = new TAPTable("B", TableType.table, "NATURAL JOIN Test table", null); + TAPTable tableC = new TAPTable("C", TableType.table, "NATURAL JOIN Test table", null); + TAPTable tableD = new TAPTable("D", TableType.table, "NATURAL JOIN Test table", null); // Describe its columns: tableA.addColumn(new TAPColumn("id", new TAPType(TAPDatatype.VARCHAR), "Object ID")); diff --git a/test/tap/data/ResultSetTableIteratorTest.java b/test/tap/data/ResultSetTableIteratorTest.java index 99d7057..bc78917 100644 --- a/test/tap/data/ResultSetTableIteratorTest.java +++ b/test/tap/data/ResultSetTableIteratorTest.java @@ -33,16 +33,18 @@ public class ResultSetTableIteratorTest { new ResultSetTableIterator(null); fail("The constructor should have failed, because: the given ResultSet is NULL."); }catch(Exception ex){ - assertEquals(ex.getClass().getName(), "java.lang.NullPointerException"); + assertEquals("java.lang.NullPointerException", ex.getClass().getName()); + assertEquals("Missing ResultSet object over which to iterate!", ex.getMessage()); } } @Test public void testWithData(){ + TableIterator it = null; try{ ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); - TableIterator it = new ResultSetTableIterator(rs); + it = new ResultSetTableIterator(rs); // TEST there is column metadata before starting the iteration: assertTrue(it.getMetadata() != null); final int expectedNbLines = 10, expectedNbColumns = 4; @@ -68,15 +70,22 @@ public class ResultSetTableIteratorTest { }catch(Exception ex){ ex.printStackTrace(System.err); fail("An exception occurs while reading a correct ResultSet (containing some valid rows)."); + }finally{ + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } } } @Test public void testWithEmptySet(){ + TableIterator it = null; try{ ResultSet rs = DBTools.select(conn, "SELECT * FROM gums WHERE id = 'foo';"); - TableIterator it = new ResultSetTableIterator(rs); + it = new ResultSetTableIterator(rs); // TEST there is column metadata before starting the iteration: assertTrue(it.getMetadata() != null); int countLines = 0; @@ -89,6 +98,12 @@ public class ResultSetTableIteratorTest { }catch(Exception ex){ ex.printStackTrace(System.err); fail("An exception occurs while reading a correct ResultSet (containing some valid rows)."); + }finally{ + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } } } diff --git a/test/tap/data/VOTableIteratorTest.java b/test/tap/data/VOTableIteratorTest.java index cc78ad3..1bf1bf0 100644 --- a/test/tap/data/VOTableIteratorTest.java +++ b/test/tap/data/VOTableIteratorTest.java @@ -35,9 +35,10 @@ public class VOTableIteratorTest { @Test public void testWithData(){ InputStream input = null; + TableIterator it = null; try{ input = new BufferedInputStream(new FileInputStream(dataVOTable)); - TableIterator it = new VOTableIterator(input); + it = new VOTableIterator(input); // TEST there is column metadata before starting the iteration: assertTrue(it.getMetadata() != null); final int expectedNbLines = 100, expectedNbColumns = 4; @@ -70,15 +71,21 @@ public class VOTableIteratorTest { }catch(IOException e){ e.printStackTrace(); } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } } } @Test public void testWithBinary(){ InputStream input = null; + TableIterator it = null; try{ input = new BufferedInputStream(new FileInputStream(binaryVOTable)); - TableIterator it = new VOTableIterator(input); + it = new VOTableIterator(input); // TEST there is column metadata before starting the iteration: assertTrue(it.getMetadata() != null); final int expectedNbLines = 100, expectedNbColumns = 4; @@ -111,15 +118,21 @@ public class VOTableIteratorTest { }catch(IOException e){ e.printStackTrace(); } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } } } @Test public void testWithEmptySet(){ InputStream input = null; + TableIterator it = null; try{ input = new BufferedInputStream(new FileInputStream(emptyVOTable)); - TableIterator it = new VOTableIterator(input); + it = new VOTableIterator(input); // TEST there is column metadata before starting the iteration: assertTrue(it.getMetadata() != null); int countLines = 0; @@ -139,15 +152,21 @@ public class VOTableIteratorTest { }catch(IOException e){ e.printStackTrace(); } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } } } @Test public void testWithEmptyBinarySet(){ InputStream input = null; + TableIterator it = null; try{ input = new BufferedInputStream(new FileInputStream(emptyBinaryVOTable)); - TableIterator it = new VOTableIterator(input); + it = new VOTableIterator(input); // TEST there is column metadata before starting the iteration: assertTrue(it.getMetadata() != null); int countLines = 0; @@ -167,6 +186,11 @@ public class VOTableIteratorTest { }catch(IOException e){ e.printStackTrace(); } + if (it != null){ + try{ + it.close(); + }catch(DataReadException dre){} + } } } } diff --git a/test/tap/db/JDBCConnectionTest.java b/test/tap/db/JDBCConnectionTest.java new file mode 100644 index 0000000..2cd7457 --- /dev/null +++ b/test/tap/db/JDBCConnectionTest.java @@ -0,0 +1,1015 @@ +package tap.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Iterator; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import tap.data.DataReadException; +import tap.data.TableIterator; +import tap.data.VOTableIterator; +import tap.metadata.TAPColumn; +import tap.metadata.TAPForeignKey; +import tap.metadata.TAPMetadata; +import tap.metadata.TAPMetadata.STDSchema; +import tap.metadata.TAPMetadata.STDTable; +import tap.metadata.TAPSchema; +import tap.metadata.TAPTable; +import tap.metadata.TAPType; +import tap.metadata.TAPType.TAPDatatype; +import testtools.DBTools; +import adql.db.DBChecker; +import adql.db.DBColumn; +import adql.db.DBTable; +import adql.parser.ADQLParser; +import adql.parser.ParseException; +import adql.query.ADQLQuery; +import adql.query.IdentifierField; +import adql.translator.PostgreSQLTranslator; + +public class JDBCConnectionTest { + + private static Connection pgConnection; + private static JDBCConnection pgJDBCConnection; + private static JDBCConnection sensPgJDBCConnection; + + private static Connection sqliteConnection; + private static JDBCConnection sqliteJDBCConnection; + private static JDBCConnection sensSqliteJDBCConnection; + + private static String uploadExamplePath; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + + String projectDir = (new File("")).getAbsolutePath(); + uploadExamplePath = projectDir + "/test/tap/db/upload_example.vot"; + + final String sqliteDbFile = projectDir + "/test/tap/db/TestTAPDb.db"; + + pgConnection = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"); + pgJDBCConnection = new JDBCConnection(pgConnection, new PostgreSQLTranslator(false), "POSTGRES", null); + sensPgJDBCConnection = new JDBCConnection(pgConnection, new PostgreSQLTranslator(true, true, true, true), "SensitivePSQL", null); + + sqliteConnection = DBTools.createConnection("sqlite", null, null, sqliteDbFile, null, null); + sqliteJDBCConnection = new JDBCConnection(sqliteConnection, new PostgreSQLTranslator(false), "SQLITE", null); + sensSqliteJDBCConnection = new JDBCConnection(sqliteConnection, new PostgreSQLTranslator(true), "SensitiveSQLite", null); + + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + dropSchema(STDSchema.TAPSCHEMA.label, conn); + dropSchema(STDSchema.UPLOADSCHEMA.label, conn); + } + pgConnection.close(); + sqliteConnection.close(); + } + + /* ***** */ + /* TESTS */ + /* ***** */ + + @Test + public void testGetTAPSchemaTablesDef(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + TAPMetadata meta = createCustomSchema(); + TAPTable customColumns = meta.getTable(STDSchema.TAPSCHEMA.toString(), STDTable.COLUMNS.toString()); + TAPTable[] tapTables = conn.mergeTAPSchemaDefs(meta); + TAPSchema stdSchema = TAPMetadata.getStdSchema(); + assertEquals(5, tapTables.length); + assertTrue(equals(tapTables[0], stdSchema.getTable(STDTable.SCHEMAS.label))); + assertEquals(customColumns.getSchema(), tapTables[0].getSchema()); + assertTrue(equals(tapTables[1], stdSchema.getTable(STDTable.TABLES.label))); + assertEquals(customColumns.getSchema(), tapTables[1].getSchema()); + assertTrue(equals(tapTables[2], customColumns)); + assertTrue(equals(tapTables[3], stdSchema.getTable(STDTable.KEYS.label))); + assertEquals(customColumns.getSchema(), tapTables[3].getSchema()); + assertTrue(equals(tapTables[4], stdSchema.getTable(STDTable.KEY_COLUMNS.label))); + assertEquals(customColumns.getSchema(), tapTables[4].getSchema()); + } + } + + @Test + public void testSetTAPSchema(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + short cnt = -1; + while(cnt < 1){ + /* NO CUSTOM DEFINITION */ + // Prepare the test: + if (cnt == -1) + dropSchema(STDSchema.TAPSCHEMA.label, conn); + else + createTAPSchema(conn); + // Do the test: + try{ + TAPMetadata meta = new TAPMetadata(); + int[] expectedCounts = getStats(meta); + conn.setTAPSchema(meta); + int[] effectiveCounts = getStats(conn, meta); + for(int i = 0; i < expectedCounts.length; i++) + assertEquals(expectedCounts[i], effectiveCounts[i]); + }catch(DBException dbe){ + dbe.printStackTrace(System.err); + fail("[" + conn.getID() + ";no def] No error should happen here ; when an empty list of metadata is given, at least the TAP_SCHEMA should be created and filled with a description of itself."); + } + + /* CUSTOM DEFINITION */ + // Prepare the test: + if (cnt == -1) + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Do the test: + try{ + TAPMetadata meta = createCustomSchema(); + int[] expectedCounts = getStats(meta); + conn.setTAPSchema(meta); + int[] effectiveCounts = getStats(conn, meta); + for(int i = 0; i < expectedCounts.length; i++) + assertEquals(expectedCounts[i], effectiveCounts[i]); + }catch(DBException dbe){ + dbe.printStackTrace(System.err); + fail("[" + conn.getID() + ";custom def] No error should happen here!"); + } + + cnt++; + } + } + } + + @Test + public void testGetCreationOrder(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + assertEquals(-1, conn.getCreationOrder(null)); + assertEquals(0, conn.getCreationOrder(STDTable.SCHEMAS)); + assertEquals(1, conn.getCreationOrder(STDTable.TABLES)); + assertEquals(2, conn.getCreationOrder(STDTable.COLUMNS)); + assertEquals(3, conn.getCreationOrder(STDTable.KEYS)); + assertEquals(4, conn.getCreationOrder(STDTable.KEY_COLUMNS)); + } + } + + @Test + public void testGetDBMSDatatype(){ + assertEquals("VARCHAR", pgJDBCConnection.getDBMSDatatype(null)); + assertEquals("TEXT", sqliteJDBCConnection.getDBMSDatatype(null)); + + assertEquals("bytea", pgJDBCConnection.getDBMSDatatype(new TAPType(TAPDatatype.VARBINARY))); + assertEquals("BLOB", sqliteJDBCConnection.getDBMSDatatype(new TAPType(TAPDatatype.VARBINARY))); + } + + @Test + public void testMergeTAPSchemaDefs(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + + // TEST WITH NO METADATA OBJECT: + // -> expected: throws a NULL exception. + try{ + conn.mergeTAPSchemaDefs(null); + }catch(Exception e){ + assertEquals(NullPointerException.class, e.getClass()); + } + + // TEST WITH EMPTY METADATA OBJECT: + // -> expected: returns at least the 5 tables of the TAP_SCHEMA. + TAPTable[] stdTables = conn.mergeTAPSchemaDefs(new TAPMetadata()); + + assertEquals(5, stdTables.length); + + for(TAPTable t : stdTables) + assertEquals(STDSchema.TAPSCHEMA.toString(), t.getADQLSchemaName()); + + assertEquals(STDTable.SCHEMAS.toString(), stdTables[0].getADQLName()); + assertEquals(STDTable.TABLES.toString(), stdTables[1].getADQLName()); + assertEquals(STDTable.COLUMNS.toString(), stdTables[2].getADQLName()); + assertEquals(STDTable.KEYS.toString(), stdTables[3].getADQLName()); + assertEquals(STDTable.KEY_COLUMNS.toString(), stdTables[4].getADQLName()); + + // TEST WITH INCOMPLETE TAP_SCHEMA TABLES LIST + 1 CUSTOM TAP_SCHEMA TABLE (here: TAP_SCHEMA.columns): + // -> expected: the 5 tables of the TAP_SCHEMA including the modification of the standard tables & ignore the additional table(s) if any (which is the case here). + TAPMetadata customMeta = createCustomSchema(); + stdTables = conn.mergeTAPSchemaDefs(customMeta); + + assertEquals(5, stdTables.length); + + for(TAPTable t : stdTables) + assertEquals(STDSchema.TAPSCHEMA.toString(), t.getADQLSchemaName()); + + assertEquals(STDTable.SCHEMAS.toString(), stdTables[0].getADQLName()); + assertEquals(STDTable.TABLES.toString(), stdTables[1].getADQLName()); + assertEquals(STDTable.COLUMNS.toString(), stdTables[2].getADQLName()); + assertEquals("Columns", stdTables[2].getDBName()); + assertNotNull(stdTables[2].getColumn("TestNewColumn")); + assertEquals(STDTable.KEYS.toString(), stdTables[3].getADQLName()); + assertEquals(STDTable.KEY_COLUMNS.toString(), stdTables[4].getADQLName()); + } + } + + @Test + public void testEquals(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + // NULL tests: + assertFalse(conn.equals("tap_schema", null, false)); + assertFalse(conn.equals("tap_schema", null, true)); + assertFalse(conn.equals(null, "tap_schema", false)); + assertFalse(conn.equals(null, "tap_schema", true)); + assertFalse(conn.equals(null, null, false)); + assertFalse(conn.equals(null, null, true)); + + // CASE SENSITIVE tests: + if (conn.supportsMixedCaseQuotedIdentifier || conn.mixedCaseQuoted){ + assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertFalse(conn.equals("Columns", "columns", true)); + assertFalse(conn.equals("columns", "Columns", true)); + }else if (conn.lowerCaseQuoted){ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertFalse(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertFalse(conn.equals("Columns", "columns", true)); + assertTrue(conn.equals("columns", "Columns", true)); + }else if (conn.upperCaseQuoted){ + assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertFalse(conn.equals("Columns", "columns", true)); + assertFalse(conn.equals("columns", "Columns", true)); + }else{ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", true)); + assertTrue(conn.equals("Columns", "columns", true)); + assertTrue(conn.equals("columns", "Columns", true)); + } + + // CASE INSENSITIVE tests: + if (conn.supportsMixedCaseUnquotedIdentifier){ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertTrue(conn.equals("Columns", "columns", false)); + assertTrue(conn.equals("columns", "Columns", false)); + }else if (conn.lowerCaseUnquoted){ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertFalse(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertFalse(conn.equals("Columns", "columns", false)); + assertTrue(conn.equals("columns", "Columns", false)); + }else if (conn.upperCaseUnquoted){ + assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertFalse(conn.equals("Columns", "columns", false)); + assertFalse(conn.equals("columns", "Columns", false)); + }else{ + assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false)); + assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false)); + assertTrue(conn.equals("Columns", "columns", false)); + assertTrue(conn.equals("columns", "Columns", false)); + } + } + } + + @Test + public void testGetTAPSchema(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + try{ + // Prepare the test: + createTAPSchema(conn); + // Try to get it (which should work without any problem here): + conn.getTAPSchema(); + }catch(DBException de){ + de.printStackTrace(System.err); + fail("No pbm should happen here (either for the creation of a std TAP_SCHEMA or for its reading)! CAUSE: " + de.getMessage()); + } + + try{ + // Prepare the test: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Try to get it (which should work without any problem here): + conn.getTAPSchema(); + fail("DBException expected, because none of the TAP_SCHEMA tables exist."); + }catch(DBException de){ + assertTrue(de.getMessage().equals("Impossible to load schemas from TAP_SCHEMA.schemas!")); + } + } + } + + @Test + public void testIsTableExisting(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + try{ + // Get the database metadata: + DatabaseMetaData dbMeta = conn.connection.getMetaData(); + + // Prepare the test: + createTAPSchema(conn); + // Test the existence of all TAP_SCHEMA tables: + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label, dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label, dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label, dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label, dbMeta)); + assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label, dbMeta)); + // Test the non-existence of any other table: + assertFalse(conn.isTableExisting(null, "foo", dbMeta)); + + // Prepare the test: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Test the non-existence of all TAP_SCHEMA tables: + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label, dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label, dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label, dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label, dbMeta)); + assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label, dbMeta)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.getID() + "} Testing the existence of a table should not throw an error!"); + } + } + } + + @Test + public void testAddUploadedTable(){ + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + TAPTable tableDef = null; + for(JDBCConnection conn : connections){ + InputStream io = null; + try{ + io = new FileInputStream(uploadExamplePath); + TableIterator it = new VOTableIterator(io); + + TAPColumn[] cols = it.getMetadata(); + tableDef = new TAPTable("UploadExample"); + for(TAPColumn c : cols) + tableDef.addColumn(c); + + // Prepare the test: no TAP_UPLOAD schema and no table TAP_UPLOAD.UploadExample: + dropSchema(STDSchema.UPLOADSCHEMA.label, conn); + // Test: + try{ + assertTrue(conn.addUploadedTable(tableDef, it)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should not happen: no TAP_UPLOAD schema."); + } + + close(io); + io = new FileInputStream(uploadExamplePath); + it = new VOTableIterator(io); + + // Prepare the test: the TAP_UPLOAD schema exist but not the table TAP_UPLOAD.UploadExample: + dropTable(tableDef.getDBSchemaName(), tableDef.getDBName(), conn); + // Test: + try{ + assertTrue(conn.addUploadedTable(tableDef, it)); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should not happen: no TAP_UPLOAD schema."); + } + + close(io); + io = new FileInputStream(uploadExamplePath); + it = new VOTableIterator(io); + + // Prepare the test: the TAP_UPLOAD schema and the table TAP_UPLOAD.UploadExample BOTH exist: + ; + // Test: + try{ + assertFalse(conn.addUploadedTable(tableDef, it)); + }catch(Exception ex){ + if (ex instanceof DBException) + assertEquals("Impossible to create the user uploaded table in the database: " + conn.translator.getQualifiedTableName(tableDef) + "! This table already exists.", ex.getMessage()); + else{ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} DBException was the expected exception!"); + } + } + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should never happen except there is a problem with the file (" + uploadExamplePath + ")."); + }finally{ + close(io); + } + } + } + + @Test + public void testDropUploadedTable(){ + TAPTable tableDef = new TAPTable("TableToDrop"); + TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.label); + uploadSchema.addTable(tableDef); + + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + try{ + // 1st TEST CASE: the schema TAP_UPLOAD does not exist -> no error should be raised! + // drop the TAP_UPLOAD schema: + dropSchema(uploadSchema.getDBName(), conn); + // try to drop the table: + assertTrue(conn.dropUploadedTable(tableDef)); + + // 2nd TEST CASE: the table does not exists -> no error should be raised! + // create the TAP_UPLOAD schema, but not the table: + createSchema(uploadSchema.getDBName(), conn); + // try to drop the table: + assertTrue(conn.dropUploadedTable(tableDef)); + + // 3rd TEST CASE: the table and the schema exist -> the table should be created without any error! + // create the fake uploaded table: + createFooTable(tableDef.getDBSchemaName(), tableDef.getDBName(), conn); + // try to drop the table: + assertTrue(conn.dropUploadedTable(tableDef)); + + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} This error should not happen. The table should be dropped and even if it does not exist, no error should be thrown."); + } + } + } + + @Test + public void testExecuteQuery(){ + TAPSchema schema = TAPMetadata.getStdSchema(); + ArrayList<DBTable> tables = new ArrayList<DBTable>(schema.getNbTables()); + for(TAPTable t : schema) + tables.add(t); + + ADQLParser parser = new ADQLParser(new DBChecker(tables)); + parser.setDebug(false); + + // There should be no difference between a POSTGRESQL connection and a SQLITE one! + JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection}; + for(JDBCConnection conn : connections){ + if (conn.ID.equalsIgnoreCase("SQLITE")){ + for(DBTable t : tables){ + TAPTable tapT = (TAPTable)t; + tapT.getSchema().setDBName(null); + tapT.setDBName(tapT.getSchema().getADQLName() + "_" + tapT.getDBName()); + } + } + + TableIterator result = null; + try{ + // Prepare the test: create the TAP_SCHEMA: + dropSchema(STDSchema.TAPSCHEMA.label, conn); + // Build the ADQLQuery object: + ADQLQuery query = parser.parseQuery("SELECT table_name FROM TAP_SCHEMA.tables;"); + // Execute the query: + result = conn.executeQuery(query); + fail("{" + conn.ID + "} This test should have failed because TAP_SCHEMA was supposed to not exist!"); + }catch(DBException de){ + assertTrue(de.getMessage().startsWith("Unexpected error while executing a SQL query: ")); + assertTrue(de.getMessage().indexOf("tap_schema") > 0 || de.getMessage().indexOf("TAP_SCHEMA") > 0); + }catch(ParseException pe){ + pe.printStackTrace(System.err); + fail("There should be no pbm to parse the ADQL expression!"); + }finally{ + if (result != null){ + try{ + result.close(); + }catch(DataReadException de){} + result = null; + } + } + + try{ + // Prepare the test: create the TAP_SCHEMA: + createTAPSchema(conn); + // Build the ADQLQuery object: + ADQLQuery query = parser.parseQuery("SELECT table_name FROM TAP_SCHEMA.tables;"); + // Execute the query: + result = conn.executeQuery(query); + assertEquals(1, result.getMetadata().length); + int cntRow = 0; + while(result.nextRow()){ + cntRow++; + assertTrue(result.hasNextCol()); + assertNotNull(TAPMetadata.resolveStdTable((String)result.nextCol())); + assertFalse(result.hasNextCol()); + } + assertEquals(5, cntRow); + }catch(DBException de){ + de.printStackTrace(System.err); + fail("No ADQL/SQL query error was expected here!"); + }catch(ParseException pe){ + fail("There should be no pbm to parse the ADQL expression!"); + }catch(DataReadException e){ + e.printStackTrace(System.err); + fail("There should be no pbm when accessing rows and the first (and only) columns of the result!"); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("There should be no pbm when reading the query result!"); + }finally{ + if (result != null){ + try{ + result.close(); + }catch(DataReadException de){} + result = null; + } + } + } + } + + /* ************** */ + /* TOOL FUNCTIONS */ + /* ************** */ + + public final static void main(final String[] args) throws Throwable{ + JDBCConnection conn = new JDBCConnection(DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"), new PostgreSQLTranslator(), "TEST_POSTGRES", null); + JDBCConnectionTest.createTAPSchema(conn); + JDBCConnectionTest.dropSchema(STDSchema.TAPSCHEMA.label, conn); + } + + private static void dropSchema(final String schemaName, final JDBCConnection conn){ + Statement stmt = null; + ResultSet rs = null; + try{ + stmt = conn.connection.createStatement(); + + final boolean caseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + if (conn.supportsSchema) + stmt.executeUpdate("DROP SCHEMA IF EXISTS " + formatIdentifier(schemaName, caseSensitive) + " CASCADE;"); + else{ + startTransaction(conn); + final String tablePrefix = conn.getTablePrefix(schemaName); + final int prefixLen = tablePrefix.length(); + if (prefixLen <= 0) + return; + rs = conn.connection.getMetaData().getTables(null, null, null, null); + ArrayList<String> tablesToDrop = new ArrayList<String>(); + while(rs.next()){ + String table = rs.getString(3); + if (table.length() > prefixLen){ + if (equals(schemaName, table.substring(0, prefixLen - 1), caseSensitive)) + tablesToDrop.add(table); + } + } + close(rs); + rs = null; + for(String t : tablesToDrop) + stmt.executeUpdate("DROP TABLE IF EXISTS \"" + t + "\";"); + commit(conn); + } + }catch(Exception ex){ + rollback(conn); + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: dropping the schema " + schemaName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void dropTable(final String schemaName, final String tableName, final JDBCConnection conn){ + Statement stmt = null; + ResultSet rs = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + stmt = conn.connection.createStatement(); + if (conn.supportsSchema) + stmt.executeUpdate("DROP TABLE IF EXISTS " + formatIdentifier(schemaName, sCaseSensitive) + "." + formatIdentifier(tableName, tCaseSensitive) + ";"); + else{ + final String tablePrefix = conn.getTablePrefix(schemaName); + final int prefixLen = tablePrefix.length(); + rs = conn.connection.getMetaData().getTables(null, null, null, null); + String tableToDrop = null; + while(rs.next()){ + String table = rs.getString(3); + if (prefixLen <= 0 && equals(tableName, table, tCaseSensitive)){ + tableToDrop = table; + break; + }else if (prefixLen > 0 && table.length() > prefixLen){ + if (equals(schemaName, table.substring(0, prefixLen - 1), sCaseSensitive) && equals(tableName, table.substring(prefixLen + 1), tCaseSensitive)){ + tableToDrop = table; + break; + } + } + } + close(rs); + if (tableToDrop != null) + stmt.executeUpdate("DROP TABLE IF EXISTS \"" + tableToDrop + "\";"); + } + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: dropping the table " + schemaName + "." + tableName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void createSchema(final String schemaName, final JDBCConnection conn){ + if (!conn.supportsSchema) + return; + + dropSchema(schemaName, conn); + + Statement stmt = null; + ResultSet rs = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + stmt = conn.connection.createStatement(); + stmt.executeUpdate("CREATE SCHEMA " + formatIdentifier(schemaName, sCaseSensitive) + ";"); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: creating the schema " + schemaName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void createFooTable(final String schemaName, final String tableName, final JDBCConnection conn){ + dropTable(schemaName, tableName, conn); + + Statement stmt = null; + ResultSet rs = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + String tablePrefix = formatIdentifier(schemaName, sCaseSensitive); + if (tablePrefix == null) + tablePrefix = ""; + else + tablePrefix += (conn.supportsSchema ? "." : "_"); + stmt = conn.connection.createStatement(); + stmt.executeUpdate("CREATE TABLE " + tablePrefix + formatIdentifier(tableName, tCaseSensitive) + " (ID integer);"); + }catch(Exception ex){ + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: creating the table " + schemaName + "." + tableName + "!"); + }finally{ + close(rs); + close(stmt); + } + } + + private static void createTAPSchema(final JDBCConnection conn){ + dropSchema(STDSchema.TAPSCHEMA.label, conn); + + Statement stmt = null; + try{ + final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA); + final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + String[] tableNames = new String[]{STDTable.SCHEMAS.label,STDTable.TABLES.label,STDTable.COLUMNS.label,STDTable.KEYS.label,STDTable.KEY_COLUMNS.label}; + if (conn.supportsSchema){ + for(int i = 0; i < tableNames.length; i++) + tableNames[i] = formatIdentifier(STDSchema.TAPSCHEMA.label, sCaseSensitive) + "." + formatIdentifier(tableNames[i], tCaseSensitive); + }else{ + for(int i = 0; i < tableNames.length; i++) + tableNames[i] = formatIdentifier(conn.getTablePrefix(STDSchema.TAPSCHEMA.label) + tableNames[i], tCaseSensitive); + } + + startTransaction(conn); + + stmt = conn.connection.createStatement(); + + if (conn.supportsSchema) + stmt.executeUpdate("CREATE SCHEMA " + formatIdentifier(STDSchema.TAPSCHEMA.label, sCaseSensitive) + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[0] + "(\"schema_name\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"schema_name\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[0] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[1] + "(\"schema_name\" VARCHAR,\"table_name\" VARCHAR,\"table_type\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"schema_name\", \"table_name\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[1] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[2] + "(\"table_name\" VARCHAR,\"column_name\" VARCHAR,\"description\" VARCHAR,\"unit\" VARCHAR,\"ucd\" VARCHAR,\"utype\" VARCHAR,\"datatype\" VARCHAR,\"size\" INTEGER,\"principal\" INTEGER,\"indexed\" INTEGER,\"std\" INTEGER, PRIMARY KEY(\"table_name\", \"column_name\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[2] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[3] + "(\"key_id\" VARCHAR,\"from_table\" VARCHAR,\"target_table\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"key_id\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[3] + ";"); + + stmt.executeUpdate("CREATE TABLE " + tableNames[4] + "(\"key_id\" VARCHAR,\"from_column\" VARCHAR,\"target_column\" VARCHAR, PRIMARY KEY(\"key_id\"));"); + stmt.executeUpdate("DELETE FROM " + tableNames[4] + ";"); + + TAPMetadata metadata = new TAPMetadata(); + metadata.addSchema(TAPMetadata.getStdSchema()); + + ArrayList<TAPTable> lstTables = new ArrayList<TAPTable>(); + for(TAPSchema schema : metadata){ + stmt.executeUpdate("INSERT INTO " + tableNames[0] + " VALUES('" + schema.getADQLName() + "','" + schema.getDescription() + "','" + schema.getUtype() + "')"); + for(TAPTable t : schema) + lstTables.add(t); + } + + ArrayList<DBColumn> lstCols = new ArrayList<DBColumn>(); + for(TAPTable table : lstTables){ + stmt.executeUpdate("INSERT INTO " + tableNames[1] + " VALUES('" + table.getADQLSchemaName() + "','" + table.getADQLName() + "','" + table.getType() + "','" + table.getDescription() + "','" + table.getUtype() + "')"); + for(DBColumn c : table) + lstCols.add(c); + + } + lstTables = null; + + for(DBColumn c : lstCols){ + TAPColumn col = (TAPColumn)c; + stmt.executeUpdate("INSERT INTO " + tableNames[2] + " VALUES('" + col.getTable().getADQLName() + "','" + col.getADQLName() + "','" + col.getDescription() + "','" + col.getUnit() + "','" + col.getUcd() + "','" + col.getUtype() + "','" + col.getDatatype().type + "'," + col.getDatatype().length + "," + (col.isPrincipal() ? 1 : 0) + "," + (col.isIndexed() ? 1 : 0) + "," + (col.isStd() ? 1 : 0) + ")"); + } + + commit(conn); + + }catch(Exception ex){ + rollback(conn); + ex.printStackTrace(System.err); + fail("{" + conn.ID + "} Impossible to prepare a test by: creating TAP_SCHEMA!"); + }finally{ + close(stmt); + } + } + + private static void startTransaction(final JDBCConnection conn){ + try{ + conn.connection.setAutoCommit(false); + }catch(SQLException se){} + } + + private static void commit(final JDBCConnection conn){ + try{ + conn.connection.commit(); + conn.connection.setAutoCommit(true); + }catch(SQLException se){} + + } + + private static void rollback(final JDBCConnection conn){ + try{ + conn.connection.rollback(); + conn.connection.setAutoCommit(true); + }catch(SQLException se){} + + } + + private static String formatIdentifier(final String identifier, final boolean caseSensitive){ + if (identifier == null) + return null; + else if (identifier.charAt(0) == '"') + return identifier; + else if (caseSensitive) + return "\"" + identifier + "\""; + else + return identifier; + } + + private static boolean equals(final String name1, final String name2, final boolean caseSensitive){ + return (name1 != null && name2 != null && (caseSensitive ? name1.equals(name2) : name1.equalsIgnoreCase(name2))); + } + + private static boolean equals(final TAPTable table1, final TAPTable table2){ + if (table1 == null || table2 == null){ + //System.out.println("[EQUALS] tables null!"); + return false; + } + + if (!table1.getFullName().equals(table2.getFullName())){ + //System.out.println("[EQUALS] tables name different: " + table1.getFullName() + " != " + table2.getFullName() + "!"); + return false; + } + + if (table1.getType() != table2.getType()){ + //System.out.println("[EQUALS] tables type different: " + table1.getType() + " != " + table2.getType() + "!"); + return false; + } + + if (table1.getNbColumns() != table2.getNbColumns()){ + //System.out.println("[EQUALS] tables length different: " + table1.getNbColumns() + " columns != " + table2.getNbColumns() + " columns!"); + return false; + } + + Iterator<TAPColumn> it = table1.getColumns(); + while(it.hasNext()){ + TAPColumn col1 = it.next(); + if (!equals(col1, table2.getColumn(col1.getADQLName()))){ + //System.out.println("[EQUALS] tables columns different!"); + return false; + } + } + + return true; + } + + private static boolean equals(final TAPColumn col1, final TAPColumn col2){ + if (col1 == null || col2 == null){ + //System.out.println("[EQUALS] columns null!"); + return false; + } + + if (!col1.getADQLName().equals(col2.getADQLName())){ + //System.out.println("[EQUALS] columns name different: " + col1.getADQLName() + " != " + col2.getADQLName() + "!"); + return false; + } + + if (!equals(col1.getDatatype(), col2.getDatatype())){ + //System.out.println("[EQUALS] columns type different: " + col1.getDatatype() + " != " + col2.getDatatype() + "!"); + return false; + } + + if (col1.getUnit() != col2.getUnit()){ + //System.out.println("[EQUALS] columns unit different: " + col1.getUnit() + " != " + col2.getUnit() + "!"); + return false; + } + + if (col1.getUcd() != col2.getUcd()){ + //System.out.println("[EQUALS] columns ucd different: " + col1.getUcd() + " != " + col2.getUcd() + "!"); + return false; + } + + return true; + } + + private static boolean equals(final TAPType type1, final TAPType type2){ + return type1 != null && type2 != null && type1.type == type2.type && type1.length == type2.length; + } + + private static TAPMetadata createCustomSchema(){ + TAPMetadata tapMeta = new TAPMetadata(); + TAPSchema tapSchema = new TAPSchema(STDSchema.TAPSCHEMA.toString()); + TAPTable customColumns = (TAPTable)TAPMetadata.getStdTable(STDTable.COLUMNS).copy("Columns", STDTable.COLUMNS.label); + customColumns.addColumn("TestNewColumn", new TAPType(TAPDatatype.VARCHAR), "This is a fake column, just for test purpose.", null, null, null); + tapSchema.addTable(customColumns); + TAPTable addTable = new TAPTable("AdditionalTable"); + addTable.addColumn("Blabla"); + addTable.addColumn("Foo"); + tapSchema.addTable(addTable); + tapMeta.addSchema(tapSchema); + return tapMeta; + } + + /** + * <p>Get the expected counts after a call of {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p> + * + * <p>Counts are computed from the given metadata ; the same metadata that will be given to {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p> + * + * @param meta + * + * @return An integer array with the following values: [0]=nbSchemas, [1]=nbTables, [2]=nbColumns, [3]=nbKeys and [4]=nbKeyColumns. + */ + private static int[] getStats(final TAPMetadata meta){ + int[] counts = new int[]{1,5,0,0,0}; + + int[] stdColCounts = new int[]{3,5,11,5,3}; + for(int c = 0; c < stdColCounts.length; c++) + counts[2] += stdColCounts[c]; + + Iterator<TAPSchema> itSchemas = meta.iterator(); + while(itSchemas.hasNext()){ + TAPSchema schema = itSchemas.next(); + + boolean isTapSchema = (schema.getADQLName().equalsIgnoreCase(STDSchema.TAPSCHEMA.toString())); + if (!isTapSchema) + counts[0]++; + + Iterator<TAPTable> itTables = schema.iterator(); + while(itTables.hasNext()){ + TAPTable table = itTables.next(); + if (isTapSchema && TAPMetadata.resolveStdTable(table.getADQLName()) != null){ + int ind = pgJDBCConnection.getCreationOrder(TAPMetadata.resolveStdTable(table.getADQLName())); + counts[2] -= stdColCounts[ind]; + }else + counts[1]++; + + Iterator<DBColumn> itColumns = table.iterator(); + while(itColumns.hasNext()){ + itColumns.next(); + counts[2]++; + } + + Iterator<TAPForeignKey> itKeys = table.getForeignKeys(); + while(itKeys.hasNext()){ + TAPForeignKey fk = itKeys.next(); + counts[3]++; + counts[4] += fk.getNbRelations(); + } + } + } + + return counts; + } + + /** + * <p>Get the effective counts after a call of {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p> + * + * <p>Counts are computed directly from the DB using the given connection; the same connection used to set the TAP schema in {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p> + * + * @param conn + * @param meta Metadata, in order to get the standard TAP tables' name. + * + * @return An integer array with the following values: [0]=nbSchemas, [1]=nbTables, [2]=nbColumns, [3]=nbKeys and [4]=nbKeyColumns. + */ + private static int[] getStats(final JDBCConnection conn, final TAPMetadata meta){ + int[] counts = new int[5]; + + Statement stmt = null; + try{ + stmt = conn.connection.createStatement(); + + TAPSchema tapSchema = meta.getSchema(STDSchema.TAPSCHEMA.toString()); + + String schemaPrefix = formatIdentifier(tapSchema.getDBName(), conn.translator.isCaseSensitive(IdentifierField.SCHEMA)); + if (schemaPrefix == null) + schemaPrefix = ""; + else + schemaPrefix += (conn.supportsSchema ? "." : "_"); + + boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE); + TAPTable tapTable = tapSchema.getTable(STDTable.SCHEMAS.toString()); + counts[0] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.TABLES.toString()); + counts[1] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.COLUMNS.toString()); + counts[2] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.KEYS.toString()); + counts[3] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + tapTable = tapSchema.getTable(STDTable.KEY_COLUMNS.toString()); + counts[4] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName()); + + }catch(SQLException se){ + fail("Can not create a statement!"); + }finally{ + try{ + if (stmt != null) + stmt.close(); + }catch(SQLException ex){} + } + return counts; + } + + private static int count(final Statement stmt, final String qualifiedTableName, final String adqlTableName){ + ResultSet rs = null; + try{ + rs = stmt.executeQuery("SELECT COUNT(*) FROM " + qualifiedTableName + ";"); + rs.next(); + return rs.getInt(1); + }catch(Exception e){ + e.printStackTrace(System.err); + fail("Can not count! Maybe " + qualifiedTableName + " (in ADQL: " + adqlTableName + ") does not exist."); + return -1; + }finally{ + close(rs); + } + } + + private static void close(final ResultSet rs){ + if (rs == null) + return; + try{ + rs.close(); + }catch(SQLException se){} + } + + private static void close(final Statement stmt){ + try{ + if (stmt != null) + stmt.close(); + }catch(SQLException se){} + } + + private static void close(final InputStream io){ + try{ + if (io != null) + io.close(); + }catch(IOException ioe){} + } + +} diff --git a/test/tap/db/TestTAPDb.db b/test/tap/db/TestTAPDb.db new file mode 100644 index 0000000000000000000000000000000000000000..227549965a7b445848f09562dc48441528f00dfd GIT binary patch literal 23552 zcmeHPeQX=YmEYN2iTdE`n_5#F+e5Ocqf(|xO0+E}PDsgeEL*Z<NmiR0S9K+=tc@v> z=8{TW+y#}dxfVr`e*DqqkG4S3rYX?$E5Y6MfTnQk-l0H&0+(x%;8Ns@>!Cfg2V6hX z9(qOY&CKp{NKuyLYf?K|LMtw3XMXeM&CHwk-n`k_a}#FCz$^LUnqI<#f-E2;q%jr* zsZ9_B6|PQpwLA&mGF;9FIX7E>1IEFBKcGmmAfjgk^o;le@dwhg(zEEV(6{+gSDRXW z>ha^~uOlUWaU;CPrzR3;BM(2a#$0B4VOEk+tDAwPou8hV%A8m@U&!Y5<*|qLwZhiv z9iua2nYl4Mml>HD!(jqhuXq^W6<(OhoKD8$@i2xW69eIe6Ju4s3sxA<jlE-TAbeI| zGo1TVC&GCA?A+M>V>1KcQ#0I;`8EBbgXKaq02i74oGmzL>z0`_IgD9zZBbulPy4Up zJAA69q4%kjJ)`+-c`awzUrp5wwNt})g=yY}97pmJEm)PbY*<T0vrsbgIfs;T&Mdk2 zOEwJy;d1Fp!EOAqUefvBmiZod=CY$uG;>R4LC<n&W^UPd*Z?>;msR3_%^6ndtnm<| z->x4|&x}uIW*)$2#vZ6@&;Snejp^?{xVKTwXh@*7SeLUf!WZ;%DNlbd*i2bS*f-5E z68~2n9!48hWx$P|8k@{4Tr#dKFldVo!%eAe)%>oX3D{47OqY8r#r&F$>+rUu7cUy6 z>cr{>+YtTzK<?zi@K&v>Pr^c?etNq?rXkE~=@;b2`*<AiIPmA-z&dIO72(>L6)H1z z)+pip3Z_!Y!dcTQnYoKt&n;snsTax5(yD>2rB!21w+5W7B#SM#aG|^iC0C7QZ02yO z4ap&atD=Q8mdyWO{W)6g)#-7-<G}070YwTmS451v{{Nlypmaif82tmf_w}XccCPJ8 zqc5bX8)KY6wZ$%#*%KhKi2!#*G2H6etRtj}JZDn5(j75sq0CYlUoE&-O$*GIX>1wA zb#uvxZQRq~3q{ncqg>sbx>~89)ClICVP4E($Y9~catRlT`E?T(5&U2oNF+;$Lu1y= zT>`2Wuh3MkU<}XY@ugh;A^L#g)5U4ku-wuEK%PO?8LmaN&N5guY^h>uY;0p(_JsoK z)l1dnGYy>bZHIKbi{rLrKeTEtt-9?b>nbJP2GENJtgr-Ri$*wx0N&J=w@8hlC|Jss zMrW3nO=3mq**Rju(kf1ijWAYBE|<M()2Gf*vD0c+?b^;ig&IRa^&*GGT2TeXlD0u| z*Z;qj^3vZ(pO(Ha{rq;Gksj?H2X0{w^vWR(Rpf*%bQ}BRP=rh7<ak53912uFvgCbo zD9WYPNE@l95?iE*J#r|>t^7<pkoCVocuEkSBHlSk2JhT+(sMiqxAo02r-)rkRqibv zay!dKU4P6^x9bAcenHRwM|-=<*~i9SRnnW(|F<!KeYcVS?@Xvs{nPpXL`9I$%L00t z&VBIBOWzcxgemkqdP2M)wupV`_u_}er^TZ9y!c0{Q~arTU0RVIk>;eQq@RJd@D1r> zoV56p(gwKl#<@P9dgu_kdXA3Zis)Qm@@rFqyroB$6#!=}6IE3sfAjQUDZE>|-t6F> zS9@#uI>0~8;rI7vv_|#(A=`mF8f;YGp4fpm*A}o<o?JUk+KNlNcZB~=jYiSc5T%#n zEm7iYe)_j$G|61w=S^X2T;5e_yW%l3CBoDre&4z14tPi?t70HYP~Ob*zX2whD7`|a za6tNl^osa|I4%0oH_%6si3Ww=2+s=Z!q^SB-mW$sYF7g4A%M_OQ&Y1*m`XQ@=s#v8 zg7pf?^O>2^Q<<4p?`~59k-Mu3a6Ql6)+>CUq6F^DR28;&Pv}ruDQB}+@a3|eHCIfq zWN9KMJ6}%i$NfqmR1H9PWOi75s8tCBkI?#?n^|k2X|TNmdu5l)25Bb-+d5Yw^%tE= z540%JZYV@~)FjmCY$x-1C*#|im1uCwj3ZevD0L_AM3bVaOEze;V2cCF_VaTOOpjr6 z#r6$@-CHagRw18Tws0wrbxd~c@LGP^$U0~vK1GX+)}nE}u~;;X72EercL_=Nuu2__ zlZ}9JhGG!r*c7oZDskB@?T-Om$Xk}V$aj~6bGktZs=7-H?HV?y+=j7^-9fw3vJ#9` z`kP^G*DRXLq2{Qh1Ve*0)X*FYWCh6Q4B*5uHq-|2x@kP*l$;WwBx?uZ2dkTvBtw$k zQD$J>!e$9utNC&k+%E>W8JKejh7_>fasVe0H0FZKU<SzXx3V3;V9ChF7114|S)q8( zUijDDPVE1E=n+90mO|(eaTNVnjH1t@ukU94xA#>mmGJwfwc^%sqjW*VveYbm|Bbe~ zLz_n3)qT_EGsh(8x}dBbK(-NBwdK99UDmptC$KfXw_AoSe><nWd45Mg*7iGT?k2m| zA!`9AmF&d#^~l;@C&AgO4|d9$>LgNmv4^j*ytJ(HI3x<7SCzGhQ-qs60Y05GDtdZl zE#%~{jQH-oa^wIa0@H3y)y{X9CPyOtQ*NugvsaD;`DaY(cC^U>l_WFq(B2~lD1yb& z*cOljchWrkN}+Vife=lth<|^(9H5n0482vAqupq;v~KB>qruvACY+loZ`qV&G&NBc z*(n@ZK0ckB!;KwsP(^HN)6Qw=m4gvGfh#Wu<Y357W*jEcNZOQ2B#j8Ek!FU;Rpt2q zVK%DLAH@F@KPk?O-RPgtN6;+l7G40?z$JLL-RGp=4-0xA5TF_W=?soc`}SCyKT=sY z3484kz&L%w)ib8}ISqs0Q@+rD^>l<|lpdh^2J{dIZN0Mhw)#2!g4D$in(ft#ds_UQ zdO^xphS&C`2b=wzc0o$RwViTtrpeFg0sPQ&S4)<-6Ez^?^K)u|4aRln4zz%=Mu2d& z02_jR?M4Y0Z}4+U05>1pR?)PbH0nOt&*=aRYP&YkT+X_CMDlYg00W@^x6=U5icrwi z0QiD8rvOYKXo;%;Fg%X6J6ivBqE5i<=xuZS|KhsXE4~Z8g#KG>K;IP~7mtgRVpO~? z{+sxu)F8#B6H=e}$_*_4?IeWv{I%u)zkq|vpw&?s`s%6-L4K(xD#M<7DucR7W$5K= z1eGDc;}Dghhpun7l;>t!H=oX_3|;kAhRz$R3>`O68QSZs3~kjagHlgr@YhutTI;9` zEj22`?YjQI*5lK|_c-8j;1=V6v;N;=WO?Lx9Pl{sI&i?T|D~@=@0Q*w{zrZP|2Bkp z{{QVwbkF~<@5c1^{Qs|||DW3bbSKVR|F@BC-eZpgZ(t6%>;G4z1*t{+BE<YOp{LQ? zg;#|y++xk1X;FN>c1;p}q5Y~DOqW0Y^kx4uA4`w*KlfzkCw^4OubDWLTb5?dgpZ~D zzx~#Gf&Szmjwf&`6-z{ik~lGp2Zu%ukB^TdnVqSfZ?0hOCzuUEBl<7b?|Y?Ld|CMC zzb_O_j5C*u(!Cc#7t&pyd4aGbnZ(HyP8^P<qJRy*29J&;0Q2DZynzN7?rc*9Hwd`7 z7YFY7)PG&OmMndFN_pR8VQJOGQ-xJ=<VE!->CP|p5Ly$5aq<X|8yh^BI7)7liIGGs zHE#%yG0=DVDxe1dbnTZLFTC`d4_q5~{MVBY_U<bT9mROUTohyPS5xWkNB3PLpedX> ziW3xc2#Cf*sgc2B<MYRigK#D+n?)aH*ykE;Y!uob0qpaArymPUJdsYl<?#<*e)++| zd*CSAoL&|_GUHp9;we0M1ezT?98D&0{3zjS0$R@?g==>`fZlKu(+<WvB9sr<Y2 z{V$#T;a@C$fRa0H6pcCgf6^Uy{gzN0AEIc(z}lfB7<z2z$jA^d;5!WQf?Pq20^;z` zNH;y79)HKdA1T`9LIJo6i2Xh27wL}szk~^50uK%1!Qt3YG!@4~&|h)F)Vt=7_1AD< zR;qw&06y?ev3%#r*7Vs6*Y)o7RN<n&hDUWP(1n`P(zibJZUP?1Dd1=_mW&<-a5C0L z;;|HPLih=z7`Ff2F&hwI96Z_6)$n!k+R^B7-*?Wu%OYcTb}293`<VL8bl2cu8c@kN zh!gH64kn??;=qzjYJ8jlo<$WZ3E=5){CraX%k<>$FI?+8{9`M53ImmEvr)M<-SP8B zF`;r0Cx>x-2<AX)m=K&yjSP?L3=f`@PX}DBaP`6kCm95y7LXl!2`&*X5+($v1O*>l zjc^gM;fJdoF7loLQr`P`9Pl`Bn{j|l<OUl5D=OkYk{ICItO0K#a-}Z|Un6nqR~zWA zFBeKgLImt>wyM-!#{X`l_W5qd_}}CE8&xm<m%ZJjs{h?lu~ifveg8+hFeYr{|0B8q z(Fw@AQkvj$eltD5^Z$ANzYL4)_TqnUhxp%$|Bp=IHsP=!U6;yIMEqCrbK;`-R`g5s zHDsZf@S^Z2Jlyn|+^cBaCM2_fe|zbHpc5TKuv~6gFFNlZxf>_&nxW?)=)71q25{W| zwW4P&!+!bGtLN@eG;I@{9p<X%;6OQjTa<lvdq<FSnxgH$R7;^7C9A^;c>AqXyFnf8 zQ?$TrEh-6_$r`zfrBxD6xULsXa_Ae|NlwVE<+OG*sAzl7)PgwKGAGVW(Aazu%1ML5 zb8x7Bi3Tes!5d)Dvm<*T@_)D%gvLX%;5eX!bqhHIpy%A97lSHvU20RorRDRZCkXj< z3mxl)c2^?k?fxbqKI9??{^zGEJ<nby=TJHls5(_(m*t{?>c$hYYz{`U2QVU(Xc9$l zW3Vr}tH5rnPI=tlt!SaC+A=I;uO{-YHb$NW)j5?<cPWtrl}_h$t*X#vNOzb=;OX0T z;Jpfb?5Kk{(Wyitj5PbajHA7A;m%v&RbojUw7WYLF0PiGzH!jV?Ka0!$oxmr=LPul zJ{|`=4&0&~nBS*p2QC6}WW{ZABw*E<kvZ~~5ct#aihjv?CCdPN!wZOHT!MEh!rMTK zaOld2Il_aj|Ca>m8EH}K_u~KIaH1Fg@5TSKgCRtXb0*3gFaDqI|C9Nz2=5i(-2Wm( z`2SS=tZ0fU^hbCPSQ%mAd+_MB`JCP32UR*Sq4_yW!dC2V+K`y;^K*uTjrRYtBW8ns z&W^AVUEAR%&4>}&cHk>DpPWYT&WadO{hSqHW9-t1INS@zhFl}U#@K-kkqkhmxi*AT z?#3p>a1ZpiYeG0>c4R?hx_N=*G_~@s;vE<ei7tR}4G0^8eQjkwob2>-_JdvICd`Ma Z4nJo;aN7CJS`TO1{hal{0p5)9@PA%N4UYf- literal 0 HcmV?d00001 diff --git a/test/tap/db/upload_example.vot b/test/tap/db/upload_example.vot new file mode 100644 index 0000000..84f83a8 --- /dev/null +++ b/test/tap/db/upload_example.vot @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<VOTABLE version="1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://www.ivoa.net/xml/VOTable/v1.2" + xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.2 http://www.ivoa.net/xml/VOTable/v1.2"> + <DESCRIPTION> + VizieR Astronomical Server vizier.u-strasbg.fr + Date: 2014-07-18T16:16:29 [V1.99+ (14-Oct-2013)] + Explanations and Statistics of UCDs: See LINK below + In case of problem, please report to: cds-question@unistra.fr + In this version, NULL integer columns are written as an empty string + <TD></TD>, explicitely possible from VOTable-1.3 + </DESCRIPTION> +<!-- VOTable description at http://www.ivoa.net/Documents/latest/VOT.html --> +<INFO ID="VERSION" name="votable-version" value="1.99+ (14-Oct-2013)"/> +<INFO ID="Ref" name="-ref" value="VIZ53c9485768a5"/> +<RESOURCE ID="yCat_2220" name="II/220"> + <DESCRIPTION>Polarisation of Be stars (McDavid, 1986-1999)</DESCRIPTION> + <COOSYS ID="J2000" system="eq_FK5" equinox="J2000"/> + <TABLE ID="II_220_stars" name="II/220/stars"> + <DESCRIPTION>Standard and Program Be stars</DESCRIPTION> + <!-- Definitions of GROUPs and FIELDs --> + <FIELD name="_RAJ2000" ucd="pos.eq.ra" ref="J2000" datatype="double" width="8" precision="4" unit="deg"><!-- ucd="POS_EQ_RA" --> + <DESCRIPTION>Right ascension (FK5, Equinox=J2000.0) (computed by VizieR, not part of the original data)</DESCRIPTION> + </FIELD> + <FIELD name="_DEJ2000" ucd="pos.eq.dec" ref="J2000" datatype="double" width="8" precision="4" unit="deg"><!-- ucd="POS_EQ_DEC" --> + <DESCRIPTION>Declination (FK5, Equinox=J2000.0) (computed by VizieR, not part of the original data)</DESCRIPTION> + </FIELD> + <FIELD name="ps" ucd="meta.ref.url" datatype="char" arraysize="1"><!-- ucd="DATA_LINK" --> + <DESCRIPTION>[p/s] Program or Standard star</DESCRIPTION> + </FIELD> + <FIELD name="Name" ucd="meta.id;meta.main" datatype="char" arraysize="7"><!-- ucd="ID_MAIN" --> + <DESCRIPTION>Star name</DESCRIPTION> + </FIELD> + <FIELD name="HD" ucd="meta.id" datatype="int" width="6"><!-- ucd="ID_ALTERNATIVE" --> + <DESCRIPTION>HD (Cat. <III/135>) catalog number</DESCRIPTION> + </FIELD> + <FIELD name="HR" ucd="meta.id" datatype="short" width="4"><!-- ucd="ID_ALTERNATIVE" --> + <DESCRIPTION>BS (Cat. <V/50>) catalog number</DESCRIPTION> + </FIELD> + <FIELD name="Vmag" ucd="meta.ref;pos.frame" datatype="float" width="5" precision="2" unit="mag"><!-- ucd="?" --> + <DESCRIPTION>Visual magnitude (BSC4, See Cat. <V/50>)</DESCRIPTION> + </FIELD> + <FIELD name="RAJ2000" ucd="pos.eq.ra;meta.main" ref="J2000" datatype="char" arraysize="10" unit=""h:m:s""><!-- ucd="?" --> + <DESCRIPTION>Right Ascension J2000</DESCRIPTION> + </FIELD> + <FIELD name="DEJ2000" ucd="pos.eq.dec;meta.main" ref="J2000" datatype="char" arraysize="9" unit=""d:m:s""><!-- ucd="?" --> + <DESCRIPTION>Declination J2000</DESCRIPTION> + </FIELD> + <FIELD name="Sp" ucd="src.spType" datatype="char" arraysize="15"><!-- ucd="?" --> + <DESCRIPTION>MK Spectral type (1)</DESCRIPTION> + </FIELD> + <FIELD name="vsini" ucd="phys.veloc.rotat" datatype="short" width="3" unit="km/s"><!-- ucd="?" --> + <DESCRIPTION>? projected rotational velocity (1) [NULL integer written as an empty string]</DESCRIPTION> + <VALUES null="-32768" /> + </FIELD> + <FIELD name="Simbad" ucd="meta.ref" datatype="char" arraysize="6"><!-- ucd="DATA_LINK" --> + <DESCRIPTION>ask the {\bf\fg{FireBrick}Simbad} data-base about this object</DESCRIPTION> + </FIELD> +<DATA><TABLEDATA> +<TR><TD>052.2671</TD><TD>+59.9403</TD><TD>s</TD><TD>2H Cam</TD><TD>21291</TD><TD>1035</TD><TD>4.23</TD><TD>03 29 04.1</TD><TD>+59 56 25</TD><TD>B9Ia</TD><TD></TD><TD>Simbad</TD></TR> +<TR><TD>245.1587</TD><TD>-24.1689</TD><TD>s</TD><TD>omi Sco</TD><TD>147084</TD><TD>6081</TD><TD>4.55</TD><TD>16 20 38.1</TD><TD>-24 10 08</TD><TD>A5II</TD><TD></TD><TD>Simbad</TD></TR> +<TR><TD>014.1758</TD><TD>+60.7169</TD><TD>p</TD><TD>gam Cas</TD><TD>5394</TD><TD>264</TD><TD>2.47</TD><TD>00 56 42.2</TD><TD>+60 43 01</TD><TD>B0.5IVe</TD><TD>230</TD><TD>Simbad</TD></TR> +<TR><TD>025.9142</TD><TD>+50.6889</TD><TD>p</TD><TD>phi Per</TD><TD>10516</TD><TD>496</TD><TD>4.07</TD><TD>01 43 39.4</TD><TD>+50 41 20</TD><TD>B1.5(V:)e-shell</TD><TD>400</TD><TD>Simbad</TD></TR> +<TR><TD>062.1646</TD><TD>+47.7131</TD><TD>p</TD><TD>48 Per</TD><TD>25940</TD><TD>1273</TD><TD>4.04</TD><TD>04 08 39.5</TD><TD>+47 42 47</TD><TD>B4Ve</TD><TD>200</TD><TD>Simbad</TD></TR> +<TR><TD>084.4108</TD><TD>+21.1428</TD><TD>p</TD><TD>zet Tau</TD><TD>37202</TD><TD>1910</TD><TD>3.00</TD><TD>05 37 38.6</TD><TD>+21 08 34</TD><TD>B1IVe-shell</TD><TD>220</TD><TD>Simbad</TD></TR> +<TR><TD>239.5471</TD><TD>-14.2792</TD><TD>p</TD><TD>48 Lib</TD><TD>142983</TD><TD>5941</TD><TD>4.88</TD><TD>15 58 11.3</TD><TD>-14 16 45</TD><TD>B3:IV:e-shell</TD><TD>400</TD><TD>Simbad</TD></TR> +<TR><TD>246.7554</TD><TD>-18.4558</TD><TD>p</TD><TD>chi Oph</TD><TD>148184</TD><TD>6118</TD><TD>4.42</TD><TD>16 27 01.3</TD><TD>-18 27 21</TD><TD>B1.5Ve</TD><TD>140</TD><TD>Simbad</TD></TR> +<TR><TD>336.3187</TD><TD>+1.3772</TD><TD>p</TD><TD>pi Aqr</TD><TD>212571</TD><TD>8539</TD><TD>4.66</TD><TD>22 25 16.5</TD><TD>+01 22 38</TD><TD>B1III-IVe</TD><TD>300</TD><TD>Simbad</TD></TR> +<TR><TD>345.4796</TD><TD>+42.3261</TD><TD>p</TD><TD>omi And</TD><TD>217675</TD><TD>8762</TD><TD>3.62</TD><TD>23 01 55.1</TD><TD>+42 19 34</TD><TD>B6III</TD><TD>260</TD><TD>Simbad</TD></TR> +</TABLEDATA></DATA> +</TABLE> + +<INFO name="Warning" value="No center provided++++"/> +</RESOURCE> +</VOTABLE> diff --git a/test/tap/formatter/JSONFormatTest.java b/test/tap/formatter/JSONFormatTest.java index a037575..4eb9449 100644 --- a/test/tap/formatter/JSONFormatTest.java +++ b/test/tap/formatter/JSONFormatTest.java @@ -75,8 +75,9 @@ public class JSONFormatTest { @Test public void testWriteResult(){ + ResultSet rs = null; try{ - ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); HashMap<String,Object> tapParams = new HashMap<String,Object>(1); tapParams.put(TAPJob.PARAM_MAX_REC, "100"); @@ -98,6 +99,12 @@ public class JSONFormatTest { }catch(Exception t){ t.printStackTrace(); fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } } } diff --git a/test/tap/formatter/SVFormatTest.java b/test/tap/formatter/SVFormatTest.java index f44dda5..29d34a3 100644 --- a/test/tap/formatter/SVFormatTest.java +++ b/test/tap/formatter/SVFormatTest.java @@ -74,8 +74,9 @@ public class SVFormatTest { @Test public void testWriteResult(){ + ResultSet rs = null; try{ - ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); HashMap<String,Object> tapParams = new HashMap<String,Object>(1); tapParams.put(TAPJob.PARAM_MAX_REC, "100"); @@ -96,6 +97,12 @@ public class SVFormatTest { }catch(Exception t){ t.printStackTrace(); fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } } } diff --git a/test/tap/formatter/TextFormatTest.java b/test/tap/formatter/TextFormatTest.java index 6739994..ca6631d 100644 --- a/test/tap/formatter/TextFormatTest.java +++ b/test/tap/formatter/TextFormatTest.java @@ -74,8 +74,9 @@ public class TextFormatTest { @Test public void testWriteResult(){ + ResultSet rs = null; try{ - ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); HashMap<String,Object> tapParams = new HashMap<String,Object>(1); tapParams.put(TAPJob.PARAM_MAX_REC, "100"); @@ -96,6 +97,12 @@ public class TextFormatTest { }catch(Exception t){ t.printStackTrace(); fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } } } diff --git a/test/tap/formatter/VOTableFormatTest.java b/test/tap/formatter/VOTableFormatTest.java index 4cbb841..d54c80d 100644 --- a/test/tap/formatter/VOTableFormatTest.java +++ b/test/tap/formatter/VOTableFormatTest.java @@ -75,8 +75,9 @@ public class VOTableFormatTest { @Test public void testWriteResult(){ + ResultSet rs = null; try{ - ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); + rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;"); HashMap<String,Object> tapParams = new HashMap<String,Object>(1); tapParams.put(TAPJob.PARAM_MAX_REC, "100"); @@ -86,7 +87,7 @@ public class VOTableFormatTest { TableIterator it = new ResultSetTableIterator(rs); - VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.FITS); + VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.TABLEDATA); OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile)); formatter.writeResult(it, output, report, Thread.currentThread()); output.close(); @@ -101,6 +102,12 @@ public class VOTableFormatTest { }catch(Exception t){ t.printStackTrace(); fail("Unexpected exception!"); + }finally{ + if (rs != null){ + try{ + rs.close(); + }catch(SQLException se){} + } } } @@ -118,7 +125,7 @@ public class VOTableFormatTest { TableIterator it = new ResultSetTableIterator(rs); - VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.FITS); + VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.TABLEDATA); OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile)); formatter.writeResult(it, output, report, Thread.currentThread()); output.close(); diff --git a/test/testtools/DBTools.java b/test/testtools/DBTools.java index a5ef0fe..5c837ba 100644 --- a/test/testtools/DBTools.java +++ b/test/testtools/DBTools.java @@ -96,7 +96,7 @@ public final class DBTools { // 3. Establish the connection: Connection connection = null; try{ - connection = DriverManager.getConnection("jdbc:" + dbms + "://" + server + ((port != null && port.trim().length() > 0) ? (":" + port) : "") + "/" + dbName, user, passwd); + connection = DriverManager.getConnection("jdbc:" + dbms + ":" + ((server != null && server.trim().length() > 0) ? "//" + server + ((port != null && port.trim().length() > 0) ? (":" + port) : "") + "/" : "") + dbName, user, passwd); }catch(SQLException e){ throw new DBToolsException("Connection failed: " + e.getMessage(), e); } diff --git a/test/testtools/MD5Checksum.java b/test/testtools/MD5Checksum.java new file mode 100644 index 0000000..d4941c5 --- /dev/null +++ b/test/testtools/MD5Checksum.java @@ -0,0 +1,46 @@ +package testtools; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.MessageDigest; + +public class MD5Checksum { + + public static byte[] createChecksum(InputStream input) throws Exception{ + byte[] buffer = new byte[1024]; + MessageDigest complete = MessageDigest.getInstance("MD5"); + int numRead; + + do{ + numRead = input.read(buffer); + if (numRead > 0){ + complete.update(buffer, 0, numRead); + } + }while(numRead != -1); + return complete.digest(); + } + + // see this How-to for a faster way to convert + // a byte array to a HEX string + public static String getMD5Checksum(InputStream input) throws Exception{ + byte[] b = createChecksum(input); + String result = ""; + + for(int i = 0; i < b.length; i++){ + result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1); + } + return result; + } + + public static String getMD5Checksum(final String content) throws Exception{ + return getMD5Checksum(new ByteArrayInputStream(content.getBytes())); + } + + public static void main(String args[]){ + try{ + System.out.println(getMD5Checksum("Blabla et Super blabla")); + }catch(Exception e){ + e.printStackTrace(); + } + } +} -- GitLab