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}