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                    break;
435                }
436                int i = it.intValue();
437                switch (i) {
438                    case CLEAR_ALL:
439                        clear();
440                        break;
441                    case CLEAR_ENTRIES:
442                        clearEntries();
443                        break;
444                    case CLEAR_ONLINE_ALL:
445                        clearOnline();
446                        break;
447                    case CLEAR_ONLINE_ENTRIES:
448                        clearOnlineEntries();
449                        break;
450                    case CLEAR_OFFLINE_ALL:
451                        clearOffline();
452                        break;
453                    case CLEAR_OFFLINE_ENTRIES:
454                        clearOfflineEntries();
455                        break;
456                    default:
457                        // no operation
458                }
459                break;
460            default:
461                // no operation
462        }
463    }
464
465    /**
466     * Dumps keys and variations to a string buffer, for debug purposes.<p>
467     *
468     * @param buffer the buffer to which the key information should be written
469     */
470    public void dumpKeys(StringBuffer buffer) {
471
472        synchronized (this) {
473            for (Map.Entry<String, CmsFlexCacheVariation> entry : synchronizedCopyMap(m_keyCache).entrySet()) {
474                String key = entry.getKey();
475                CmsFlexCacheVariation variations = entry.getValue();
476                Map<String, I_CmsLruCacheObject> variationMap = variations.m_map;
477                for (Map.Entry<String, I_CmsLruCacheObject> varEntry : variationMap.entrySet()) {
478                    String varKey = varEntry.getKey();
479                    I_CmsLruCacheObject value = varEntry.getValue();
480                    buffer.append(key + " VAR " + varKey + "\n");
481                    if (value instanceof CmsFlexCacheEntry) {
482                        CmsFlexCacheEntry singleCacheEntry = (CmsFlexCacheEntry)value;
483                        BucketSet buckets = singleCacheEntry.getBucketSet();
484                        if (buckets != null) {
485                            buffer.append("buckets = " + buckets.toString() + "\n");
486                        }
487                    }
488                }
489            }
490        }
491    }
492
493    /**
494     * Returns the CmsFlexCacheKey data structure for a given
495     * key (i.e. resource name).<p>
496     *
497     * Useful if you want to show the cache key for a resources,
498     * like on the FlexCache administration page.<p>
499     *
500     * Only users with administrator permissions are allowed
501     * to perform this operation.<p>
502     *
503     * @param key the resource name for which to look up the variation for
504     * @param cms the CmsObject used for user authorization
505     * @return the CmsFlexCacheKey data structure found for the resource
506     */
507    public CmsFlexCacheKey getCachedKey(String key, CmsObject cms) {
508
509        if (!isEnabled() || !OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_MANAGER)) {
510            return null;
511        }
512        Object o = m_keyCache.get(key);
513        if (o != null) {
514            return ((CmsFlexCacheVariation)o).m_key;
515        }
516        return null;
517    }
518
519    /**
520     * Returns a set of all cached resource names.<p>
521     *
522     * Useful if you want to show a list of all cached resources,
523     * like on the FlexCache administration page.<p>
524     *
525     * Only users with administrator permissions are allowed
526     * to perform this operation.<p>
527     *
528     * @param cms the CmsObject used for user authorization
529     * @return a Set of cached resource names (which are of type String)
530     */
531    public Set<String> getCachedResources(CmsObject cms) {
532
533        if (!isEnabled() || !OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_MANAGER)) {
534            return null;
535        }
536        return synchronizedCopyKeys(m_keyCache);
537    }
538
539    /**
540     * Returns all variations in the cache for a given resource name.
541     * The variations are of type String.<p>
542     *
543     * Useful if you want to show a list of all cached entry - variations,
544     * like on the FlexCache administration page.<p>
545     *
546     * Only users with administrator permissions are allowed
547     * to perform this operation.<p>
548     *
549     * @param key the resource name for which to look up the variations for
550     * @param cms the CmsObject used for user authorization
551     * @return a Set of cached variations (which are of type String)
552     */
553    public Set<String> getCachedVariations(String key, CmsObject cms) {
554
555        if (!isEnabled() || !OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_MANAGER)) {
556            return null;
557        }
558        Object o = m_keyCache.get(key);
559        if (o != null) {
560            return synchronizedCopyKeys(((CmsFlexCacheVariation)o).m_map);
561        }
562        return null;
563    }
564
565    /**
566     * Returns the LRU cache where the CacheEntries are cached.<p>
567     *
568     * @return the LRU cache where the CacheEntries are cached
569     */
570    public CmsLruCache getEntryLruCache() {
571
572        return m_variationCache;
573    }
574
575    /**
576     * Initializes the flex cache.<p>
577     *
578     * @param adminCms a CMS context with admin privileges
579     */
580    public void initializeCms(CmsObject adminCms) {
581
582        try {
583            m_cmsObject = adminCms;
584            try {
585                String path = CONFIG_PATH;
586                if (m_cmsObject.existsResource(path)) {
587                    LOG.info("Flex configuration found at " + CONFIG_PATH + ", initializing...");
588                    m_bucketConfiguration = CmsFlexBucketConfiguration.loadFromVfsFile(m_cmsObject, path);
589                }
590            } catch (Exception e) {
591                LOG.error(e.getLocalizedMessage(), e);
592            }
593        } catch (Exception e) {
594            LOG.error(e.getLocalizedMessage(), e);
595        }
596    }
597
598    /**
599     * Indicates if the cache is enabled (i.e. actually
600     * caching entries) or not.<p>
601     *
602     * @return true if the cache is enabled, false if not
603     */
604    public boolean isEnabled() {
605
606        return m_enabled;
607    }
608
609    /**
610     * Returns the total number of cached resource keys.
611     *
612     * @return the number of resource keys in the cache
613     */
614    public int keySize() {
615
616        if (!isEnabled()) {
617            return 0;
618        }
619        return m_keyCache.size();
620    }
621
622    /**
623     * Returns the total number of entries in the cache.<p>
624     *
625     * @return the number of entries in the cache
626     */
627    public int size() {
628
629        return m_variationCache.size();
630    }
631
632    /**
633     * Looks up a specific entry in the cache.<p>
634     *
635     * In case a found entry has a timeout set, it will be checked upon lookup.
636     * In case the timeout of the entry has been reached, it will be removed from
637     * the cache (and null will be returned in this case).<p>
638     *
639     * @param key The key to look for in the cache
640     * @return the entry found for the key, or null if key is not in the cache
641     */
642    CmsFlexCacheEntry get(CmsFlexRequestKey key) {
643
644        if (!isEnabled()) {
645            // cache is disabled
646            return null;
647        }
648        Object o = m_keyCache.get(key.getResource());
649        if (o != null) {
650            // found a matching key in the cache
651            CmsFlexCacheVariation v = (CmsFlexCacheVariation)o;
652            String variation = v.m_key.matchRequestKey(key);
653
654            if (CmsStringUtil.isEmpty(variation)) {
655                // requested resource is not cacheable
656                return null;
657            }
658            CmsFlexCacheEntry entry = (CmsFlexCacheEntry)v.m_map.get(variation);
659            if (entry == null) {
660                // no cache entry available for variation
661                return null;
662            }
663            if (entry.getDateExpires() < System.currentTimeMillis()) {
664                // cache entry avaiable but expired, remove entry
665                m_variationCache.remove(entry);
666                return null;
667            }
668            // return the found cache entry
669            return entry;
670        } else {
671            return null;
672        }
673    }
674
675    /**
676     * Returns the CmsFlexCacheKey data structure for a given resource name.<p>
677     *
678     * @param resource the resource name for which to look up the key for
679     * @return the CmsFlexCacheKey data structure found for the resource
680     */
681    CmsFlexCacheKey getKey(String resource) {
682
683        if (!isEnabled()) {
684            return null;
685        }
686        Object o = m_keyCache.get(resource);
687        if (o != null) {
688            if (LOG.isDebugEnabled()) {
689                LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_FOUND_1, resource));
690            }
691            return ((CmsFlexCacheVariation)o).m_key;
692        } else {
693            if (LOG.isDebugEnabled()) {
694                LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_NOT_FOUND_1, resource));
695            }
696            return null;
697        }
698    }
699
700    /**
701     * Checks if the cache is empty or if at last one element is contained.<p>
702     *
703     * @return true if the cache is empty, false otherwise
704     */
705    boolean isEmpty() {
706
707        if (!isEnabled()) {
708            return true;
709        }
710        return m_keyCache.isEmpty();
711    }
712
713    /**
714     * This method adds new entries to the cache.<p>
715     *
716     * The key describes the conditions under which the value can be cached.
717     * Usually the key belongs to the response.
718     * The variation describes the conditions under which the
719     * entry was created. This is usually calculated from the request.
720     * If the variation is != null, the entry is cachable.<p>
721     *
722     * @param key the key for the new value entry
723     * @param entry the CmsFlexCacheEntry to store in the cache
724     * @param variation the pre-calculated variation for the entry
725     * @param requestKey the request key from which the variation was determined
726     * @return true if the value was added to the cache, false otherwise
727     */
728    boolean put(CmsFlexCacheKey key, CmsFlexCacheEntry entry, String variation, CmsFlexRequestKey requestKey) {
729
730        if (!isEnabled()) {
731            return false;
732        }
733        if (LOG.isDebugEnabled()) {
734            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_ADD_ENTRY_1, key.getResource()));
735        }
736        if (variation != null) {
737            // This is a cachable result
738            key.setVariation(variation);
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                        key.getVariation()));
745            }
746            put(key, entry);
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     * Removes an entry from the cache.<p>
796     *
797     * @param key the key which describes the entry to remove from the cache
798     */
799    void remove(CmsFlexCacheKey key) {
800
801        if (!isEnabled()) {
802            return;
803        }
804        CmsFlexCacheVariation o = m_keyCache.get(key.getResource());
805        if (o != null) {
806            I_CmsLruCacheObject old = o.m_map.get(key.getVariation());
807            if (old != null) {
808                getEntryLruCache().remove(old);
809            }
810        }
811    }
812
813    /**
814     * Empties the cache completely.<p>
815     */
816    private synchronized void clear() {
817
818        if (!isEnabled()) {
819            return;
820        }
821        m_keyCache.clear();
822        m_size = 0;
823
824        m_variationCache.clear();
825
826        if (LOG.isInfoEnabled()) {
827            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_0));
828        }
829    }
830
831    /**
832     * Internal method to perform cache clearance.<p>
833     *
834     * It clears "one half" of the cache, i.e. either
835     * the online or the offline parts.
836     * A parameter is used to indicate if only
837     * the entries or keys and entries are to be cleared.<p>
838     *
839     * @param suffix used to distinguish between "[Online]" and "[Offline]" entries
840     * @param entriesOnly if <code>true</code>, only entries will be cleared, otherwise
841     *         the entries and the keys will be cleared
842     */
843    private synchronized void clearAccordingToSuffix(String suffix, boolean entriesOnly) {
844
845        Set<String> keys = synchronizedCopyKeys(m_keyCache);
846        Iterator<String> i = keys.iterator();
847        while (i.hasNext()) {
848            String s = i.next();
849            if (s.endsWith(suffix)) {
850                CmsFlexCacheVariation v = m_keyCache.get(s);
851                if (entriesOnly) {
852                    // Clear only entry
853                    m_size -= v.m_map.size();
854                    Iterator<I_CmsLruCacheObject> allEntries = v.m_map.values().iterator();
855                    while (allEntries.hasNext()) {
856                        I_CmsLruCacheObject nextObject = allEntries.next();
857                        allEntries.remove();
858                        m_variationCache.remove(nextObject);
859                    }
860                    v.m_map = new Hashtable<String, I_CmsLruCacheObject>(INITIAL_CAPACITY_VARIATIONS);
861                } else {
862                    // Clear key and entry
863                    m_size -= v.m_map.size();
864                    Iterator<I_CmsLruCacheObject> allEntries = v.m_map.values().iterator();
865                    while (allEntries.hasNext()) {
866                        I_CmsLruCacheObject nextObject = allEntries.next();
867                        allEntries.remove();
868                        m_variationCache.remove(nextObject);
869                    }
870
871                    v.m_map = null;
872                    v.m_key = null;
873                    m_keyCache.remove(s);
874                }
875            }
876        }
877        if (LOG.isInfoEnabled()) {
878            LOG.info(
879                Messages.get().getBundle().key(
880                    Messages.LOG_FLEXCACHE_CLEAR_HALF_2,
881                    suffix,
882                    Boolean.valueOf(entriesOnly)));
883        }
884    }
885
886    /**
887     * Clears the Flex cache buckets matching the given publish list.<p>
888     *
889     * @param bucketConfig the bucket configuration to be used for checking which flex cache entry should be purged
890     * @param publishId the publish id
891     * @param publishedResources the published resources
892     *
893     * @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)
894     */
895    private boolean clearBucketsForPublishList(
896        CmsFlexBucketConfiguration bucketConfig,
897        CmsUUID publishId,
898        List<CmsPublishedResource> publishedResources) {
899
900        long startTime = System.currentTimeMillis();
901        String p = "[" + publishId + "] "; // Prefix for log messages
902        try {
903
904            LOG.debug(p + "Trying bucket-based flex entry cleanup");
905            if (bucketConfig.shouldClearAll(publishedResources)) {
906                LOG.info(p + "Clearing Flex cache completely based on Flex bucket configuration.");
907                return false;
908            } else {
909                long totalEntries = 0;
910                long removedEntries = 0;
911                List<String> paths = Lists.newArrayList();
912                for (CmsPublishedResource pubRes : publishedResources) {
913                    paths.add(pubRes.getRootPath());
914                    LOG.info(p + "Published resource: " + pubRes.getRootPath());
915                }
916                BucketSet publishListBucketSet = bucketConfig.getBucketSet(paths);
917                if (LOG.isInfoEnabled()) {
918                    LOG.info(p + "Flex cache buckets for publish list: " + publishListBucketSet.toString());
919                }
920                synchronized (this) {
921                    List<CmsFlexCacheEntry> entriesToDelete = Lists.newArrayList();
922                    for (Map.Entry<String, CmsFlexCacheVariation> entry : synchronizedCopyMap(m_keyCache).entrySet()) {
923                        CmsFlexCacheVariation variation = entry.getValue();
924                        if (LOG.isDebugEnabled()) {
925                            LOG.debug(p + "Processing entries for " + entry.getKey());
926                        }
927                        entriesToDelete.clear();
928
929                        for (Map.Entry<String, I_CmsLruCacheObject> variationEntry : synchronizedCopyMap(
930                            variation.m_map).entrySet()) {
931                            CmsFlexCacheEntry flexEntry = (CmsFlexCacheEntry)(variationEntry.getValue());
932                            totalEntries += 1;
933                            BucketSet entryBucketSet = flexEntry.getBucketSet();
934                            if (publishListBucketSet.matchForDeletion(entryBucketSet)) {
935                                entriesToDelete.add(flexEntry);
936                                if (LOG.isInfoEnabled()) {
937                                    LOG.info(p + "Match: " + variationEntry.getKey());
938                                }
939                            } else {
940                                if (LOG.isDebugEnabled()) {
941                                    LOG.debug(p + "No match: " + variationEntry.getKey());
942                                }
943                            }
944                        }
945                        for (CmsFlexCacheEntry entryToDelete : entriesToDelete) {
946                            m_variationCache.remove(entryToDelete);
947                            removedEntries += 1;
948                        }
949                    }
950                    long endTime = System.currentTimeMillis();
951                    LOG.info(
952                        p
953                            + "Removed "
954                            + removedEntries
955                            + " of "
956                            + totalEntries
957                            + " Flex cache entries, took "
958                            + (endTime - startTime)
959                            + " milliseconds");
960                    return true;
961                }
962            }
963        } catch (Exception e) {
964            LOG.error(p + "Exception while trying to selectively purge flex cache: " + e.getLocalizedMessage(), e);
965            return false;
966        }
967    }
968
969    /**
970     * Clears all entries in the cache, online or offline.<p>
971     *
972     * The keys are not cleared.<p>
973     *
974     * Only users with administrator permissions are allowed
975     * to perform this operation.<p>
976     */
977    private synchronized void clearEntries() {
978
979        if (!isEnabled()) {
980            return;
981        }
982        if (LOG.isInfoEnabled()) {
983            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_ALL_0));
984        }
985        // create new set to avoid ConcurrentModificationExceptions
986        Set<String> cacheKeys = synchronizedCopyKeys(m_keyCache);
987        Iterator<String> i = cacheKeys.iterator();
988        while (i.hasNext()) {
989            CmsFlexCacheVariation v = m_keyCache.get(i.next());
990            Iterator<I_CmsLruCacheObject> allEntries = v.m_map.values().iterator();
991            while (allEntries.hasNext()) {
992                I_CmsLruCacheObject nextObject = allEntries.next();
993                allEntries.remove();
994                m_variationCache.remove(nextObject);
995            }
996            v.m_map = new Hashtable<String, I_CmsLruCacheObject>(INITIAL_CAPACITY_VARIATIONS);
997        }
998        m_size = 0;
999    }
1000
1001    /**
1002     * Clears all entries and all keys from offline projects in the cache.<p>
1003     *
1004     * Cached resources from the online project are not touched.<p>
1005     *
1006     * Only users with administrator permissions are allowed
1007     * to perform this operation.<p>
1008     */
1009    private void clearOffline() {
1010
1011        if (!isEnabled()) {
1012            return;
1013        }
1014        if (LOG.isInfoEnabled()) {
1015            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_KEYS_AND_ENTRIES_0));
1016        }
1017        clearAccordingToSuffix(CACHE_OFFLINESUFFIX, false);
1018    }
1019
1020    /**
1021     * Clears all entries from offline projects in the cache.<p>
1022     *
1023     * The keys from the offline projects are not cleared.
1024     * Cached resources from the online project are not touched.<p>
1025     *
1026     * Only users with administrator permissions are allowed
1027     * to perform this operation.<p>
1028     */
1029    private void clearOfflineEntries() {
1030
1031        if (!isEnabled()) {
1032            return;
1033        }
1034        if (LOG.isInfoEnabled()) {
1035            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_OFFLINE_ENTRIES_0));
1036        }
1037        clearAccordingToSuffix(CACHE_OFFLINESUFFIX, true);
1038    }
1039
1040    /**
1041     * Clears all entries and all keys from the online project in the cache.<p>
1042     *
1043     * Cached resources from the offline projects are not touched.<p>
1044     *
1045     * Only users with administrator permissions are allowed
1046     * to perform this operation.<p>
1047     */
1048    private void clearOnline() {
1049
1050        if (!isEnabled()) {
1051            return;
1052        }
1053        if (LOG.isInfoEnabled()) {
1054            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_ONLINE_KEYS_AND_ENTRIES_0));
1055        }
1056        clearAccordingToSuffix(CACHE_ONLINESUFFIX, false);
1057    }
1058
1059    /**
1060     * Clears all entries from the online project in the cache.<p>
1061     *
1062     * The keys from the online project are not cleared.
1063     * Cached resources from the offline projects are not touched.<p>
1064     *
1065     * Only users with administrator permissions are allowed
1066     * to perform this operation.<p>
1067     */
1068    private void clearOnlineEntries() {
1069
1070        if (!isEnabled()) {
1071            return;
1072        }
1073        if (LOG.isInfoEnabled()) {
1074            LOG.info(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_CLEAR_ONLINE_ENTRIES_0));
1075        }
1076        clearAccordingToSuffix(CACHE_ONLINESUFFIX, true);
1077    }
1078
1079    /**
1080     * This method purges the JSP repository dirs,
1081     * i.e. it deletes all JSP files that OpenCms has written to the
1082     * real FS.<p>
1083     *
1084     * Obviously this method must be used with caution.
1085     * Purpose of this method is to allow
1086     * a complete purge of all JSP pages on a machine after
1087     * a major update of JSP templates was made.<p>
1088     */
1089    private synchronized void purgeJspRepository() {
1090
1091        CmsJspLoader cmsJspLoader = (CmsJspLoader)OpenCms.getResourceManager().getLoader(
1092            CmsJspLoader.RESOURCE_LOADER_ID);
1093
1094        cmsJspLoader.triggerPurge(new Runnable() {
1095
1096            @SuppressWarnings("synthetic-access")
1097            public void run() {
1098
1099                clear();
1100            }
1101        });
1102    }
1103
1104    /**
1105     * Save a value to the cache.<p>
1106     *
1107     * @param key the key under which the value is saved
1108     * @param theCacheEntry the entry to cache
1109     */
1110    private void put(CmsFlexCacheKey key, CmsFlexCacheEntry theCacheEntry) {
1111
1112        CmsFlexCacheVariation o = m_keyCache.get(key.getResource());
1113        if (key.getTimeout() > 0) {
1114            theCacheEntry.setDateExpiresToNextTimeout(key.getTimeout());
1115        }
1116        if (o != null) {
1117            // We already have a variation map for this resource
1118            Map<String, I_CmsLruCacheObject> m = o.m_map;
1119            boolean wasAdded = true;
1120            if (!m.containsKey(key.getVariation())) {
1121                wasAdded = m_variationCache.add(theCacheEntry);
1122            } else {
1123                wasAdded = m_variationCache.touch(theCacheEntry);
1124            }
1125
1126            if (wasAdded) {
1127                theCacheEntry.setVariationData(key.getVariation(), m);
1128                m.put(key.getVariation(), theCacheEntry);
1129            }
1130        } else {
1131            // No variation map for this resource yet, so create one
1132            CmsFlexCacheVariation list = new CmsFlexCacheVariation(key);
1133
1134            boolean wasAdded = m_variationCache.add(theCacheEntry);
1135
1136            if (wasAdded) {
1137                theCacheEntry.setVariationData(key.getVariation(), list.m_map);
1138                list.m_map.put(key.getVariation(), theCacheEntry);
1139                m_keyCache.put(key.getResource(), list);
1140            }
1141        }
1142
1143        if (LOG.isDebugEnabled()) {
1144            LOG.debug(
1145                Messages.get().getBundle().key(
1146                    Messages.LOG_FLEXCACHE_ADDED_ENTRY_FOR_RESOURCE_WITH_VARIATION_3,
1147                    new Integer(m_size),
1148                    key.getResource(),
1149                    key.getVariation()));
1150            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHE_ADDED_ENTRY_1, theCacheEntry.toString()));
1151        }
1152    }
1153}