001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 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 javax.annotation.PostConstruct; 038import javax.annotation.PreDestroy; 039import javax.enterprise.context.RequestScoped; 040import javax.enterprise.inject.spi.CDI; 041import org.jooq.DSLContext; 042import org.jooq.Select; 043import org.jooq.impl.DSL; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047@RequestScoped 048public class Session { 049 050 private static final String GET_INSTANCE = "getInstance"; 051 private static final String GET_ENTITY_TYPE_NAME = "getEntityTypeName"; 052 053 private static final Map<Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>>, String> entityNameCache = new ConcurrentHashMap<>(); 054 055 protected final Logger log = LoggerFactory.getLogger(this.getClass()); 056 057 private DSLContext dslContext; 058 private Connection connection; 059 060 private ValueCache valueCache = ValueCacheProviderImpl.getInstance().getValueCache(); 061 private SessionEntityCache sessionEntityCache = new SessionEntityCache(this); 062 063 private final Map<EntityInstancePK, Integer> eventTimeSequences = new HashMap<>(); 064 065 private Map<String, PreparedStatement> preparedStatementCache; 066 067 private MimeType preferredClobMimeType; 068 private Set<String> options; 069 private TransferProperties transferProperties; 070 private Map<String, Limit> limits; 071 072 public static final long MAX_TIME = Long.MAX_VALUE; 073 private final long START_TIME; 074 075 public long getStartTime() { 076 return START_TIME; 077 } 078 079 /** 080 * Creates a new instance of Session 081 */ 082 public Session() { 083 START_TIME = System.currentTimeMillis(); 084 } 085 086 @PostConstruct 087 public void init() { 088 if(PersistenceDebugFlags.LogSessions) { 089 log.info("Session()"); 090 } 091 092 dslContext = DslContextFactory.getInstance().getDslContext(); 093 connection = dslContext.parsingConnection(); 094 095 if(PersistenceDebugFlags.LogConnections) { 096 log.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 public DSLContext getDslContext() { 127 return dslContext; 128 } 129 130 public Connection getConnection() { 131 return connection; 132 } 133 134 public static <T extends BaseModelControl> T getModelController(Class<T> modelController) { 135 return ThreadSession.currentSession().getSessionModelController(modelController); 136 } 137 138 public <T extends BaseModelControl> T getSessionModelController(Class<T> modelController) { 139 return CDI.current().select(modelController).get(); 140 } 141 142 private String getEntityTypeNameFromFactory(final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory) { 143 var result = Session.entityNameCache.get(entityFactory); 144 145 if(result == null) { 146 try { 147 var entityInstance = entityFactory.getDeclaredMethod(GET_INSTANCE).invoke(entityFactory); 148 149 if(entityInstance != null) { 150 result = (String)entityFactory.getDeclaredMethod(Session.GET_ENTITY_TYPE_NAME).invoke(entityInstance); 151 } 152 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 153 throw new RuntimeException(e); 154 } 155 156 Session.entityNameCache.put(entityFactory, result); 157 } 158 159 return result; 160 } 161 162 public boolean hasLimits() { 163 return limits != null; 164 } 165 166 public boolean hasLimit(final String entityName) { 167 return hasLimits() && limits.get(entityName) != null; 168 } 169 170 public boolean hasLimit(final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory) { 171 return hasLimits() && limits.get(getEntityTypeNameFromFactory(entityFactory)) != null; 172 } 173 174 public void copyLimit(final String sourceEntityName, final String destinationEntityName) { 175 if(hasLimits()) { 176 var limit = limits.get(sourceEntityName); 177 178 if(limit != null) { 179 limits.put(destinationEntityName, limit); 180 } 181 } 182 } 183 184 public Select<?> applyLimit(final Select<?> query, 185 final Class<? extends BaseFactory<? extends BasePK, ? extends BaseEntity>> entityFactory) { 186 var result = query; 187 188 if(hasLimits()) { 189 var limit = limits.get(getEntityTypeNameFromFactory(entityFactory)); 190 191 if(limit != null && limit.getCount() != null) { 192 var count = Long.valueOf(limit.getCount()); 193 var rawOffset = limit.getOffset(); 194 195 // The QOM methods preserve the completed Select<?> type while adding 196 // optional pagination. Keep this isolated here because the API is experimental. 197 result = result.$limit(DSL.val(count)); 198 199 if(rawOffset != null) { 200 result = result.$offset(DSL.val(Long.valueOf(rawOffset))); 201 } 202 } 203 } 204 205 return result; 206 } 207 208 /** 209 * Creates a <code>PreparedStatement</code> object for sending 210 * parameterized SQL statements to the database. 211 * @param sql SQL statement to use for the PreparedStatement 212 * @return Returns a PreparedStatement 213 * @throws PersistenceDatabaseException Thrown if the PreparedStatement was unable to be created 214 */ 215 public PreparedStatement prepareStatement(final String sql) { 216 PreparedStatement preparedStatement = null; 217 218 if(sql != null) { 219 // Attempt to get a PreparedStatement from preparedStatementCache... 220 if(preparedStatementCache == null) { 221 preparedStatementCache = new HashMap<>(); 222 } else { 223 preparedStatement = preparedStatementCache.get(sql); 224 } 225 226 if(preparedStatement == null) { 227 // If it hasn't been cached before, go ahead and cache it for future use... 228 try { 229 preparedStatement = connection.prepareStatement(sql); 230 preparedStatementCache.put(sql, preparedStatement); 231 } catch(SQLException se) { 232 throw new PersistenceDatabaseException(se); 233 } 234 } else { 235 // Cached PreparedStatement was found, call clearParameters() to clean out any previous usage of it. 236 // Clearing of batch parameters happens after executing of each batch. 237 try { 238 preparedStatement.clearParameters(); 239 } catch(SQLException se) { 240 throw new PersistenceDatabaseException(se); 241 } 242 } 243 } 244 245 return preparedStatement; 246 } 247 248 public static void setQueryParams(final PreparedStatement ps, final Object... params) { 249 try { 250 for(var param = 0; param < params.length; param++) { 251 switch(params[param]) { 252 case BaseEntity baseEntity -> ps.setLong(param + 1, baseEntity.getPrimaryKey().getEntityId()); 253 case BasePK basePK -> ps.setLong(param + 1, basePK.getEntityId()); 254 case Long l -> ps.setLong(param + 1, l); 255 case Integer i -> ps.setInt(param + 1, i); 256 case String s -> ps.setString(param + 1, s); 257 case byte[] bytes -> ps.setBytes(param + 1, bytes); 258 case Boolean b -> ps.setBoolean(param + 1, b); 259 case null -> throw new PersistenceDatabaseException("null Object in setQueryParams, index = " + param); 260 default -> throw new PersistenceDatabaseException("unsupported Object in setQueryParams, " + params[param].getClass().getCanonicalName() + ", index = " + param); 261 } 262 } 263 } catch (SQLException se) { 264 throw new PersistenceDatabaseException(se); 265 } 266 } 267 268 public void query(final String sql, final Object... params) { 269 try { 270 var ps = prepareStatement(sql); 271 272 setQueryParams(ps, params); 273 274 ps.execute(); 275 } catch (SQLException se) { 276 throw new PersistenceDatabaseException(se); 277 } 278 } 279 280 private void freePreparedStatementCache() { 281 var preparedStatements = preparedStatementCache.values(); 282 283 preparedStatements.forEach((preparedStatement) -> { 284 try { 285 preparedStatement.close(); 286 } catch (SQLException se) { 287 // not much to do to recover from this problem, connection is closing soon. 288 throw new PersistenceDatabaseException(se); 289 } 290 }); 291 292 preparedStatementCache = null; 293 } 294 295 @SuppressWarnings("Finally") 296 @PreDestroy 297 public void close() { 298 if(PersistenceDebugFlags.LogSessions) { 299 log.info("close()"); 300 } 301 302 if(connection != null) { 303 if(PersistenceDebugFlags.LogConnections) { 304 log.info("closing connection " + connection); 305 } 306 307 try { 308 if(PersistenceDebugFlags.LogConnections) { 309 log.info("flushing entities for " + connection); 310 } 311 312 sessionEntityCache = sessionEntityCache.popLastSessionEntityCache(); 313 314 if(PersistenceDebugFlags.LogValueCaches) { 315 log.info("discarding valueCache " + valueCache); 316 } 317 318 if(valueCache != null) { 319 valueCache = null; 320 } 321 322 if(PersistenceDebugFlags.LogConnections) { 323 log.info("freeing prepared statement cache " + connection); 324 } 325 326 if(preparedStatementCache != null) { 327 freePreparedStatementCache(); 328 } 329 } finally { 330 try { 331 if(PersistenceDebugFlags.LogConnections) { 332 log.info("closing connection " + connection); 333 } 334 335 connection.close(); 336 connection = null; 337 dslContext = null; 338 } catch(SQLException se) { 339 throw new PersistenceDatabaseException(se); 340 } 341 } 342 } 343 } 344 345 public void putReadOnlyEntity(BasePK basePK, BaseEntity baseEntity) { 346 sessionEntityCache.putReadOnlyEntity(basePK, baseEntity); 347 } 348 349 public void putReadWriteEntity(BasePK basePK, BaseEntity baseEntity) { 350 sessionEntityCache.putReadWriteEntity(basePK, baseEntity); 351 } 352 353 public BaseEntity getEntity(BasePK basePK) { 354 return sessionEntityCache.getEntity(basePK); 355 } 356 357 public void removed(BasePK basePK, boolean missingPermitted) { 358 sessionEntityCache.removed(basePK, missingPermitted); 359 } 360 361 public void setPreferredClobMimeType(MimeType preferredClobMimeType) { 362 this.preferredClobMimeType = preferredClobMimeType; 363 } 364 365 public MimeType getPreferredClobMimeType() { 366 return preferredClobMimeType; 367 } 368 369 public void setOptions(Set<String> options) { 370 this.options = options; 371 } 372 373 public Set<String> getOptions() { 374 if(options == null) { 375 options = new HashSet<>(); 376 } 377 378 return options; 379 } 380 381 public void setTransferProperties(TransferProperties transferProperties) { 382 this.transferProperties = transferProperties; 383 } 384 385 public TransferProperties getTransferProperties() { 386 return transferProperties; 387 } 388 389 public void setLimits(Map<String, Limit> limits) { 390 this.limits = limits; 391 } 392 393 public Map<String, Limit> getLimits() { 394 if(limits == null) { 395 limits = new HashMap<>(); 396 } 397 398 return limits; 399 } 400 401}