SimpleResults.java

/*
** Module   : SimpleResults.java
** Abstract : Simple implementation of an abstraction layer for query results
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------------------------------
** 001 ECF 20060214   @24712 Created initial version. An implementation of
**                           Results which uses a List object to store the
**                           backing row data.
** 002 ECF 20060725   @28191 Added cleanup() method.
** 003 ECF 20070508   @33462 Fixed cleanup() method. Needed a call to
**                           reset the results.
** 004 ECF 20070711   @34449 Added sessionClosing() method. This is a
**                           no-op implementation required by the Results
**                           interface.
** 005 ECF 20080124   @37003 Changed constructor signature. Parameter data
**                           type is more specific.
** 006 ECF 20080310   @37485 Made backing list of rows less specific.
** 007 SVL 20090723   @43404 Added getNumberOfLoadedRows() function.
** 008 SVL 20141015          Fixed edge cases. Added deleteRow.
** 009 ECF 20160330          Fixed NPE in get(int).
** 010 ECF 20190427          Fixed results caching.
** 011 SVL 20191002          Added addRow.
** 012 ECF 20200906          New ORM implementation.
** 013 AL2 20231027          Made SimpleResults implement FullResults and implement isEmpty.
** 014 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;

import java.util.*;

/**
 * A simple implementation of {@code Results} which stores row data in a list provided to the
 * constructor. The row data consists of a list of array objects. Each object in the array is
 * either a {@link Record} object (a DMO instance) or a {@code Long} which
 * represents a DMO's primary key ID.  Note that this implementation may have a heavy memory
 * requirement for large results sets, especially if the entire DMO is stored and not just its
 * primary key ID.
 */
class SimpleResults
implements FullResults
{
   /** Results list;  each row is an array of primary keys or DMOs */
   private final List<Object[]> rows;
   
   /** Index of current position in results list;  -1 is off end */
   private int position = -1;
   
   /**
    * Constructor.
    *
    * @param   rows
    *          List of row data.
    */
   public SimpleResults(List<Object[]> rows)
   {
      this.rows = (rows == null ? new ArrayList<>() : rows);
   }
   
   /**
    * Move cursor to the first results row.
    *
    * @return  {@code true} if there are any results.
    */
   public boolean first()
   {
      if (rows.isEmpty())
      {
         return false;
      }
      
      position = 0;
      
      return true;
   }
   
   /**
    * Move cursor to the last results row.
    *
    * @return  {@code true} if there are any results.
    */
   public boolean last()
   {
      if (rows.isEmpty())
      {
         return false;
      }
      
      position = getNumberOfLoadedRows() - 1;
      
      return true;
   }
   
   /**
    * Move cursor to the next results row.
    *
    * @return  {@code true} if there is a result under the cursor under the move.
    */
   public boolean next()
   {
      position++;
      
      if (position >= getNumberOfLoadedRows())
      {
         position = getNumberOfLoadedRows();
         
         return false;
      }
      
      return true;
   }
   
   /**
    * Move cursor to the previous results row.
    *
    * @return  {@code true} if there is a result under the cursor under the move.
    */
   public boolean previous()
   {
      position--;
      
      if (position < 0)
      {
         position = -1;
         
         return false;
      }
      
      return true;
   }
   
   /**
    * Is the cursor on the first row in the results set?
    *
    * @return  {@code true} if the cursor is on the first row.
    */
   public boolean isFirst()
   {
      return (position == 0);
   }
   
   /**
    * Is the cursor on the last row in the results set?
    *
    * @return  {@code true} if the cursor is on the first row.
    */
   public boolean isLast()
   {
      return (position >= 0 && position == getNumberOfLoadedRows() - 1);
   }
   
   /**
    * 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)
   {
      if (position < 0 || position > getNumberOfLoadedRows() - 1)
      {
         return null;
      }
      
      Object res = rows.get(position);
      if (!(res instanceof Object[]))
      {
          res = new Object[] { res };
      }
      
      if (forceOnlyId)
      {
         Object[] results = new Object[((Object[]) res).length];
         for (int i = 0; i < results.length; i++)
         {
            if (results[i] instanceof Record)
            {
               results[i] = ((Record) results[i]).primaryKey();
            }
            else 
            {
               results[i] = ((Object[]) res)[i];
            }
         }
         return results;
      }
      
      return (Object[]) res;
   }
   
   /**
    * 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}.
    */
   public Object get(int column)
   {
      if (position < 0)
      {
         return null;
      }
      
      Object[] row = get();
      
      return (row != null) ? row[column] : null;
   }
   
   /**
    * 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}.
    */
   public Long getID(int column)
   {
      Object result = get(column);
      if (result instanceof Record)
      {
         return ((Record) result).primaryKey();
      }
      
      return (Long) result;
   }
   
   /**
    * 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.
    */
   public int getRowNumber()
   {
      return position;
   }
   
   /**
    * 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}.
    */
   public boolean setRowNumber(int row)
   {
      int size = getNumberOfLoadedRows();
      
      if (row < 0)
      {
         position = -1;
         
         return false;
      }
      else if (row >= size)
      {
         position = size;
         
         return false;
      }
      
      position = row;
      
      return true;
   }
   
   /**
    * 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}.
    */
   public boolean scroll(int rows)
   {
      return setRowNumber(position + rows);
   }
   
   /**
    * Reset the cursor to its natural starting position, before the first result row.
    */
   public void reset()
   {
      position = -1;
   }
   
   /**
    * Required by the {@code Results} interface, but this is a no-op implementation.
    */
   public void sessionClosing()
   {
   }
   
   /**
    * Clear the internal list of result rows.
    */
   public void cleanup()
   {
      rows.clear();
      reset();
   }
   
   /**
    * 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. Return value for this
    *          default implementation is {@code true}.
    */
   @Override
   public boolean isResultSetCached()
   {
      return true;
   }
   
   /**
    * Returns the number of rows which have been loaded by this {@code Results}.
    *
    * @return  See above.
    */
   public int getNumberOfLoadedRows()
   {
      return rows.size();
   }
   
   /**
    * Delete the current row.
    *
    * @return  {@code true} if the row was deleted. {@code false} if there is no row at
    *          the current position.
    */
   public boolean deleteRow()
   {
      if (position < 0 || position > getNumberOfLoadedRows() - 1)
      {
         return false;
      }
      
      rows.remove(position);
      
      return true;
   }

   /**
    * Add a new row.
    *
    * @param row
    *        Row data.
    * @param beforeRow
    *        <code>true</code> if the current position is before the row. <code>false</code> if
    *        the current position is on the row.
    */
   public void addRow(Object[] row, boolean beforeRow)
   {
      if (position < 0)
      {
         position = 0;
      }
      else if (!beforeRow && position < getNumberOfLoadedRows())
      {
         position++;
      }

      rows.add(position, row);
   }
   
   /**
    * Get the list of row data which backs this results object.
    *
    * @return  List of rows.
    */
   protected List<Object[]> getRows()
   {
      return rows;
   }
   
   /**
    * Check if this has records. This is part of the {@link FullRecords} implementation.
    * 
    * @return  {@code true} if there are rows inside this results.
    */
   @Override
   public boolean isEmpty()
   {
      return rows.isEmpty();
   }
}