View Javadoc
1   package org.synchronoss.cpo.jdbc;
2   
3   /*-
4    * [[
5    * jdbc
6    * ==
7    * Copyright (C) 2003 - 2026 Exaxis LLC, Synchronoss Technologies Inc
8    * ==
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ]]
23   */
24  
25  import java.lang.reflect.Constructor;
26  import java.sql.CallableStatement;
27  import java.sql.Connection;
28  import java.sql.PreparedStatement;
29  import java.sql.ResultSet;
30  import java.sql.ResultSetMetaData;
31  import java.sql.SQLException;
32  import java.sql.Statement;
33  import java.sql.Types;
34  import java.util.ArrayList;
35  import java.util.Collection;
36  import java.util.List;
37  import java.util.Spliterator;
38  import java.util.Spliterators;
39  import java.util.function.Consumer;
40  import java.util.stream.Stream;
41  import java.util.stream.StreamSupport;
42  import javax.naming.Context;
43  import javax.naming.InitialContext;
44  import javax.naming.NamingException;
45  import javax.sql.DataSource;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  import org.synchronoss.cpo.core.*;
49  import org.synchronoss.cpo.core.enums.Comparison;
50  import org.synchronoss.cpo.core.enums.Crud;
51  import org.synchronoss.cpo.core.enums.Logical;
52  import org.synchronoss.cpo.core.helper.ExceptionHelper;
53  import org.synchronoss.cpo.core.meta.CpoMetaDescriptor;
54  import org.synchronoss.cpo.core.meta.DataTypeMapEntry;
55  import org.synchronoss.cpo.core.meta.domain.CpoArgument;
56  import org.synchronoss.cpo.core.meta.domain.CpoAttribute;
57  import org.synchronoss.cpo.core.meta.domain.CpoClass;
58  import org.synchronoss.cpo.core.meta.domain.CpoFunction;
59  import org.synchronoss.cpo.jdbc.meta.JdbcCpoMetaDescriptor;
60  import org.synchronoss.cpo.jdbc.meta.JdbcMethodMapper;
61  import org.synchronoss.cpo.jdbc.meta.JdbcResultSetCpoData;
62  
63  /**
64   * JdbcCpoAdapter is the JDBC implementation of {@link org.synchronoss.cpo.core.CpoAdapter}, a set
65   * of routines that are responsible for managing value beans from a jdbc datasource.
66   *
67   * @author david berry
68   */
69  public class JdbcCpoAdapter extends CpoBaseAdapter<DataSource> {
70  
71    /** Version Id for this class. */
72    private static final long serialVersionUID = 1L;
73  
74    private static final Logger logger = LoggerFactory.getLogger(JdbcCpoAdapter.class);
75  
76    /** The JNDI context used to resolve a {@code jndiName}-configured data source, if any. */
77    private transient Context context_ = null;
78  
79    /** The capabilities of the database behind this adapter, probed once at construction. */
80    private final JdbcDatabaseCapabilities capabilities;
81  
82    /** How this adapter obtains and releases its connections; see JdbcConnectionStrategy. */
83    private transient JdbcConnectionStrategy connectionStrategy;
84  
85    /** CpoMetaDescriptor allows you to get the metadata for a class. */
86    private JdbcCpoMetaDescriptor metaDescriptor = null;
87  
88    /**
89     * Creates a JdbcCpoAdapter.
90     *
91     * @param metaDescriptor This datasource that identifies the cpo metadata datasource
92     * @param jdsiTrx The datasoruce that identifies the transaction database.
93     * @throws CpoException exception
94     */
95    protected JdbcCpoAdapter(JdbcCpoMetaDescriptor metaDescriptor, DataSourceInfo<DataSource> jdsiTrx)
96        throws CpoException {
97      super(jdsiTrx.getDataSourceName(), jdsiTrx.getFetchSize(), jdsiTrx.getBatchSize());
98      this.metaDescriptor = metaDescriptor;
99      setWriteDataSource(jdsiTrx.getDataSource());
100     setReadDataSource(jdsiTrx.getDataSource());
101     this.connectionStrategy =
102         new JdbcPooledConnectionStrategy(getReadDataSource(), getWriteDataSource());
103     this.capabilities = JdbcDatabaseCapabilities.probe(connectionStrategy);
104   }
105 
106   /**
107    * Creates a JdbcCpoAdapter.
108    *
109    * @param metaDescriptor This datasource that identifies the cpo metadata datasource
110    * @param jdsiWrite The datasource that identifies the transaction database for write
111    *     transactions.
112    * @param jdsiRead The datasource that identifies the transaction database for read-only
113    *     transactions.
114    * @throws CpoException exception
115    */
116   protected JdbcCpoAdapter(
117       JdbcCpoMetaDescriptor metaDescriptor,
118       DataSourceInfo<DataSource> jdsiWrite,
119       DataSourceInfo<DataSource> jdsiRead)
120       throws CpoException {
121     super(jdsiWrite.getDataSourceName(), jdsiWrite.getFetchSize(), jdsiWrite.getBatchSize());
122     this.metaDescriptor = metaDescriptor;
123     setWriteDataSource(jdsiWrite.getDataSource());
124     setReadDataSource(jdsiRead.getDataSource());
125     this.connectionStrategy =
126         new JdbcPooledConnectionStrategy(getReadDataSource(), getWriteDataSource());
127     this.capabilities = JdbcDatabaseCapabilities.probe(connectionStrategy);
128   }
129 
130   /**
131    * This constructor is used specifically to clone a JdbcCpoAdapter.
132    *
133    * @param jdbcCpoAdapter - the JdbcCpoAdapter to clone
134    * @throws CpoException An exception occurred copying the datasource.
135    */
136   protected JdbcCpoAdapter(JdbcCpoAdapter jdbcCpoAdapter) throws CpoException {
137     super(
138         jdbcCpoAdapter.getDataSourceName(),
139         jdbcCpoAdapter.getFetchSize(),
140         jdbcCpoAdapter.getBatchSize());
141     this.metaDescriptor = (JdbcCpoMetaDescriptor) jdbcCpoAdapter.getCpoMetaDescriptor();
142     this.capabilities = jdbcCpoAdapter.capabilities;
143     setWriteDataSource(jdbcCpoAdapter.getWriteDataSource());
144     setReadDataSource(jdbcCpoAdapter.getReadDataSource());
145     this.connectionStrategy =
146         new JdbcPooledConnectionStrategy(getReadDataSource(), getWriteDataSource());
147   }
148 
149   /**
150    * Gets the connection strategy this adapter uses
151    *
152    * @return The connection strategy
153    */
154   JdbcConnectionStrategy getConnectionStrategy() {
155     return connectionStrategy;
156   }
157 
158   /**
159    * Replaces the connection strategy; used by JdbcCpoTrxAdapter to pin a transaction connection
160    *
161    * @param connectionStrategy The connection strategy to use
162    */
163   void setConnectionStrategy(JdbcConnectionStrategy connectionStrategy) {
164     this.connectionStrategy = connectionStrategy;
165   }
166 
167   /**
168    * Are batch updates supported
169    *
170    * @return true if batch updates are supported
171    */
172   protected boolean isBatchUpdatesSupported() {
173     return capabilities.batchUpdatesSupported();
174   }
175 
176   /**
177    * Finds or Creates a JdbcCpoAdapter.
178    *
179    * @param metaDescriptor - The meta descriptor for the datasource
180    * @param jdsiTrx - The datasurce info
181    * @return A JdbcCpoAdapter
182    * @throws CpoException - An error occurred finding of creating the datasource adapter
183    */
184   public static JdbcCpoAdapter getInstance(
185       JdbcCpoMetaDescriptor metaDescriptor, DataSourceInfo<DataSource> jdsiTrx)
186       throws CpoException {
187     String adapterKey = metaDescriptor + ":" + jdsiTrx.getDataSourceName();
188     JdbcCpoAdapter adapter = (JdbcCpoAdapter) findCpoAdapter(adapterKey);
189     if (adapter == null) {
190       adapter = new JdbcCpoAdapter(metaDescriptor, jdsiTrx);
191       addCpoAdapter(adapterKey, adapter);
192     }
193     return adapter;
194   }
195 
196   /**
197    * Creates a JdbcCpoAdapter.
198    *
199    * @param metaDescriptor This datasource that identifies the cpo metadata datasource
200    * @param jdsiWrite The datasource that identifies the transaction database for write
201    *     transactions.
202    * @param jdsiRead The datasource that identifies the transaction database for read-only
203    *     transactions.
204    * @return - A JdbcCpoAdapter
205    * @throws CpoException exception
206    */
207   public static JdbcCpoAdapter getInstance(
208       JdbcCpoMetaDescriptor metaDescriptor,
209       DataSourceInfo<DataSource> jdsiWrite,
210       DataSourceInfo<DataSource> jdsiRead)
211       throws CpoException {
212     String adapterKey =
213         metaDescriptor + ":" + jdsiWrite.getDataSourceName() + ":" + jdsiRead.getDataSourceName();
214     JdbcCpoAdapter adapter = (JdbcCpoAdapter) findCpoAdapter(adapterKey);
215     if (adapter == null) {
216       adapter = new JdbcCpoAdapter(metaDescriptor, jdsiWrite, jdsiRead);
217       addCpoAdapter(adapterKey, adapter);
218     }
219     return adapter;
220   }
221 
222   @Override
223   public <T> long existsBean(CpoQuery query, T bean) throws CpoException {
224     Connection c = null;
225     long objCount = -1;
226 
227     try {
228       c = getReadConnection();
229 
230       objCount = existsBean(query.groupName(), bean, c, query.wheres());
231     } finally {
232       // The read connection runs with autocommit off (required for cursor-based fetch), so
233       // this read-only transaction must still be terminated before the connection returns
234       // to the pool. Otherwise the open transaction pins a stale MVCC snapshot (and holds
235       // any FOR UPDATE locks) for the next borrower. Rollback is used because no data was
236       // changed; it releases the snapshot and locks just like commit would.
237       rollbackLocalConnection(c);
238       closeLocalConnection(c);
239     }
240 
241     return objCount;
242   }
243 
244   /**
245    * The CpoAdapter will check to see if this bean exists in the datasource.
246    *
247    * @param <T> The type of the bean
248    * @param groupName The groupName which identifies which EXISTS, INSERT, and UPDATE Function
249    *     Groups to execute to upsert the bean.
250    * @param bean This is a bean that has been defined within the metadata of the datasource. If the
251    *     class is not defined an exception will be thrown.
252    * @param con The datasource Connection with which to check if the bean exists
253    * @param wheres A collection of where clauses
254    * @return The number of matching beans: for a single-row result with one numeric column the
255    *     column value is returned (count(*) style), otherwise the number of rows returned
256    * @throws CpoException exception will be thrown if the Function Group has a function count != 1
257    */
258   protected <T> long existsBean(
259       String groupName, T bean, Connection con, Collection<CpoWhere> wheres) throws CpoException {
260     PreparedStatement ps = null;
261     ResultSet rs = null;
262     ResultSetMetaData rsmd;
263     CpoClass cpoClass;
264     long objCount = 0;
265     Logger localLogger = logger;
266 
267     if (bean == null) {
268       throw new CpoException("NULL Bean passed into existsBean");
269     }
270 
271     try {
272       cpoClass = metaDescriptor.getMetaClass(bean);
273       List<CpoFunction> cpoFunctions =
274           cpoClass.getFunctionGroup(Crud.EXIST, groupName).getFunctions();
275       localLogger = LoggerFactory.getLogger(cpoClass.getMetaClass());
276 
277       for (CpoFunction cpoFunction : cpoFunctions) {
278         localLogger.info(cpoFunction.getExpression());
279         JdbcPreparedStatementFactory jpsf =
280             new JdbcPreparedStatementFactory(
281                 con, this, cpoClass, cpoFunction, bean, wheres, null, null);
282         ps = jpsf.getPreparedStatement();
283 
284         long qCount = 0; // set the results for this function to 0
285 
286         rs = ps.executeQuery();
287         jpsf.release();
288         rsmd = rs.getMetaData();
289 
290         // A single-row result with one numeric column is interpreted as a count(*)
291         // style EXIST function; everything else is counted row by row.
292         if (rsmd.getColumnCount() == 1 && isCountResult(rsmd.getColumnType(1)) && rs.next()) {
293           qCount = rs.getLong(1); // the number of beans that exist
294           if (rs.next()) {
295             // EXIST function has more than one record so not a count(*)
296             qCount = 2;
297           }
298         }
299 
300         while (rs.next()) {
301           qCount++;
302         }
303 
304         objCount += qCount;
305 
306         rs.close();
307         ps.close();
308         rs = null;
309         ps = null;
310       }
311     } catch (SQLException e) {
312       String msg = "existsBean(groupName, bean, con) failed:";
313       throw new CpoException(msg, e);
314     } finally {
315       resultSetClose(rs);
316       statementClose(ps);
317     }
318 
319     return objCount;
320   }
321 
322   private static boolean isCountResult(int sqlType) {
323     return switch (sqlType) {
324       case Types.BIGINT,
325           Types.INTEGER,
326           Types.NUMERIC,
327           Types.DECIMAL,
328           Types.SMALLINT,
329           Types.TINYINT ->
330           true;
331       default -> false;
332     };
333   }
334 
335   @Override
336   public CpoWhere newWhere() {
337     return new JdbcCpoWhere();
338   }
339 
340   @Override
341   public <T> CpoWhere newWhere(Logical logical, String attr, Comparison comp, T value) {
342     return new JdbcCpoWhere(logical, attr, comp, value);
343   }
344 
345   @Override
346   public <T> CpoWhere newWhere(
347       Logical logical, String attr, Comparison comp, T value, boolean not) {
348     return new JdbcCpoWhere(logical, attr, comp, value, not);
349   }
350 
351   /**
352    * Sets the JNDI context
353    *
354    * @param context The JNDI Context
355    * @throws CpoException An exception setting the context
356    */
357   protected void setContext(Context context) throws CpoException {
358     try {
359       if (context == null) {
360         context_ = new InitialContext();
361       } else {
362         context_ = context;
363       }
364     } catch (NamingException e) {
365       throw new CpoException("Error setting Context", e);
366     }
367   }
368 
369   /**
370    * Gets the JNDI Context
371    *
372    * @return The current context
373    */
374   protected Context getContext() {
375     return context_;
376   }
377 
378   /**
379    * Validates the crud of query being performed. If it is an UPSERT Group, it checks the database
380    * to see if this is an update or an insert, and returns the query group. Otherwise, it sends back
381    * the original query group. Upserts only work for single beans.
382    *
383    * @param <T> The crud of the bean
384    * @param bean The bean to insert or update
385    * @param crud The group crud
386    * @param groupName The groupName
387    * @param c The connection to use
388    * @return The crud operation to use
389    * @throws CpoException An exception occurred
390    */
391   protected <T> Crud adjustCrud(T bean, Crud crud, String groupName, Connection c)
392       throws CpoException {
393     Crud retType = crud;
394     long objCount;
395 
396     if (Crud.UPSERT == retType) {
397       objCount = existsBean(groupName, bean, c, null);
398 
399       if (objCount == 0) {
400         retType = Crud.CREATE;
401       } else if (objCount == 1) {
402         retType = Crud.UPDATE;
403       } else {
404         throw new CpoException(
405             "UPSERT can only UPDATE one record. Your EXISTS function returned 2 or more.");
406       }
407     }
408 
409     return retType;
410   }
411 
412   /**
413    * Gets the read connection for this CpoAdapter
414    *
415    * @return A read connection
416    * @throws CpoException An error has occurred.
417    */
418   protected Connection getReadConnection() throws CpoException {
419     return connectionStrategy.getReadConnection();
420   }
421 
422   /**
423    * Gets the write connection for this CpoAdapter
424    *
425    * @return A write connection
426    * @throws CpoException An error has occurred.
427    */
428   protected Connection getWriteConnection() throws CpoException {
429     return connectionStrategy.getWriteConnection();
430   }
431 
432   /**
433    * Closes the local connection.
434    *
435    * @param connection The connection to close
436    */
437   protected void closeLocalConnection(Connection connection) {
438     connectionStrategy.closeLocalConnection(connection);
439   }
440 
441   /**
442    * Commits the local connection
443    *
444    * @param connection The connection to commit
445    */
446   protected void commitLocalConnection(Connection connection) {
447     connectionStrategy.commitLocalConnection(connection);
448   }
449 
450   /**
451    * Rollback the local connection
452    *
453    * @param connection The connection to rollback
454    */
455   protected void rollbackLocalConnection(Connection connection) {
456     connectionStrategy.rollbackLocalConnection(connection);
457   }
458 
459   @Override
460   protected <T, C> T processExecuteGroup(String groupName, C criteria, T result)
461       throws CpoException {
462     Connection c = null;
463     T bean = null;
464 
465     try {
466       c = getWriteConnection();
467       bean = processExecuteGroup(groupName, criteria, result, c);
468       commitLocalConnection(c);
469     } catch (Exception e) {
470       // Any exception has to try to rollback the work;
471       rollbackLocalConnection(c);
472       ExceptionHelper.reThrowCpoException(
473           e, "processExecuteGroup(String groupName, C criteria, T result) failed");
474     } finally {
475       closeLocalConnection(c);
476     }
477 
478     return bean;
479   }
480 
481   /**
482    * Executes a Bean whose MetaData contains a stored procedure. An assumption is that the bean
483    * exists in the datasource.
484    *
485    * @param <T> The result bean type
486    * @param <C> The criteria bean type
487    * @param groupName The filter groupName which tells the datasource which beans should be
488    *     returned. The groupName also signifies what data in the bean will be populated.
489    * @param criteria This is a bean that has been defined within the metadata of the datasource. If
490    *     the class is not defined an exception will be thrown. If the bean does not exist in the
491    *     datasource, an exception will be thrown. This bean is used to populate the IN arguments
492    *     used to retrieve the collection of beans.
493    * @param conn The connection to use to execute this group
494    * @param result This is a bean that has been defined within the metadata of the datasource. If
495    *     the class is not defined an exception will be thrown. If the bean does not exist in the
496    *     datasource, an exception will be thrown. This bean defines the bean type that will be
497    *     returned in the
498    * @return A result bean populate with the OUT arguments
499    * @throws CpoException if the criteria or result bean's metadata cannot be resolved, the callable
500    *     statement fails to execute, or the result bean cannot be instantiated
501    */
502   protected <T, C> T processExecuteGroup(String groupName, C criteria, T result, Connection conn)
503       throws CpoException {
504     CallableStatement cstmt = null;
505     CpoClass criteriaClass;
506     CpoClass resultClass;
507     T returnBean = null;
508     Logger localLogger = criteria == null ? logger : LoggerFactory.getLogger(criteria.getClass());
509 
510     JdbcCallableStatementFactory jcsf = null;
511 
512     if (criteria == null || result == null) {
513       throw new CpoException("NULL Bean passed into executeBean");
514     }
515 
516     try {
517       criteriaClass = metaDescriptor.getMetaClass(criteria);
518       resultClass = metaDescriptor.getMetaClass(result);
519 
520       List<CpoFunction> functions =
521           criteriaClass.getFunctionGroup(Crud.EXECUTE, groupName).getFunctions();
522       localLogger.info(
523           "===================processExecuteGroup ("
524               + groupName
525               + ") Count<"
526               + functions.size()
527               + ">=========================");
528 
529       try {
530         returnBean = (T) result.getClass().getDeclaredConstructor().newInstance();
531       } catch (IllegalAccessException iae) {
532         throw new CpoException("Unable to access the constructor of the Return Bean", iae);
533       } catch (InstantiationException iae) {
534         throw new CpoException("Unable to instantiate Return Bean", iae);
535       }
536 
537       // Loop through the queries and process each one
538       for (CpoFunction function : functions) {
539 
540         localLogger.debug("Executing Call:" + criteriaClass.getName() + ":" + groupName);
541 
542         jcsf = new JdbcCallableStatementFactory(conn, this, function, criteria, resultClass);
543         cstmt = jcsf.getCallableStatement();
544         cstmt.execute();
545         jcsf.release();
546 
547         localLogger.debug("Processing Call:" + criteriaClass.getName() + ":" + groupName);
548 
549         // Todo: Add Code here to go through the arguments, find record sets,
550         // and process them
551         // Process the non-record set out params and make it the first
552         // bean in the collection
553 
554         // Loop through the OUT Parameters and set them in the result
555         // bean
556         int j = 1;
557         for (CpoArgument cpoArgument : jcsf.getOutArguments()) {
558           JdbcCpoArgument jdbcArgument = (JdbcCpoArgument) cpoArgument;
559           if (jdbcArgument.isOutParameter()) {
560             JdbcCpoAttribute jdbcAttribute = jdbcArgument.getAttribute();
561             if (jdbcAttribute == null) {
562               jdbcAttribute =
563                   (JdbcCpoAttribute) resultClass.getAttributeJava(jdbcArgument.getName());
564               if (jdbcAttribute == null) {
565                 throw new CpoException(
566                     "Attribute <"
567                         + jdbcArgument.getName()
568                         + "> does not exist on class <"
569                         + resultClass.getName()
570                         + ">");
571               }
572             }
573             jdbcAttribute.invokeSetter(
574                 returnBean, new CallableStatementCpoData(cstmt, jdbcAttribute, j));
575           }
576           j++;
577         }
578 
579         cstmt.close();
580       }
581     } catch (Throwable t) {
582       String msg =
583           "processExecuteGroup(String groupName, C criteria, T result, Connection conn)"
584               + " failed. SQL=";
585       localLogger.error(msg, t);
586       throw new CpoException(msg, t);
587     } finally {
588       statementClose(cstmt);
589       if (jcsf != null) {
590         jcsf.release();
591       }
592     }
593 
594     return returnBean;
595   }
596 
597   @Override
598   protected <T> T processSelectGroup(
599       T bean,
600       String groupName,
601       Collection<CpoWhere> wheres,
602       Collection<CpoOrderBy> orderBy,
603       Collection<CpoNativeFunction> nativeExpressions)
604       throws CpoException {
605     Connection c = null;
606     T result = null;
607 
608     try {
609       c = getReadConnection();
610       result = processSelectGroup(bean, groupName, wheres, orderBy, nativeExpressions, c);
611 
612       // The select may have a for update clause on it
613       // Since the connection is cached we need to get rid of this
614       commitLocalConnection(c);
615     } catch (Exception e) {
616       // Any exception has to try to rollback the work;
617       rollbackLocalConnection(c);
618       ExceptionHelper.reThrowCpoException(e, "processSelectGroup(T bean, String groupName) failed");
619     } finally {
620       closeLocalConnection(c);
621     }
622 
623     return result;
624   }
625 
626   /**
627    * Retrieves the Bean from the datasource.
628    *
629    * @param <T> The bean type
630    * @param bean This is a bean that has been defined within the metadata of the datasource. If the
631    *     class is not defined an exception will be thrown. The input bean is used to specify the
632    *     search criteria.
633    * @param groupName The name which identifies which RETRIEVE Function Group to execute to retrieve
634    *     the bean.
635    * @param wheres A collection of CpoWhere beans to be used by the function
636    * @param orderBy A collection of CpoOrderBy beans to be used by the function
637    * @param nativeExpressions A collection of CpoNativeFunction beans to be used by the function
638    * @param con The connection to use for this select
639    * @return A populated bean of the same type as the Bean passed in as a argument. If no beans
640    *     match the criteria a NULL will be returned.
641    * @throws CpoException the retrieve function defined for this beans returns more than one row, an
642    *     exception will be thrown.
643    */
644   protected <T> T processSelectGroup(
645       T bean,
646       String groupName,
647       Collection<CpoWhere> wheres,
648       Collection<CpoOrderBy> orderBy,
649       Collection<CpoNativeFunction> nativeExpressions,
650       Connection con)
651       throws CpoException {
652     PreparedStatement ps = null;
653     ResultSet rs = null;
654     ResultSetMetaData rsmd;
655     CpoClass cpoClass;
656     JdbcCpoAttribute attribute;
657     T criteriaObj = bean;
658     boolean recordsExist = false;
659     Logger localLogger = bean == null ? logger : LoggerFactory.getLogger(bean.getClass());
660 
661     int recordCount = 0;
662     int attributesSet = 0;
663 
664     int k;
665     T rObj = null;
666 
667     if (bean == null) {
668       throw new CpoException("NULL Bean passed into retrieveBean");
669     }
670 
671     try {
672       cpoClass = metaDescriptor.getMetaClass(criteriaObj);
673       List<CpoFunction> functions =
674           cpoClass.getFunctionGroup(Crud.RETRIEVE, groupName).getFunctions();
675 
676       localLogger.info(buildCpoClassLogLine(criteriaObj.getClass(), Crud.RETRIEVE, groupName));
677 
678       try {
679         rObj = (T) bean.getClass().getDeclaredConstructor().newInstance();
680       } catch (IllegalAccessException iae) {
681         localLogger.error(
682             "=================== Could not access default constructor for Class=<"
683                 + bean.getClass()
684                 + "> ==================");
685         throw new CpoException("Unable to access the constructor of the Return Bean", iae);
686       } catch (InstantiationException iae) {
687         throw new CpoException("Unable to instantiate Return Bean", iae);
688       }
689 
690       for (CpoFunction cpoFunction : functions) {
691 
692         JdbcPreparedStatementFactory jpsf =
693             new JdbcPreparedStatementFactory(
694                 con, this, cpoClass, cpoFunction, criteriaObj, wheres, orderBy, nativeExpressions);
695         ps = jpsf.getPreparedStatement();
696 
697         // insertions on
698         // selectgroup
699         rs = ps.executeQuery();
700         jpsf.release();
701 
702         if (rs.isBeforeFirst()) {
703           rsmd = rs.getMetaData();
704 
705           if ((rsmd.getColumnCount() == 2)
706               && "CPO_ATTRIBUTE".equalsIgnoreCase(rsmd.getColumnLabel(1))
707               && "CPO_VALUE".equalsIgnoreCase(rsmd.getColumnLabel(2))) {
708             while (rs.next()) {
709               recordsExist = true;
710               recordCount++;
711               attribute = (JdbcCpoAttribute) cpoClass.getAttributeData(rs.getString(1));
712 
713               if (attribute != null) {
714                 attribute.invokeSetter(
715                     rObj,
716                     new JdbcResultSetCpoData(JdbcMethodMapper.getMethodMapper(), rs, attribute, 2));
717                 attributesSet++;
718               }
719             }
720           } else if (rs.next()) {
721             recordsExist = true;
722             recordCount++;
723             for (k = 1; k <= rsmd.getColumnCount(); k++) {
724               attribute = (JdbcCpoAttribute) cpoClass.getAttributeData(rsmd.getColumnLabel(k));
725 
726               if (attribute != null) {
727                 attribute.invokeSetter(
728                     rObj,
729                     new JdbcResultSetCpoData(JdbcMethodMapper.getMethodMapper(), rs, attribute, k));
730                 attributesSet++;
731               }
732             }
733 
734             if (rs.next()) {
735               String msg =
736                   "processSelectGroup(T bean, String groupName) failed: Multiple Records Returned";
737               localLogger.error(msg);
738               throw new CpoException(msg);
739             }
740           }
741           criteriaObj = rObj;
742         }
743 
744         rs.close();
745         rs = null;
746         ps.close();
747         ps = null;
748       }
749 
750       if (!recordsExist) {
751         rObj = null;
752         localLogger.info(
753             buildRecordsLogLine(0, 0, criteriaObj.getClass(), Crud.RETRIEVE, groupName));
754       } else {
755         localLogger.info(
756             buildRecordsLogLine(
757                 recordCount, attributesSet, criteriaObj.getClass(), Crud.RETRIEVE, groupName));
758       }
759     } catch (Throwable t) {
760       String msg = "processSeclectGroup(T bean) failed: " + ExceptionHelper.getLocalizedMessage(t);
761       localLogger.error(msg, t);
762       rObj = null;
763       throw new CpoException(msg, t);
764     } finally {
765       resultSetClose(rs);
766       statementClose(ps);
767     }
768 
769     return rObj;
770   }
771 
772   @Override
773   protected <T, C> Stream<T> processSelectGroup(
774       String groupName,
775       C criteria,
776       T result,
777       Collection<CpoWhere> wheres,
778       Collection<CpoOrderBy> orderBy,
779       Collection<CpoNativeFunction> nativeExpressions,
780       boolean useRetrieve)
781       throws CpoException {
782     Connection con = null;
783 
784     try {
785       con = getReadConnection();
786       return processSelectGroup(
787           groupName, criteria, result, wheres, orderBy, nativeExpressions, con, useRetrieve);
788     } catch (Exception e) {
789       // Any exception has to try to rollback the work;
790       rollbackLocalConnection(con);
791       closeLocalConnection(con);
792       ExceptionHelper.reThrowCpoException(
793           e,
794           "processSelectGroup(String groupName, C criteria, T result,C poWhere where,"
795               + " Collection orderBy, boolean useRetrieve) failed");
796     }
797     return Stream.empty();
798   }
799 
800   /**
801    * Retrieves Beans from the datasource.
802    *
803    * @param <T> The result bean type
804    * @param <C> The criteria bean type
805    * @param groupName Query group groupName
806    * @param criteria The criteria bean
807    * @param result The result bean
808    * @param wheres A collection of CpoWhere beans to be used by the function
809    * @param orderBy A collection of CpoOrderBy beans to be used by the function
810    * @param nativeExpressions A collection of CpoNativeFunction beans to be used by the function
811    * @param useRetrieve Use the RETRIEVE_GROUP instead of the LIST_GROUP
812    * @param con The connection to use for this select
813    * @return A stream of T
814    * @throws CpoException Any errors retrieving the data from the datasource
815    */
816   protected <T, C> Stream<T> processSelectGroup(
817       String groupName,
818       C criteria,
819       T result,
820       Collection<CpoWhere> wheres,
821       Collection<CpoOrderBy> orderBy,
822       Collection<CpoNativeFunction> nativeExpressions,
823       Connection con,
824       boolean useRetrieve)
825       throws CpoException {
826     Logger localLogger = criteria == null ? logger : LoggerFactory.getLogger(criteria.getClass());
827     PreparedStatement ps = null;
828     List<CpoFunction> cpoFunctions;
829     CpoClass criteriaClass;
830     CpoClass resultClass;
831     ResultSet rs = null;
832     ResultSetMetaData rsmd;
833     int columnCount;
834     JdbcCpoAttribute[] attributes;
835     JdbcPreparedStatementFactory jpsf;
836 
837     if (criteria == null || result == null) {
838       throw new CpoException("NULL Bean passed into retrieveBean or retrieveBeans");
839     }
840 
841     try {
842       criteriaClass = metaDescriptor.getMetaClass(criteria);
843       resultClass = metaDescriptor.getMetaClass(result);
844       if (useRetrieve) {
845         localLogger.info(buildCpoClassLogLine(criteria.getClass(), Crud.RETRIEVE, groupName));
846         cpoFunctions = criteriaClass.getFunctionGroup(Crud.RETRIEVE, groupName).getFunctions();
847       } else {
848         localLogger.info(buildCpoClassLogLine(criteria.getClass(), Crud.LIST, groupName));
849         cpoFunctions = criteriaClass.getFunctionGroup(Crud.LIST, groupName).getFunctions();
850       }
851 
852       CpoFunction cpoFunction = cpoFunctions.getFirst();
853       jpsf =
854           new JdbcPreparedStatementFactory(
855               con, this, criteriaClass, cpoFunction, criteria, wheres, orderBy, nativeExpressions);
856       ps = jpsf.getPreparedStatement();
857 
858       localLogger.debug("Retrieving Records");
859 
860       rs = ps.executeQuery();
861       jpsf.release();
862 
863       localLogger.debug("Processing Records");
864 
865       rsmd = rs.getMetaData();
866 
867       columnCount = rsmd.getColumnCount();
868 
869       attributes = new JdbcCpoAttribute[columnCount + 1];
870 
871       for (int k = 1; k <= columnCount; k++) {
872         attributes[k] = (JdbcCpoAttribute) resultClass.getAttributeData(rsmd.getColumnLabel(k));
873       }
874 
875       // resolved once per query, not once per row: the bean constructor and one reusable
876       // data wrapper per column (the wrapper's result set, attribute, and index are fixed)
877       Constructor<?> resultConstructor = result.getClass().getDeclaredConstructor();
878       JdbcResultSetCpoData[] columnData = new JdbcResultSetCpoData[columnCount + 1];
879       for (int k = 1; k <= columnCount; k++) {
880         if (attributes[k] != null) {
881           columnData[k] =
882               new JdbcResultSetCpoData(JdbcMethodMapper.getMethodMapper(), rs, attributes[k], k);
883         }
884       }
885 
886       ResultSet finalRs = rs;
887       PreparedStatement finalPs = ps;
888       return StreamSupport.stream(
889               new Spliterators.AbstractSpliterator<T>(Long.MAX_VALUE, Spliterator.ORDERED) {
890                 @Override
891                 public boolean tryAdvance(Consumer<? super T> action) {
892                   try {
893                     if (!finalRs.next()) return false;
894                     T bean = null;
895                     try {
896                       bean = (T) resultConstructor.newInstance();
897                     } catch (IllegalAccessException iae) {
898                       localLogger.error(
899                           "=================== Could not access default constructor for Class=<"
900                               + result.getClass()
901                               + "> ==================");
902                       throw new CpoException(
903                           "Unable to access the constructor of the Return Bean", iae);
904                     } catch (InstantiationException iae) {
905                       throw new CpoException("Unable to instantiate Return Bean", iae);
906                     }
907 
908                     for (int k = 1; k <= columnCount; k++) {
909                       if (columnData[k] != null) {
910                         attributes[k].invokeSetter(bean, columnData[k]);
911                       }
912                     }
913                     action.accept(bean);
914                     return true;
915                   } catch (Exception ex) {
916                     throw new RuntimeException(ex);
917                   }
918                 }
919               },
920               false)
921           .onClose(
922               () -> {
923                 try {
924                   // The select may have a for update clause on it
925                   // Since the connection is cached we need to get rid of this
926                   resultSetClose(finalRs);
927                   statementClose(finalPs);
928                   commitLocalConnection(con);
929                   closeLocalConnection(con);
930                 } catch (Exception e) {
931                   resultSetClose(finalRs);
932                   statementClose(finalPs);
933                   rollbackLocalConnection(con);
934                   closeLocalConnection(con);
935                   throw new RuntimeException(e);
936                 }
937               });
938     } catch (Throwable t) {
939       resultSetClose(rs);
940       statementClose(ps);
941       rollbackLocalConnection(con);
942       closeLocalConnection(con);
943       String msg =
944           "processSelectGroup(String groupName, C criteria, T result, CpoWhere where,"
945               + " Collection orderBy, Connection con) failed. Error:";
946       localLogger.error(msg, t);
947       throw new CpoException(msg, t);
948     }
949   }
950 
951   @Override
952   protected <T> long processUpdateGroup(
953       T bean,
954       Crud crud,
955       String groupName,
956       Collection<CpoWhere> wheres,
957       Collection<CpoOrderBy> orderBy,
958       Collection<CpoNativeFunction> nativeExpressions)
959       throws CpoException {
960     Connection c = null;
961     long updateCount = 0;
962 
963     try {
964       c = getWriteConnection();
965       updateCount =
966           processUpdateGroup(bean, crud, groupName, wheres, orderBy, nativeExpressions, c);
967       commitLocalConnection(c);
968     } catch (Exception e) {
969       // Any exception has to try to rollback the work;
970       rollbackLocalConnection(c);
971       ExceptionHelper.reThrowCpoException(
972           e, "processUdateGroup(T bean, String crud, String groupName) failed");
973     } finally {
974       closeLocalConnection(c);
975     }
976 
977     return updateCount;
978   }
979 
980   /**
981    * Updates beans in the datasource
982    *
983    * @param <T> The bean type
984    * @param bean The bean instance
985    * @param crud The query group type
986    * @param groupName The query group type
987    * @param wheres A collection of CpoWhere beans to be used by the function
988    * @param orderBy A collection of CpoOrderBy beans to be used by the function
989    * @param nativeExpressions A collection of CpoNativeFunction beans to be used by the function
990    * @param con The connection to use for the update
991    * @return The number of records updated
992    * @throws CpoException any errors processing the update
993    */
994   protected <T> long processUpdateGroup(
995       T bean,
996       Crud crud,
997       String groupName,
998       Collection<CpoWhere> wheres,
999       Collection<CpoOrderBy> orderBy,
1000       Collection<CpoNativeFunction> nativeExpressions,
1001       Connection con)
1002       throws CpoException {
1003     Logger localLogger = bean == null ? logger : LoggerFactory.getLogger(bean.getClass());
1004     CpoClass cpoClass;
1005     PreparedStatement ps = null;
1006 
1007     JdbcPreparedStatementFactory jpsf = null;
1008     long updateCount = 0;
1009 
1010     if (bean == null) {
1011       throw new CpoException(
1012           "NULL Bean passed into insertBean, deleteBean, updateBean, or upsertBean");
1013     }
1014 
1015     try {
1016       cpoClass = metaDescriptor.getMetaClass(bean);
1017       List<CpoFunction> cpoFunctions =
1018           cpoClass
1019               .getFunctionGroup(adjustCrud(bean, crud, groupName, con), groupName)
1020               .getFunctions();
1021       localLogger.info(buildCpoClassLogLine(bean.getClass(), crud, groupName));
1022 
1023       int numRows = 0;
1024 
1025       for (CpoFunction cpoFunction : cpoFunctions) {
1026         jpsf =
1027             new JdbcPreparedStatementFactory(
1028                 con, this, cpoClass, cpoFunction, bean, wheres, orderBy, nativeExpressions);
1029         ps = jpsf.getPreparedStatement();
1030         numRows += ps.executeUpdate();
1031         jpsf.release();
1032         ps.close();
1033       }
1034       localLogger.info(buildUpdatesLogLine(numRows, bean.getClass(), crud, groupName));
1035 
1036       if (numRows > 0) {
1037         updateCount++;
1038       }
1039     } catch (Throwable t) {
1040       String msg =
1041           "ProcessUpdateGroup failed:"
1042               + crud.operation
1043               + ","
1044               + groupName
1045               + ","
1046               + bean.getClass().getName();
1047       // TODO FIX THIS
1048       // localLogger.error("bound values:" + this.parameterToString(jq));
1049       localLogger.error(msg, t);
1050       throw new CpoException(msg, t);
1051     } finally {
1052       statementClose(ps);
1053       if (jpsf != null) {
1054         jpsf.release();
1055       }
1056     }
1057 
1058     return updateCount;
1059   }
1060 
1061   /**
1062    * Updates beans in the datasource
1063    *
1064    * @param <T> The bean type
1065    * @param beans The list of T to update
1066    * @param crud The query group type
1067    * @param groupName The query group type
1068    * @param wheres A collection of CpoWhere beans to be used by the function
1069    * @param orderBy A collection of CpoOrderBy beans to be used by the function
1070    * @param nativeExpressions A collection of CpoNativeFunction beans to be used by the function
1071    * @param con The connection to use for the update
1072    * @return The number of records updated
1073    * @throws CpoException any errors processing the update
1074    */
1075   protected <T> long processBatchUpdateGroup(
1076       List<T> beans,
1077       Crud crud,
1078       String groupName,
1079       Collection<CpoWhere> wheres,
1080       Collection<CpoOrderBy> orderBy,
1081       Collection<CpoNativeFunction> nativeExpressions,
1082       Connection con)
1083       throws CpoException {
1084     CpoClass jmc;
1085     List<CpoFunction> cpoFunctions;
1086     PreparedStatement ps = null;
1087     CpoFunction cpoFunction;
1088     JdbcPreparedStatementFactory jpsf = null;
1089     long updateCount = 0;
1090     Logger localLogger = logger;
1091 
1092     if (beans.isEmpty()) return updateCount;
1093 
1094     T firstBean = beans.getFirst();
1095     try {
1096       jmc = metaDescriptor.getMetaClass(firstBean);
1097       cpoFunctions =
1098           jmc.getFunctionGroup(adjustCrud(firstBean, crud, groupName, con), groupName)
1099               .getFunctions();
1100       localLogger = LoggerFactory.getLogger(jmc.getMetaClass());
1101 
1102       // Only Batch if there is only one function
1103       if (cpoFunctions.size() == 1) {
1104         localLogger.info(buildBatchLogLine(firstBean.getClass(), crud, groupName));
1105         cpoFunction = cpoFunctions.get(0);
1106         jpsf =
1107             new JdbcPreparedStatementFactory(
1108                 con, this, jmc, cpoFunction, firstBean, wheres, orderBy, nativeExpressions);
1109         updateCount =
1110             new JdbcBatchExecutor(getBatchSize()).executeBatchedUpdates(jpsf, cpoFunction, beans);
1111         jpsf.release();
1112         localLogger.info(buildUpdatesLogLine(updateCount, firstBean.getClass(), crud, groupName));
1113       } else {
1114         localLogger.info(buildCpoClassLogLine(firstBean.getClass(), crud, groupName));
1115         for (T bean : beans) {
1116           for (CpoFunction function : cpoFunctions) {
1117             jpsf =
1118                 new JdbcPreparedStatementFactory(
1119                     con, this, jmc, function, bean, wheres, orderBy, nativeExpressions);
1120             ps = jpsf.getPreparedStatement();
1121             updateCount += ps.executeUpdate();
1122             jpsf.release();
1123             ps.close();
1124           }
1125         }
1126         localLogger.info(buildUpdatesLogLine(updateCount, firstBean.getClass(), crud, groupName));
1127       }
1128 
1129     } catch (Throwable t) {
1130       String msg =
1131           "ProcessUpdateGroup failed:"
1132               + crud.operation
1133               + ","
1134               + groupName
1135               + ","
1136               + firstBean.getClass().getName();
1137       // TODO FIX This
1138       // localLogger.error("bound values:" + this.parameterToString(jq));
1139       localLogger.error(msg, t);
1140       throw new CpoException(msg, t);
1141     } finally {
1142       statementClose(ps);
1143       if (jpsf != null) {
1144         jpsf.release();
1145       }
1146     }
1147 
1148     return updateCount;
1149   }
1150 
1151   @Override
1152   protected <T> long processUpdateGroup(
1153       List<T> beans,
1154       Crud crud,
1155       String groupName,
1156       Collection<CpoWhere> wheres,
1157       Collection<CpoOrderBy> orderBy,
1158       Collection<CpoNativeFunction> nativeExpressions)
1159       throws CpoException {
1160     Connection c = null;
1161     long updateCount = 0;
1162 
1163     try {
1164       c = getWriteConnection();
1165 
1166       updateCount =
1167           processUpdateGroup(beans, crud, groupName, wheres, orderBy, nativeExpressions, c);
1168       commitLocalConnection(c);
1169     } catch (Exception e) {
1170       // Any exception has to try to rollback the work;
1171       rollbackLocalConnection(c);
1172       ExceptionHelper.reThrowCpoException(
1173           e, "processUpdateGroup(Collection beans, String crud, String groupName) failed");
1174     } finally {
1175       closeLocalConnection(c);
1176     }
1177 
1178     return updateCount;
1179   }
1180 
1181   /**
1182    * Updates beans in the datasource
1183    *
1184    * @param <T> The bean type
1185    * @param beans The collection of T to update
1186    * @param crud The query group type
1187    * @param groupName The query group type
1188    * @param wheres A collection of CpoWhere beans to be used by the function
1189    * @param orderBy A collection of CpoOrderBy beans to be used by the function
1190    * @param nativeExpressions A collection of CpoNativeFunction beans to be used by the function
1191    * @param con The connection to use for the update
1192    * @return The number of records updated
1193    * @throws CpoException any errors processing the update
1194    */
1195   protected <T> long processUpdateGroup(
1196       List<T> beans,
1197       Crud crud,
1198       String groupName,
1199       Collection<CpoWhere> wheres,
1200       Collection<CpoOrderBy> orderBy,
1201       Collection<CpoNativeFunction> nativeExpressions,
1202       Connection con)
1203       throws CpoException {
1204     long updateCount = 0;
1205 
1206     if (beans.isEmpty()) return updateCount;
1207 
1208     if (capabilities.batchUpdatesSupported() && !Crud.UPSERT.equals(crud)) {
1209       updateCount =
1210           processBatchUpdateGroup(beans, crud, groupName, wheres, orderBy, nativeExpressions, con);
1211     } else {
1212       for (T bean : beans) {
1213         updateCount +=
1214             processUpdateGroup(bean, crud, groupName, wheres, orderBy, nativeExpressions, con);
1215       }
1216     }
1217 
1218     return updateCount;
1219   }
1220 
1221   private void statementClose(Statement s) {
1222     if (s != null) {
1223       try {
1224         s.close();
1225       } catch (Exception e) {
1226         if (logger.isTraceEnabled()) {
1227           logger.trace(e.getMessage());
1228         }
1229       }
1230     }
1231   }
1232 
1233   private void resultSetClose(ResultSet rs) {
1234     if (rs != null) {
1235       try {
1236         rs.close();
1237       } catch (Exception e) {
1238         if (logger.isTraceEnabled()) {
1239           logger.trace(e.getMessage());
1240         }
1241       }
1242     }
1243   }
1244 
1245   @Override
1246   public CpoMetaDescriptor getCpoMetaDescriptor() {
1247     return metaDescriptor;
1248   }
1249 
1250   @Override
1251   public List<CpoAttribute> getCpoAttributes(String expression) throws CpoException {
1252     List<CpoAttribute> attributes = new ArrayList<>();
1253 
1254     if (expression != null && !expression.isEmpty()) {
1255       Connection c = null;
1256       PreparedStatement ps = null;
1257       ResultSet rs = null;
1258       try {
1259         c = getReadConnection();
1260         ps = c.prepareStatement(expression);
1261         rs = ps.executeQuery();
1262         ResultSetMetaData rsmd = rs.getMetaData();
1263         for (int i = 1; i <= rsmd.getColumnCount(); i++) {
1264           JdbcCpoAttribute attribute = new JdbcCpoAttribute();
1265           attribute.setDataName(rsmd.getColumnLabel(i));
1266           attribute.setDbColumn(rsmd.getColumnName(i));
1267           try {
1268             attribute.setDbTable(rsmd.getTableName(i));
1269           } catch (Exception e) {
1270             // do nothing if this call is not supported
1271             logger.info("Could not get table name", e);
1272           }
1273 
1274           DataTypeMapEntry<?> dataTypeMapEntry =
1275               metaDescriptor.getDataTypeMapEntry(rsmd.getColumnType(i));
1276           attribute.setDataType(dataTypeMapEntry.dataTypeName());
1277           attribute.setDataTypeInt(dataTypeMapEntry.dataTypeInt());
1278           attribute.setJavaType(dataTypeMapEntry.javaClass().getName());
1279           attribute.setJavaName(dataTypeMapEntry.makeJavaName(rsmd.getColumnLabel(i)));
1280 
1281           attributes.add(attribute);
1282         }
1283       } catch (Throwable t) {
1284         logger.error(ExceptionHelper.getLocalizedMessage(t), t);
1285         throw new CpoException("Error Generating Attributes", t);
1286       } finally {
1287         resultSetClose(rs);
1288         statementClose(ps);
1289         // terminate the read-only transaction before pooling the connection (see existsBean)
1290         rollbackLocalConnection(c);
1291         closeLocalConnection(c);
1292       }
1293     }
1294     return attributes;
1295   }
1296 }