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