001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.flex;
029
030import org.opencms.cache.CmsLruCache;
031import org.opencms.cache.I_CmsLruCacheObject;
032import org.opencms.db.CmsPublishedResource;
033import org.opencms.file.CmsObject;
034import org.opencms.flex.CmsFlexBucketConfiguration.BucketSet;
035import org.opencms.loader.CmsJspLoader;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.I_CmsEventListener;
039import org.opencms.main.OpenCms;
040import org.opencms.security.CmsRole;
041import org.opencms.util.CmsCollectionsGenericWrapper;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.util.CmsUUID;
044
045import java.util.ArrayList;
046import java.util.Collection;
047import java.util.Collections;
048import java.util.HashMap;
049import java.util.HashSet;
050import java.util.Hashtable;
051import java.util.Iterator;
052import java.util.List;
053import java.util.Map;
054import java.util.Set;
055
056import org.apache.commons.collections.map.LRUMap;
057import org.apache.commons.logging.Log;
058
059import com.google.common.collect.Lists;
060
061/**
062 * This class implements the FlexCache.<p>
063 *
064 * The data structure used is a two-level hashtable.
065 * This is optimized for the structure of the keys that are used to describe the
066 * caching behaviour of the entries.
067 * The first hash-level is calculated from the resource name, i.e. the
068 * name of the resource as it is referred to in the VFS of OpenCms.
069 * The second hash-level is calculated from the cache-key of the resource,
070 * which also is a String representing the specifc variation of the cached entry.<p>
071 *
072 * A suffix [online] or [offline] is appended to te resource name
073 * to distinguish between the online and offline projects of OpenCms.
074 * Also, for support of JSP based workplace pages, a suffix [workplace]
075 * is appended. The same cached workplace pages are used both in the online and
076 * all offline projects.<p>
077 *
078 * Entries in the first level of the cache are of type CmsFlexCacheVariation,
079 * which is a sub-class of CmsFlexCache.
080 * This class is a simple data type that contains of a Map of CmsFlexCacheEntries,
081 * with variations - Strings as keys.<p>
082 *
083 * Here's a short summary of used terms:
084 * <ul>
085 * <li><b>key:</b>
086 * A combination of a resource name and a variation.
087 * The data structure used is CmsFlexCacheKey.
088 * <li><b>resource:</b>
089 * A String with the resource name and an appended [online] of [offline] suffix.
090 * <li><b>variation:</b>
091 * A String describing a variation of a cached entry in the CmsFlexCache language.
092 * <li><b>entry:</b>
093 * A CmsFlexCacheEntry data structure which is describes a cached OpenCms resource.
094 * For every entry a key is saved which contains the resource name and the variation.
095 * </ul>
096 *
097 * Cache clearing is handled using events.
098 * The cache is fully flushed if an event {@link I_CmsEventListener#EVENT_PUBLISH_PROJECT}
099 * or {@link I_CmsEventListener#EVENT_CLEAR_CACHES} is caught.<p>
100 *
101 * @since 6.0.0
102 *
103 * @see org.opencms.flex.CmsFlexCacheKey
104 * @see org.opencms.flex.CmsFlexCacheEntry
105 * @see org.opencms.cache.CmsLruCache
106 * @see org.opencms.cache.I_CmsLruCacheObject
107 */
108public class CmsFlexCache extends Object implements I_CmsEventListener {
109
110    /**
111     * A simple data container class for the FlexCache variations.<p>
112     */
113    public static class CmsFlexCacheVariation extends Object {
114
115        /** The key belonging to the resource. */
116        public CmsFlexCacheKey m_key;
117
118        /** Maps variations to CmsFlexCacheEntries. */
119        public Map<String, I_CmsLruCacheObject> m_map;
120
121        /**
122         * Generates a new instance of CmsFlexCacheVariation.<p>
123         *
124         * @param theKey The (resource) key to contruct this variation list for
125         */
126        public CmsFlexCacheVariation(CmsFlexCacheKey theKey) {
127
128            m_key = theKey;
129            m_map = new Hashtable<String, I_CmsLruCacheObject>(INITIAL_CAPACITY_VARIATIONS);
130        }
131    }
132
133    /**
134     * Extended LRUMap that handles the variations in case a key is removed.<p>
135     */
136    class CmsFlexKeyMap extends LRUMap {
137
138        /** Serial version UID required for safe serialization. */
139        private static final long serialVersionUID = 6931995916013396902L;
140
141        /**
142         * Initialize the map with the given size.<p>
143         *
144         * @param maxSize the maximum number of key to cache
145         */
146        public CmsFlexKeyMap(int maxSize) {
147
148            super(maxSize);
149        }
150
151        /**
152         * Ensures that all variations that referenced by this key are released
153         * if the key is released.<p>
154         *
155         * @param entry the entry to remove
156         *
157         * @return <code>true</code> to actually delete the entry
158         *
159         * @see LRUMap#removeLRU(LinkEntry)
160         */
161        @Override
162        protected boolean removeLRU(LinkEntry entry) {
163
164            CmsFlexCacheVariation v = (CmsFlexCacheVariation)entry.getValue();
165            if (v == null) {
166                return true;
167            }
168            Map<String, I_CmsLruCacheObject> m = v.m_map;
169            if ((m == null) || (m.size() == 0)) {
170                return true;
171            }
172
173            // make a copy to safely iterate over because the line "m_variationCache.remove(e)" modifies the variation map for the key
174            Collection<I_CmsLruCacheObject> entries = new ArrayList<I_CmsLruCacheObject>(m.values());
175            synchronized (m_variationCache) {
176                for (I_CmsLruCacheObject e : entries) {
177                    m_variationCache.remove(e);
178                }
179                v.m_map.clear();
180                v.m_map = null;
181                v.m_key = null;
182            }
183            return true;
184        }
185    }
186
187    /**Constant for distinguish cache action.*/
188    public static final String CACHE_ACTION = "action";
189
190    /** Suffix to append to online cache entries. */
191    public static final String CACHE_OFFLINESUFFIX = " [offline]";
192
193    /** Suffix to append to online cache entries. */
194    public static final String CACHE_ONLINESUFFIX = " [online]";
195
196    /** Trigger for clearcache event: Clear complete cache. */
197    public static final int CLEAR_ALL = 0;
198
199    /** Trigger for clearcache event: Clear only entries. */
200    public static final int CLEAR_ENTRIES = 1;
201
202    /** Trigger for clearcache event: Clear complete offine cache. */
203    public static final int CLEAR_OFFLINE_ALL = 4;
204
205    /** Trigger for clearcache event: Clear only offline entries. */
206    public static final int CLEAR_OFFLINE_ENTRIES = 5;
207
208    /** Trigger for clearcache event: Clear complete online cache. */
209    public static final int CLEAR_ONLINE_ALL = 2;
210
211    /** Trigger for clearcache event: Clear only online entries. */
212    public static final int CLEAR_ONLINE_ENTRIES = 3;
213
214    /** The configuration for the Flex cache buckets. */
215    public static final String CONFIG_PATH = "/system/config/flexconfig.properties";
216
217    /** Initial cache size, this should be a power of 2 because of the Java collections implementation. */
218    public static final int INITIAL_CAPACITY_CACHE = 512;
219
220    /** Initial size for variation lists, should be a power of 2. */
221    public static final int INITIAL_CAPACITY_VARIATIONS = 8;
222
223    /** Offline repository constant. */
224    public static final String REPOSITORY_OFFLINE = "offline";
225
226    /** Online repository constant. */
227    public static final String REPOSITORY_ONLINE = "online";
228
229    /** The log object for this class. */
230    private static final Log LOG = CmsLog.getLog(CmsFlexCache.class);
231
232    /** The LRU cache to organize the cached entries. */
233    protected CmsLruCache m_variationCache;
234
235    /** The Flex bucket configuration. */
236    private CmsFlexBucketConfiguration m_bucketConfiguration;
237
238    /** Indicates if offline resources should be cached or not. */
239    private boolean m_cacheOffline;
240
241    /** The CMS object used for VFS operations. */
242    private CmsObject m_cmsObject;
243
244    /** Indicates if the cache is enabled or not. */
245    private boolean m_enabled;
246
247    /** Map to store the entries for fast lookup. */
248    private Map<String, CmsFlexCacheVariation> m_keyCache;
249
250    /** Counter for the size. */
251    private int m_size;
252
253    /**
254     * Constructor for class CmsFlexCache.<p>
255     *
256     * The parameter "enabled" is used to control if the cache is
257     * actually on or off. Even if you don't need the cache, you still
258     * have to create an instance of it with enabled=false.
259     * This is because you need some of the FlexCache data structures
260     * for JSP inclusion buffering.<p>
261     *
262     * @param configuration the flex cache configuration
263     */
264    public CmsFlexCache(CmsFlexCacheConfiguration configuration) {
265
266        m_enabled = configuration.isCacheEnabled();
267        m_cacheOffline = configuration.isCacheOffline();
268
269        long maxCacheBytes = configuration.getMaxCacheBytes();
270        long avgCacheBytes = configuration.getAvgCacheBytes();
271        int maxEntryBytes = configuration.getMaxEntryBytes();
272        int maxKeys = configuration.getMaxKeys();
273
274        m_variationCache = new CmsLruCache(maxCacheBytes, avgCacheBytes, maxEntryBytes);
275        OpenCms.getMemoryMonitor().register(getClass().getName() + ".m_entryLruCache", m_variationCache);
276
277        if (m_enabled) {
278            CmsFlexKeyMap flexKeyMap = new CmsFlexKeyMap(maxKeys);
279            m_keyCache = Collections.synchronizedMap(
280                CmsCollectionsGenericWrapper.<String, CmsFlexCacheVariation> map(flexKeyMap));
281            OpenCms.getMemoryMonitor().register(getClass().getName() + ".m_resourceMap", flexKeyMap);
282
283            OpenCms.addCmsEventListener(
284                this,
285                new int[] {
286                    I_CmsEventListener.EVENT_PUBLISH_PROJECT,
287                    I_CmsEventListener.EVENT_CLEAR_CACHES,
288                    I_CmsEventListener.EVENT_FLEX_PURGE_JSP_REPOSITORY,
289                    I_CmsEventListener.EVENT_FLEX_CACHE_CLEAR});
290        }
291
292        if (LOG.isInfoEnabled()) {
293            LOG.info(
294                Messages.get().getBundle().key(
295                    Messages.INIT_FLEXCACHE_CREATED_2,
296                    Boolean.valueOf(m_enabled),
297                    Boolean.valueOf(m_cacheOffline)));
298        }
299    }
300
301    /**
302     * Copies the key set of a map while synchronizing on the map.<p>
303     *
304     * @param map the map whose key set should be copied
305     * @return the copied key set
306     */
307    private static <K, V> Set<K> synchronizedCopyKeys(Map<K, V> map) {
308
309        if (map == null) {
310            return new HashSet<K>();
311        }
312        synchronized (map) {
313            return new HashSet<K>(map.keySet());
314        }
315    }
316
317    /**
318     * Copies a map while synchronizing on it.<p>
319     *
320     * @param map the map to copy
321     * @return the copied map
322     */
323    private static <K, V> Map<K, V> synchronizedCopyMap(Map<K, V> map) {
324
325        if (map == null) {
326            return new HashMap<K, V>();
327        }
328
329        synchronized (map) {
330
331            return new HashMap<K, V>(map);
332        }
333    }
334
335    /**
336     * Indicates if offline project resources are cached.<p>
337     *
338     * @return true if offline projects are cached, false if not
339     */
340    public boolean cacheOffline() {
341
342        return m_cacheOffline;
343    }
344
345    /**
346     * Implements the CmsEvent interface,
347     * the FlexCache uses the events to clear itself in case a project is published.<p>
348     *
349     * @param event CmsEvent that has occurred
350     */
351    public void cmsEvent(org.opencms.main.CmsEvent event) {
352
353        if (!isEnabled()) {
354            return;
355        }
356
357        switch (event.getType()) {
358            case I_CmsEventListener.EVENT_PUBLISH_PROJECT:
359                if (LOG.isDebugEnabled()) {
360                    LOG.debug("FlexCache: Received event PUBLISH_PROJECT");
361                }
362                String publishIdStr = (String)(event.getData().get(I_CmsEventListener.KEY_PUBLISHID));
363                if (!CmsUUID.isValidUUID(publishIdStr)) {
364                    clear();
365                } else {
366                    try {
367                        CmsUUID publishId = new CmsUUID(publishIdStr);
368                        List<CmsPublishedResource> publishedResources = m_cmsObject.readPublishedResources(publishId);
369                        boolean updateConfiguration = false;
370                        for (CmsPublishedResource res : publishedResources) {
371                            if (res.getRootPath().equals(CONFIG_PATH)) {
372                                updateConfiguration = true;
373                                break;
374                            }
375                        }
376                        CmsFlexBucketConfiguration bucketConfig = m_bucketConfiguration;
377                        if (updateConfiguration) {
378                            LOG.info("Flex bucket configuration was updated, re-initializing configuration...");
379                            try {
380                                m_bucketConfiguration = CmsFlexBucketConfiguration.loadFromVfsFile(
381                                    m_cmsObject,
382                                    CONFIG_PATH);
383                            } catch (CmsException e) {
384                                LOG.error(e.getLocalizedMessage(), e);
385                            }
386                            // Make sure no entries built for the old configuration remain in the cache
387                            clear();
388                        } else if (bucketConfig != null) {
389                            boolean bucketClearOk = clearBucketsForPublishList(
390                                bucketConfig,
391                                publishId,
392                                publishedResources);
393                            if (!bucketClearOk) {
394                                clear();
395                            }
396                        } else {
397                            clear();
398                        }
399
400                    } catch (CmsException e1) {
401                        LOG.error(e1.getLocalizedMessage(), e1);
402                        clear();
403                    }
404                }
405                break;
406            case I_CmsEventListener.EVENT_CLEAR_CACHES:
407                if (LOG.isDebugEnabled()) {
408                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_RECEIVED_EVENT_CLEAR_CACHE_0));
409                }
410                clear();
411                break;
412            case I_CmsEventListener.EVENT_FLEX_PURGE_JSP_REPOSITORY:
413                if (LOG.isDebugEnabled()) {
414                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_RECEIVED_EVENT_PURGE_REPOSITORY_0));
415                }
416                purgeJspRepository();
417                break;
418            case I_CmsEventListener.EVENT_FLEX_CACHE_CLEAR:
419                if (LOG.isDebugEnabled()) {
420                    LOG.debug(
421                        Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_RECEIVED_EVENT_CLEAR_CACHE_PARTIALLY_0));
422                }
423                Map<String, ?> m = event.getData();
424                if (m == null) {
425                    break;
426                }
427                Integer it = null;
428                try {
429                    it = (Integer)m.get(CACHE_ACTION);
430                } catch (Exception e) {
431                    // it will be null
432                }
433                if (it == null) {
434                    LOG.error("Flex cache clear event with no action parameter received");
435                    break;
436                }
437                int i = it.intValue();
438                switch (i) {
439                    case CLEAR_ALL:
440                        clear();
441                        break;
442                    case CLEAR_ENTRIES:
443                        clearEntries();
444                        break;
445                    case CLEAR_ONLINE_ALL:
446                        clearOnline();
447                        break;
448                    case CLEAR_ONLINE_ENTRIES:
449                        clearOnlineEntries();
450                        break;
451                    case CLEAR_OFFLINE_ALL:
452                        clearOffline();
453                        break;
454                    case CLEAR_OFFLINE_ENTRIES:
455                        clearOfflineEntries();
456                        break;
457                    default:
458                        // no operation
459                }
460                break;
461            default:
462                // no operation
463        }
464    }
465
466    /**
467     * Dumps keys and variations to a string buffer, for debug purposes.<p>
468     *
469     * @param buffer the buffer to which the key information should be written
470     */
471    public void dumpKeys(StringBuffer buffer) {
472
473        synchronized (this) {
474            for (Map.Entry<String, CmsFlexCacheVariation> entry : synchronizedCopyMap(m_keyCache).entrySet()) {
475                String key = entry.getKey();
476                CmsFlexCacheVariation variations = entry.getValue();
477                Map<String, I_CmsLruCacheObject> variationMap = variations.m_map;
478                for (Map.Entry<String, I_CmsLruCacheObject> varEntry : variationMap.entrySet()) {
479                    String varKey = varEntry.getKey();
480                    I_CmsLruCacheObject value = varEntry.getValue();
481                    buffer.append(key + " VAR " + varKey + "\n");
482                    if (value instanceof CmsFlexCacheEntry) {
483                        CmsFlexCacheEntry singleCacheEntry = (CmsFlexCacheEntry)value;
484                        BucketSet buckets = singleCacheEntry.getBucketSet();
485                        if (buckets != null) {
486                            buffer.append("buckets = " + buckets.toString() + "\n");
487                        }
488                    }
489                }
490            }
491        }
492    }
493
494    /**
495     * Returns the CmsFlexCacheKey data structure for a given
496     * key (i.e. resource name).<p>
497     *
498     * Useful if you want to show the cache key for a resources,
499     * like on the FlexCache administration page.<p>
500     *
501     * Only users with administrator permissions are allowed
502     * to perform this operation.<p>
503     *
504     * @param key the resource name for which to look up the variation for
505     * @param cms the CmsObject used for user authorization
506     * @return the CmsFlexCacheKey data structure found for the resource
507     */
508    public CmsFlexCacheKey getCachedKey(String key, CmsObject cms) {
509
510        if (!isEnabled() || !OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_MANAGER)) {
511            return null;
512        }
513        Object o = m_keyCache.get(key);
514        if (o != null) {
515            return ((CmsFlexCacheVariation)o).m_key;
516        }
517        return null;
518    }
519
520    /**
521     * Returns a set of all cached resource names.<p>
522     *
523     * Useful if you want to show a list of all cached resources,
524     * like on the FlexCache administration page.<p>
525     *
526     * Only users with administrator permissions are allowed
527     * to perform this operation.<p>
528     *
529     * @param cms the CmsObject used for user authorization
530     * @return a Set of cached resource names (which are of type String)
531     */
532    public Set<String> getCachedResources(CmsObject cms) {
533
534        if (!isEnabled() || !OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_MANAGER)) {
535            return null;
536        }
537        return synchronizedCopyKeys(m_keyCache);
538    }
539
540    /**
541     * Returns all variations in the cache for a given resource name.
542     * The variations are of type String.<p>
543     *
544     * Useful if you want to show a list of all cached entry - variations,
545     * like on the FlexCache administration page.<p>
546     *
547     * Only users with administrator permissions are allowed
548     * to perform this operation.<p>
549     *
550     * @param key the resource name for which to look up the variations for
551     * @param cms the CmsObject used for user authorization
552     * @return a Set of cached variations (which are of type String)
553     */
554    public Set<String> getCachedVariations(String key, CmsObject cms) {
555
556        if (!isEnabled() || !OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_MANAGER)) {
557            return null;
558        }
559        Object o = m_keyCache.get(key);
560        if (o != null) {
561            return synchronizedCopyKeys(((CmsFlexCacheVariation)o).m_map);
562        }
563        return null;
564    }
565
566    /**
567     * Returns the LRU cache where the CacheEntries are cached.<p>
568     *
569     * @return the LRU cache where the CacheEntries are cached
570     */
571    public CmsLruCache getEntryLruCache() {
572
573        return m_variationCache;
574    }
575
576    /**
577     * Initializes the flex cache.<p>
578     *
579     * @param adminCms a CMS context with admin privileges
580     */
581    public void initializeCms(CmsObject adminCms) {
582
583        try {
584            m_cmsObject = adminCms;
585            try {
586                String path = CONFIG_PATH;
587                if (m_cmsObject.existsResource(path)) {
588                    LOG.info("Flex configuration found at " + CONFIG_PATH + ", initializing...");
589                    m_bucketConfiguration = CmsFlexBucketConfiguration.loadFromVfsFile(m_cmsObject, path);
590                }
591            } catch (Exception e) {
592                LOG.error(e.getLocalizedMessage(), e);
593            }
594        } catch (Exception e) {
595            LOG.error(e.getLocalizedMessage(), e);
596        }
597    }
598
599    /**
600     * Indicates if the cache is enabled (i.e. actually
601     * caching entries) or not.<p>
602     *
603     * @return true if the cache is enabled, false if not
604     */
605    public boolean isEnabled() {
606
607        return m_enabled;
608    }
609
610    /**
611     * Returns the total number of cached resource keys.
612     *
613     * @return the number of resource keys in the cache
614     */
615    public int keySize() {
616
617        if (!isEnabled()) {
618            return 0;
619        }
620        return m_keyCache.size();
621    }
622
623    /**
624     * Returns the total number of entries in the cache.<p>
625     *
626     * @return the number of entries in the cache
627     */
628    public int size() {
629
630        return m_variationCache.size();
631    }
632
633    /**
634     * Looks up a specific entry in the cache.<p>
635     *
636     * In case a found entry has a timeout set, it will be checked upon lookup.
637     * In case the timeout of the entry has been reached, it will be removed from
638     * the cache (and null will be returned in this case).<p>
639     *
640     * @param key The key to look for in the cache
641     * @return the entry found for the key, or null if key is not in the cache
642     */
643    CmsFlexCacheEntry get(CmsFlexRequestKey key) {
644
645        if (!isEnabled()) {
646            // cache is disabled
647            return null;
648        }
649        Object o = m_keyCache.get(key.getResource());
650        if (o != null) {
651            // found a matching key in the cache
652            CmsFlexCacheVariation v = (CmsFlexCacheVariation)o;
653            String variation = v.m_key.matchRequestKey(key);
654
655            if (CmsStringUtil.isEmpty(variation)) {
656                // requested resource is not cacheable
657                return null;
658            }
659            CmsFlexCacheEntry entry = (CmsFlexCacheEntry)v.m_map.get(variation);
660            if (entry == null) {
661                // no cache entry available for variation
662                return null;
663            }
664            if (entry.getDateExpires() < System.currentTimeMillis()) {
665                // cache entry avaiable but expired, remove entry
666                m_variationCache.remove(entry);
667                return null;
668            }
669            // return the found cache entry
670            return entry;
671        } else {
672            return null;
673        }
674    }
675
676    /**
677     * Returns the CmsFlexCacheKey data structure for a given resource name.<p>
678     *
679     * @param resource the resource name for which to look up the key for
680     * @return the CmsFlexCacheKey data structure found for the resource
681     */
682    CmsFlexCacheKey getKey(String resource) {
683
684        if (!isEnabled()) {
685            return null;
686        }
687        Object o = m_keyCache.get(resource);
688        if (o != null) {
689            if (LOG.isDebugEnabled()) {
690                LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_FOUND_1, resource));
691            }
692            return ((CmsFlexCacheVariation)o).m_key;
693        } else {
694            if (LOG.isDebugEnabled()) {
695                LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_NOT_FOUND_1, resource));
696            }
697            return null;
698        }
699    }
700
701    /**
702     * Checks if the cache is empty or if at last one element is contained.<p>
703     *
704     * @return true if the cache is empty, false otherwise
705     */
706    boolean isEmpty() {
707
708        if (!isEnabled()) {
709            return true;
710        }
711        return m_keyCache.isEmpty();
712    }
713
714    /**
715     * This method adds new entries to the cache.<p>
716     *
717     * The key describes the conditions under which the value can be cached.
718     * Usually the key belongs to the response.
719     * The variation describes the conditions under which the
720     * entry was created. This is usually calculated from the request.
721     * If the variation is != null, the entry is cachable.<p>
722     *
723     * @param key the key for the new value entry
724     * @param entry the CmsFlexCacheEntry to store in the cache
725     * @param variation the pre-calculated variation for the entry
726     * @param requestKey the request key from which the variation was determined
727     * @return true if the value was added to the cache, false otherwise
728     */
729    boolean put(CmsFlexCacheKey key, CmsFlexCacheEntry entry, String variation, CmsFlexRequestKey requestKey) {
730
731        if (!isEnabled()) {
732            return false;
733        }
734        if (LOG.isDebugEnabled()) {
735            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_ADD_ENTRY_1, key.getResource()));
736        }
737        if (variation != null) {
738            // This is a cachable result
739            if (LOG.isDebugEnabled()) {
740                LOG.debug(
741                    Messages.get().getBundle().key(
742                        Messages.LOG_FLEXCACHE_ADD_ENTRY_WITH_VARIATION_2,
743                        key.getResource(),
744                        variation));
745            }
746            put(key, entry, variation);
747            if (m_bucketConfiguration != null) {
748                try {
749                    List<String> paths = key.getPathsForBuckets(requestKey);
750                    if (paths.size() > 0) {
751                        BucketSet buckets = m_bucketConfiguration.getBucketSet(paths);
752                        entry.setBucketSet(buckets);
753                    } else {
754                        entry.setBucketSet(null); // bucket set of null means entries will be deleted for every publish job
755                    }
756                } catch (Exception e) {
757                    LOG.error(e.getLocalizedMessage(), e);
758                }
759            }
760            // Note that duplicates are NOT checked, it it assumed that this is done beforehand,
761            // while checking if the entry is already in the cache or not.
762            return true;
763        } else {
764            // Result is not cachable
765            if (LOG.isDebugEnabled()) {
766                LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_RESOURCE_NOT_CACHEABLE_0));
767            }
768            return false;
769        }
770    }
771
772    /**
773     * Adds a key with a new, empty variation map to the cache.<p>
774     *
775     * @param key the key to add to the cache.
776     */
777    void putKey(CmsFlexCacheKey key) {
778
779        if (!isEnabled()) {
780            return;
781        }
782        Object o = m_keyCache.get(key.getResource());
783        if (o == null) {
784            // No variation map for this resource yet, so create one
785            CmsFlexCacheVariation variationMap = new CmsFlexCacheVariation(key);
786            m_keyCache.put(key.getResource(), variationMap);
787            if (LOG.isDebugEnabled()) {
788                LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_ADD_KEY_1, key.getResource()));
789            }
790        }
791        // If != null the key is already in the cache, so we just do nothing
792    }
793
794    /**
795     * Empties the cache completely.<p>
796     */
797    private synchronized void clear() {
798
799        if (!isEnabled()) {
800            return;
801        }
802        m_keyCache.clear();
803        m_size = 0;
804
805        m_variationCache.clear();
806
807        if (LOG.isInfoEnabled()) {
808            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_0));
809        }
810    }
811
812    /**
813     * Internal method to perform cache clearance.<p>
814     *
815     * It clears "one half" of the cache, i.e. either
816     * the online or the offline parts.
817     * A parameter is used to indicate if only
818     * the entries or keys and entries are to be cleared.<p>
819     *
820     * @param suffix used to distinguish between "[Online]" and "[Offline]" entries
821     * @param entriesOnly if <code>true</code>, only entries will be cleared, otherwise
822     *         the entries and the keys will be cleared
823     */
824    private synchronized void clearAccordingToSuffix(String suffix, boolean entriesOnly) {
825
826        Set<String> keys = synchronizedCopyKeys(m_keyCache);
827        Iterator<String> i = keys.iterator();
828        while (i.hasNext()) {
829            String s = i.next();
830            if (s.endsWith(suffix)) {
831                CmsFlexCacheVariation v = m_keyCache.get(s);
832                if (entriesOnly) {
833                    // Clear only entry
834                    m_size -= v.m_map.size();
835                    Iterator<I_CmsLruCacheObject> allEntries = v.m_map.values().iterator();
836                    while (allEntries.hasNext()) {
837                        I_CmsLruCacheObject nextObject = allEntries.next();
838                        allEntries.remove();
839                        m_variationCache.remove(nextObject);
840                    }
841                    v.m_map = new Hashtable<String, I_CmsLruCacheObject>(INITIAL_CAPACITY_VARIATIONS);
842                } else {
843                    // Clear key and entry
844                    m_size -= v.m_map.size();
845                    Iterator<I_CmsLruCacheObject> allEntries = v.m_map.values().iterator();
846                    while (allEntries.hasNext()) {
847                        I_CmsLruCacheObject nextObject = allEntries.next();
848                        allEntries.remove();
849                        m_variationCache.remove(nextObject);
850                    }
851
852                    v.m_map = null;
853                    v.m_key = null;
854                    m_keyCache.remove(s);
855                }
856            }
857        }
858        if (LOG.isInfoEnabled()) {
859            LOG.info(
860                Messages.get().getBundle().key(
861                    Messages.LOG_FLEXCACHE_CLEAR_HALF_2,
862                    suffix,
863                    Boolean.valueOf(entriesOnly)));
864        }
865    }
866
867    /**
868     * Clears the Flex cache buckets matching the given publish list.<p>
869     *
870     * @param bucketConfig the bucket configuration to be used for checking which flex cache entry should be purged
871     * @param publishId the publish id
872     * @param publishedResources the published resources
873     *
874     * @return true if the flex buckets could be cleared successfully (if this returns false, the flex cache should fall back to the old behavior, i.e. clearing everything)
875     */
876    private boolean clearBucketsForPublishList(
877        CmsFlexBucketConfiguration bucketConfig,
878        CmsUUID publishId,
879        List<CmsPublishedResource> publishedResources) {
880
881        long startTime = System.currentTimeMillis();
882        String p = "[" + publishId + "] "; // Prefix for log messages
883        try {
884
885            LOG.debug(p + "Trying bucket-based flex entry cleanup");
886            if (bucketConfig.shouldClearAll(publishedResources)) {
887                LOG.info(p + "Clearing Flex cache completely based on Flex bucket configuration.");
888                return false;
889            } else {
890                long totalEntries = 0;
891                long removedEntries = 0;
892                List<String> paths = Lists.newArrayList();
893                for (CmsPublishedResource pubRes : publishedResources) {
894                    paths.add(pubRes.getRootPath());
895                    LOG.info(p + "Published resource: " + pubRes.getRootPath());
896                }
897                BucketSet publishListBucketSet = bucketConfig.getBucketSet(paths);
898                if (LOG.isInfoEnabled()) {
899                    LOG.info(p + "Flex cache buckets for publish list: " + publishListBucketSet.toString());
900                }
901                synchronized (this) {
902                    List<CmsFlexCacheEntry> entriesToDelete = Lists.newArrayList();
903                    for (Map.Entry<String, CmsFlexCacheVariation> entry : synchronizedCopyMap(m_keyCache).entrySet()) {
904                        CmsFlexCacheVariation variation = entry.getValue();
905                        if (LOG.isDebugEnabled()) {
906                            LOG.debug(p + "Processing entries for " + entry.getKey());
907                        }
908                        entriesToDelete.clear();
909
910                        for (Map.Entry<String, I_CmsLruCacheObject> variationEntry : synchronizedCopyMap(
911                            variation.m_map).entrySet()) {
912                            CmsFlexCacheEntry flexEntry = (CmsFlexCacheEntry)(variationEntry.getValue());
913                            totalEntries += 1;
914                            BucketSet entryBucketSet = flexEntry.getBucketSet();
915                            if (publishListBucketSet.matchForDeletion(entryBucketSet)) {
916                                entriesToDelete.add(flexEntry);
917                                if (LOG.isInfoEnabled()) {
918                                    LOG.info(p + "Match: " + variationEntry.getKey());
919                                }
920                            } else {
921                                if (LOG.isDebugEnabled()) {
922                                    LOG.debug(p + "No match: " + variationEntry.getKey());
923                                }
924                            }
925                        }
926                        for (CmsFlexCacheEntry entryToDelete : entriesToDelete) {
927                            m_variationCache.remove(entryToDelete);
928                            removedEntries += 1;
929                        }
930                    }
931                    long endTime = System.currentTimeMillis();
932                    LOG.info(
933                        p
934                            + "Removed "
935                            + removedEntries
936                            + " of "
937                            + totalEntries
938                            + " Flex cache entries, took "
939                            + (endTime - startTime)
940                            + " milliseconds");
941                    return true;
942                }
943            }
944        } catch (Exception e) {
945            LOG.error(p + "Exception while trying to selectively purge flex cache: " + e.getLocalizedMessage(), e);
946            return false;
947        }
948    }
949
950    /**
951     * Clears all entries in the cache, online or offline.<p>
952     *
953     * The keys are not cleared.<p>
954     *
955     * Only users with administrator permissions are allowed
956     * to perform this operation.<p>
957     */
958    private synchronized void clearEntries() {
959
960        if (!isEnabled()) {
961            return;
962        }
963        if (LOG.isInfoEnabled()) {
964            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_ALL_0));
965        }
966        // create new set to avoid ConcurrentModificationExceptions
967        Set<String> cacheKeys = synchronizedCopyKeys(m_keyCache);
968        Iterator<String> i = cacheKeys.iterator();
969        while (i.hasNext()) {
970            CmsFlexCacheVariation v = m_keyCache.get(i.next());
971            Iterator<I_CmsLruCacheObject> allEntries = v.m_map.values().iterator();
972            while (allEntries.hasNext()) {
973                I_CmsLruCacheObject nextObject = allEntries.next();
974                allEntries.remove();
975                m_variationCache.remove(nextObject);
976            }
977            v.m_map = new Hashtable<String, I_CmsLruCacheObject>(INITIAL_CAPACITY_VARIATIONS);
978        }
979        m_size = 0;
980    }
981
982    /**
983     * Clears all entries and all keys from offline projects in the cache.<p>
984     *
985     * Cached resources from the online project are not touched.<p>
986     *
987     * Only users with administrator permissions are allowed
988     * to perform this operation.<p>
989     */
990    private void clearOffline() {
991
992        if (!isEnabled()) {
993            return;
994        }
995        if (LOG.isInfoEnabled()) {
996            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_KEYS_AND_ENTRIES_0));
997        }
998        clearAccordingToSuffix(CACHE_OFFLINESUFFIX, false);
999    }
1000
1001    /**
1002     * Clears all entries from offline projects in the cache.<p>
1003     *
1004     * The keys from the offline projects are not cleared.
1005     * Cached resources from the online project are not touched.<p>
1006     *
1007     * Only users with administrator permissions are allowed
1008     * to perform this operation.<p>
1009     */
1010    private void clearOfflineEntries() {
1011
1012        if (!isEnabled()) {
1013            return;
1014        }
1015        if (LOG.isInfoEnabled()) {
1016            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_OFFLINE_ENTRIES_0));
1017        }
1018        clearAccordingToSuffix(CACHE_OFFLINESUFFIX, true);
1019    }
1020
1021    /**
1022     * Clears all entries and all keys from the online project in the cache.<p>
1023     *
1024     * Cached resources from the offline projects are not touched.<p>
1025     *
1026     * Only users with administrator permissions are allowed
1027     * to perform this operation.<p>
1028     */
1029    private void clearOnline() {
1030
1031        if (!isEnabled()) {
1032            return;
1033        }
1034        if (LOG.isInfoEnabled()) {
1035            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_ONLINE_KEYS_AND_ENTRIES_0));
1036        }
1037        clearAccordingToSuffix(CACHE_ONLINESUFFIX, false);
1038    }
1039
1040    /**
1041     * Clears all entries from the online project in the cache.<p>
1042     *
1043     * The keys from the online project are not cleared.
1044     * Cached resources from the offline projects are not touched.<p>
1045     *
1046     * Only users with administrator permissions are allowed
1047     * to perform this operation.<p>
1048     */
1049    private void clearOnlineEntries() {
1050
1051        if (!isEnabled()) {
1052            return;
1053        }
1054        if (LOG.isInfoEnabled()) {
1055            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_ONLINE_ENTRIES_0));
1056        }
1057        clearAccordingToSuffix(CACHE_ONLINESUFFIX, true);
1058    }
1059
1060    /**
1061     * This method purges the JSP repository dirs,
1062     * i.e. it deletes all JSP files that OpenCms has written to the
1063     * real FS.<p>
1064     *
1065     * Obviously this method must be used with caution.
1066     * Purpose of this method is to allow
1067     * a complete purge of all JSP pages on a machine after
1068     * a major update of JSP templates was made.<p>
1069     */
1070    private synchronized void purgeJspRepository() {
1071
1072        CmsJspLoader cmsJspLoader = (CmsJspLoader)OpenCms.getResourceManager().getLoader(
1073            CmsJspLoader.RESOURCE_LOADER_ID);
1074
1075        cmsJspLoader.triggerPurge(new Runnable() {
1076
1077            @SuppressWarnings("synthetic-access")
1078            public void run() {
1079
1080                clear();
1081            }
1082        });
1083    }
1084
1085    /**
1086     * Save a value to the cache.<p>
1087     *
1088     * @param key the key under which the value is saved
1089     * @param theCacheEntry the entry to cache
1090     * @param variation the variation string
1091     */
1092    private void put(CmsFlexCacheKey key, CmsFlexCacheEntry theCacheEntry, String variation) {
1093
1094        CmsFlexCacheVariation o = m_keyCache.get(key.getResource());
1095        if (o != null) {
1096            // We already have a variation map for this resource
1097            Map<String, I_CmsLruCacheObject> m = o.m_map;
1098            boolean wasAdded = true;
1099            if (!m.containsKey(variation)) {
1100                wasAdded = m_variationCache.add(theCacheEntry);
1101            } else {
1102                wasAdded = m_variationCache.touch(theCacheEntry);
1103            }
1104
1105            if (wasAdded) {
1106                theCacheEntry.setVariationData(variation, m);
1107                m.put(variation, theCacheEntry);
1108            }
1109        } else {
1110            // No variation map for this resource yet, so create one
1111            CmsFlexCacheVariation list = new CmsFlexCacheVariation(key);
1112
1113            boolean wasAdded = m_variationCache.add(theCacheEntry);
1114
1115            if (wasAdded) {
1116                theCacheEntry.setVariationData(variation, list.m_map);
1117                list.m_map.put(variation, theCacheEntry);
1118                m_keyCache.put(key.getResource(), list);
1119            }
1120        }
1121
1122        if (LOG.isDebugEnabled()) {
1123            LOG.debug(
1124                Messages.get().getBundle().key(
1125                    Messages.LOG_FLEXCACHE_ADDED_ENTRY_FOR_RESOURCE_WITH_VARIATION_3,
1126                    Integer.valueOf(m_size),
1127                    key.getResource(),
1128                    variation));
1129            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_ADDED_ENTRY_1, theCacheEntry.toString()));
1130        }
1131    }
1132}