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