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