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