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