/** * OrbisGIS is a java GIS application dedicated to research in GIScience. * OrbisGIS is developed by the GIS group of the DECIDE team of the * Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>. * * The GIS group of the DECIDE team is located at : * * Laboratoire Lab-STICC – CNRS UMR 6285 * Equipe DECIDE * UNIVERSITÉ DE BRETAGNE-SUD * Institut Universitaire de Technologie de Vannes * 8, Rue Montaigne - BP 561 56017 Vannes Cedex * * OrbisGIS is distributed under GPL 3 license. * * Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488) * Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285) * * This file is part of OrbisGIS. * * OrbisGIS is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * OrbisGIS 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along with * OrbisGIS. If not, see <http://www.gnu.org/licenses/>. * * For more information, please consult: <http://www.orbisgis.org/> * or contact directly: * info_at_ orbisgis.org */ package org.orbisgis.corejdbc.internal; import com.vividsolutions.jts.geom.Geometry; import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.collections4.map.LRUMap; import org.h2gis.utilities.JDBCUtilities; import org.h2gis.utilities.SFSUtilities; import org.h2gis.utilities.SpatialResultSetMetaData; import org.h2gis.utilities.TableLocation; import org.orbisgis.corejdbc.AbstractRowSet; import org.orbisgis.corejdbc.ReadRowSet; import org.orbisgis.corejdbc.MetaData; import org.orbisgis.corejdbc.common.IntegerUnion; import org.orbisgis.commons.progress.NullProgressMonitor; import org.orbisgis.commons.progress.ProgressMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnap.commons.i18n.I18n; import org.xnap.commons.i18n.I18nFactory; import javax.sql.DataSource; import javax.sql.rowset.JdbcRowSet; import javax.sql.rowset.RowSetWarning; import java.beans.EventHandler; import java.beans.PropertyChangeListener; import java.io.InputStream; import java.io.PrintWriter; import java.io.Reader; import java.math.BigDecimal; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; import java.sql.Date; import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.Ref; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.RowId; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLWarning; import java.sql.SQLXML; import java.sql.Savepoint; import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * RowSet implementation that can be only linked with a table (or view). * * @author Nicolas Fortin */ public class ReadRowSetImpl extends AbstractRowSet implements JdbcRowSet, DataSource, SpatialResultSetMetaData, ReadRowSet { private static final int WAITING_FOR_RESULTSET = 5; public static final int DEFAULT_FETCH_SIZE = 30; public static final int DEFAULT_CACHE_SIZE = 100; // Like binary search, max intermediate batch fetching private static final int MAX_INTERMEDIATE_BATCH = 5; private static final Logger LOGGER = LoggerFactory.getLogger(ReadRowSetImpl.class); protected static final I18n I18N = I18nFactory.getI18n(ReadRowSetImpl.class, Locale.getDefault(), I18nFactory.FALLBACK); protected TableLocation location; protected final DataSource dataSource; protected Row currentRow; protected long rowId = 0; /** If the table has been updated or never read, rowCount is set to -1 (unknown) */ protected long cachedRowCount = -1; private int cachedColumnCount = -1; protected BidiMap<String, Integer> cachedColumnNames; private boolean wasNull = true; /** Used to managed table without primary key (ResultSet are kept {@link ResultSetHolder#RESULT_SET_TIMEOUT} */ protected final ResultSetHolder resultSetHolder; /** If the table contains a unique non null index then this variable contain the batch first row PK value */ protected List<Long> rowFetchFirstPk = new ArrayList<>(Arrays.asList(new Long[]{null})); protected String pk_name = ""; protected String select_fields = "*"; protected String select_where = ""; // Parameters for prepared statement protected Map<Integer, Object> parameters = new HashMap<>(); protected int firstGeometryIndex = -1; protected final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); protected final Lock readLock = rwl.writeLock(); // Read here is exclusive protected int fetchSize = DEFAULT_FETCH_SIZE; // Cache of requested rows protected Map<Long, Row> cache = new LRUMap<>(DEFAULT_CACHE_SIZE); // Cache of last queried batch protected long currentBatchId = -1; protected List<Row> currentBatch = new ArrayList<>(); private int fetchDirection = FETCH_UNKNOWN; // When close is called, in how many ms the result set is really closed private int closeDelay = 0; protected boolean isH2; //Limit the size of the clob to 1000 characters private static final int NUMBER_CHARACTERS= 1000; /** * Constructor, row set based on primary key, significant faster on large table * @param dataSource Connection properties * @throws IllegalArgumentException If one of the argument is incorrect, that lead to a SQL exception */ public ReadRowSetImpl(DataSource dataSource) { this.dataSource = dataSource; resultSetHolder = new ResultSetHolder(fetchSize, this); } @Override public Lock getReadLock() { return readLock; } private void setWasNull(boolean wasNull) { this.wasNull = wasNull; } protected void checkColumnIndex(int columnIndex) throws SQLException { if(columnIndex < 1 || columnIndex > cachedColumnCount) { throw new SQLException(new IndexOutOfBoundsException("Column index "+columnIndex+" out of bound[1-"+cachedColumnCount+"]")); } } @Override public long getPk() throws SQLException { checkCurrentRow(); return currentRow.pk; } @Override public SortedSet<Integer> getRowNumberFromRowPk(SortedSet<Long> pkSet) throws SQLException { SortedSet<Integer> rowsNum = new IntegerUnion(); if(rowFetchFirstPk == null) { for(long pk : pkSet) { rowsNum.add((int)pk); } } else { // Use first Pk value of batch in order to fetch only batch that contains a selected pk Iterator<Long> fetchPkIt = pkSet.iterator(); int batchIterId = -1; List<Long> batchPK = new ArrayList<>(fetchSize); while (fetchPkIt.hasNext()) { Long fetchPk = fetchPkIt.next(); if(fetchPk != null) { if(batchIterId == -1 || fetchPk > batchPK.get(batchPK.size() - 1)) { batchPK.clear(); // Iterate through batch until next PK is superior than search pk. // For optimisation sake, a binary search could be faster than serial search Long nextPk = Long.MAX_VALUE; final int batchCount = getBatchCount(); do { batchIterId++; if (batchIterId + 1 >= rowFetchFirstPk.size() || rowFetchFirstPk.get(batchIterId + 1) == null) { fetchBatchPk(batchIterId + 1); } if(rowFetchFirstPk.size() > batchIterId + 1) { nextPk = rowFetchFirstPk.get(batchIterId + 1); } else { break; } } while (nextPk < fetchPk && batchIterId + 1 < batchCount - 1); if(nextPk <= fetchPk) { batchIterId++; } } fetchBatchPk(batchIterId); Long batchFirstPk = rowFetchFirstPk.get(batchIterId); // We are in good batch // Query only PK for this batch if(batchPK.isEmpty()) { try (Connection connection = dataSource.getConnection(); PreparedStatement st = createBatchQuery(connection, batchFirstPk, false, 0, fetchSize, true)) { try (ResultSet rs = st.executeQuery()) { while (rs.next()) { batchPK.add(rs.getLong(1)); } } } } // Target batch is in memory, just find the target pk index in it rowsNum.add(batchIterId * fetchSize + Collections.binarySearch(batchPK, fetchPk) + 1); } } } return rowsNum; } private int getBatchCount() throws SQLException { return (int)Math.ceil(getRowCount() / (double)fetchSize); } /** * Check the current RowId, throw an exception if the cursor is at wrong position. * @throws SQLException */ protected void checkCurrentRow() throws SQLException { if(rowId < 1 || rowId > getRowCount()) { throw new SQLException("Not in a valid row "+rowId+"/"+getRowCount()); } if(currentRow == null) { refreshRowCache(); if(currentRow == null) { throw new SQLException("Not in a valid row "+rowId+"/"+getRowCount()); } } } protected void cacheColumnNames() throws SQLException { cachedColumnNames = new DualHashBidiMap<>(); try(Resource res = resultSetHolder.getResource()) { ResultSetMetaData meta = res.getResultSet().getMetaData(); for (int idColumn = 1; idColumn <= meta.getColumnCount(); idColumn++) { cachedColumnNames.put(meta.getColumnName(idColumn), idColumn); } } } /** * Clear local cache of rows */ protected void clearRowCache() { currentRow = null; } private PreparedStatement createBatchQuery(Connection connection, Long firstPk, boolean cacheData, int queryOffset, int limit, boolean queryPk) throws SQLException { StringBuilder command = new StringBuilder(); if(cachedColumnNames == null) { cacheColumnNames(); } command.append("SELECT "); if (queryPk) { command.append(pk_name); if(cacheData) { command.append(","); } } if(cacheData) { command.append(select_fields); } command.append(" FROM "); command.append(getTable()); if(firstPk != null || !select_where.isEmpty()) { command.append(" WHERE "); if(!select_where.isEmpty()) { command.append(select_where); } if (firstPk != null) { if(!select_where.isEmpty()) { command.append(" AND "); } command.append(pk_name); command.append(" >= ?"); } } if (isH2 || !pk_name.equals(MetaData.POSTGRE_ROW_IDENTIFIER)) { command.append(" ORDER BY "); command.append(pk_name); } command.append(" LIMIT "); command.append(limit); if(queryOffset > 0) { command.append(" OFFSET "); command.append(queryOffset); } PreparedStatement st = connection.prepareStatement(command.toString()); for(Map.Entry<Integer, Object> entry : parameters.entrySet()) { st.setObject(entry.getKey(), entry.getValue()); } if(firstPk != null) { if (isH2 || !pk_name.equals(MetaData.POSTGRE_ROW_IDENTIFIER)) { st.setLong(parameters.size() + 1, firstPk); } else { Ref pkRef = new Tid(firstPk); st.setRef(parameters.size() + 1, pkRef); } } return st; } /** * Fetch a batch that start with firstPk * * @param firstPk First row PK * @param cacheData False to only feed rowFetchFirstPk * @param queryOffset Offset pk fetching by this number of rows * @return Pk of next batch * @throws SQLException */ private Long fetchBatch(Long firstPk, boolean cacheData, int queryOffset) throws SQLException { if (cacheData) { currentBatch.clear(); } final int columnCount = getColumnCount(); if (cachedColumnNames == null) { cacheColumnNames(); } boolean ignoreFirstColumn = !cachedColumnNames.containsKey(pk_name); try (Connection connection = dataSource.getConnection(); PreparedStatement st = createBatchQuery(connection, firstPk, cacheData, queryOffset, cacheData ? fetchSize + 1 : 1, ignoreFirstColumn || !cacheData); ResultSet rsBatch = st.executeQuery()) { int curRow = 1; while (rsBatch.next()) { long currentRowPk = rsBatch.getLong(pk_name); if (cacheData) { int offset = ignoreFirstColumn ? 1 : 0; Object[] row = new Object[columnCount]; for (int idColumn = 1 + offset; idColumn <= columnCount + offset; idColumn++) { Object obj = rsBatch.getObject(idColumn); if(obj instanceof Clob){ Clob clob = (Clob) obj; obj = clob.getSubString(1, NUMBER_CHARACTERS)+" ..."; } row[idColumn - 1 - offset] = obj; } currentBatch.add(new Row(row, currentRowPk)); if (curRow++ == fetchSize + 1) { return rsBatch.getLong(pk_name); } } else { return currentRowPk; } } return null; } } /** * Read the content of the DB near the current row id */ protected void refreshRowCache() throws SQLException { if(!cache.containsKey(rowId) && rowId > 0 && rowId <= getRowCount()) { try(Resource res = resultSetHolder.getResource()) { ResultSet rs = res.getResultSet(); final int columnCount = getColumnCount(); if(cachedColumnNames == null) { cacheColumnNames(); } // Do not use pk if not available or if using indeterminate fetch without filtering if(pk_name.isEmpty()) { boolean validRow = false; if(rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { if(rowId < rs.getRow()) { // If the result set is Forward only, we have to re-execute the request in order to read the row resultSetHolder.close(); res.close(); try(Resource res2 = resultSetHolder.getResource()) { rs = res2.getResultSet(); } } while (rs.getRow() < rowId) { validRow = rs.next(); } } else { validRow = rs.absolute((int)rowId); } if(validRow) { Object[] row = new Object[columnCount]; for(int idColumn=1; idColumn <= columnCount; idColumn++) { Object obj = rs.getObject(idColumn); if(obj instanceof Clob){ Clob clob = (Clob) obj; obj = clob.getSubString(1, (int) clob.length()); } row[idColumn-1] = obj; } cache.put(rowId, new Row(row, null)); } } else { // Fetch block pk of current row final int targetBatch = (int) (rowId - 1) / fetchSize; if (currentBatchId != targetBatch) { if (targetBatch >= rowFetchFirstPk.size() || (targetBatch != 0 && rowFetchFirstPk.get(targetBatch) == null)) { // For optimisation sake // Like binary search if the gap of target batch is too wide, require average PK values int topBatchCount = getBatchCount(); int lowerBatchCount = 0; int intermediateBatchFetching = 0; while(lowerBatchCount + ((topBatchCount - lowerBatchCount) / 2) != targetBatch && intermediateBatchFetching < MAX_INTERMEDIATE_BATCH) { int midleBatchTarget = lowerBatchCount + ((topBatchCount - lowerBatchCount) / 2); if(targetBatch < midleBatchTarget) { topBatchCount = midleBatchTarget; } else { if(midleBatchTarget >= rowFetchFirstPk.size() || rowFetchFirstPk.get(midleBatchTarget) == null) { fetchBatchPk(midleBatchTarget); } intermediateBatchFetching++; lowerBatchCount = midleBatchTarget; } } fetchBatchPk(targetBatch); } // Fetch all data of current batch Long firstPk = fetchBatch(rowFetchFirstPk.get(targetBatch), true, 0); if(firstPk!=null) { if(targetBatch + 1 < rowFetchFirstPk.size()) { rowFetchFirstPk.set(targetBatch + 1, firstPk); } else { rowFetchFirstPk.add(firstPk); } } currentBatchId = targetBatch; } // Ok, still in current batch int targetRowInBatch = (int) (rowId - 1) % fetchSize; if(targetRowInBatch < currentBatch.size()) { cache.put(rowId, currentBatch.get(targetRowInBatch)); } } } } currentRow = cache.get(rowId); } private void fetchBatchPk(int targetBatch) throws SQLException { Long firstPk = null; if (targetBatch >= rowFetchFirstPk.size() || rowFetchFirstPk.get(targetBatch) == null) { // Using limit and offset in query try to reduce query time // There is another batchs between target batch and the end of cached batch PK, add null intermediate pk for those. if(targetBatch > rowFetchFirstPk.size()) { Long[] intermediateSkipped = new Long[targetBatch - rowFetchFirstPk.size()]; Arrays.fill(intermediateSkipped, null); rowFetchFirstPk.addAll(Arrays.asList(intermediateSkipped)); } int lastNullBatchPK = targetBatch > 0 ? targetBatch - 1 : 0; while(firstPk == null && lastNullBatchPK > 0) { firstPk = rowFetchFirstPk.get(lastNullBatchPK); if(firstPk == null) { lastNullBatchPK--; } } firstPk = fetchBatch(firstPk, false, (targetBatch - lastNullBatchPK) * fetchSize); if(firstPk != null) { if(targetBatch >= rowFetchFirstPk.size()) { rowFetchFirstPk.add(firstPk); } else { rowFetchFirstPk.set(targetBatch, firstPk); } } } } /** * Reestablish connection if necessary * @throws SQLException */ public Connection getConnection() throws SQLException { Connection connection = dataSource.getConnection(); if(getQueryTimeout() > 0) { try(Statement st = connection.createStatement()) { st.execute("SET QUERY_TIMEOUT "+getQueryTimeout()); } } return connection; } @Override public int getTransactionIsolation() { try(Connection connection = getConnection()) { return connection.getMetaData().getDefaultTransactionIsolation(); } catch (SQLException ex) { return 0; } } protected String getCommandWithoutFields(String additionalWhere) { return " FROM " + location + " " +(select_where.isEmpty() ? additionalWhere : "WHERE " + select_where + (additionalWhere.isEmpty() ? "" : " AND "+additionalWhere)); } @Override public String getCommand() { return "SELECT " + select_fields + getCommandWithoutFields(""); } @Override public void setCommand(String s) throws SQLException { // Extract catalog,schema and table name final Pattern selectFieldPattern = Pattern.compile("^select(.+?)from", Pattern.CASE_INSENSITIVE); final Pattern commandPattern = Pattern.compile("from\\s+((([\"`][^\"`]+[\"`])|(\\w+))\\.){0,2}(([\"`][^\"`]+[\"`])|(\\w+))", Pattern.CASE_INSENSITIVE); final Pattern commandPatternTable = Pattern.compile("^from\\s+", Pattern.CASE_INSENSITIVE); final Pattern wherePattern = Pattern.compile("where\\s+((.+?)+)", Pattern.CASE_INSENSITIVE); String table = ""; Matcher matcher = commandPattern.matcher(s); Matcher selectFieldMatcher = selectFieldPattern.matcher(s); if(selectFieldMatcher.find()) { select_fields = selectFieldMatcher.group(1); } if (matcher.find()) { Matcher tableMatcher = commandPatternTable.matcher(matcher.group()); if(tableMatcher.find()) { table = matcher.group().substring(tableMatcher.group().length()); } } Matcher whereMatcher = wherePattern.matcher(s); if(whereMatcher.find()) { select_where = whereMatcher.group(1); } if(table.isEmpty()) { throw new SQLException("Command does not contain a table name, should be like this \"select * from tablename\""); } this.location = TableLocation.parse(table); try(Connection connection = dataSource.getConnection()) { this.pk_name = MetaData.getPkName(connection, location.toString(), true); } } @Override public String getTable() { return location.toString(); } @Override public void initialize(String tableIdentifier, String pk_name, ProgressMonitor pm) throws SQLException { initialize(TableLocation.parse(tableIdentifier), pk_name, pm); } /** * Initialize this row set. This method cache primary key. * @param location Table location * @param pk_name Primary key name {@link org.orbisgis.corejdbc.MetaData#getPkName(java.sql.Connection, String, boolean)} * @param pm Progress monitor Progression of primary key caching */ public void initialize(TableLocation location,String pk_name, ProgressMonitor pm) throws SQLException { this.location = location; this.pk_name = pk_name; execute(pm); } @Override public void execute(ProgressMonitor pm) throws SQLException { try(Connection connection = dataSource.getConnection()) { isH2 = JDBCUtilities.isH2DataBase(connection.getMetaData()); // Cache Columncount here cachedColumnCount = -1; // Cache Rowcount here cachedRowCount = -1; getRowCount(pm); } if(resultSetHolder.getStatus() == ResultSetHolder.STATUS.NEVER_STARTED) { resultSetHolder.setParameters(parameters); if (!pk_name.isEmpty()) { // Always use PK to fetch rows resultSetHolder.setCommand(getCommand() + " LIMIT 0"); } else { resultSetHolder.setCommand(getCommand()); PropertyChangeListener listener = EventHandler.create(PropertyChangeListener.class, resultSetHolder, "cancel"); pm.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, listener); try (Resource resource = resultSetHolder.getResource()) { } finally { pm.removePropertyChangeListener(listener); } } } else { // Clear cache of all rows rowFetchFirstPk = new ArrayList<>(Arrays.asList(new Long[]{null})); moveCursorTo(Math.min(getRowCount(), rowId)); refreshRow(); } } @Override public void execute() throws SQLException { if(location == null) { throw new SQLException("You must execute RowSet.setCommand(String sql) first"); } initialize(location, pk_name, new NullProgressMonitor()); } @Override public boolean next() throws SQLException { return moveCursorTo(rowId + 1); } @Override public void close() throws SQLException { clearRowCache(); try { resultSetHolder.delayedClose(closeDelay); } catch (Exception ex) { throw new SQLException(ex); } } @Override public void setCloseDelay(int milliseconds) { closeDelay = milliseconds; } public long getRowCount(ProgressMonitor pm) throws SQLException { if(cachedRowCount == -1) { try (Connection connection = getConnection(); PreparedStatement st = createPreparedStatement(connection, "COUNT(*) CPT", "")) { PropertyChangeListener listener = EventHandler.create(PropertyChangeListener.class, st, "cancel"); pm.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, listener); try(ResultSet rs = st.executeQuery()) { if (rs.next()) { cachedRowCount = rs.getLong(1); } } finally { pm.removePropertyChangeListener(listener); } } } return cachedRowCount; } @Override public long getRowCount() throws SQLException { return getRowCount(new NullProgressMonitor()); } protected PreparedStatement createPreparedStatement(Connection connection, String fields, String additionalWhere) throws SQLException { PreparedStatement st = connection.prepareStatement("SELECT "+fields+" "+getCommandWithoutFields(additionalWhere)); for(Map.Entry<Integer, Object> entry : parameters.entrySet()) { st.setObject(entry.getKey(), entry.getValue()); } return st; } @Override public boolean wasNull() throws SQLException { return wasNull; } @Override public SQLWarning getWarnings() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getWarnings(); } } @Override public void clearWarnings() throws SQLException { try(Resource res = resultSetHolder.getResource()) { res.getResultSet().clearWarnings(); } } @Override public String getCursorName() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getCursorName(); } } @Override public ResultSetMetaData getMetaData() throws SQLException { return this; } @Override public Object getObject(int i) throws SQLException { checkColumnIndex(i); checkCurrentRow(); Object cell = currentRow.row[i-1]; setWasNull(cell == null); return cell; } @Override public int findColumn(String label) throws SQLException { if(cachedColumnNames == null) { cacheColumnNames(); } Integer columnId = cachedColumnNames.get(label); if(columnId == null) { // Search with insensitive case for(Map.Entry<String, Integer> entry : cachedColumnNames.entrySet()) { if(entry.getKey().equalsIgnoreCase(label)) { return entry.getValue(); } } throw new SQLException("Column "+label+" does not exists"); } return columnId; } @Override public boolean isBeforeFirst() throws SQLException { return rowId == 0; } @Override public boolean isAfterLast() throws SQLException { return rowId > getRowCount(); } @Override public boolean isFirst() throws SQLException { return rowId == 1; } @Override public boolean isLast() throws SQLException { return rowId == getRowCount(); } @Override public void beforeFirst() throws SQLException { moveCursorTo(0); } @Override public void afterLast() throws SQLException { moveCursorTo((int) (getRowCount() + 1)); } @Override public boolean first() throws SQLException { return moveCursorTo(1); } @Override public boolean last() throws SQLException { return moveCursorTo((int)getRowCount()); } @Override public int getRow() throws SQLException { return (int)rowId; } @Override public boolean absolute(int i) throws SQLException { return moveCursorTo(i); } private boolean moveCursorTo(long i) throws SQLException { i = Math.max(0, i); i = Math.min(getRowCount() + 1, i); long oldRowId = rowId; rowId = i; boolean validRow = !(rowId == 0 || rowId > getRowCount()); if(validRow) { refreshRowCache(); } else { currentRow = null; } if(rowId != oldRowId) { notifyCursorMoved(); } return validRow && currentRow != null; } @Override public boolean relative(int i) throws SQLException { return moveCursorTo((int)(rowId + i)); } @Override public boolean previous() throws SQLException { return moveCursorTo(rowId - 1); } @Override public void setFetchDirection(int i) throws SQLException { fetchDirection = i; } @Override public int getFetchDirection() throws SQLException { return fetchDirection; } @Override public void setFetchSize(int i) throws SQLException { fetchSize = i; LRUMap<Long, Row> lruMap = new LRUMap<>(fetchSize + 1); lruMap.putAll(cache); cache = lruMap; rowFetchFirstPk = new ArrayList<>(Arrays.asList(new Long[]{null})); currentBatch.clear(); currentBatchId = -1; } @Override public int getFetchSize() throws SQLException { return fetchSize; } @Override public int getType() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getType(); } } @Override public int getConcurrency() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getConcurrency(); } } @Override public boolean rowUpdated() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().rowUpdated(); } } @Override public boolean rowInserted() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().rowInserted(); } } @Override public boolean rowDeleted() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().rowDeleted(); } } @Override public void refreshRows(SortedSet<Integer> rowsIndex) throws SQLException { try(Resource res = resultSetHolder.getResource()) { Set<Integer> batchIds = new HashSet<>(); for(int refRowId : rowsIndex) { batchIds.add(refRowId / fetchSize); cache.remove(((long)refRowId)); } for(int batchId : batchIds) { if(batchId < rowFetchFirstPk.size() && batchId >= 0) { rowFetchFirstPk.set(batchId, null); } if(batchId == currentBatchId) { currentBatchId = -1; currentBatch = new ArrayList<>(fetchSize + 1); } } } catch (SQLException ex) { LOGGER.warn(ex.getLocalizedMessage(), ex); } } @Override public void refreshRow() throws SQLException { try(Resource res = resultSetHolder.getResource()) { currentRow = null; cache.clear(); currentBatch = new ArrayList<>(fetchSize + 1); currentBatchId = -1; if(res.getResultSet().getRow() > 0 && !res.getResultSet().isAfterLast()) { res.getResultSet().refreshRow(); } } catch (SQLException ex) { LOGGER.warn(ex.getLocalizedMessage(), ex); } } @Override public Statement getStatement() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getStatement(); } } @Override public RowId getRowId(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getRowId(i); } } @Override public RowId getRowId(String s) throws SQLException { return getRowId(findColumn(s)); } @Override public int getHoldability() throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getHoldability(); } } @Override public boolean isClosed() throws SQLException { return false; // Never closed } // DataSource methods @Override public Connection getConnection(String s, String s2) throws SQLException { throw new UnsupportedOperationException(); } @Override public PrintWriter getLogWriter() throws SQLException { throw new UnsupportedOperationException(); } @Override public void setLogWriter(PrintWriter printWriter) throws SQLException { throw new UnsupportedOperationException(); } @Override public void setLoginTimeout(int i) throws SQLException { throw new UnsupportedOperationException(); } @Override public int getLoginTimeout() throws SQLException { throw new UnsupportedOperationException(); } @Override public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new UnsupportedOperationException(); } // ResultSetMetaData functions @Override public String getCatalogName(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getCatalogName(i); } } @Override public int getColumnCount() throws SQLException { if(cachedColumnCount == -1) { try(Resource res = resultSetHolder.getResource()) { cachedColumnCount = res.getResultSet().getMetaData().getColumnCount(); return cachedColumnCount; } } return cachedColumnCount; } @Override public boolean isAutoIncrement(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isAutoIncrement(i); } } @Override public boolean isCaseSensitive(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isCaseSensitive(i); } } @Override public boolean isSearchable(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isSearchable(i); } } @Override public boolean isCurrency(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isCurrency(i); } } @Override public int isNullable(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isNullable(i); } } @Override public boolean isSigned(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isSigned(i); } } @Override public int getColumnDisplaySize(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getColumnDisplaySize(i); } } @Override public void setObject(int parameterIndex, Object x) throws SQLException { parameters.put(parameterIndex, x); } @Override public String getColumnLabel(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getColumnLabel(i); } } @Override public String getColumnName(int i) throws SQLException { if(cachedColumnNames == null) { cacheColumnNames(); } return cachedColumnNames.getKey(i); } @Override public String getSchemaName(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getSchemaName(i); } } @Override public int getPrecision(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getPrecision(i); } } @Override public int getScale(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getScale(i); } } @Override public String getTableName(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getTableName(i); } } @Override public int getColumnType(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getColumnType(i); } } @Override public String getColumnTypeName(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getColumnTypeName(i); } } @Override public boolean isReadOnly(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isReadOnly(i); } } @Override public boolean isWritable(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isWritable(i); } } @Override public boolean isDefinitelyWritable(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().isDefinitelyWritable(i); } } @Override public String getColumnClassName(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { return res.getResultSet().getMetaData().getColumnClassName(i); } } @Override public void updateNull(int i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBoolean(int i, boolean b) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateByte(int i, byte b) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateShort(int i, short i2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateInt(int i, int i2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateLong(int i, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateFloat(int i, float v) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateDouble(int i, double v) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBigDecimal(int i, BigDecimal bigDecimal) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateString(int i, String s) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBytes(int i, byte[] bytes) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateDate(int i, Date date) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateTime(int i, Time time) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateTimestamp(int i, Timestamp timestamp) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateAsciiStream(int i, InputStream inputStream, int i2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBinaryStream(int i, InputStream inputStream, int i2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateCharacterStream(int i, Reader reader, int i2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateObject(int i, Object o, int i2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateObject(int i, Object o) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNull(String s) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBoolean(String s, boolean b) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateByte(String s, byte b) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateShort(String s, short i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateInt(String s, int i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateLong(String s, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateFloat(String s, float v) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateDouble(String s, double v) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBigDecimal(String s, BigDecimal bigDecimal) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateString(String s, String s2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBytes(String s, byte[] bytes) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateDate(String s, Date date) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateTime(String s, Time time) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateTimestamp(String s, Timestamp timestamp) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateAsciiStream(String s, InputStream inputStream, int i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBinaryStream(String s, InputStream inputStream, int i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateCharacterStream(String s, Reader reader, int i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateObject(String s, Object o, int i) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateObject(String s, Object o) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void insertRow() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateRow() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void deleteRow() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void cancelRowUpdates() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void moveToInsertRow() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void moveToCurrentRow() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateRef(int i, Ref ref) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateRef(String s, Ref ref) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBlob(int i, Blob blob) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBlob(String s, Blob blob) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateClob(int i, Clob clob) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateClob(String s, Clob clob) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateArray(int i, Array array) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateArray(String s, Array array) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateRowId(int i, RowId rowId) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateRowId(String s, RowId rowId) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNString(int i, String s) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNString(String s, String s2) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNClob(int i, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNClob(String s, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateSQLXML(int i, SQLXML sqlxml) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateSQLXML(String s, SQLXML sqlxml) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNCharacterStream(int i, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNCharacterStream(String s, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateAsciiStream(int i, InputStream inputStream, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBinaryStream(int i, InputStream inputStream, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateCharacterStream(int i, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateAsciiStream(String s, InputStream inputStream, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBinaryStream(String s, InputStream inputStream, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateCharacterStream(String s, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBlob(int i, InputStream inputStream, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBlob(String s, InputStream inputStream, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateClob(int i, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateClob(String s, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNClob(int i, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNClob(String s, Reader reader, long l) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNCharacterStream(int i, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNCharacterStream(String s, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateAsciiStream(int i, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBinaryStream(int i, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateCharacterStream(int i, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateAsciiStream(String s, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBinaryStream(String s, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateCharacterStream(String s, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBlob(int i, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateBlob(String s, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateClob(int i, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateClob(String s, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNClob(int i, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateNClob(String s, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public RowSetWarning getRowSetWarnings() throws SQLException { throw new SQLFeatureNotSupportedException("getRowSetWarnings not supported"); } @Override public void commit() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public boolean getAutoCommit() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void setAutoCommit(boolean autoCommit) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void rollback() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void rollback(Savepoint s) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void setMatchColumn(int columnIdx) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void setMatchColumn(int[] columnIdxes) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void setMatchColumn(String columnName) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void setMatchColumn(String[] columnNames) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public int[] getMatchColumnIndexes() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public String[] getMatchColumnNames() throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void unsetMatchColumn(int columnIdx) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void unsetMatchColumn(int[] columnIdxes) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void unsetMatchColumn(String columnName) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void unsetMatchColumn(String[] columnName) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public Geometry getGeometry() throws SQLException { if(firstGeometryIndex == -1) { try(Connection connection = dataSource.getConnection()) { List<String> geoFields = SFSUtilities.getGeometryFields(connection, location); if(!geoFields.isEmpty()) { firstGeometryIndex = JDBCUtilities.getFieldIndex(getMetaData(), geoFields.get(0)); } else { throw new SQLException("No geometry column found"); } } } return getGeometry(firstGeometryIndex); } @Override public void updateGeometry(int columnIndex, Geometry geometry) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public void updateGeometry(String columnLabel, Geometry geometry) throws SQLException { throw new SQLFeatureNotSupportedException("Read only RowSet"); } @Override public String getPkName() { return pk_name; } @Override public int getGeometryType(int i) throws SQLException { try(Resource res = resultSetHolder.getResource()) { SpatialResultSetMetaData meta = res.getResultSet().getMetaData().unwrap(SpatialResultSetMetaData.class); return meta.getGeometryType(i); } } @Override public int getGeometryType() throws SQLException { try(Resource res = resultSetHolder.getResource()) { SpatialResultSetMetaData meta = res.getResultSet().getMetaData().unwrap(SpatialResultSetMetaData.class); return meta.getGeometryType(); } } @Override public int getFirstGeometryFieldIndex() throws SQLException { try(Resource res = resultSetHolder.getResource()) { SpatialResultSetMetaData meta = res.getResultSet().getMetaData().unwrap(SpatialResultSetMetaData.class); return meta.getFirstGeometryFieldIndex(); } } /** * This thread guaranty that the connection,ResultSet is released when no longer used. */ private static class ResultSetHolder implements Runnable,AutoCloseable { private static final int SLEEP_TIME = 1000; private static final int RESULT_SET_TIMEOUT = 60000; private final int fetchSize; public enum STATUS { NEVER_STARTED, STARTED , READY, CLOSING, CLOSED, EXCEPTION} private Exception ex; private ResultSet resultSet; private DataSource dataSource; private String command; private Map<Integer, Object> parameters = new HashMap<>(); private STATUS status = STATUS.NEVER_STARTED; private long lastUsage = System.currentTimeMillis(); private static final Logger LOGGER = LoggerFactory.getLogger(ResultSetHolder.class); private int openCount = 0; private Statement cancelStatement; private ResultSetHolder(int fetchSize, DataSource dataSource) { this.fetchSize = fetchSize; this.dataSource = dataSource; } /** * @param command SQL command to execute */ public void setCommand(String command) { this.command = command; } public void setParameters(Map<Integer, Object> parameters) { this.parameters = parameters; } /** * @return SQL Command, may be null if not set */ public String getCommand() { return command; } @Override public void run() { lastUsage = System.currentTimeMillis(); status = STATUS.STARTED; try (Connection connection = dataSource.getConnection()) { boolean isH2 = JDBCUtilities.isH2DataBase(connection.getMetaData()); try ( PreparedStatement st = connection.prepareStatement(command, isH2 ? ResultSet .TYPE_SCROLL_SENSITIVE : ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { cancelStatement = st; st.setFetchSize(fetchSize); for(Map.Entry<Integer, Object> entry : parameters.entrySet()) { st.setObject(entry.getKey(), entry.getValue()); } if (!isH2) { // Memory optimisation for PostGre connection.setAutoCommit(false); } // PostGreSQL use cursor only if auto commit is false try (ResultSet activeResultSet = st.executeQuery()) { resultSet = activeResultSet; status = STATUS.READY; while (lastUsage + RESULT_SET_TIMEOUT > System.currentTimeMillis() || openCount != 0) { Thread.sleep(SLEEP_TIME); } } } } catch (Exception ex) { LOGGER.error(ex.getLocalizedMessage(), ex); this.ex = ex; status = STATUS.EXCEPTION; } finally { if (status != STATUS.EXCEPTION) { status = STATUS.CLOSED; } } } @Override public void close() throws SQLException { lastUsage = 0; openCount = 0; status = STATUS.CLOSING; } public void delayedClose(int milliSec) { lastUsage = System.currentTimeMillis() - RESULT_SET_TIMEOUT + milliSec; openCount = 0; } /** * {@link java.sql.Statement#cancel()} * @throws SQLException */ public void cancel() throws SQLException { Statement cancelObj = cancelStatement; if(cancelObj != null && !cancelObj.isClosed()) { cancelObj.cancel(); } } /** * @return ResultSet status */ public STATUS getStatus() { return status; } public Resource getResource() throws SQLException { // Wait execution of request while(getStatus() != STATUS.READY) { // Reactivate result set if necessary if(getStatus() == ResultSetHolder.STATUS.CLOSED || getStatus() == ResultSetHolder.STATUS.NEVER_STARTED) { Thread resultSetThread = new Thread(this, "ResultSet of "+command); resultSetThread.start(); } if(status == STATUS.EXCEPTION) { if(ex instanceof SQLException) { throw (SQLException)ex; } else { throw new SQLException(ex); } } try { Thread.sleep(WAITING_FOR_RESULTSET); } catch (InterruptedException e) { throw new SQLException(e); } } lastUsage = System.currentTimeMillis(); openCount++; return new Resource(this, resultSet); } /** * Even if the timer should close the result set, the connection is not closed */ public void onResourceClosed() { openCount = Math.max(0, openCount-1); } } /** * This class is created each time the result set is necessary, close this object to release the result set. */ private static class Resource implements AutoCloseable { private final ResultSet resultSet; private final ResultSetHolder holder; private Resource(ResultSetHolder holder, ResultSet resultSet) { this.holder = holder; this.resultSet = resultSet; } @Override public void close() { holder.onResourceClosed(); } private ResultSet getResultSet() { return resultSet; } } private static class Tid implements Ref { private long value; private Tid(long value) { this.value = value; } @Override public String getBaseTypeName() throws SQLException { return "tid"; } @Override public Object getObject(Map<String, Class<?>> map) throws SQLException { return value; } @Override public Object getObject() throws SQLException { return value; } @Override public void setObject(Object value) throws SQLException { throw new UnsupportedOperationException(); } } protected static class Row { public final Object[] row; public final Long pk; public Row(Object[] row, Long pk) { this.row = row; this.pk = pk; } } }