| /* |
| * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0, |
| * or the Eclipse Distribution License v. 1.0 which is available at |
| * http://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
| */ |
| |
| // Contributors: |
| // Oracle - initial API and implementation from Oracle TopLink |
| package org.eclipse.persistence.queries; |
| |
| import java.util.*; |
| import java.sql.*; |
| import org.eclipse.persistence.exceptions.*; |
| import org.eclipse.persistence.internal.helper.InvalidObject; |
| import org.eclipse.persistence.internal.queries.JoinedAttributeManager; |
| import org.eclipse.persistence.internal.sessions.AbstractRecord; |
| import org.eclipse.persistence.internal.databaseaccess.*; |
| |
| public class ScrollableCursor extends Cursor implements ListIterator { |
| protected transient Object nextObject; |
| protected transient Object previousObject; |
| /** Store the previous row, for 1-m joining. */ |
| protected AbstractRecord previousRow; |
| /** Internal flag indicating if the end of the cursor has been reached */ |
| protected boolean atEndOfCursor = false; |
| |
| /** |
| * INTERNAL: |
| * Default constructor. |
| */ |
| public ScrollableCursor() { |
| super(); |
| } |
| |
| /** |
| * INTERNAL: |
| * constructor. |
| */ |
| public ScrollableCursor(DatabaseCall call, ScrollableCursorPolicy policy) { |
| super(call, policy); |
| setPosition(-1); |
| } |
| |
| /** |
| * PUBLIC: |
| * Moves the cursor to the given row number in the result set |
| */ |
| public boolean absolute(int rows) throws DatabaseException { |
| clearNextAndPrevious(); |
| try { |
| boolean suceeded = false; |
| int initiallyConforming = this.objectCollection.size(); |
| if ((rows >= 0) && (rows <= initiallyConforming)) { |
| this.resultSet.beforeFirst(); |
| setPosition(rows); |
| return true; |
| } else if (rows > initiallyConforming) { |
| suceeded = this.resultSet.absolute(rows - initiallyConforming); |
| if (suceeded) { |
| setPosition(initiallyConforming + rows); |
| } else { |
| // Must be afterLast. |
| setPosition(size() + 1); |
| } |
| return suceeded; |
| } else {// (rows < 0) |
| // Need to know how big the result set is... |
| return absolute(size() + rows); |
| } |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Add is not support for scrollable cursors. |
| */ |
| @Override |
| public void add(Object object) throws QueryException { |
| QueryException.invalidOperation("add"); |
| } |
| |
| /** |
| * PUBLIC: |
| * Moves the cursor to the end of the result set, just after the last row. |
| */ |
| public void afterLast() throws DatabaseException { |
| clearNextAndPrevious(); |
| try { |
| this.resultSet.afterLast(); |
| setPosition(size() + 1); |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Moves the cursor to the front of the result set, just before the first row |
| */ |
| public void beforeFirst() throws DatabaseException { |
| clearNextAndPrevious(); |
| try { |
| this.resultSet.beforeFirst(); |
| setPosition(0); |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Clear the cached next and previous object and row values. |
| * This must be called whenever the cursor is re-positioned. |
| */ |
| protected void clearNextAndPrevious() { |
| this.nextObject = null; |
| this.previousObject = null; |
| this.nextRow = null; |
| this.previousRow = null; |
| this.atEndOfCursor = false; |
| } |
| |
| /** |
| * INTERNAL: |
| * Clear only the cached next and previous object values. |
| * Called by previous() and next() to maintain the cached next |
| * and previous row values. |
| */ |
| protected void clearNextAndPreviousObject() { |
| this.nextObject = null; |
| this.previousObject = null; |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieves the current row index number |
| */ |
| public int currentIndex() throws DatabaseException { |
| return getPosition(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Moves the cursor to the first row in the result set |
| */ |
| public boolean first() throws DatabaseException { |
| clearNextAndPrevious(); |
| try { |
| if (this.objectCollection.size() > 0) { |
| setPosition(1); |
| this.resultSet.beforeFirst(); |
| return true; |
| } else { |
| return this.resultSet.first(); |
| } |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Retrieve the size of the open cursor by executing a count on the same query as the cursor. |
| */ |
| @Override |
| protected int getCursorSize() throws DatabaseException { |
| if (getKnownCursorSize() != -1) { |
| return getKnownCursorSize(); |
| } |
| int currentPos = 0; |
| |
| // If afterLast getRow() will return 0! |
| boolean wasAfterLast = false; |
| |
| try { |
| wasAfterLast = this.resultSet.isAfterLast(); |
| currentPos = this.resultSet.getRow(); |
| this.resultSet.last(); |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| |
| int size = 0; |
| try { |
| size = this.resultSet.getRow(); |
| if (wasAfterLast) {// Move the cursor back to where we were before calling last() |
| this.resultSet.afterLast(); |
| } else if (currentPos == 0) { |
| this.resultSet.beforeFirst(); |
| } else { |
| this.resultSet.absolute(currentPos); |
| } |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| |
| return size; |
| } |
| |
| protected int getKnownCursorSize() { |
| if (size == -1) { |
| return size; |
| } else { |
| return size - this.objectCollection.size(); |
| } |
| } |
| |
| protected Object getNextObject() { |
| return nextObject; |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieves the current cursor position (current row). The first row is number 1, the second number 2, and so on. |
| * Unlike java.sql.ResultSet.getRow(), 0 is not returned if afterLast. |
| * Instead size() + 1 is returned. |
| * @return the current row number; 0 if there is no current row |
| * @exception DatabaseException if a database access error occurs |
| */ |
| @Override |
| public int getPosition() throws DatabaseException { |
| try { |
| if (this.position == -1) { |
| this.position = this.resultSet.getRow(); |
| if (this.position == 0) { |
| // This could mean either beforeFirst or afterLast! |
| if (isAfterLast()) { |
| this.position = size() + 1; |
| } |
| } else { |
| this.position += this.objectCollection.size(); |
| } |
| } |
| return this.position; |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| protected Object getPreviousObject() { |
| return previousObject; |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor can move to the the next row |
| */ |
| @Override |
| public boolean hasMoreElements() throws DatabaseException { |
| return hasNext(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor can move to the the next row |
| */ |
| @Override |
| public boolean hasNext() throws DatabaseException { |
| if (isClosed()) { |
| return false; |
| } |
| loadNext(); |
| return (this.nextObject != null); |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor can move to the the next row |
| */ |
| public boolean hasNextElement() throws DatabaseException { |
| return hasNext(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor can move to the the previous row |
| */ |
| @Override |
| public boolean hasPrevious() throws DatabaseException { |
| if (isClosed()) { |
| return false; |
| } |
| |
| loadPrevious(); |
| return (getPreviousObject() != null); |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor is after the last row in the result set. |
| */ |
| public boolean isAfterLast() throws DatabaseException { |
| try { |
| if (this.nextObject != null) { |
| return false; |
| } |
| if ((this.objectCollection.size() > 0) && (getPosition() <= this.objectCollection.size())) { |
| return false; |
| } |
| return this.resultSet.isAfterLast(); |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor is before the first row in the result set. |
| */ |
| public boolean isBeforeFirst() throws DatabaseException { |
| if (getPreviousObject() != null) { |
| return false; |
| } |
| return getPosition() == 0; |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor is on the first row of the result set. |
| */ |
| public boolean isFirst() throws DatabaseException { |
| if (getPreviousObject() != null) { |
| return false; |
| } |
| try { |
| if (this.objectCollection.size() > 0) { |
| return getPosition() == 1; |
| } |
| return this.resultSet.isFirst(); |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Indicates whether the cursor is on the last row of the result set. |
| */ |
| public boolean isLast() throws DatabaseException { |
| if (this.nextObject != null) { |
| return false; |
| } |
| try { |
| return this.resultSet.isLast(); |
| } catch (UnsupportedOperationException ex) { |
| // isLast() is not supported by some drivers (specifically JConnect5.0) |
| // Do this the hard way instead. |
| try { |
| return this.resultSet.getRow() == getCursorSize(); |
| } catch (SQLException ex2) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, ex2, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(ex2, getAccessor(), this.session, false); |
| } |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Moves the cursor to the last row in the result set |
| */ |
| public boolean last() throws DatabaseException { |
| clearNextAndPrevious(); |
| try { |
| boolean isLast = this.resultSet.last(); |
| if (!isLast) { |
| // cursor must be empty. |
| if (this.objectCollection.size() > 0) { |
| setPosition(this.objectCollection.size()); |
| isLast = true; |
| } |
| } else { |
| setSize(this.objectCollection.size() + this.resultSet.getRow()); |
| setPosition(size); |
| } |
| return isLast; |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * Load the next object |
| */ |
| protected void loadNext() { |
| if (this.nextObject == null) { |
| Object next = retrieveNextObject(); |
| this.nextObject = next; |
| } |
| } |
| |
| /** |
| * Load the previous object. This is used solely for scrollable cursor support |
| */ |
| protected void loadPrevious() { |
| // CR#4139 |
| if (this.previousObject == null) { |
| this.previousObject = retrievePreviousObject(); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * This method differs slightly from conventional read() operation on a Java stream. This |
| * method return the next object in the collection rather than specifying the number of |
| * bytes to be read in. |
| * |
| * Return the next object from the collection, if beyond the read limit read from the cursor |
| * @return - next object in stream |
| * @throws DatabaseException if read pass end of stream |
| */ |
| @Override |
| public Object next() throws DatabaseException, QueryException { |
| loadNext(); |
| if (this.nextObject == null) { |
| throw QueryException.readBeyondStream(this.query); |
| } |
| Object next = this.nextObject; |
| clearNextAndPreviousObject(); |
| return next; |
| } |
| |
| /** |
| * PUBLIC: |
| * This method differs slightly from conventional read() operation on a Java stream. This |
| * method returns the next number of objects in the collection in a vector. |
| * |
| * Return the next specified number of objects from the collection, if beyond the read limit read from the cursor |
| * @param number - number of objects to be returned |
| * @return - vector containing next number of objects |
| * @throws DatabaseException if read pass end of stream |
| */ |
| public List<Object> next(int number) throws DatabaseException { |
| List<Object> result = new ArrayList(number); |
| for (int index = 0; index < number; index++) { |
| result.add(next()); |
| } |
| return result; |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the next object from the collection, if beyond the read limit read from the cursor. |
| * @return next object in stream |
| */ |
| @Override |
| public Object nextElement() throws DatabaseException, QueryException { |
| return next(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieves the next row index (against the current row) |
| */ |
| @Override |
| public int nextIndex() throws DatabaseException { |
| return currentIndex() + 1; |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the previous object from the collection. |
| * |
| * @return - previous object in stream |
| * @throws DatabaseException if read pass first of stream |
| */ |
| @Override |
| public Object previous() throws DatabaseException, QueryException { |
| loadPrevious(); |
| if (this.previousObject == null) { |
| throw QueryException.readBeyondStream(this.query); |
| } |
| Object previous = this.previousObject; |
| clearNextAndPreviousObject(); |
| return previous; |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieves the previous row index (against the current row) |
| */ |
| @Override |
| public int previousIndex() throws DatabaseException { |
| return currentIndex() - 1; |
| } |
| |
| /** |
| * PUBLIC: |
| * Moves the cursor a relative number of rows, either positive or negative. |
| * Attempting to move beyond the first/last row in the result set positions the cursor before/after the |
| * the first/last row |
| */ |
| public boolean relative(int rows) throws DatabaseException { |
| clearNextAndPrevious(); |
| try { |
| boolean suceeded = false; |
| int oldPosition = getPosition(); |
| int newPosition = getPosition() + rows; |
| int initiallyConforming = this.objectCollection.size(); |
| if (newPosition <= initiallyConforming) { |
| setPosition(newPosition); |
| if (oldPosition > initiallyConforming) { |
| this.resultSet.beforeFirst(); |
| } |
| if (newPosition < 0) { |
| setPosition(0); |
| } |
| suceeded = (newPosition > 0); |
| } else { |
| suceeded = this.resultSet.relative(rows); |
| if (!suceeded) { |
| // Must be afterLast now. |
| setPosition(size() + 1); |
| } else { |
| setPosition(newPosition); |
| } |
| } |
| return suceeded; |
| } catch (SQLException exception) { |
| DatabaseException commException = getAccessor().processExceptionForCommError(this.session, exception, null); |
| if (commException != null) throw commException; |
| throw DatabaseException.sqlException(exception, getAccessor(), this.session, false); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Read the next row from the result set. |
| */ |
| @Override |
| protected Object retrieveNextObject() throws DatabaseException { |
| while (true) { |
| int currentPosition = getPosition(); |
| if (currentPosition < this.objectCollection.size()) { |
| this.position = currentPosition + 1; |
| return this.objectCollection.get(currentPosition); |
| } |
| if (isClosed()) { |
| return null; |
| } |
| AbstractRecord row = null; |
| // if the end of the cursor has been reached, do not retrieve more rows |
| if (!this.atEndOfCursor) { |
| if (this.nextRow == null) { |
| row = getAccessor().cursorRetrieveNextRow(this.fields, this.resultSet, this.executionSession); |
| } else { |
| row = this.nextRow; |
| this.nextRow = null; |
| } |
| } |
| |
| this.position = currentPosition + 1; // bug 309142 |
| if (row == null) { |
| this.atEndOfCursor = true; |
| return null; |
| } |
| |
| // If using 1-m joining need to fetch 1-m rows as well. |
| if (this.query.isObjectLevelReadQuery() && ((ObjectLevelReadQuery)this.query).hasJoining()) { |
| JoinedAttributeManager joinManager = ((ObjectLevelReadQuery)this.query).getJoinedAttributeManager(); |
| if (joinManager.isToManyJoin()) { |
| this.nextRow = joinManager.processDataResults(row, this, true); |
| // if the join manager returns a null next row, we are at the end of the cursor |
| if (this.nextRow == null) { |
| this.atEndOfCursor = true; |
| } |
| } |
| } |
| |
| Object object = buildAndRegisterObject(row); |
| if (object == InvalidObject.instance) { |
| continue; |
| } |
| return object; |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * CR#4139 |
| * Read the previous row from the result set. It is used solely |
| * for scrollable cursor support. |
| */ |
| protected Object retrievePreviousObject() throws DatabaseException { |
| while (true) { |
| int currentPosition = getPosition(); |
| if (currentPosition <= (this.objectCollection.size() + 1)) { |
| // If at first of cursor, move cursor to beforeFirst. |
| if ((currentPosition == (this.objectCollection.size() + 1)) && (!isClosed())) { |
| getAccessor().cursorRetrievePreviousRow(this.fields, this.resultSet, this.executionSession); |
| } |
| if (currentPosition <= 1) { |
| // Cursor can not move back further than beforeFirst. |
| this.position = 0; |
| return null; |
| } else { |
| this.position = currentPosition - 1; |
| return this.objectCollection.get(this.position - 1); |
| } |
| } |
| if (isClosed()) { |
| return null; |
| } |
| AbstractRecord row = null; |
| if (this.previousRow == null) { |
| row = getAccessor().cursorRetrievePreviousRow(this.fields, this.resultSet, this.executionSession); |
| } else { |
| row = this.previousRow; |
| this.previousRow = null; |
| } |
| |
| this.position = currentPosition - 1; |
| if (row == null) { |
| return null; |
| } |
| |
| // If using 1-m joining need to fetch 1-m rows as well. |
| if (this.query.isObjectLevelReadQuery() && ((ObjectLevelReadQuery)this.query).hasJoining()) { |
| JoinedAttributeManager joinManager = ((ObjectLevelReadQuery)this.query).getJoinedAttributeManager(); |
| if (joinManager.isToManyJoin()) { |
| this.previousRow = joinManager.processDataResults(row, this, false); |
| } |
| } |
| |
| Object object = buildAndRegisterObject(row); |
| |
| // keep going until find one that conforms. |
| if (object == InvalidObject.instance) { |
| continue; |
| } |
| return object; |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Set is not supported for scrollable cursors. |
| */ |
| @Override |
| public void set(Object object) throws QueryException { |
| QueryException.invalidOperation("set"); |
| } |
| |
| protected void setNextObject(Object nextObject) { |
| this.nextObject = nextObject; |
| } |
| |
| protected void setPreviousObject(Object previousObject) { |
| this.previousObject = previousObject; |
| } |
| } |