Commit 8915359b authored by gmantele's avatar gmantele
Browse files

[TAP] Add the interface TableIterator (whose implementations let iterate over...

[TAP] Add the interface TableIterator (whose implementations let iterate over any kind of table data set) and its first implementation for ResultSet + JUnit test case
parent 3ee8d15f
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
package tap.data;

import tap.TAPException;

/**
 * Exception that occurs when reading a data input (can be an InputStream, a ResultSet, a SavotTable, ...).
 * 
 * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
 * @version 2.0 (06/2014)
 * @since 2.0
 * 
 * @see TableIterator
 */
public class DataReadException extends TAPException {
	private static final long serialVersionUID = 1L;

	public DataReadException(final String message){
		super(message);
	}

	public DataReadException(Throwable cause){
		super(cause);
	}

	public DataReadException(String message, Throwable cause){
		super(message, cause);
	}

}
+161 −0
Original line number Diff line number Diff line
package tap.data;

/*
 * 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 Institut (ARI)
 */

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.NoSuchElementException;

/**
 * <p>{@link TableIterator} which lets iterate over a SQL {@link ResultSet}.</p>
 * 
 * <p>{@link #getColType()} will return the type declared in the {@link ResultSetMetaData} object.</p>
 * 
 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
 * @version 2.0 (06/2014)
 * @since 2.0
 */
public class ResultSetTableIterator implements TableIterator {

	/** ResultSet/Dataset to read. */
	private final ResultSet data;

	/** Number of columns to read. */
	private final int nbColumns;
	/** Type of all columns. */
	private final String[] colTypes;

	/** Indicate whether the row iteration has already started. */
	private boolean iterationStarted = false;
	/** Indicate whether the last row has already been reached. */
	private boolean endReached = false;
	/** Index of the last read column (=0 just after {@link #nextRow()} and before {@link #nextCol()}, ={@link #nbColumns} after the last column has been read). */
	private int colIndex;

	/**
	 * Build a TableIterator able to read rows and columns of the given ResultSet.
	 * 
	 * @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.
	 */
	public ResultSetTableIterator(final ResultSet dataSet) 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;

		// Count columns and determine their type:
		try{
			// get the metadata:
			ResultSetMetaData metadata = data.getMetaData();
			// count columns:
			nbColumns = metadata.getColumnCount();
			// determine their type:
			colTypes = new String[nbColumns];
			for(int i = 1; i <= nbColumns; i++)
				colTypes[i - 1] = metadata.getColumnTypeName(i);
		}catch(SQLException se){
			throw new DataReadException("Can not get the column types of the given ResultSet!", se);
		}
	}

	@Override
	public boolean nextRow() throws DataReadException{
		try{
			// go to the next row:
			boolean rowFetched = data.next();
			endReached = !rowFetched;
			// prepare the iteration over its columns:
			colIndex = 0;
			iterationStarted = true;
			return rowFetched;
		}catch(SQLException e){
			throw new DataReadException("Unable to read a result set row!", e);
		}
	}

	/**
	 * <p>Check the row iteration state. That's to say whether:</p>
	 * <ul>
	 * 	<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{
		if (!iterationStarted)
			throw new IllegalStateException("No row has yet been read!");
		else if (endReached)
			throw new IllegalStateException("End of ResultSet already reached!");
	}

	@Override
	public boolean hasNextCol() throws IllegalStateException, DataReadException{
		// Check the read state:
		checkReadState();

		// Determine whether the last column has been reached or not:
		return (colIndex < nbColumns);
	}

	@Override
	public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
		// Check the read state and ensure there is still at least one column to read:
		if (!hasNextCol())
			throw new NoSuchElementException("No more column to read!");

		// Get the column value:
		try{
			return data.getObject(++colIndex);
		}catch(SQLException se){
			throw new DataReadException("Can not read the value of the " + colIndex + "-th column!", se);
		}
	}

	@Override
	public String getColType() throws IllegalStateException, DataReadException{
		// Basically check the read state (for rows iteration):
		checkReadState();

		// Check deeper the read state (for columns iteration):
		if (colIndex <= 0)
			throw new IllegalStateException("No column has yet been read!");
		else if (colIndex > nbColumns)
			throw new IllegalStateException("All columns have already been read!");

		// Return the column type:
		return colTypes[colIndex - 1];
	}

}
+100 −0
Original line number Diff line number Diff line
package tap.data;

/*
 * 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 Institut (ARI)
 */

import java.util.NoSuchElementException;

/**
 * <p>Let's iterate on each row and then on each column over a table dataset.</p>
 * 
 * <p>Initially, no rows are loaded and the "cursor" inside the dataset is set before the first row.
 * Thus, a first call to {@link #nextRow()} is required to read each of the column values of the first row.</p>
 * 
 * <p>Example of an expected usage:</p>
 * <pre>
 * 	TableIterator it = ...;
 * 	try{
 * 		while(it.nextRow()){
 * 			while(it.hasNextCol()){
 * 				Object colValue = it.nextCol();
 * 				String colType = it.getColType();
 * 				...
 * 			}
 * 		}
 * 	}catch(DataReadException dre){
 * 		...
 * 	}
 * </pre>
 * 
 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
 * @version 2.0 (06/2014)
 * @since 2.0
 */
public interface TableIterator {
	/**
	 * <p>Go to the next row if there is one.</p>
	 * 
	 * <p><i>Note: After a call to this function the columns must be fetched individually using {@link #nextCol()}
	 * <b>IF</b> this function returned </i>true<i>.</i></p>
	 * 
	 * @return	<i>true</i> if the next row has been successfully reached,
	 *          <i>false</i> if no more rows can be read.
	 * 
	 * @throws DataReadException	If an error occurs while reading the table dataset.
	 */
	public boolean nextRow() throws DataReadException;

	/**
	 * Tell whether another column is available.
	 * 
	 * @return	<i>true</i> if {@link #nextCol()} will return the value of the next column with no error,
	 *          <i>false</i> otherwise.
	 * 
	 * @throws IllegalStateException	If {@link #nextRow()} has not yet been called.
	 * @throws DataReadException	If an error occurs while reading the table dataset.
	 */
	public boolean hasNextCol() throws IllegalStateException, DataReadException;

	/**
	 * <p>Return the value of the next column.</p>
	 * 
	 * <p><i>Note: The column type can be fetched using {@link #getColType()} <b>after</b> a call to {@link #nextCol()}.</i></p>
	 * 
	 * @return	Get the value of the next column.
	 * 
	 * @throws NoSuchElementException	If no more column value is available.
	 * @throws IllegalStateException	If {@link #nextRow()} has not yet been called.
	 * @throws DataReadException	If an error occurs while reading the table dataset.
	 */
	public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException;

	/**
	 * <p>Get the type of the current column value.</p>
	 * 
	 * <p><i>Note: "Current column value" means here "the value last returned by {@link #nextCol()}".</i></p>
	 * 
	 * @return	Type of the current column value,
	 *          or NULL if this information is not available or if this function is not implemented. 
	 * 
	 * @throws IllegalStateException	If {@link #nextCol()} has not yet been called.
	 * @throws DataReadException	If an error occurs while reading the table dataset.
	 */
	public String getColType() throws IllegalStateException, DataReadException;
}
+108 −0
Original line number Diff line number Diff line
package tap.data;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.sql.Connection;
import java.sql.ResultSet;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import testtools.DBTools;

public class ResultSetTableIteratorTest {

	private static Connection conn;

	@BeforeClass
	public static void setUpBeforeClass() throws Exception{
		conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd");
	}

	@AfterClass
	public static void tearDownAfterClass() throws Exception{
		DBTools.closeConnection(conn);
	}

	@Test
	public void testWithRSNULL(){
		try{
			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");
		}
	}

	@Test
	public void testWithData(){
		try{
			ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");

			TableIterator it = new ResultSetTableIterator(rs);
			final int expectedNbLines = 10, expectedNbColumns = 4;
			int countLines = 0, countColumns = 0;
			while(it.nextRow()){
				// count lines:
				countLines++;
				// reset columns count:
				countColumns = 0;
				while(it.hasNextCol()){
					it.nextCol();
					// count columns
					countColumns++;
					// TEST the column type is set (not null):
					assertTrue(it.getColType() != null);
				}
				// TEST that all columns have been read:
				assertEquals(expectedNbColumns, countColumns);
			}
			// TEST that all lines have been read: 
			assertEquals(expectedNbLines, countLines);

		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("An exception occurs while reading a correct ResultSet (containing some valid rows).");
		}
	}

	@Test
	public void testWithEmptySet(){
		try{
			ResultSet rs = DBTools.select(conn, "SELECT * FROM gums WHERE id = 'foo';");

			TableIterator it = new ResultSetTableIterator(rs);
			int countLines = 0;
			// count lines:
			while(it.nextRow())
				countLines++;
			// TEST that no line has been read: 
			assertEquals(countLines, 0);

		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("An exception occurs while reading a correct ResultSet (containing some valid rows).");
		}
	}

	@Test
	public void testWithClosedSet(){
		try{
			// create a valid ResultSet:
			ResultSet rs = DBTools.select(conn, "SELECT * FROM gums WHERE id = 'foo';");

			// close the ResultSet:
			rs.close();

			// TRY to create a TableIterator with a closed ResultSet:
			new ResultSetTableIterator(rs);

			fail("The constructor should have failed, because: the given ResultSet is closed.");
		}catch(Exception ex){
			assertEquals(ex.getClass().getName(), "tap.data.DataReadException");
		}
	}
}
+141 −0
Original line number Diff line number Diff line
package testtools;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;

public final class DBTools {

	public static int count = 0;

	public final static void main(final String[] args) throws Throwable{
		for(int i = 0; i < 3; i++){
			Thread t = new Thread(new Runnable(){
				@Override
				public void run(){
					count++;
					try{
						Connection conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd");
						System.out.println("Start - " + count + ": ");
						String query = "SELECT * FROM gums.smc WHERE magg BETWEEN " + (15 + count) + " AND " + (20 + count) + ";";
						System.out.println(query);
						ResultSet rs = DBTools.select(conn, query);
						try{
							rs.last();
							System.out.println("Nb rows: " + rs.getRow());
						}catch(SQLException e){
							e.printStackTrace();
						}
						if (DBTools.closeConnection(conn))
							System.out.println("[DEBUG] Connection closed!");
					}catch(DBToolsException e){
						e.printStackTrace();
					}
					System.out.println("End - " + count);
					count--;
				}
			});
			t.start();
		}
	}

	public static class DBToolsException extends Exception {

		private static final long serialVersionUID = 1L;

		public DBToolsException(){
			super();
		}

		public DBToolsException(String message, Throwable cause){
			super(message, cause);
		}

		public DBToolsException(String message){
			super(message);
		}

		public DBToolsException(Throwable cause){
			super(cause);
		}

	}

	public final static HashMap<String,String> VALUE_JDBC_DRIVERS = new HashMap<String,String>(4);
	static{
		VALUE_JDBC_DRIVERS.put("oracle", "oracle.jdbc.OracleDriver");
		VALUE_JDBC_DRIVERS.put("postgresql", "org.postgresql.Driver");
		VALUE_JDBC_DRIVERS.put("mysql", "com.mysql.jdbc.Driver");
		VALUE_JDBC_DRIVERS.put("sqlite", "org.sqlite.JDBC");
	}

	private DBTools(){}

	public final static Connection createConnection(String dbms, final String server, final String port, final String dbName, final String user, final String passwd) throws DBToolsException{
		// 1. Resolve the DBMS and get its JDBC driver:
		if (dbms == null)
			throw new DBToolsException("Missing DBMS (expected: oracle, postgresql, mysql or sqlite)!");
		dbms = dbms.toLowerCase();
		String jdbcDriver = VALUE_JDBC_DRIVERS.get(dbms);
		if (jdbcDriver == null)
			throw new DBToolsException("Unknown DBMS (\"" + dbms + "\")!");

		// 2. Load the JDBC driver:
		try{
			Class.forName(jdbcDriver);
		}catch(ClassNotFoundException e){
			throw new DBToolsException("Impossible to load the JDBC driver: " + e.getMessage(), e);
		}

		// TODO DEBUG MSG
		System.out.println("[DEBUG] " + dbms + " JDBC Driver Registered!");

		// 3. Establish the connection:
		Connection connection = null;
		try{
			connection = DriverManager.getConnection("jdbc:" + dbms + "://" + server + ((port != null && port.trim().length() > 0) ? (":" + port) : "") + "/" + dbName, user, passwd);
		}catch(SQLException e){
			throw new DBToolsException("Connection failed: " + e.getMessage(), e);
		}

		if (connection == null)
			throw new DBToolsException("Failed to make connection!");

		// TODO DEBUG MSG
		System.out.println("[DEBUG] Connection to " + dbName + " established!");

		return connection;
	}

	public final static boolean closeConnection(final Connection conn) throws DBToolsException{
		try{
			if (conn != null && !conn.isClosed()){
				conn.close();
				try{
					Thread.sleep(200);
				}catch(InterruptedException e){
					System.err.println("WARNING: can't wait/sleep before testing the connection close status! [" + e.getMessage() + "]");
				}
				return conn.isClosed();
			}else
				return true;
		}catch(SQLException e){
			throw new DBToolsException("Closing connection failed: " + e.getMessage(), e);
		}
	}

	public final static ResultSet select(final Connection conn, final String selectQuery) throws DBToolsException{
		if (conn == null || selectQuery == null || selectQuery.trim().length() == 0)
			throw new DBToolsException("One parameter is missing!");

		try{
			Statement stmt = conn.createStatement();
			return stmt.executeQuery(selectQuery);
		}catch(SQLException e){
			throw new DBToolsException("Can't execute the given SQL query: " + e.getMessage(), e);
		}
	}

}