/* * Copyright (c) 2013 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available 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. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.io.jdbc; import java.net.URI; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.core.service.ServiceProvider; import eu.esdihumboldt.hale.common.instance.geometry.CRSProvider; import eu.esdihumboldt.hale.common.instance.model.Filter; import eu.esdihumboldt.hale.common.instance.model.Instance; import eu.esdihumboldt.hale.common.instance.model.InstanceCollection; import eu.esdihumboldt.hale.common.instance.model.InstanceReference; import eu.esdihumboldt.hale.common.instance.model.ResourceIterator; import eu.esdihumboldt.hale.common.instance.model.ext.InstanceIterator; import eu.esdihumboldt.hale.common.instance.model.impl.FilteredInstanceCollection; import eu.esdihumboldt.hale.common.instance.model.impl.PseudoInstanceReference; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.io.jdbc.constraints.DatabaseTable; import eu.esdihumboldt.hale.io.jdbc.constraints.SQLQuery; /** * Instance collection for instances belonging to a specific database table or a * schema type with an associated SQL query. * * @author Simon Templer */ public class JDBCTableCollection implements InstanceCollection { private static final ALogger log = ALoggerFactory.getLogger(JDBCTableCollection.class); /** * Iterator other a JDBC table. */ private class JDBCTableIterator implements InstanceIterator { private final TableInstanceBuilder builder; // private static final int ROW_LIMIT = 100; private final Connection connection; private ResultSet currentResults; // private final int currentOffset = 0; /** * States if the row at the current cursor position was already * consumed. * * Initially the result set points to the row before the first row, * which by definition is consumed. */ private boolean consumed = true; private boolean hasNext = false; private boolean done = false; /** * Default constructor. * * @param crsProvider the CRS provider */ public JDBCTableIterator(CRSProvider crsProvider) { super(); builder = new TableInstanceBuilder(crsProvider); try { connection = createConnection(); } catch (SQLException e) { throw new IllegalStateException("Could not create database connection", e); } } @Override public TypeDefinition typePeek() { if (hasNext()) { // always the same type returned in this iterator return type; } return null; } @Override public boolean supportsTypePeek() { return true; } @Override public boolean hasNext() { proceedToNext(); return hasNext; } /** * Proceed to the next result */ private void proceedToNext() { if (done) { return; } try { if (currentResults != null) { // move cursor if necessary if (consumed) { hasNext = currentResults.next(); consumed = false; } } if (currentResults != null && !hasNext) { // currentResults has been completely processed // set iterator to done close(); } else if (currentResults == null) { // retrieve result set connection.setAutoCommit(false); Statement st = JDBCUtil.createReadStatement(connection, 500); currentResults = st.executeQuery(sqlQuery); proceedToNext(); } } catch (SQLException e) { log.error(e.getMessage(), e); close(); } } @Override public Instance next() { proceedToNext(); if (hasNext) { // create instance from current cursor Instance instance = builder.createInstance(type, currentResults, connection); consumed = true; return instance; } else { throw new IllegalStateException(); } } @Override public void skip() { proceedToNext(); if (hasNext) { // mark as consumed consumed = true; } } @Override public void remove() { throw new UnsupportedOperationException(); } @Override public void close() { if (currentResults != null) { try { currentResults.close(); } catch (SQLException e) { // ignore } } currentResults = null; done = true; hasNext = false; try { connection.close(); } catch (SQLException e) { // ignore } } } private final URI jdbcURI; private final String user; private final String password; private final TypeDefinition type; private final String sqlQuery; private final String countQuery; private final CRSProvider crsProvider; /** * Constructor. * * @param type the type definition associated to the table * @param jdbcURI the JDBC URI to access the database * @param user the database user * @param password the user's password * @param crsProvider crs provider * @param services the service provider */ public JDBCTableCollection(TypeDefinition type, URI jdbcURI, String user, String password, CRSProvider crsProvider, ServiceProvider services) { this.type = type; this.jdbcURI = jdbcURI; this.user = user; this.password = password; this.crsProvider = crsProvider; String query = type.getConstraint(SQLQuery.class).getQuery(); if (type.getConstraint(DatabaseTable.class).isTable() || query == null) { // database table queries String fullTableName = type.getConstraint(DatabaseTable.class).getFullTableName(); query = "SELECT * FROM " + fullTableName; this.countQuery = "SELECT COUNT(*) FROM " + fullTableName; } else { // custom queries (not a database table) // support project variables query = JDBCUtil.replaceVariables(query, services); // this.countQuery = null; // countQuery = null caused to return UNKNOWN_SIZE in size() with causes isEmpty() // to return false and that causes problems on iterating with MultiInstanceCollection this.countQuery = "SELECT COUNT(*) FROM (\n" + query + "\n) tmp"; // note 1: this sub query is not supported in all SQL dialects // note 2: the line breaks '\n' prevent from problems using comments in the embedded query } this.sqlQuery = query; } /** * Create a connection to the database. * * @return the database connection * @throws SQLException if opening the connection fails */ protected Connection createConnection() throws SQLException { return JDBCConnection.getConnection(jdbcURI, user, password); } @Override public InstanceReference getReference(Instance instance) { // TODO create a database backed reference instead? return new PseudoInstanceReference(instance); } @Override public Instance getInstance(InstanceReference reference) { if (reference instanceof PseudoInstanceReference) { return ((PseudoInstanceReference) reference).getInstance(); } return null; } @Override public ResourceIterator<Instance> iterator() { return new JDBCTableIterator(crsProvider); } @Override public boolean hasSize() { return countQuery != null; } @Override public int size() { if (countQuery == null) { return UNKNOWN_SIZE; } try (Connection connection = createConnection()) { Statement st = connection.createStatement(); ResultSet res = st.executeQuery(countQuery); int count = 0; if (res.next()) { count = res.getInt(1); } return count; } catch (SQLException e) { log.warn("Could not determine query size by count query"); try (Connection connection = createConnection()) { Statement st = connection.createStatement(); st.setMaxRows(1); ResultSet res = st.executeQuery(sqlQuery); if (res.next()) { return UNKNOWN_SIZE; } else { return 0; } } catch (SQLException e2) { log.warn("Could not determine query size by counting query result", e2); return UNKNOWN_SIZE; } } } @Override public boolean isEmpty() { int size = size(); return size == 0; } @Override public InstanceCollection select(Filter filter) { // TODO apply filter to query instead! return FilteredInstanceCollection.applyFilter(this, filter); } }