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