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.I_CmsLruCacheObject;
031import org.opencms.file.CmsResource;
032import org.opencms.flex.CmsFlexBucketConfiguration.BucketSet;
033import org.opencms.i18n.CmsMessageContainer;
034import org.opencms.jsp.util.CmsJspStandardContextBean;
035import org.opencms.main.CmsLog;
036import org.opencms.monitor.CmsMemoryMonitor;
037import org.opencms.monitor.I_CmsMemoryMonitorable;
038import org.opencms.util.CmsCollectionsGenericWrapper;
039
040import java.io.IOException;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.HashMap;
044import java.util.Iterator;
045import java.util.List;
046import java.util.Map;
047import java.util.Map.Entry;
048
049import javax.servlet.ServletException;
050
051import org.apache.commons.lang3.ObjectUtils;
052import org.apache.commons.logging.Log;
053
054/**
055 * Contains the contents of a cached resource.<p>
056 *
057 * It is basically a list of pre-generated output,
058 * include() calls to other resources (with request parameters) and http headers that this
059 * resource requires to be set.<p>
060 *
061 * A CmsFlexCacheEntry might also describe a redirect-call, but in this case
062 * nothing else will be cached.<p>
063 *
064 * The pre-generated output is saved in <code>byte[]</code> arrays.
065 * The include() calls are saved as Strings of the included resource name,
066 * the parameters for the calls are saved in a HashMap.
067 * The headers are saved in a HashMap.
068 * In case of a redirect, the redirect target is cached in a String.<p>
069 *
070 * The CmsFlexCacheEntry can also have an expire date value, which indicates the time
071 * that his entry will become invalid and should thus be cleared from the cache.<p>
072 *
073 * @since 6.0.0
074 *
075 * @see org.opencms.cache.I_CmsLruCacheObject
076 */
077public class CmsFlexCacheEntry implements I_CmsLruCacheObject, I_CmsMemoryMonitorable {
078
079    /** Initial size for lists. */
080    public static final int INITIAL_CAPACITY_LISTS = 10;
081
082    /** The log object for this class. */
083    private static final Log LOG = CmsLog.getLog(CmsFlexCacheEntry.class);
084
085    /** the assigned bucket set for this flex entry (may be null). */
086    private BucketSet m_bucketSet;
087
088    /** The CacheEntry's size in bytes. */
089    private int m_byteSize;
090
091    /** Indicates if this cache entry is completed. */
092    private boolean m_completed;
093
094    /** The "expires" date for this Flex cache entry. */
095    private long m_dateExpires;
096
097    /** The "last modified" date for this Flex cache entry. */
098    private long m_dateLastModified;
099
100    /** The list of items for this resource. */
101    private List<Object> m_elements;
102
103    /** A Map of cached headers for this resource. */
104    private Map<String, List<String>> m_headers;
105
106    /** Pointer to the next cache entry in the LRU cache. */
107    private I_CmsLruCacheObject m_next;
108
109    /** Pointer to the previous cache entry in the LRU cache. */
110    private I_CmsLruCacheObject m_previous;
111
112    /** Flag which indicates whether a cached redirect is permanent. */
113    private boolean m_redirectPermanent;
114
115    /** A redirection target (if redirection is set). */
116    private String m_redirectTarget;
117
118    /** The key under which this cache entry is stored in the variation map. */
119    private String m_variationKey;
120
121    /** The variation map where this cache entry is stored. */
122    private Map<String, I_CmsLruCacheObject> m_variationMap;
123
124    /**
125     * Constructor for class CmsFlexCacheEntry.<p>
126     *
127     * The way to use this class is to first use this empty constructor
128     * and later add data with the various add methods.
129     */
130    public CmsFlexCacheEntry() {
131
132        m_elements = new ArrayList<Object>(INITIAL_CAPACITY_LISTS);
133        m_dateExpires = CmsResource.DATE_EXPIRED_DEFAULT;
134        m_dateLastModified = -1;
135        // base memory footprint of this object with all referenced objects
136        m_byteSize = 1024;
137
138        setNextLruObject(null);
139        setPreviousLruObject(null);
140    }
141
142    /**
143     * Adds an array of bytes to this cache entry,
144     * this will usually be the result of some kind of output - stream.<p>
145     *
146     * @param bytes the output to save in the cache
147     */
148    public void add(byte[] bytes) {
149
150        if (m_completed) {
151            return;
152        }
153        if (m_redirectTarget == null) {
154            // Add only if not already redirected
155            m_elements.add(bytes);
156            m_byteSize += CmsMemoryMonitor.getMemorySize(bytes);
157        }
158    }
159
160    /**
161     * Add an include - call target resource to this cache entry.<p>
162     *
163     * @param resource a name of a resource in the OpenCms VFS
164     * @param parameters a map of parameters specific to this include call
165     * @param attrs a map of request attributes specific to this include call
166     */
167    public void add(String resource, Map<String, String[]> parameters, Map<String, Object> attrs) {
168
169        if (m_completed) {
170            return;
171        }
172        if (m_redirectTarget == null) {
173            // Add only if not already redirected
174            m_elements.add(resource);
175            m_byteSize += CmsMemoryMonitor.getMemorySize(resource);
176            if (parameters == null) {
177                parameters = Collections.emptyMap();
178            }
179            m_elements.add(parameters);
180            m_byteSize += CmsMemoryMonitor.getValueSize(parameters);
181            if (attrs == null) {
182                attrs = Collections.emptyMap();
183            }
184            m_elements.add(attrs);
185            m_byteSize += CmsMemoryMonitor.getValueSize(attrs);
186        }
187    }
188
189    /**
190     * Add a map of headers to this cache entry,
191     * which are usually collected in the class CmsFlexResponse first.<p>
192     *
193     * @param headers the map of headers to add to the entry
194     */
195    public void addHeaders(Map<String, List<String>> headers) {
196
197        if (m_completed) {
198            return;
199        }
200        m_headers = headers;
201
202        Iterator<String> allHeaders = m_headers.keySet().iterator();
203        while (allHeaders.hasNext()) {
204            m_byteSize += CmsMemoryMonitor.getMemorySize(allHeaders.next());
205        }
206    }
207
208    /**
209     * @see org.opencms.cache.I_CmsLruCacheObject#addToLruCache()
210     */
211    public void addToLruCache() {
212
213        // do nothing here...
214        if (LOG.isDebugEnabled()) {
215            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEENTRY_ADDED_ENTRY_1, this));
216        }
217    }
218
219    /**
220     * Completes this cache entry.<p>
221     *
222     * A completed cache entry is made "unmodifiable",
223     * so that no further data can be added and existing data can not be changed.<p>
224     *
225     * This is to prevent the (unlikely) case that some user-written class
226     * tries to make changes to a cache entry.<p>
227     */
228    public void complete() {
229
230        m_completed = true;
231        // Prevent changing of the cached lists
232        if (m_headers != null) {
233            m_headers = Collections.unmodifiableMap(m_headers);
234        }
235        if (m_elements != null) {
236            m_elements = Collections.unmodifiableList(m_elements);
237        }
238        if (LOG.isDebugEnabled()) {
239            LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEENTRY_ENTRY_COMPLETED_1, toString()));
240        }
241    }
242
243    /**
244     * Returns the list of data entries of this cache entry.<p>
245     *
246     * Data entries are byte arrays representing some kind of output
247     * or Strings representing include calls to other resources.<p>
248     *
249     * @return the list of data elements of this cache entry
250     */
251    public List<Object> elements() {
252
253        return m_elements;
254    }
255
256    /**
257     * Gets the bucket set for this flex cache entry (may be null).<p>
258     *
259     * @return the bucket set for this flex cache entry
260     */
261    public BucketSet getBucketSet() {
262
263        return m_bucketSet;
264    }
265
266    /**
267     * Returns the expiration date of this cache entry,
268     * this is set to the time when the entry becomes invalid.<p>
269     *
270     * @return the expiration date value for this resource
271     */
272    public long getDateExpires() {
273
274        return m_dateExpires;
275    }
276
277    /**
278     * Returns the "last modified" date for this Flex cache entry.<p>
279     *
280     * @return the "last modified" date for this Flex cache entry
281     */
282    public long getDateLastModified() {
283
284        return m_dateLastModified;
285    }
286
287    /**
288     * @see org.opencms.cache.I_CmsLruCacheObject#getLruCacheCosts()
289     */
290    public int getLruCacheCosts() {
291
292        return m_byteSize;
293    }
294
295    /**
296     * @see org.opencms.monitor.I_CmsMemoryMonitorable#getMemorySize()
297     */
298    public int getMemorySize() {
299
300        return getLruCacheCosts();
301    }
302
303    /**
304     * @see org.opencms.cache.I_CmsLruCacheObject#getNextLruObject()
305     */
306    public I_CmsLruCacheObject getNextLruObject() {
307
308        return m_next;
309    }
310
311    /**
312     * @see org.opencms.cache.I_CmsLruCacheObject#getPreviousLruObject()
313     */
314    public I_CmsLruCacheObject getPreviousLruObject() {
315
316        return m_previous;
317    }
318
319    /**
320     * @see org.opencms.cache.I_CmsLruCacheObject#getValue()
321     */
322    public Object getValue() {
323
324        return m_elements;
325    }
326
327    /**
328     * Ensures that the expiration date is at most 'limit'.
329     *
330     * @param limit the maximum allowed expiration date
331     */
332    public void limitDateExpires(long limit) {
333
334        if (m_dateExpires > limit) {
335            m_dateExpires = limit;
336        }
337    }
338
339    /**
340     * @see org.opencms.cache.I_CmsLruCacheObject#removeFromLruCache()
341     */
342    public void removeFromLruCache() {
343
344        if ((m_variationMap != null) && (m_variationKey != null)) {
345            m_variationMap.remove(m_variationKey);
346        }
347        if (LOG.isDebugEnabled()) {
348            LOG.debug(
349                Messages.get().getBundle().key(
350                    Messages.LOG_FLEXCACHEENTRY_REMOVED_ENTRY_FOR_VARIATION_1,
351                    m_variationKey));
352        }
353    }
354
355    /**
356     * Processing method for this cached entry.<p>
357     *
358     * If this method is called, it delivers the contents of
359     * the cached entry to the given request / response.
360     * This includes calls to all included resources.<p>
361     *
362     * @param req the request from the client
363     * @param res the server response
364     *
365     * @throws CmsFlexCacheException is thrown when problems writing to the response output-stream occur
366     * @throws ServletException might be thrown from call to RequestDispatcher.include()
367     * @throws IOException might be thrown from call to RequestDispatcher.include() or from Response.sendRedirect()
368     */
369    public void service(CmsFlexRequest req, CmsFlexResponse res)
370    throws CmsFlexCacheException, ServletException, IOException {
371
372        if (!m_completed) {
373            return;
374        }
375
376        if (m_redirectTarget != null) {
377            res.setOnlyBuffering(false);
378            res.setCmsCachingRequired(false);
379            // redirect the response, no further output required
380            res.sendRedirect(m_redirectTarget, m_redirectPermanent);
381        } else {
382            // process cached headers first
383            CmsFlexResponse.processHeaders(m_headers, res);
384            // check if this cache entry is a "leaf" (i.e. no further includes)
385            boolean hasNoSubElements = (m_elements.size() == 1);
386            // write output to stream and process all included elements
387            for (int i = 0; i < m_elements.size(); i++) {
388                Object o = m_elements.get(i);
389                if (o instanceof String) {
390                    // handle cached parameters
391                    i++;
392                    Map<String, String[]> paramMap = CmsCollectionsGenericWrapper.map(m_elements.get(i));
393                    Map<String, String[]> oldParamMap = null;
394                    if (paramMap.size() > 0) {
395                        oldParamMap = req.getParameterMap();
396                        req.addParameterMap(paramMap);
397                    }
398                    // handle cached attributes
399                    i++;
400                    Map<String, Object> attrMap = CmsCollectionsGenericWrapper.map(m_elements.get(i));
401                    Map<String, Object> oldAttrMap = null;
402                    if (attrMap.size() > 0) {
403                        oldAttrMap = req.getAttributeMap();
404                        // to avoid issues with multi threading, try to clone the attribute instances
405                        req.addAttributeMap(cloneAttributes(attrMap));
406                        //req.addAttributeMap(attrMap);
407                    }
408                    // do the include call
409                    req.getRequestDispatcher((String)o).include(req, res);
410                    // reset parameters if necessary
411                    if (oldParamMap != null) {
412                        req.setParameterMap(oldParamMap);
413                    }
414                    // reset attributes if necessary
415                    if (oldAttrMap != null) {
416                        req.setAttributeMap(oldAttrMap);
417                    }
418                } else {
419                    try {
420                        res.writeToOutputStream((byte[])o, hasNoSubElements);
421                    } catch (IOException e) {
422                        CmsMessageContainer message = Messages.get().container(
423                            Messages.LOG_FLEXCACHEKEY_NOT_FOUND_1,
424                            getClass().getName());
425                        if (LOG.isDebugEnabled()) {
426                            LOG.debug(message.key());
427                        }
428
429                        throw new CmsFlexCacheException(message, e);
430                    }
431                }
432            }
433        }
434    }
435
436    /**
437     * Sets the bucket set for this flex cache entry.<p>
438     *
439     * @param bucketSet the bucket set to set
440     */
441    public void setBucketSet(BucketSet bucketSet) {
442
443        m_bucketSet = bucketSet;
444    }
445
446    /**
447     * Sets the expiration date of this Flex cache entry exactly to the
448     * given time.<p>
449     *
450     * @param dateExpires the time to expire this cache entry
451     */
452    public void setDateExpires(long dateExpires) {
453
454        m_dateExpires = dateExpires;
455        if (LOG.isDebugEnabled()) {
456            long now = System.currentTimeMillis();
457            LOG.debug(
458                Messages.get().getBundle().key(
459                    Messages.LOG_FLEXCACHEENTRY_SET_EXPIRATION_DATE_3,
460                    Long.valueOf(m_dateExpires),
461                    Long.valueOf(now),
462                    Long.valueOf(m_dateExpires - now)));
463        }
464    }
465
466    /**
467     * Sets an expiration date for this cache entry to the next timeout,
468     * which indicates the time this entry becomes invalid.<p>
469     *
470     * The timeout parameter represents the minute - interval in which the cache entry
471     * is to be cleared.
472     * The interval always starts at 0.00h.
473     * A value of 60 would indicate that this entry will reach it's expiration date at the beginning of the next
474     * full hour, a timeout of 20 would indicate that the entry is invalidated at x.00, x.20 and x.40 of every hour etc.<p>
475     *
476     * @param timeout the timeout value to be set
477     */
478    public void setDateExpiresToNextTimeout(long timeout) {
479
480        if ((timeout < 0) || !m_completed) {
481            return;
482        }
483
484        long now = System.currentTimeMillis();
485        long daytime = now % 86400000;
486        long timeoutMinutes = timeout * 60000;
487        setDateExpires((now - (daytime % timeoutMinutes)) + timeoutMinutes);
488    }
489
490    /**
491     * Sets the "last modified" date for this Flex cache entry with the given value.<p>
492     *
493     * @param dateLastModified the value to set for the "last modified" date
494     */
495    public void setDateLastModified(long dateLastModified) {
496
497        m_dateLastModified = dateLastModified;
498    }
499
500    /**
501     * Sets the "last modified" date for this Flex cache entry by using the last passed timeout value.<p>
502     *
503     * If a cache entry uses the timeout feature, it becomes invalid every time the timeout interval
504     * passes. Thus the "last modified" date is the time the last timeout passed.<p>
505     *
506     * @param timeout the timeout value to use to calculate the date last modified
507     */
508    public void setDateLastModifiedToPreviousTimeout(long timeout) {
509
510        long now = System.currentTimeMillis();
511        long daytime = now % 86400000;
512        long timeoutMinutes = timeout * 60000;
513        setDateLastModified(now - (daytime % timeoutMinutes));
514    }
515
516    /**
517     * @see org.opencms.cache.I_CmsLruCacheObject#setNextLruObject(org.opencms.cache.I_CmsLruCacheObject)
518     */
519    public void setNextLruObject(I_CmsLruCacheObject theNextEntry) {
520
521        m_next = theNextEntry;
522    }
523
524    /**
525     * @see org.opencms.cache.I_CmsLruCacheObject#setPreviousLruObject(org.opencms.cache.I_CmsLruCacheObject)
526     */
527    public void setPreviousLruObject(I_CmsLruCacheObject thePreviousEntry) {
528
529        m_previous = thePreviousEntry;
530    }
531
532    /**
533     * Set a redirect target for this cache entry.<p>
534     *
535     * <b>Important:</b>
536     * When a redirect target is set, all saved data is thrown away,
537     * and new data will not be saved in the cache entry.
538     * This is so since with a redirect nothing will be displayed
539     * in the browser anyway, so there is no point in saving the data.<p>
540     *
541     * @param target The redirect target (must be a valid URL).
542     * @param permanent true if this is a permanent redirect
543     */
544    public void setRedirect(String target, boolean permanent) {
545
546        if (m_completed || (target == null)) {
547            return;
548        }
549        m_redirectTarget = target;
550        m_redirectPermanent = permanent;
551        m_byteSize = 512 + CmsMemoryMonitor.getMemorySize(target);
552        // If we have a redirect we don't need any other output or headers
553        m_elements = null;
554        m_headers = null;
555    }
556
557    /**
558     * Stores a backward reference to the map and key where this cache entry is stored.<p>
559     *
560     * This is required for the FlexCache.<p>
561     *
562     * @param theVariationKey the variation key
563     * @param theVariationMap the variation map
564     */
565    public void setVariationData(String theVariationKey, Map<String, I_CmsLruCacheObject> theVariationMap) {
566
567        m_variationKey = theVariationKey;
568        m_variationMap = theVariationMap;
569    }
570
571    /**
572     * @see java.lang.Object#toString()
573     *
574     * @return a basic String representation of this CmsFlexCache entry
575     */
576    @Override
577    public String toString() {
578
579        String str = null;
580        if (m_redirectTarget == null) {
581            str = "CmsFlexCacheEntry [" + m_elements.size() + " Elements/" + getLruCacheCosts() + " bytes]\n";
582            Iterator<Object> i = m_elements.iterator();
583            int count = 0;
584            while (i.hasNext()) {
585                count++;
586                Object o = i.next();
587                if (o instanceof String) {
588                    str += "" + count + " - <cms:include target=" + o + ">\n";
589                } else if (o instanceof byte[]) {
590                    str += "" + count + " - <![CDATA[" + new String((byte[])o) + "]]>\n";
591                } else {
592                    str += "<!--[" + o.toString() + "]-->";
593                }
594            }
595        } else {
596            str = "CmsFlexCacheEntry [Redirect to target=" + m_redirectTarget + "]";
597        }
598        return str;
599    }
600
601    /**
602     * Clones the attribute instances if possible.<p>
603     *
604     * @param attrs the attributes
605     *
606     * @return a new map instance with the cloned attributes
607     */
608    private Map<String, Object> cloneAttributes(Map<String, Object> attrs) {
609
610        Map<String, Object> result = new HashMap<String, Object>();
611        for (Entry<String, Object> entry : attrs.entrySet()) {
612            if (entry.getValue() instanceof CmsJspStandardContextBean) {
613                result.put(entry.getKey(), ((CmsJspStandardContextBean)entry.getValue()).createCopy());
614            } else if (entry.getValue() instanceof Cloneable) {
615                Object clone = null;
616                try {
617                    clone = ObjectUtils.clone(entry.getValue());
618                } catch (Exception e) {
619                    LOG.info(e.getMessage(), e);
620                }
621
622                result.put(entry.getKey(), clone != null ? clone : entry.getValue());
623            } else {
624                result.put(entry.getKey(), entry.getValue());
625            }
626
627        }
628
629        return result;
630    }
631
632}