001/*
002 *  Copyright 2012 Anyware Services
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 */
016package org.ametys.web.cache.pageelement;
017
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023import java.util.concurrent.atomic.AtomicInteger;
024
025import org.apache.avalon.framework.activity.Disposable;
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.logger.AbstractLogEnabled;
031import org.apache.cocoon.components.ContextHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.xml.SaxBuffer;
034
035import org.ametys.core.DevMode;
036import org.ametys.web.renderingcontext.RenderingContext;
037
038/**
039 * Cache the page elements renderings.<br>
040 * This class handles several caches, one per site and per JCR workspace.
041 */
042public class PageElementCache extends AbstractLogEnabled implements Component, Contextualizable, Disposable
043{
044    /** Avalon role. */
045    public static final String ROLE = PageElementCache.class.getName();
046    
047    /** "Disable page element cache" request attribute. */
048    public static final String DISABLE_PE_CACHE_ATTRIBUTE = "disable-page-element-cache";
049    
050    // Map<workspace, Map<site, Map<elementType, Map<elementId, Map<subelementId, Map<renderingContext, SaxBuffer>>>>>>
051    private Map<String, Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>>> _cache = new HashMap<>();
052    
053    // Map<workspace, Map<site, Map<elementType, hitCount>>>
054    private Map<String, Map<String, Map<String, AtomicInteger>>> _currentHitCount = new HashMap<>();
055    
056    // Map<workspace, Map<site, Map<elementType, hitCount>>>
057    private Map<String, Map<String, Map<String, AtomicInteger>>> _hitCount = new HashMap<>();
058    
059    // Map<workspace, Map<site, Map<elementType, missCount>>>
060    private Map<String, Map<String, Map<String, AtomicInteger>>> _missCount = new HashMap<>();
061    
062    private Context _context;
063    
064    @Override
065    public void contextualize(Context context) throws ContextException
066    {
067        _context = context;
068    }
069    
070    /**
071     * Add a content in the cache.
072     * @param workspace the current JCR workspace.
073     * @param site the current site.
074     * @param pageElementType the page element type.
075     * @param elementId the element id (page id or zoneItem id).
076     * @param renderingContext the current rendering context.
077     * @param content the actual content.
078     */
079    public void storePageElement(String workspace, String site, String pageElementType, String elementId, RenderingContext renderingContext, SaxBuffer content)
080    {
081        storePageElement(workspace, site, pageElementType, elementId, "", renderingContext, content);
082    }
083    
084    /**
085     * Add a content in the cache.
086     * @param workspace the current JCR workspace.
087     * @param site the current site.
088     * @param pageElementType the page element type.
089     * @param elementId the element id (page id or zoneItem id).
090     * @param subElementId the sub-element id (page id, in case the element id is a zoneItem id).
091     * @param renderingContext the current rendering context.
092     * @param content the actual content.
093     */
094    public void storePageElement(String workspace, String site, String pageElementType, String elementId, String subElementId, RenderingContext renderingContext, SaxBuffer content)
095    {
096        if (isEnabled())
097        {
098            // Hit count
099            if (getLogger().isDebugEnabled())
100            {
101                incrementCount(_missCount, workspace, site, pageElementType);
102            }
103            
104            Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
105            if (workspaceCache == null)
106            {
107                workspaceCache = new HashMap<>();
108                _cache.put(workspace, workspaceCache);
109            }
110            
111            Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
112            if (siteCache == null)
113            {
114                siteCache = new HashMap<>();
115                workspaceCache.put(site, siteCache);
116            }
117            
118            Map<String, Map<String, Map<RenderingContext, SaxBuffer>>> elementCache = siteCache.get(pageElementType);
119            if (elementCache == null)
120            {
121                elementCache = new HashMap<>();
122                siteCache.put(pageElementType, elementCache);
123            }
124            
125            Map<String, Map<RenderingContext, SaxBuffer>> subElementCache = elementCache.get(elementId);
126            if (subElementCache == null)
127            {
128                subElementCache = new HashMap<>();
129                elementCache.put(elementId, subElementCache);
130            }
131            
132            Map<RenderingContext, SaxBuffer> renderingContextCache = subElementCache.get(subElementId);
133            if (renderingContextCache == null)
134            {
135                renderingContextCache = new HashMap<>();
136                subElementCache.put(subElementId, renderingContextCache);
137            }
138            
139            renderingContextCache.put(renderingContext, content);
140        }
141    }
142    
143    /**
144     * Returns the data in the cache, or null if none.
145     * @param workspace the current JCR workspace.
146     * @param site the current site.
147     * @param pageElementType the element type.
148     * @param elementId the element id (page id or zoneItem id).
149     * @param renderingContext the current rendering context.
150     * @return the stored content.
151     */
152    public SaxBuffer getPageElement(String workspace, String site, String pageElementType, String elementId, RenderingContext renderingContext)
153    {
154        return getPageElement(workspace, site, pageElementType, elementId, "", renderingContext);
155    }
156    
157    /**
158     * Returns the data in the cache, or null if none.
159     * @param workspace the current JCR workspace.
160     * @param site the current site.
161     * @param pageElementType the element type.
162     * @param elementId the element id (page id or zoneItem id).
163     * @param subElementId the sub-element id (page id, in case the element id is a zoneItem id).
164     * @param renderingContext the current rendering context.
165     * @return the stored content.
166     */
167    public SaxBuffer getPageElement(String workspace, String site, String pageElementType, String elementId, String subElementId, RenderingContext renderingContext)
168    {
169        if (isEnabled())
170        {
171            Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
172            if (workspaceCache != null)
173            {
174                Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
175                if (siteCache != null)
176                {
177                    Map<String, Map<String, Map<RenderingContext, SaxBuffer>>> elementCache = siteCache.get(pageElementType);
178                    
179                    if (elementCache != null)
180                    {
181                        Map<String, Map<RenderingContext, SaxBuffer>> subElementCache = elementCache.get(elementId);
182                        
183                        if (subElementCache != null)
184                        {
185                            Map<RenderingContext, SaxBuffer> renderingContextCache = subElementCache.get(subElementId);
186                            
187                            if (renderingContextCache != null)
188                            {
189                                // Cache hit
190                                if (getLogger().isDebugEnabled())
191                                {
192                                    incrementCount(_currentHitCount, workspace, site, pageElementType);
193                                    incrementCount(_hitCount, workspace, site, pageElementType);
194                                }
195                                
196                                return renderingContextCache.get(renderingContext);
197                            }
198                        }
199                    }
200                }
201            }
202        }
203        
204        return null;
205    }
206    
207    /**
208     * Returns all stored element types, or null if none.
209     * @param workspace the current JCR workspace.
210     * @param site the current site.
211     * @return all stored element types.
212     */
213    public Set<String> getPageElementTypes(String workspace, String site)
214    {
215        if (isEnabled())
216        {
217            Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
218            if (workspaceCache != null)
219            {
220                Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
221                if (siteCache != null)
222                {
223                    return new HashSet<>(siteCache.keySet());
224                }
225            }
226        }
227        
228        return Collections.emptySet();
229    }
230    
231    /**
232     * Returns all cached data for a given InputData, or null if none.
233     * @param workspace the current JCR workspace.
234     * @param site the current site.
235     * @param pageElementType the element type.
236     * @return the stored content.
237     */
238    public Map<String, Map<String, Map<RenderingContext, SaxBuffer>>> getElementCache(String workspace, String site, String pageElementType)
239    {
240        if (isEnabled())
241        {
242            Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
243            if (workspaceCache != null)
244            {
245                Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
246                if (siteCache != null)
247                {
248                    return siteCache.get(pageElementType);
249                }
250            }
251        }
252        
253        return null;
254    }
255    
256    /**
257     * Removes a single item from the cache.
258     * @param workspace the target JCR workspace or null to remove from all workspaces.
259     * @param site the target site.
260     * @param pageElementType the element type.
261     * @param elementId the element id.
262     */
263    public void removeItem(String workspace, String site, String pageElementType, String elementId)
264    {
265        if (isEnabled())
266        {
267            if (workspace != null)
268            {
269                Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
270                if (workspaceCache != null)
271                {
272                    Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
273    
274                    if (siteCache != null)
275                    {
276                        Map<String, Map<String, Map<RenderingContext, SaxBuffer>>> elementCache = siteCache.get(pageElementType);
277                        
278                        if (elementCache != null)
279                        {
280                            elementCache.remove(elementId);
281                            
282                            logFullHitCount(workspace, site, pageElementType);
283                        }
284                    }
285                }
286            }
287            else
288            {
289                // removes from all workspaces
290                for (String wspName : _cache.keySet())
291                {
292                    Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(wspName);
293                    if (workspaceCache != null)
294                    {
295                        Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
296    
297                        if (siteCache != null)
298                        {
299                            Map<String, Map<String, Map<RenderingContext, SaxBuffer>>> elementCache = siteCache.get(pageElementType);
300                            
301                            if (elementCache != null)
302                            {
303                                elementCache.remove(elementId);
304                                
305                                logFullHitCount(wspName, site, pageElementType);
306                            }
307                        }
308                    }
309                }
310            }
311        }
312    }
313
314    /**
315     * Removes all stored data.
316     */
317    public void clear()
318    {
319        logAndResetHitCount();
320        
321        _cache.clear();
322        _currentHitCount.clear();
323        _hitCount.clear();
324        _missCount.clear();
325    }
326    
327    /**
328     * Removes all data associated with a JCR workspace and a site.
329     * @param workspace the current JCR workspace. If null, means all workspaces.
330     * @param site the current site.
331     */
332    public void clear(String workspace, String site)
333    {
334        logAndResetHitCount(workspace, site);
335        
336        if (workspace != null)
337        {
338            Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
339            if (workspaceCache != null)
340            {
341                workspaceCache.remove(site);
342            }
343        }
344        else
345        {
346            for (Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache : _cache.values())
347            {
348                workspaceCache.remove(site);
349            }
350        }
351    }
352    
353    /**
354     * Removes all data associated with a JCR workspace, a site and a given element type.
355     * @param workspace the current JCR workspace.
356     * @param site the current site.
357     * @param pageElementType the element type.
358     */
359    public void clear(String workspace, String site, String pageElementType)
360    {
361        logAndResetHitCount(workspace, site, pageElementType);
362        
363        Map<String, Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>>> workspaceCache = _cache.get(workspace);
364        if (workspaceCache != null)
365        {
366            Map<String, Map<String, Map<String, Map<RenderingContext, SaxBuffer>>>> siteCache = workspaceCache.get(site);
367            
368            if (siteCache != null)
369            {
370                siteCache.remove(pageElementType);
371            }
372        }
373    }
374    
375    @Override
376    public void dispose()
377    {
378        clear();
379    }
380    
381    /**
382     * Test if the cache is enabled.
383     * @return true if the cache is enabled, false otherwise.
384     */
385    protected boolean isEnabled()
386    {
387        Request request = ContextHelper.getRequest(_context);
388        Object disabledRequestStr = request.getAttribute(DISABLE_PE_CACHE_ATTRIBUTE);
389        
390        boolean disabledRequest = disabledRequestStr != null && disabledRequestStr.equals("true");
391        boolean disabled = DevMode.isDeveloperMode(request);
392        
393        return !disabled && !disabledRequest;
394    }
395    
396    /**
397     * Add a hit count for a workspace, site and page element type.
398     * @param countMap the map to fill.
399     * @param workspace the workspace.
400     * @param siteName the site name.
401     * @param pageElementType the page element type.
402     */
403    protected void incrementCount(Map<String, Map<String, Map<String, AtomicInteger>>> countMap, String workspace, String siteName, String pageElementType)
404    {
405        Map<String, Map<String, AtomicInteger>> workspaceCount = countMap.get(workspace);
406        if (workspaceCount == null)
407        {
408            workspaceCount = new HashMap<>();
409            countMap.put(workspace, workspaceCount);
410        }
411        
412        Map<String, AtomicInteger> siteCount = workspaceCount.get(siteName);
413        if (siteCount == null)
414        {
415            siteCount = new HashMap<>();
416            workspaceCount.put(siteName, siteCount);
417        }
418        
419        AtomicInteger count = siteCount.get(pageElementType);
420        if (count == null)
421        {
422            count = new AtomicInteger();
423            siteCount.put(pageElementType, count);
424        }
425        
426        count.incrementAndGet();
427    }
428    
429    /**
430     * Get a hit or miss count for a workspace, site and page element type.
431     * @param countMap the count map.
432     * @param workspace the workspace.
433     * @param siteName the site name.
434     * @param pageElementType the page element type.
435     * @return the count.
436     */
437    protected int getCount(Map<String, Map<String, Map<String, AtomicInteger>>> countMap, String workspace, String siteName, String pageElementType)
438    {
439        int count = 0;
440        
441        if (countMap.containsKey(workspace))
442        {
443            Map<String, Map<String, AtomicInteger>> workspaceCount = countMap.get(workspace);
444            
445            if (workspaceCount.containsKey(siteName))
446            {
447                Map<String, AtomicInteger> siteCount = workspaceCount.get(siteName);
448                
449                if (siteCount.containsKey(pageElementType))
450                {
451                    count = siteCount.get(pageElementType).get();
452                }
453            }
454        }
455        
456        return count;
457    }
458    
459    /**
460     * Reset the hit count after logging the old value.
461     */
462    protected void logAndResetHitCount()
463    {
464        for (String workspace : _currentHitCount.keySet())
465        {
466            Map<String, Map<String, AtomicInteger>> workspaceHitCount = _currentHitCount.get(workspace);
467            
468            for (String siteName : workspaceHitCount.keySet())
469            {
470                Map<String, AtomicInteger> siteHitCount = workspaceHitCount.get(siteName);
471                
472                for (String pageElementType : siteHitCount.keySet())
473                {
474                    logAndResetHitCount(workspace, siteName, pageElementType, siteHitCount.get(pageElementType));
475                }
476            }
477        }
478    }
479    
480    /**
481     * Reset the hit count after logging the old value.
482     * @param workspace the workspace.
483     * @param siteName the site name.
484     */
485    protected void logAndResetHitCount(String workspace, String siteName)
486    {
487        Map<String, Map<String, AtomicInteger>> workspaceHitCount = _currentHitCount.get(workspace);
488        if (workspaceHitCount == null)
489        {
490            workspaceHitCount = new HashMap<>();
491            _currentHitCount.put(workspace, workspaceHitCount);
492        }
493        
494        Map<String, AtomicInteger> siteHitCount = workspaceHitCount.get(siteName);
495        if (siteHitCount == null)
496        {
497            siteHitCount = new HashMap<>();
498            workspaceHitCount.put(siteName, siteHitCount);
499        }
500        
501        for (String pageElementType : siteHitCount.keySet())
502        {
503            logAndResetHitCount(workspace, siteName, pageElementType, siteHitCount.get(pageElementType));
504        }
505    }
506    
507    /**
508     * Reset the hit count after logging the old value.
509     * @param workspace the workspace.
510     * @param siteName the site name.
511     * @param pageElementType the page element type.
512     */
513    protected void logAndResetHitCount(String workspace, String siteName, String pageElementType)
514    {
515        Map<String, Map<String, AtomicInteger>> workspaceHitCount = _currentHitCount.get(workspace);
516        if (workspaceHitCount == null)
517        {
518            workspaceHitCount = new HashMap<>();
519            _currentHitCount.put(workspace, workspaceHitCount);
520        }
521        
522        Map<String, AtomicInteger> siteHitCount = workspaceHitCount.get(siteName);
523        if (siteHitCount == null)
524        {
525            siteHitCount = new HashMap<>();
526            workspaceHitCount.put(siteName, siteHitCount);
527        }
528        
529        AtomicInteger hitCount = siteHitCount.get(pageElementType);
530        if (hitCount == null)
531        {
532            hitCount = new AtomicInteger();
533            siteHitCount.put(pageElementType, hitCount);
534        }
535        
536        logAndResetHitCount(workspace, siteName, pageElementType, hitCount);
537    }
538    
539    /**
540     * Reset the hit count after logging the old value.
541     * @param workspace the workspace.
542     * @param siteName the site name.
543     * @param pageElementType the page element type.
544     * @param hitCount the old hit count.
545     */
546    protected void logAndResetHitCount(String workspace, String siteName, String pageElementType, AtomicInteger hitCount)
547    {
548        if (getLogger().isDebugEnabled())
549        {
550            StringBuilder buff = new StringBuilder();
551            
552            buff.append("Page element cache cleared for workspace '").append(workspace)
553                .append("', site '").append(siteName).append("' and type '").append(pageElementType).append("'. ")
554                .append(hitCount.get()).append(" reads had been made.");
555            
556            logFullHitCount(workspace, siteName, pageElementType);
557        }
558        
559        hitCount.set(0);
560    }
561    
562    /**
563     * Log the full hit and miss count for a given workspace, site and page element type.
564     * @param workspace the workspace.
565     * @param siteName the site name.
566     * @param pageElementType the page element type.
567     */
568    protected void logFullHitCount(String workspace, String siteName, String pageElementType)
569    {
570        if (getLogger().isDebugEnabled())
571        {
572            int fullHitCount = getCount(_hitCount, workspace, siteName, pageElementType);
573            int fullMissCount = getCount(_missCount, workspace, siteName, pageElementType);
574            
575            StringBuilder buff = new StringBuilder();
576            
577            buff.append("Workspace '").append(workspace).append("', site '").append(siteName).append("' and type '").append(pageElementType)
578                .append("': so far, there were ").append(fullMissCount).append(" misses and ").append(fullHitCount).append(" hits.");
579            
580            getLogger().debug(buff.toString());
581        }
582    }
583    
584}