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}