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}