Skip to content
JDBCConnection.java 134 KiB
Newer Older
			}
			sqlBuf.append(");");
			// ...execute the update query:
			stmt.executeUpdate(sqlBuf.toString());

			// 3. Fill the table:
			int nbUploadedRows = fillUploadedTable(tableDef, data);
				logger.logDB(LogLevel.INFO, this, "TABLE_CREATED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") created (" + nbUploadedRows + " rows).", null);
			return true;

		}catch(SQLException se){
			rollback();
			if (!isCancelled() && logger != null)
				logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
			throw new DBException("Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
		}catch(DBException de){
			rollback();
			throw de;
		}catch(DataReadException dre){
			rollback();
			throw dre;
		}finally{
			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>
	 * 
	 * <p><i>Note:
	 * 	This function proceeds to a formatting of TIMESTAMP and GEOMETRY (point, circle, box, polygon) values.
	 * </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.getTableName(metaTable, supportsSchema)).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()){
					Object val = data.nextCol();
					if (val != null && cols[c - 1] != null){
						/* TIMESTAMP FORMATTING */
						if (cols[c - 1].getDatatype().type == DBDatatype.TIMESTAMP){
							try{
								val = new Timestamp(ISO8601Format.parse(val.toString()));
							}catch(ParseException pe){
								if (logger != null)
									logger.logDB(LogLevel.ERROR, this, "UPLOAD", "[l. " + nbRows + ", c. " + c + "] Unexpected date format for the value: \"" + val + "\"! A date formatted in ISO8601 was expected.", pe);
								throw new DBException("[l. " + nbRows + ", c. " + c + "] Unexpected date format for the value: \"" + val + "\"! A date formatted in ISO8601 was expected.", pe);
							}
						}
						/* GEOMETRY FORMATTING */
						else if (cols[c - 1].getDatatype().type == DBDatatype.POINT || cols[c - 1].getDatatype().type == DBDatatype.REGION){
							Region region;
							// parse the region as an STC-S expression:
							try{
								region = STCS.parseRegion(val.toString());
							}catch(adql.parser.ParseException e){
								if (logger != null)
									logger.logDB(LogLevel.ERROR, this, "UPLOAD", "[l. " + nbRows + ", c. " + c + "] Incorrect STC-S syntax for the geometrical value \"" + val + "\"! " + e.getMessage(), e);
								throw new DataReadException("[l. " + nbRows + ", c. " + c + "] Incorrect STC-S syntax for the geometrical value \"" + val + "\"! " + e.getMessage(), e);
							}
							// translate this STC region into the corresponding column value:
							try{
								val = translator.translateGeometryToDB(region);
							}catch(adql.parser.ParseException e){
								if (logger != null)
									logger.logDB(LogLevel.ERROR, this, "UPLOAD", "[l. " + nbRows + ", c. " + c + "] Impossible to import the ADQL geometry \"" + val + "\" into the database! " + e.getMessage(), e);
								throw new DataReadException("[l. " + nbRows + ", c. " + c + "] Impossible to import the ADQL geometry \"" + val + "\" into the database! " + e.getMessage(), e);
						/* BOOLEAN CASE (more generally, type incompatibility) */
						else if (val != null && cols[c - 1].getDatatype().type == DBDatatype.SMALLINT && val instanceof Boolean)
							val = ((Boolean)val) ? (short)1 : (short)0;
						/* NULL CHARACTER CASE (JUST FOR POSTGRESQL) */
						else if ((dbms == null || dbms.equalsIgnoreCase(DBMS_POSTGRES)) && val instanceof Character && (Character)val == 0x00)
							val = null;
				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 if schemas are not supported by this connection.
	 * 	In this case, this function will prefix the table's DB name by the schema's DB name directly inside the given
	 * 	{@link TAPTable} object. Then the DB name of the schema will be set to NULL.
	 * </i></p>
	 * 
	 * <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 synchronized 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;

		// Starting of new query execution => disable the cancel flag:
		// Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD):
		checkUploadedTableDef(tableDef);

		try{

			// Check the existence of the table to drop:
			if (!isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), connection.getMetaData()))
				return true;

			// Execute the update:
			int cnt = getStatement().executeUpdate("DROP TABLE " + translator.getTableName(tableDef, supportsSchema) + ";");
				if (cnt >= 0)
					logger.logDB(LogLevel.INFO, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") dropped.", null);
					logger.logDB(LogLevel.ERROR, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") NOT dropped.", null);
			// Ensure the update is successful:
			if (!isCancelled() && logger != null)
				logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
			throw new DBException("Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
		}
	}

	/**
	 * <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>
	 * 		The schema name of the given 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.
	 * 	</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. 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 given table is not in a schema
	 *                    	or if the ADQL name of this schema is not {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD").
	 */
	protected void checkUploadedTableDef(final TAPTable tableDef) throws DBException{
		// If the table has no defined schema or if the ADQL name of the schema is not TAP_UPLOAD, throw an exception:
		if (tableDef.getSchema() == null || !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 (!supportsSchema){
			if (tableDef.getADQLSchemaName() != null && tableDef.getADQLSchemaName().trim().length() > 0 && !tableDef.getDBName().startsWith(tableDef.getADQLSchemaName() + "_"))
				tableDef.setDBName(tableDef.getADQLSchemaName() + "_" + tableDef.getDBName());
			if (tableDef.getSchema() != null)
				tableDef.getSchema().setDBName(null);
		}
	}

	/* ************** */
	/* TOOL FUNCTIONS */
	/* ************** */

	/**
	 * <p>Convert the given TAP type into the corresponding DBMS column type.</p>
	 * 
	 * <p>
	 * 	This function tries first the type conversion using the translator ({@link JDBCTranslator#convertTypeToDB(DBType)}).
	 * 	If it fails, a default conversion is done considering all the known types of the following DBMS:
	 * 	PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby.
	 * </p>
	 * 
	 * @param type	TAP type to convert.
	 * 
	 * @return	The corresponding DBMS type.
	 * 
	 * @see JDBCTranslator#convertTypeToDB(DBType)
	 * @see #defaultTypeConversion(DBType)
	 */
	protected String convertTypeToDB(final DBType type){
		String dbmsType = translator.convertTypeToDB(type);
		return (dbmsType == null) ? defaultTypeConversion(type) : dbmsType;
	}

	/**
	 * <p>Get the DBMS compatible datatype corresponding to the given column {@link DBType}.</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 VARCHAR if the given type is not managed or is NULL.
	protected String defaultTypeConversion(DBType datatype){
			datatype = new DBType(DBDatatype.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);
				if (logger != null)
					logger.logDB(LogLevel.INFO, this, "START_TRANSACTION", "Transaction STARTED.", null);
			}
		}catch(SQLException se){
			supportsTransaction = false;
			if (logger != null)
				logger.logDB(LogLevel.ERROR, this, "START_TRANSACTION", "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();
				if (logger != null)
					logger.logDB(LogLevel.INFO, this, "COMMIT", "Transaction COMMITED.", null);
			}
		}catch(SQLException se){
			supportsTransaction = false;
			if (logger != null)
				logger.logDB(LogLevel.ERROR, this, "COMMIT", "Transaction COMMIT impossible!", se);
			throw new DBException("Transaction COMMIT impossible!", se);
		}
	}

	/**
	 * <p>Rollback the current transaction.
	 * The success or the failure of the rollback operation is always logged (except if no logger is available).</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.
	 * 
	 * @see #rollback(boolean)
	 */
	protected final void rollback(){
		rollback(true);
	}

	/**
	 * <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>
	 * 
	 * @param log	<code>true</code> to log the success/failure of the rollback operation,
	 *           	<code>false</code> to be quiet whatever happens.
	 * 
	 * @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(final boolean log){
			if (supportsTransaction && !connection.getAutoCommit()){
				if (log && logger != null)
					logger.logDB(LogLevel.INFO, this, "ROLLBACK", "Transaction ROLLBACKED.", null);
			}
		}catch(SQLException se){
			supportsTransaction = false;
			if (log && logger != null)
				logger.logDB(LogLevel.ERROR, this, "ROLLBACK", "Transaction ROLLBACK impossible!", se);
	/**
	 * <p>End the current transaction.
	 * The success or the failure of the transaction ending operation is always logged (except if no logger is available).</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.
	 * 
	 * @see #endTransaction(boolean)
	 */
	protected final void endTransaction(){
		endTransaction(true);
	}

	/**
	 * <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>
	 * 
	 * @param log	<code>true</code> to log the success/failure of the transaction ending operation,
	 *           	<code>false</code> to be quiet whatever happens.
	 * 
	 * @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(final boolean log){
		try{
			if (supportsTransaction){
				connection.setAutoCommit(true);
				if (log && logger != null)
					logger.logDB(LogLevel.INFO, this, "END_TRANSACTION", "Transaction ENDED.", null);
			}
		}catch(SQLException se){
			supportsTransaction = false;
			if (log && logger != null)
				logger.logDB(LogLevel.ERROR, this, "END_TRANSACTION", "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 TAPLog#logDB(uws.service.log.UWSLog.LogLevel, DBConnection, String, String, Throwable)}).
	 * 	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){
			if (logger != null)
				logger.logDB(LogLevel.WARNING, this, "CLOSE", "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>
	 * 	The given statement is explicitly canceled by this function before being closed.
	 * 	Thus the corresponding DBMS process is ensured to be stopped. Of course, this
	 * 	cancellation is effective only if this operation is supported by the JDBC driver
	 * 	and the DBMS.
	 * </p>
	 * 
	 * <p><b>Important note:</b>
	 * 	In case of cancellation, <b>NO</b> rollback is performed.
	 * </p>
	 * 
	 * <p>
	 * 	If any {@link SQLException} occurs during this operation, it is caught and just logged
	 * 	(see {@link TAPLog#logDB(uws.service.log.UWSLog.LogLevel, DBConnection, String, String, Throwable)}).
	 * 	No error is thrown and nothing else is done.
	 * </p>
	 * 
	 * @param stmt	{@link Statement} to close.
	 */
	protected final void close(final Statement stmt){
		try{
			if (stmt != null){
				cancel(stmt, false);
			if (logger != null)
				logger.logDB(LogLevel.WARNING, this, "CLOSE", "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 string 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 JDBCTranslator#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 (!supportsSchema || 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.
	 * 	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 JDBCTranslator#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{

			// 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){
				if (logger != null)
					logger.logDB(LogLevel.ERROR, this, "TABLE_EXIST", "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>Tell whether the specified column exists in the specified table of the database.
	 * 	To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing columns.</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.
	 * 	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 JDBCTranslator#isCaseSensitive(IdentifierField)}).
	 * </i></p>
	 * 
	 * <p><i>Note:
	 * 	This function is used by {@link #loadSchemas(TAPTable, TAPMetadata, Statement)}, {@link #loadTables(TAPTable, TAPMetadata, Statement)}
	 * 	and {@link #loadColumns(TAPTable, List, Statement)}.
	 * </i></p>
	 * 
	 * @param schemaName	DB name of the table schema. <i>MAY BE NULL</i>
	 * @param tableName		DB name of the table containing the column to search. <i>MAY BE NULL</i>
	 * @param columnName	DB name of the column to search.
	 * @param dbMeta		Metadata about the database, and mainly the list of all existing tables.
	 * 
	 * @return	<i>true</i> if the specified column exists, <i>false</i> otherwise.
	 * 
	 * @throws SQLException	If any error occurs while interrogating the database about existing columns.
	 */
	protected boolean isColumnExisting(String schemaName, String tableName, String columnName, final DatabaseMetaData dbMeta) throws DBException, SQLException{
		if (columnName == null || columnName.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);
		boolean columnCaseSensitive = translator.isCaseSensitive(IdentifierField.COLUMN);

		ResultSet rsT = null, rsC = null;
		try{
			/* Note:
			 * 
			 *     The DatabaseMetaData.getColumns(....) function does not work properly
			 * with the SQLite driver: when all parameters are set to null, meaning all columns of the database
			 * must be returned, absolutely no rows are selected.
			 * 
			 *     The solution proposed here, is to first search all (matching) tables, and then for each table get
			 * all its columns and find the matching one(s).
			 */

			// List all matching tables:
			if (supportsSchema){
				String schemaPattern = schemaCaseSensitive ? schemaName : null;
				String tablePattern = tableCaseSensitive ? tableName : null;
				rsT = dbMeta.getTables(null, schemaPattern, tablePattern, null);
			}else{
				String tablePattern = tableCaseSensitive ? tableName : null;
				rsT = dbMeta.getTables(null, null, tablePattern, null);
			}

			// For each matching table:
			int cnt = 0;
			String columnPattern = columnCaseSensitive ? columnName : null;
			while(rsT.next()){
				String rsSchema = nullifyIfNeeded(rsT.getString(2));
				String rsTable = rsT.getString(3);
				// test the schema name:
				if (!supportsSchema || schemaName == null || equals(rsSchema, schemaName, schemaCaseSensitive)){
					// test the table name:
					if ((tableName == null || equals(rsTable, tableName, tableCaseSensitive))){
						// list its columns:
						rsC = dbMeta.getColumns(null, rsSchema, rsTable, columnPattern);
						// count all matching columns:
						while(rsC.next()){
							String rsColumn = rsC.getString(4);
							if (equals(rsColumn, columnName, columnCaseSensitive))
								cnt++;
						}
						close(rsC);
					}
				}
			}

			if (cnt > 1){
				if (logger != null)
					logger.logDB(LogLevel.ERROR, this, "COLUMN_EXIST", "More than one column match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + ") && column=" + columnName + " (case sensitive?" + columnCaseSensitive + "))!", null);
				throw new DBException("More than one column match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + ") && column=" + columnName + " (case sensitive?" + columnCaseSensitive + "))!");
			}

			return cnt == 1;

		}finally{
			close(rsT);
			close(rsC);
		}
	}

	/*
	 * <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.