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}