ResultsAdapter.java

/*
** Module   : ResultsAdapter.java
** Abstract : Adapter object which abstracts underlying Results implementation
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 ECF 20060803   @28376 Created initial version. Adapter object which abstracts the
**                           underlying Results implementation for a Results user, allowing
**                           Results instances to be connected and disconnected without the
**                           consumer being aware of the change.
** 002 ECF 20060808   @28572 Integrated ResultsProvider mechanism. This device allows client code
**                           to expire the current delegate. Thereafter the adapter will request
**                           a new delegate from the registered results provider at the soonest
**                           opportunity.
** 003 ECF 20070225   @32230 Plug results resource leak. Clean up results resources when a new
**                           results delegate is supplied. This was causing both prepared
**                           statements and result sets to remain unclosed in the backing JDBC
**                           connection.
** 004 ECF 20070711   @34453 Added sessionClosing() method. Delegates the handling of the session
**                           close event to the currently connected Results object, if any.
** 005 ECF 20080122   @36887 Added getResults(). Retrieves delegate worker for this adapter.
** 006 ECF 20080709   @39088 Fixed setResults(). If new results object is null, set delegateNeeded
**                           to true.
** 007 ECF 20190427          Fixed results caching.
** 008 ECF 20200906          New ORM implementation.
** 009 AL2 20230315          Implemented isAutoFetch.
** 010 AL2 20240906          Implemented get(forceOnlyId).
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

/**
 * An implementation of the {@code Results} interface which delegates all work to another {@code
 * Results} implementation.  A worker can be connected at any time using {@link #setResults}, or
 * disconnected by passing a {@code null} parameter to this method.
 * <p>
 * Client code may expire the current delegate worker by invoking the {@link
 * #prepareDelegateChange()} method. This will cause the adapter to replace the delegate at the
 * next safe opportunity. This is accomplished by requesting a new {@code Results} object from a
 * {@link ResultsProvider} which is registered at construction time.
 */
final class ResultsAdapter
implements Results
{
   /** Object which provides results to this adapter upon request */
   private final ResultsProvider provider;
   
   /** Delegate worker object */
   private Results results = null;
   
   /** State flag indicating a new delegate worker must be installed */
   private boolean delegateNeeded = true;
   
   /**
    * Default constructor.
    *
    * @param   provider
    *          Object which provides results to this adapter upon request.
    */
   ResultsAdapter(ResultsProvider provider)
   {
      this.provider = provider;
   }
   
   /**
    * Move cursor to the first result row.
    *
    * @return  {@code true} if there are any results.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean first()
   {
      checkDelegate();
      
      return results.first();
   }
   
   /**
    * Move cursor to the last result row.
    *
    * @return  {@code true} if there are any results.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean last()
   {
      checkDelegate();
      
      return results.last();
   }
   
   /**
    * Move cursor to the next result row.
    *
    * @return  {@code true} if there is a result under the cursor after the move.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean next()
   {
      checkDelegate();
      
      return results.next();
   }
   
   /**
    * Move cursor to the previous result row.
    *
    * @return  {@code true} if there is a result under the cursor after the move.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean previous()
   {
      checkDelegate();
      
      return results.previous();
   }
   
   /**
    * Is the cursor on the first row in the result set?
    *
    * @return  {@code true} if the cursor is on the first row.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean isFirst()
   {
      return results.isFirst();
   }
   
   /**
    * Is the cursor on the last row in the result set?
    *
    * @return  {@code true} if the cursor is on the first row.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean isLast()
   {
      return results.isLast();
   }
   
   /**
    * Is the result delegate responsible for fetching? This is usually true when using
    * query delegates which handle the fetching process already.
    * 
    * @return   {@code true} for complex results which also do the fetching.
    * 
    */
   @Override
   public boolean isAutoFetch()
   {
      return results.isAutoFetch();
   }
   
   /**
    * Returns the row at the current position. It can be either only an array of PK or
    * an array of DMOs. 
    * 
    * @param   forceOnlyId
    *          Indicates whether it's only the primary keys that must be returned, 
    *          or original data found is returned (either PK or DMO).
    *
    * @return  Object array or {@code null}.
    */
   public Object[] get(boolean forceOnlyId)
   {
      return results.get(forceOnlyId);
   }
   
   /**
    * Get the object at the current result row, at the specified column.
    *
    * @param   column
    *          Zero-based index column of the desired object.
    *
    * @return  Object at {@code column} or {@code null}.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public Object get(int column)
   {
      return results.get(column);
   }
   
   /**
    * Get the primary key ID at the current result row, at the specified column.
    *
    * @param   column
    *          Zero-based index column of the desired ID.
    *
    * @return  ID of the record or {@code null}.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public Long getID(int column)
   {
      return results.getID(column);
   }
   
   /**
    * Get the row number currently under the cursor.
    *
    * @return  Zero-based index of the current row, or {@code -1} if the cursor is not currently
    * on a result.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public int getRowNumber()
   {
      return results.getRowNumber();
   }
   
   /**
    * Set the row number currently under the cursor.
    *
    * @param   row
    *          Zero-based index of the row to be set as the current row.
    *
    * @return  {@code true} if there is a row at the specified row number, else {@code false}.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean setRowNumber(int row)
   {
      checkDelegate();
      
      return results.setRowNumber(row);
   }
   
   /**
    * Scroll the cursor ahead by the specified number of rows. If the number is negative, the
    * cursor is moved backward.  This may put the cursor off the end (either end) of the query's
    * results.
    *
    * @param   rows
    *          Number of rows to jump ahead or back.
    *
    * @return  {@code true} if there is a row at the new location, else {@code false}.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public boolean scroll(int rows)
   {
      checkDelegate();
      
      return results.scroll(rows);
   }
   
   /**
    * Reset the cursor to its natural starting position, before the first result row.
    *
    * @throws  NullPointerException
    *          if the adapter is not {@link #isConnected() connected}.
    */
   public void reset()
   {
      checkDelegate();
      
      results.reset();
   }
   
   /**
    * Give the underlying worker a chance to respond to a session closing event.
    * 
    * @see  Results#sessionClosing()
    */
   public void sessionClosing()
   {
      if (isConnected())
      {
         results.sessionClosing();
      }
   }
   
   /**
    * Delegate cleanup to the underlying worker.
    */
   public void cleanup()
   {
      if (isConnected())
      {
         results.cleanup();
      }
   }
   
   /**
    * Indicate whether the full result set is cached in this object.
    * 
    * @return  {@code true} if all results are contained this object; {@code false} if not or
    *          if unknown (such as for a scrolling or cursored result set. This implementation
    *          passes on the invocation to the delegate results object, if any. If none, it
    *          returns {@code false}.
    */
   public boolean isResultSetCached()
   {
      return isConnected() ? results.isResultSetCached() : false;
   }
   
   /**
    * Prepare for a disconnect and for a request for new delegate results from the results
    * provider. The current delegate will be disconnected, and a request for new results made, on
    * the next call to any method which would change the current position within the result set.
    */
   void prepareDelegateChange()
   {
      delegateNeeded = true;
   }
   
   /**
    * Get the underlying {@code Results} worker object for this adapter.
    *
    * @return  Underlying delegate worker.
    */
   Results getResults()
   {
      return results;
   }
   
   /**
    * Set the underlying {@code Results} worker object for this adapter.
    *
    * @param   results
    *          Underlying delegate worker.
    */
   void setResults(Results results)
   {
      if (this.results != null)
      {
         this.results.cleanup();
      }
      
      this.results = results;
      delegateNeeded = (results == null);
   }
   
   /**
    * Report whether this adapter currently is connected to a delegate worker.
    *
    * @return  {@code true} if there is a delegate {@code Results} object connected to this
    * adapter, else {@code false}.
    */
   boolean isConnected()
   {
      return (results != null);
   }
   
   /**
    * Check whether a new delegate worker is needed and if so, request the {@link ResultsProvider}
    * registered with this object to create a new one and to attach it to this adapter.
    */
   private void checkDelegate()
   {
      if (delegateNeeded)
      {
         provider.provideNewResults(this);
      }
   }
}