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