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