001// --------------------------------------------------------------------------------
002// Copyright 2002-2025 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.util.server.persistence;
018
019import com.echothree.model.data.core.common.pk.EntityInstancePK;
020import com.echothree.model.data.core.server.entity.MimeType;
021import com.echothree.util.common.exception.PersistenceDatabaseException;
022import com.echothree.util.common.form.TransferProperties;
023import com.echothree.util.common.persistence.BasePK;
024import com.echothree.util.common.transfer.Limit;
025import com.echothree.util.server.control.BaseModelControl;
026import com.echothree.util.server.persistence.valuecache.ValueCache;
027import com.echothree.util.server.persistence.valuecache.ValueCacheProviderImpl;
028import java.lang.reflect.InvocationTargetException;
029import java.sql.Connection;
030import java.sql.PreparedStatement;
031import java.sql.SQLException;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.Map;
035import java.util.Set;
036import java.util.concurrent.ConcurrentHashMap;
037import java.util.regex.Pattern;
038import javax.enterprise.inject.spi.CDI;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.jooq.DSLContext;
042
043public class Session {
044    
045    private static final String GET_INSTANCE = "getInstance";
046    private static final String GET_ALL_COLUMNS = "getAllColumns";
047    private static final String GET_PK_COLUMN = "getPKColumn";
048    private static final String GET_ENTITY_TYPE_NAME = "getEntityTypeName";
049    
050    private static final Pattern PK_FIELD_PATTERN = Pattern.compile("_PK_");
051    private static final Pattern ALL_FIELDS_PATTERN = Pattern.compile("_ALL_");
052    private static final Pattern LIMIT_PATTERN = Pattern.compile("_LIMIT_");
053    
054    private static final Map<Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>>, String> allColumnsCache = new ConcurrentHashMap<>();
055    private static final Map<Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>>, String> pkColumnCache = new ConcurrentHashMap<>();
056    private static final Map<Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>>, String> entityNameCache = new ConcurrentHashMap<>();
057    
058    private Log log;
059    
060    private DSLContext dslContext;
061    private Connection connection;
062
063    private ValueCache valueCache = ValueCacheProviderImpl.getInstance().getValueCache();
064    private SessionEntityCache sessionEntityCache = new SessionEntityCache(this);
065
066    private final Map<EntityInstancePK, Integer> eventTimeSequences = new HashMap<>();
067
068    private Map<String, PreparedStatement> preparedStatementCache;
069    
070    private MimeType preferredClobMimeType;
071    private Set<String> options;
072    private TransferProperties transferProperties;
073    private Map<String, Limit> limits;
074    
075    public static final long MAX_TIME = Long.MAX_VALUE;
076    public static final Long MAX_TIME_LONG = Long.MAX_VALUE;
077
078    public final long START_TIME;
079    public final Long START_TIME_LONG;
080    
081    /**
082     * Creates a new instance of Session
083     */
084    public Session() {
085        if(PersistenceDebugFlags.LogSessions) {
086            getLog().info("Session()");
087        }
088        
089        dslContext = DslContextFactory.getInstance().getDslContext();
090        connection = dslContext.parsingConnection();
091        
092        START_TIME = System.currentTimeMillis();
093        START_TIME_LONG = START_TIME;
094        
095        if(PersistenceDebugFlags.LogConnections) {
096            getLog().info("new connection is " + connection);
097        }
098    }
099
100    public Integer getNextEventTimeSequence(final EntityInstancePK entityInstancePK) {
101        var value = eventTimeSequences.get(entityInstancePK);
102
103        if(value == null) {
104            value = 1;
105        } else {
106            value++;
107        }
108
109        eventTimeSequences.put(entityInstancePK, value);
110
111        return value;
112    }
113
114    public ValueCache getValueCache() {
115        return valueCache;
116    }
117
118    public void pushSessionEntityCache() {
119        sessionEntityCache = new SessionEntityCache(sessionEntityCache);
120    }
121
122    public void popSessionEntityCache() {
123        sessionEntityCache = sessionEntityCache.popSessionEntityCache();
124    }
125
126    final protected Log getLog() {
127        if(log == null) {
128            log = LogFactory.getLog(this.getClass());
129        }
130        
131        return log;
132    }
133    
134    public Connection getConnection() {
135        return connection;
136    }
137    
138    public static <T extends BaseModelControl> T getModelController(Class<T> modelController) {
139        return ThreadSession.currentSession().getSessionModelController(modelController);
140    }
141    
142    public <T extends BaseModelControl> T getSessionModelController(Class<T> modelController) {
143        return CDI.current().select(modelController).get();
144    }
145    
146    private String getStringFromBaseFactory(final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory,
147            final Map<Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>>, String> cache, final String methodName) {
148        var result = cache.get(entityFactory);
149        
150        if(result == null) {
151            try {
152                var entityInstance = entityFactory.getDeclaredMethod(GET_INSTANCE).invoke(entityFactory);
153
154                if(entityInstance != null) {
155                    result = (String)entityFactory.getDeclaredMethod(methodName).invoke(entityInstance);
156                }
157            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
158                throw new RuntimeException(e);
159            }
160            
161            cache.put(entityFactory, result);
162        }
163        
164        return result;
165    }
166
167    public boolean hasLimits() {
168        return limits != null;
169    }
170
171    public boolean hasLimit(final String entityName) {
172        return hasLimits() && limits.get(entityName) != null;
173    }
174    
175    public boolean hasLimit(final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory) {
176        return hasLimits() && limits.get(getStringFromBaseFactory(entityFactory, entityNameCache, GET_ENTITY_TYPE_NAME)) != null;
177    }
178    
179    public void copyLimit(final String sourceEntityName, final String destinationEntityName) {
180        if(hasLimits()) {
181            var limit = limits.get(sourceEntityName);
182            
183            if(limit != null) {
184                limits.put(destinationEntityName, limit);
185            }
186        }
187    }
188    
189    private String getLimit(final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory) {
190        String result = null;
191
192        if(hasLimits()) {
193            var limit = limits.get(getStringFromBaseFactory(entityFactory, entityNameCache, GET_ENTITY_TYPE_NAME));
194
195            if(limit != null) {
196                var rawCount = limit.getCount();
197
198                if(rawCount != null) {
199                    var count = Long.valueOf(rawCount);
200                    var limitBuilder = new StringBuilder(" LIMIT ").append(count);
201                    var rawOffset = limit.getOffset();
202
203                    if(rawOffset != null) {
204                        var offset = Long.valueOf(rawOffset);
205
206                        limitBuilder.append(" OFFSET ").append(offset);
207                    }
208
209                    result = limitBuilder.append(' ').toString();
210                }
211            }
212        }
213
214        return result == null ? "" : result;
215    }
216
217    /**
218     * Creates a <code>PreparedStatement</code> object for sending
219     * parameterized SQL statements to the database.
220     * @param sql SQL statement to use for the PreparedStatement
221     * @return Returns a PreparedStatement
222     * @throws PersistenceDatabaseException Thrown if the PreparedStatement was unable to be created
223     */
224    public PreparedStatement prepareStatement(final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory,
225            final String sql) {
226        PreparedStatement preparedStatement = null;
227
228        if(sql != null) {
229            // Perform replacements on specific patterns that may be in the SQL...
230            var replacedSql = sql;
231            if(entityFactory != null) {
232                // _LIMIT_ expands to any limit passed in by the client
233                var matcher = LIMIT_PATTERN.matcher(replacedSql);
234                replacedSql = matcher.replaceAll(getLimit(entityFactory));
235
236                // _ALL_ expands to all columns in table
237                matcher = ALL_FIELDS_PATTERN.matcher(replacedSql);
238                replacedSql = matcher.replaceAll(getStringFromBaseFactory(entityFactory, allColumnsCache, GET_ALL_COLUMNS));
239
240                // _PK_ expands to PK column in table
241                matcher = PK_FIELD_PATTERN.matcher(replacedSql);
242                replacedSql = matcher.replaceAll(getStringFromBaseFactory(entityFactory, pkColumnCache, GET_PK_COLUMN));
243            }
244
245            // Attempt to get a PreparedStatement from preparedStatementCache...
246            if(preparedStatementCache == null) {
247                preparedStatementCache = new HashMap<>();
248            } else {
249                preparedStatement = preparedStatementCache.get(replacedSql);
250            }
251
252            if(preparedStatement == null) {
253                // If it hasn't been cached before, go ahead and cache it for future use...
254                try {
255                    preparedStatement = connection.prepareStatement(replacedSql);
256                    preparedStatementCache.put(sql, preparedStatement);
257                } catch(SQLException se) {
258                    throw new PersistenceDatabaseException(se);
259                }
260            } else {
261                // Cached PreparedStatement was found, call clearParameters() to clean out any previous usage of it.
262                // Clearing of batch parameters happens after executing of each batch.
263                try {
264                    preparedStatement.clearParameters();
265                } catch(SQLException se) {
266                    throw new PersistenceDatabaseException(se);
267                }
268            }
269        }
270
271        return preparedStatement;
272    }
273    
274    /**
275     * Creates a <code>PreparedStatement</code> object for sending
276     * parameterized SQL statements to the database.
277     * @param sql SQL statement to use for the PreparedStatement
278     * @return Returns a PreparedStatement
279     * @throws PersistenceDatabaseException Thrown if the PreparedStatement was unable to be created
280     */
281    public PreparedStatement prepareStatement(final String sql) {
282        return prepareStatement(null, sql);
283    }
284
285    public static void setQueryParams(final PreparedStatement ps, final Object... params) {
286        try {
287            for(var i = 0; i < params.length; i++) {
288                if(params[i] instanceof BaseEntity) {
289                    ps.setLong(i + 1, ((BaseEntity)params[i]).getPrimaryKey().getEntityId());
290                } else if(params[i] instanceof BasePK) {
291                    ps.setLong(i + 1, ((BasePK)params[i]).getEntityId());
292                } else if(params[i] instanceof Long) {
293                    ps.setLong(i + 1, ((Long)params[i]));
294                } else if(params[i] instanceof Integer) {
295                    ps.setInt(i + 1, ((Integer)params[i]));
296                } else if(params[i] instanceof String) {
297                    ps.setString(i + 1, (String)params[i]);
298                } else {
299                    if(params[i] == null) {
300                        throw new PersistenceDatabaseException("null Object in setQueryParams, index = " + i);
301                    } else {
302                        throw new PersistenceDatabaseException("unsupported Object in setQueryParams, " + params[i].getClass().getCanonicalName() + ", index = " + i);
303                    }
304                }
305            }
306        } catch (SQLException se) {
307            throw new PersistenceDatabaseException(se);
308        }
309    }
310    
311    public void query(final String sql, final Object... params) {
312        try {
313            var ps = prepareStatement(sql);
314
315            setQueryParams(ps, params);
316
317            ps.execute();
318        } catch (SQLException se) {
319            throw new PersistenceDatabaseException(se);
320        }
321    }
322
323    public Integer queryForInteger(final String sql, final Object... params) {
324        Integer result = null;
325
326        try {
327            var ps = prepareStatement(sql);
328
329            setQueryParams(ps, params);
330
331            ps.executeQuery();
332            
333            try(var rs = ps.getResultSet()) {
334                if(rs.next()) {
335                    result = rs.getInt(1);
336                }
337
338                if(rs.wasNull()) {
339                    result = null;
340                }
341
342                if(rs.next()) {
343                    throw new PersistenceDatabaseException("queryForInteger result contains multiple ints");
344                }
345            } catch (SQLException se) {
346                throw new PersistenceDatabaseException(se);
347            }
348        } catch (SQLException se) {
349            throw new PersistenceDatabaseException(se);
350        } 
351
352        return result;
353    }
354
355    public Long queryForLong(final String sql, final Object... params) {
356        Long result = null;
357        
358        try {
359            var ps = prepareStatement(sql);
360            
361            setQueryParams(ps, params);
362            
363            ps.executeQuery();
364            try(var rs = ps.getResultSet()) {
365                if(rs.next()) {
366                    result = rs.getLong(1);
367
368                    if(rs.wasNull()) {
369                        result = null;
370                    }
371
372                    if(rs.next()) {
373                        throw new PersistenceDatabaseException("queryForLong result contains multiple longs");
374                    }
375                }
376            } catch (SQLException se) {
377                throw new PersistenceDatabaseException(se);
378            }
379        } catch (SQLException se) {
380            throw new PersistenceDatabaseException(se);
381        }
382        
383        return result;
384    }
385    
386    private void freePreparedStatementCache() {
387        var preparedStatements = preparedStatementCache.values();
388
389        preparedStatements.forEach((preparedStatement) -> {
390            try {
391                preparedStatement.close();
392            } catch (SQLException se) {
393                // not much to do to recover from this problem, connection is closing soon.
394                throw new PersistenceDatabaseException(se);
395            }
396        });
397        
398        preparedStatementCache = null;
399    }
400
401    @SuppressWarnings("Finally")
402    public void close() {
403        if(PersistenceDebugFlags.LogSessions) {
404            getLog().info("close()");
405        }
406
407        if(connection != null) {
408            if(PersistenceDebugFlags.LogConnections) {
409                getLog().info("closing connection " + connection);
410            }
411            
412            try {
413                if(PersistenceDebugFlags.LogConnections) {
414                    getLog().info("flushing entities for " + connection);
415                }
416
417                sessionEntityCache = sessionEntityCache.popLastSessionEntityCache();
418
419                if(PersistenceDebugFlags.LogValueCaches) {
420                    getLog().info("discarding valueCache " + valueCache);
421                }
422
423                if(valueCache != null) {
424                    valueCache = null;
425                }
426                
427                if(PersistenceDebugFlags.LogConnections) {
428                    getLog().info("freeing prepared statement cache " + connection);
429                }
430
431                if(preparedStatementCache != null) {
432                    freePreparedStatementCache();
433                }
434            } finally {
435                try {
436                    if(PersistenceDebugFlags.LogConnections) {
437                        getLog().info("closing connection " + connection);
438                    }
439
440                    connection.close();
441                    connection = null;
442                    dslContext = null;
443                } catch(SQLException se) {
444                    throw new PersistenceDatabaseException(se);
445                }
446            }
447        }
448    }
449
450    public void putReadOnlyEntity(BasePK basePK, BaseEntity baseEntity) {
451        sessionEntityCache.putReadOnlyEntity(basePK, baseEntity);
452    }
453    
454    public void putReadWriteEntity(BasePK basePK, BaseEntity baseEntity) {
455        sessionEntityCache.putReadWriteEntity(basePK, baseEntity);
456    }
457    
458    public BaseEntity getEntity(BasePK basePK) {
459        return sessionEntityCache.getEntity(basePK);
460    }
461    
462    public void removed(BasePK basePK, boolean missingPermitted) {
463        sessionEntityCache.removed(basePK, missingPermitted);
464    }
465    
466    public void setPreferredClobMimeType(MimeType preferredClobMimeType) {
467        this.preferredClobMimeType = preferredClobMimeType;
468    }
469    
470    public MimeType getPreferredClobMimeType() {
471        return preferredClobMimeType;
472    }
473    
474    public void setOptions(Set<String> options) {
475        this.options = options;
476    }
477    
478    public Set<String> getOptions() {
479        if(options == null) {
480            options = new HashSet<>();
481        }
482        
483        return options;
484    }
485
486    public void setTransferProperties(TransferProperties transferProperties) {
487        this.transferProperties = transferProperties;
488    }
489    
490    public TransferProperties getTransferProperties() {
491        return transferProperties;
492    }
493    
494    public void setLimits(Map<String, Limit> limits) {
495        this.limits = limits;
496    }
497    
498    public Map<String, Limit> getLimits() {
499        if(limits == null) {
500            limits = new HashMap<>();
501        }
502
503        return limits;
504    }
505    
506}