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.util.common.exception.PersistenceSessionEntityCacheException; 020import com.echothree.util.common.persistence.BasePK; 021import static com.echothree.util.server.persistence.PersistenceDebugFlags.LogSessionEntityCacheActions; 022import static com.echothree.util.server.persistence.PersistenceDebugFlags.LogSessionEntityCacheStatistics; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030public class SessionEntityCache { 031 032 protected final Logger log = LoggerFactory.getLogger(this.getClass()); 033 034 private Session session; 035 036 private SessionEntityCache parentSessionEntityCache; 037 038 private Map<BasePK, BaseEntity> entitiesReadOnly = new HashMap<>(); 039 private Map<BasePK, BaseEntity> entitiesReadWrite = new HashMap<>(); 040 041 SessionEntityCacheStatistics cumulativeSessionEntityCacheStatistics; 042 SessionEntityCacheStatistics sessionEntityCacheStatistics; 043 044 private void init(Session session, SessionEntityCache parentSessionEntityCache) { 045 if(LogSessionEntityCacheStatistics) { 046 log.info("SessionEntityCache(session = " + session + ", parentSessionEntityCache = " + parentSessionEntityCache + ")"); 047 } 048 049 this.session = session; 050 this.parentSessionEntityCache = parentSessionEntityCache; 051 052 if(LogSessionEntityCacheStatistics) { 053 if(parentSessionEntityCache == null) { 054 cumulativeSessionEntityCacheStatistics = new SessionEntityCacheStatistics(); 055 } 056 057 sessionEntityCacheStatistics = new SessionEntityCacheStatistics(); 058 } 059 } 060 061 /** Creates a new instance of SessionEntityCache */ 062 public SessionEntityCache(SessionEntityCache parentSessionEntityCache) { 063 if(parentSessionEntityCache == null) { 064 throw new PersistenceSessionEntityCacheException("parentSessionEntityCache cannot be null"); 065 } 066 067 init(parentSessionEntityCache.session, parentSessionEntityCache); 068 } 069 070 /** Creates a new instance of SessionEntityCache */ 071 public SessionEntityCache(Session session) { 072 if(session == null) { 073 throw new PersistenceSessionEntityCacheException("session cannot be null"); 074 } 075 076 init(session, null); 077 } 078 079 // Removed the RO entity, and drops it into the RW cache 080 private void replaceReadOnlyEntity(BaseEntity baseEntity) { 081 var basePK = baseEntity.getPrimaryKey(); 082 083 entitiesReadOnly.remove(basePK); 084 entitiesReadWrite.put(basePK, baseEntity); 085 086 if(LogSessionEntityCacheStatistics) { 087 sessionEntityCacheStatistics.readOnlyUpgradedToReadWrite++; 088 } 089 } 090 091 private boolean entitiesReadOnlyContains(BasePK basePK) { 092 return entitiesReadOnly.containsKey(basePK); 093 } 094 095 public void putReadOnlyEntity(BasePK basePk, BaseEntity baseEntity) { 096 if(LogSessionEntityCacheStatistics) { 097 sessionEntityCacheStatistics.putReadOnlyEntity++; 098 } 099 100 if(LogSessionEntityCacheActions) { 101 log.info("putReadOnlyEntity(" + baseEntity.getPrimaryKey() + ")"); 102 } 103 104 entitiesReadOnly.put(basePk, baseEntity); 105 } 106 107 public void putReadWriteEntity(BasePK basePK, BaseEntity baseEntity) { 108 var cacheToExamine = this; 109 var addedToCache = false; 110 111 if(LogSessionEntityCacheActions) { 112 log.info("putReadWriteEntity(" + baseEntity.getPrimaryKey() + ")"); 113 } 114 115 // Check this cache, and all parent caches, and see if one of them contains a RO copy of this 116 // entity. If so, replace it in the cache it was found in, otherwise, please it in our RW cache. 117 do { 118 if(cacheToExamine.entitiesReadOnlyContains(basePK)) { 119 cacheToExamine.replaceReadOnlyEntity(baseEntity); 120 addedToCache = true; 121 break; 122 } 123 124 cacheToExamine = cacheToExamine.parentSessionEntityCache; 125 } while(cacheToExamine != null); 126 127 if(addedToCache) { 128 if(LogSessionEntityCacheStatistics) { 129 sessionEntityCacheStatistics.putReadWriteEntityToParent++; 130 } 131 } else { 132 if(LogSessionEntityCacheStatistics) { 133 sessionEntityCacheStatistics.putReadWriteEntity++; 134 } 135 136 entitiesReadWrite.put(basePK, baseEntity); 137 } 138 } 139 140 public BaseEntity getEntity(BasePK basePK) { 141 BaseEntity entity; 142 143 entity = entitiesReadWrite.get(basePK); 144 145 if(LogSessionEntityCacheStatistics && entity != null) { 146 sessionEntityCacheStatistics.gotEntityFromReadWrite++; 147 } 148 149 if(entity == null) { 150 entity = entitiesReadOnly.get(basePK); 151 } 152 153 if(LogSessionEntityCacheStatistics && entity != null) { 154 sessionEntityCacheStatistics.gotEntityFromReadOnly++; 155 } 156 157 // If it isn't found in either of our caches, and there is a parentSessionEntityCache, check it. 158 if(entity == null && parentSessionEntityCache != null) { 159 entity = parentSessionEntityCache.getEntity(basePK); 160 161 if(LogSessionEntityCacheStatistics && entity != null) { 162 sessionEntityCacheStatistics.gotEntityFromParent++; 163 } 164 } 165 166 if(LogSessionEntityCacheStatistics && entity == null) { 167 sessionEntityCacheStatistics.entityNotGotten++; 168 } 169 170 if(LogSessionEntityCacheActions) { 171 log.info("getEntity(" + basePK + ") = " + (entity == null ? null : entity.getPrimaryKey())); 172 } 173 174 return entity; 175 } 176 177 public void removed(BasePK basePK, boolean missingPermitted) { 178 if(LogSessionEntityCacheActions) { 179 log.info("removed(" + basePK + ")"); 180 } 181 182 if(entitiesReadWrite.remove(basePK) == null) { 183 if(entitiesReadOnly.remove(basePK) == null) { 184 // If the basePK wasn't in either of our caches, check the parentSessionEntityCache. 185 if(parentSessionEntityCache == null) { 186 if(!missingPermitted) { 187 throw new PersistenceSessionEntityCacheException("removed(...) called on BasePK that is not in a cache"); 188 } 189 } else { 190 parentSessionEntityCache.removed(basePK, missingPermitted); 191 } 192 } 193 } 194 } 195 196 @SuppressWarnings("unchecked") 197 private void flushEntities() { 198 Map<Class, List<BaseEntity>> values = new HashMap<>(); 199 200 entitiesReadWrite.values().stream().filter((baseEntity) -> baseEntity.hasBeenModified()).forEach((baseEntity) -> { 201 Class baseEntityClass = baseEntity.getClass(); 202 var baseEntities = values.get(baseEntity.getClass()); 203 204 if(baseEntities == null) { 205 baseEntities = new ArrayList<>(); 206 values.put(baseEntityClass, baseEntities); 207 } 208 209 baseEntities.add(baseEntity); 210 }); 211 212 values.entrySet().stream().map((entry) -> entry.getValue()).forEach((baseEntities) -> { 213 var firstBaseEntity = baseEntities.get(0); 214 var baseFactory = firstBaseEntity.getBaseFactoryInstance(); 215 216 baseFactory.store(baseEntities); 217 }); 218 219 if(LogSessionEntityCacheStatistics) { 220 sessionEntityCacheStatistics.finalReadWriteCount = entitiesReadWrite.size(); 221 sessionEntityCacheStatistics.finalReadOnlyCount = entitiesReadOnly.size(); 222 } 223 224 entitiesReadOnly = null; 225 entitiesReadWrite = null; 226 } 227 228 private void dumpStats() { 229 log.info("--------------------------------------------------------------------------------"); 230 log.info("this = " + this); 231 log.info("parentSessionEntityCache = " + parentSessionEntityCache); 232 sessionEntityCacheStatistics.dumpStats(); 233 log.info("--------------------------------------------------------------------------------"); 234 } 235 236 private void dumpCumulativeStats() { 237 log.info("--------------------------------------------------------------------------------"); 238 log.info("this = " + this); 239 cumulativeSessionEntityCacheStatistics.dumpStats(); 240 log.info("--------------------------------------------------------------------------------"); 241 } 242 243 private SessionEntityCache pop(boolean lastInStack) { 244 if(LogSessionEntityCacheStatistics) { 245 log.info("pop(lastInStack = " + lastInStack + ")"); 246 } 247 248 if(parentSessionEntityCache == null && !lastInStack) { 249 throw new PersistenceSessionEntityCacheException("Cannot call popSessionEntityCache() on last SessionEntityCache"); 250 } 251 252 if(parentSessionEntityCache != null && lastInStack) { 253 throw new PersistenceSessionEntityCacheException("Can only call popLastSessionEntityCache() on last SessionEntityCache"); 254 } 255 256 flushEntities(); 257 258 259 if(LogSessionEntityCacheStatistics) { 260 addStatisticsToRootCache(this, sessionEntityCacheStatistics); 261 262 dumpStats(); 263 264 if(parentSessionEntityCache == null) { 265 dumpCumulativeStats(); 266 } 267 } 268 269 return parentSessionEntityCache; 270 } 271 272 private void addStatisticsToRootCache(SessionEntityCache parentSessionEntityCache, SessionEntityCacheStatistics sessionEntityCacheStatistics) { 273 if(parentSessionEntityCache.parentSessionEntityCache != null) { 274 addStatisticsToRootCache(parentSessionEntityCache.parentSessionEntityCache, sessionEntityCacheStatistics); 275 } else { 276 parentSessionEntityCache.cumulativeSessionEntityCacheStatistics.Add(sessionEntityCacheStatistics); 277 } 278 } 279 280 public SessionEntityCache popSessionEntityCache() { 281 return pop(false); 282 } 283 284 public SessionEntityCache popLastSessionEntityCache() { 285 return pop(true); 286 } 287 288}