diff --git a/src/tap/data/DataReadException.java b/src/tap/data/DataReadException.java new file mode 100644 index 0000000000000000000000000000000000000000..f3d5db54026e94f99fa7c1bff92f57ddd1105fa5 --- /dev/null +++ b/src/tap/data/DataReadException.java @@ -0,0 +1,29 @@ +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); + } + +} diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..9325771b0b8485072ed052938c0554a7b2f72f94 --- /dev/null +++ b/src/tap/data/ResultSetTableIterator.java @@ -0,0 +1,161 @@ +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é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]; + } + +} diff --git a/src/tap/data/TableIterator.java b/src/tap/data/TableIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..5e6234ee389c758ae3bfdb21d552206d7c6fccd2 --- /dev/null +++ b/src/tap/data/TableIterator.java @@ -0,0 +1,100 @@ +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é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; +} diff --git a/test/tap/data/ResultSetTableIteratorTest.java b/test/tap/data/ResultSetTableIteratorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cd0e7f9d6da32e816791ff9d24dc0b761de1dcbc --- /dev/null +++ b/test/tap/data/ResultSetTableIteratorTest.java @@ -0,0 +1,108 @@ +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"); + } + } +} diff --git a/test/testtools/DBTools.java b/test/testtools/DBTools.java new file mode 100644 index 0000000000000000000000000000000000000000..ca032c05a6877a18d34a851be833c24013b96e76 --- /dev/null +++ b/test/testtools/DBTools.java @@ -0,0 +1,141 @@ +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); + } + } + +}