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