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.ade.detailpage.CmsDetailPageResourceHandler;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsResource;
033import org.opencms.main.CmsLog;
034import org.opencms.util.CmsRequestUtil;
035
036import java.util.HashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040import java.util.Vector;
041
042import javax.servlet.ServletRequest;
043import javax.servlet.http.HttpServletRequest;
044import javax.servlet.http.HttpServletResponse;
045
046import org.apache.commons.logging.Log;
047
048/**
049 * Controller for getting access to the CmsObject, should be used as a 
050 * request attribute.<p>
051 * 
052 * @since 6.0.0 
053 */
054public class CmsFlexController {
055
056    /** Constant for the controller request attribute name. */
057    public static final String ATTRIBUTE_NAME = "org.opencms.flex.CmsFlexController";
058
059    /** The log object for this class. */
060    private static final Log LOG = CmsLog.getLog(CmsFlexController.class);
061
062    /** Set of uncacheable attributes. */
063    private static Set<String> uncacheableAttributes = new HashSet<String>();
064
065    /** The CmsFlexCache where the result will be cached in, required for the dispatcher. */
066    private CmsFlexCache m_cache;
067
068    /** The wrapped CmsObject provides JSP with access to the core system. */
069    private CmsObject m_cmsObject;
070
071    /** List of wrapped RequestContext info object. */
072    private List<CmsFlexRequestContextInfo> m_flexContextInfoList;
073
074    /** List of wrapped CmsFlexRequests. */
075    private List<CmsFlexRequest> m_flexRequestList;
076
077    /** List of wrapped CmsFlexResponses. */
078    private List<CmsFlexResponse> m_flexResponseList;
079
080    /** Indicates if this controller is currently in "forward" mode. */
081    private boolean m_forwardMode;
082
083    /** Wrapped top request. */
084    private HttpServletRequest m_req;
085
086    /** Wrapped top response. */
087    private HttpServletResponse m_res;
088
089    /** The CmsResource that was initialized by the original request, required for URI actions. */
090    private CmsResource m_resource;
091
092    /** Indicates if the response should be streamed. */
093    private boolean m_streaming;
094
095    /** Exception that was caught during inclusion of sub elements. */
096    private Throwable m_throwable;
097
098    /** URI of a VFS resource that caused the exception. */
099    private String m_throwableResourceUri;
100
101    /** Indicates if the request is the top request. */
102    private boolean m_top;
103
104    /**
105     * Creates a new controller form the old one, exchanging just the provided OpenCms user context.<p>
106     * 
107     * @param cms the OpenCms user context for this controller
108     * @param base the base controller
109     */
110    public CmsFlexController(CmsObject cms, CmsFlexController base) {
111
112        m_cmsObject = cms;
113        m_resource = base.m_resource;
114        m_cache = base.m_cache;
115        m_req = base.m_req;
116        m_res = base.m_res;
117        m_streaming = base.m_streaming;
118        m_top = base.m_top;
119        m_flexRequestList = base.m_flexRequestList;
120        m_flexResponseList = base.m_flexResponseList;
121        m_flexContextInfoList = base.m_flexContextInfoList;
122        m_forwardMode = base.m_forwardMode;
123        m_throwableResourceUri = base.m_throwableResourceUri;
124    }
125
126    /**
127     * Default constructor.<p>
128     * 
129     * @param cms the initial CmsObject to wrap in the controller
130     * @param resource the file requested 
131     * @param cache the instance of the flex cache
132     * @param req the current request
133     * @param res the current response
134     * @param streaming indicates if the response is streaming
135     * @param top indicates if the response is the top response
136     */
137    public CmsFlexController(
138        CmsObject cms,
139        CmsResource resource,
140        CmsFlexCache cache,
141        HttpServletRequest req,
142        HttpServletResponse res,
143        boolean streaming,
144        boolean top) {
145
146        m_cmsObject = cms;
147        m_resource = resource;
148        m_cache = cache;
149        m_req = req;
150        m_res = res;
151        m_streaming = streaming;
152        m_top = top;
153        m_flexRequestList = new Vector<CmsFlexRequest>();
154        m_flexResponseList = new Vector<CmsFlexResponse>();
155        m_flexContextInfoList = new Vector<CmsFlexRequestContextInfo>();
156        m_forwardMode = false;
157        m_throwableResourceUri = null;
158    }
159
160    /**
161     * Returns the wrapped CmsObject form the provided request, or <code>null</code> if the 
162     * request is not running inside OpenCms.<p>
163     * 
164     * @param req the current request
165     * @return the wrapped CmsObject
166     */
167    public static CmsObject getCmsObject(ServletRequest req) {
168
169        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
170        if (controller != null) {
171            return controller.getCmsObject();
172        } else {
173            return null;
174        }
175    }
176
177    /**
178     * Returns the controller from the given request, or <code>null</code> if the 
179     * request is not running inside OpenCms.<p>
180     * 
181     * @param req the request to get the controller from
182     * 
183     * @return the controller from the given request, or <code>null</code> if the request is not running inside OpenCms
184     */
185    public static CmsFlexController getController(ServletRequest req) {
186
187        return (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
188    }
189
190    /**
191     * Provides access to a root cause Exception that might have occurred in a complex include scenario.<p>
192     * 
193     * @param req the current request
194     * 
195     * @return the root cause exception or null if no root cause exception is available
196     * 
197     * @see #getThrowable()
198     */
199    public static Throwable getThrowable(ServletRequest req) {
200
201        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
202        if (controller != null) {
203            return controller.getThrowable();
204        } else {
205            return null;
206        }
207    }
208
209    /**
210     * Provides access to URI of a VFS resource that caused an exception that might have occurred in a complex include scenario.<p>
211     * 
212     * @param req the current request
213     * 
214     * @return to URI of a VFS resource that caused an exception, or <code>null</code>
215     * 
216     * @see #getThrowableResourceUri()
217     */
218    public static String getThrowableResourceUri(ServletRequest req) {
219
220        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
221        if (controller != null) {
222            return controller.getThrowableResourceUri();
223        } else {
224            return null;
225        }
226    }
227
228    /**
229     * Checks if the provided request is running in OpenCms and the current users project is the online project.<p>
230     *
231     * @param req the current request
232     * 
233     * @return <code>true</code> if the request is running in OpenCms and the current users project is 
234     *      the online project, <code>false</code> otherwise
235     */
236    public static boolean isCmsOnlineRequest(ServletRequest req) {
237
238        if (req == null) {
239            return false;
240        }
241        return getController(req).getCmsObject().getRequestContext().getCurrentProject().isOnlineProject();
242    }
243
244    /**
245     * Checks if the provided request is running in OpenCms.<p>
246     *
247     * @param req the current request
248     * 
249     * @return <code>true</code> if the request is running in OpenCms, <code>false</code> otherwise
250     */
251    public static boolean isCmsRequest(ServletRequest req) {
252
253        return ((req != null) && (req.getAttribute(ATTRIBUTE_NAME) != null));
254    }
255
256    /**
257     * Checks if the request has the "If-Modified-Since" header set, and if so,
258     * if the header date value is equal to the provided last modification date.<p>
259     * 
260     * @param req the request to set the "If-Modified-Since" date header from
261     * @param dateLastModified the date to compare the header with
262     *  
263     * @return <code>true</code> if the header is set and the header date is equal to the provided date
264     */
265    public static boolean isNotModifiedSince(HttpServletRequest req, long dateLastModified) {
266
267        // check if the request contains a last modified header
268        try {
269            long lastModifiedHeader = req.getDateHeader(CmsRequestUtil.HEADER_IF_MODIFIED_SINCE);
270            // if last modified header is set (> -1), compare it to the requested resource                           
271            return ((lastModifiedHeader > -1) && (((dateLastModified / 1000) * 1000) == lastModifiedHeader));
272        } catch (Exception ex) {
273            // some clients (e.g. User-Agent: BlackBerry7290/4.1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/111)
274            // send an invalid "If-Modified-Since" header (e.g. in german locale) 
275            // which breaks with http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
276            // this has to be caught because the subsequent request for the 500 error handler 
277            // would run into the same exception. 
278            LOG.warn(Messages.get().getBundle().key(
279                Messages.ERR_HEADER_IFMODIFIEDSINCE_FORMAT_3,
280                new Object[] {
281                    CmsRequestUtil.HEADER_IF_MODIFIED_SINCE,
282                    req.getHeader(CmsRequestUtil.HEADER_USER_AGENT),
283                    req.getHeader(CmsRequestUtil.HEADER_IF_MODIFIED_SINCE)}));
284        }
285        return false;
286    }
287
288    /**
289     * Tells the flex controller to never cache the given attribute.<p>
290     * 
291     * @param attributeName the attribute which shouldn't be cached 
292     */
293    public static void registerUncacheableAttribute(String attributeName) {
294
295        uncacheableAttributes.add(attributeName);
296    }
297
298    /**
299     * Removes the controller attribute from a request.<p>
300     * 
301     * @param req the request to remove the controller from
302     */
303    public static void removeController(ServletRequest req) {
304
305        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
306        if (controller != null) {
307            controller.clear();
308        }
309    }
310
311    /** 
312     * Stores the given controller in the given request (using a request attribute).<p>
313     * 
314     * @param req the request where to store the controller in 
315     * @param controller the controller to store
316     */
317    public static void setController(ServletRequest req, CmsFlexController controller) {
318
319        req.setAttribute(CmsFlexController.ATTRIBUTE_NAME, controller);
320    }
321
322    /**
323     * Sets the <code>Expires</code> date header for a given http request.<p>
324     * 
325     * Also sets the <code>cache-control: max-age</code> header to the time of the expiration.
326     * A certain upper limit is imposed on the expiration date parameter to ensure the resources are
327     * not cached to long in proxies. This can be controlled by the <code>maxAge</code> parameter. 
328     * If <code>maxAge</code> is lower then 0, then a default max age of 86400000 msec (1 day) is used.<p> 
329     * 
330     * @param res the response to set the "Expires" date header for
331     * @param maxAge maximum amount of time in milliseconds the response remains valid
332     * @param dateExpires the date to set (if this is not in the future, it is ignored)
333     */
334    public static void setDateExpiresHeader(HttpServletResponse res, long dateExpires, long maxAge) {
335
336        long now = System.currentTimeMillis();
337        if ((dateExpires > now) && (dateExpires != CmsResource.DATE_EXPIRED_DEFAULT)) {
338
339            // important: many caches (browsers or proxy) use the "Expires" header
340            // to avoid re-loading of pages that are not expired
341            // while this is right in general, no changes before the expiration date
342            // will be displayed
343            // therefore it is better to not use an expiration to far in the future 
344
345            // if no valid max age is set, restrict it to 24 hrs
346            if (maxAge < 0L) {
347                maxAge = 86400000;
348            }
349
350            if ((dateExpires - now) > maxAge) {
351                // set "Expires" header max one day into the future
352                dateExpires = now + maxAge;
353            }
354            res.setDateHeader(CmsRequestUtil.HEADER_EXPIRES, dateExpires);
355
356            // setting the "Expires" header only is not sufficient - even expired documents seems to be cached
357            // therefore, the "cache-control: max-age" is also set
358            res.setHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_MAX_AGE + (maxAge / 1000L));
359        }
360    }
361
362    /**
363     * Sets the "last modified" date header for a given http request.<p>
364     * 
365     * @param res the response to set the "last modified" date header for
366     * @param dateLastModified the date to set (if this is lower then 0, the current time is set)
367     */
368    public static void setDateLastModifiedHeader(HttpServletResponse res, long dateLastModified) {
369
370        if (dateLastModified > -1) {
371            // set date last modified header (precision is only second, not millisecond
372            res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, (dateLastModified / 1000) * 1000);
373        } else {
374            // this resource can not be optimized for "last modified", use current time as header
375            res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis());
376            // avoiding issues with IE8+
377            res.setHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, "public, max-age=0");
378        }
379    }
380
381    /**
382     * Clears all data of this controller.<p>
383     */
384    public void clear() {
385
386        if (m_flexRequestList != null) {
387            m_flexRequestList.clear();
388        }
389        m_flexRequestList = null;
390        if (m_flexResponseList != null) {
391            m_flexResponseList.clear();
392        }
393        m_flexResponseList = null;
394        if (m_req != null) {
395            m_req.removeAttribute(ATTRIBUTE_NAME);
396        }
397        m_req = null;
398        m_res = null;
399        m_cmsObject = null;
400        m_resource = null;
401        m_cache = null;
402        m_throwable = null;
403    }
404
405    /**
406     * Returns the CmsFlexCache instance where all results from this request will be cached in.<p>
407     * 
408     * This is public so that pages like the Flex Cache Administration page
409     * have a way to access the cache object.<p>
410     *
411     * @return the CmsFlexCache instance where all results from this request will be cached in
412     */
413    public CmsFlexCache getCmsCache() {
414
415        return m_cache;
416    }
417
418    /**
419     * Returns the wrapped CmsObject.<p>
420     * 
421     * @return the wrapped CmsObject
422     */
423    public CmsObject getCmsObject() {
424
425        return m_cmsObject;
426    }
427
428    /** 
429     * This method provides access to the top-level CmsResource of the request
430     * which is of a type that supports the FlexCache,
431     * i.e. usually the CmsFile that is identical to the file uri requested by the user,
432     * not he current included element.<p>
433     * 
434     * @return the requested top-level CmsFile
435     */
436    public CmsResource getCmsResource() {
437
438        return m_resource;
439    }
440
441    /**
442     * Returns the current flex request.<p>
443     * 
444     * @return the current flex request
445     */
446    public CmsFlexRequest getCurrentRequest() {
447
448        return m_flexRequestList.get(m_flexRequestList.size() - 1);
449    }
450
451    /**
452     * Returns the current flex response.<p>
453     * 
454     * @return the current flex response
455     */
456    public CmsFlexResponse getCurrentResponse() {
457
458        return m_flexResponseList.get(m_flexResponseList.size() - 1);
459    }
460
461    /**
462     * Returns the combined "expires" date for all resources read during this request.<p>
463     * 
464     * @return the combined "expires" date for all resources read during this request
465     */
466    public long getDateExpires() {
467
468        int pos = m_flexContextInfoList.size() - 1;
469        if (pos < 0) {
470            // ensure a valid position is used
471            return CmsResource.DATE_EXPIRED_DEFAULT;
472        }
473        return (m_flexContextInfoList.get(pos)).getDateExpires();
474    }
475
476    /**
477     * Returns the combined "last modified" date for all resources read during this request.<p>
478     * 
479     * @return the combined "last modified" date for all resources read during this request
480     */
481    public long getDateLastModified() {
482
483        int pos = m_flexContextInfoList.size() - 1;
484        if (pos < 0) {
485            // ensure a valid position is used
486            return CmsResource.DATE_RELEASED_DEFAULT;
487        }
488        return (m_flexContextInfoList.get(pos)).getDateLastModified();
489    }
490
491    /**
492     * Returns the size of the response stack.<p>
493     * 
494     * @return the size of the response stack
495     */
496    public int getResponseStackSize() {
497
498        return m_flexResponseList.size();
499    }
500
501    /**
502     * Returns an exception (Throwable) that was caught during inclusion of sub elements, 
503     * or null if no exceptions where thrown in sub elements.<p>
504     * 
505     * @return an exception (Throwable) that was caught during inclusion of sub elements
506     */
507    public Throwable getThrowable() {
508
509        return m_throwable;
510    }
511
512    /**
513     * Returns the URI of a VFS resource that caused the exception that was caught during inclusion of sub elements,
514     * might return null if no URI information was available for the exception.<p>
515     * 
516     * @return the URI of a VFS resource that caused the exception that was caught during inclusion of sub elements
517     */
518    public String getThrowableResourceUri() {
519
520        return m_throwableResourceUri;
521    }
522
523    /**
524     * Returns the current http request.<p>
525     * 
526     * @return the current http request
527     */
528    public HttpServletRequest getTopRequest() {
529
530        return m_req;
531    }
532
533    /**
534     * Returns the current http response.<p>
535     * 
536     * @return the current http response
537     */
538    public HttpServletResponse getTopResponse() {
539
540        return m_res;
541    }
542
543    /**
544     * Returns <code>true</code> if the controller does not yet contain any requests.<p>
545     * 
546     * @return <code>true</code> if the controller does not yet contain any requests
547     */
548    public boolean isEmptyRequestList() {
549
550        return (m_flexRequestList != null) && m_flexRequestList.isEmpty();
551    }
552
553    /**
554     * Returns <code>true</code> if this controller is currently in "forward" mode.<p>
555     *
556     * @return <code>true</code> if this controller is currently in "forward" mode
557     */
558    public boolean isForwardMode() {
559
560        return m_forwardMode;
561    }
562
563    /**
564     * Returns <code>true</code> if the generated output of the response should 
565     * be written to the stream directly.<p>
566     * 
567     * @return <code>true</code> if the generated output of the response should be written to the stream directly
568     */
569    public boolean isStreaming() {
570
571        return m_streaming;
572    }
573
574    /**
575     * Returns <code>true</code> if this controller was generated as top level controller.<p>
576     * 
577     * If a resource (e.g. a JSP) is processed and it's content is included in 
578     * another resource, then this will be <code>false</code>.   
579     * 
580     * @return <code>true</code> if this controller was generated as top level controller
581     * 
582     * @see org.opencms.loader.I_CmsResourceLoader#dump(CmsObject, CmsResource, String, java.util.Locale, HttpServletRequest, HttpServletResponse)
583     * @see org.opencms.jsp.CmsJspActionElement#getContent(String)
584     */
585    public boolean isTop() {
586
587        return m_top;
588    }
589
590    /**
591     * Removes the topmost request/response pair from the stack.<p>
592     */
593    public void pop() {
594
595        if ((m_flexRequestList != null) && !m_flexRequestList.isEmpty()) {
596            m_flexRequestList.remove(m_flexRequestList.size() - 1);
597        }
598        if ((m_flexResponseList != null) && !m_flexRequestList.isEmpty()) {
599            m_flexResponseList.remove(m_flexResponseList.size() - 1);
600        }
601        if ((m_flexContextInfoList != null) && !m_flexContextInfoList.isEmpty()) {
602            CmsFlexRequestContextInfo info = m_flexContextInfoList.remove(m_flexContextInfoList.size() - 1);
603            if (m_flexContextInfoList.size() > 0) {
604                (m_flexContextInfoList.get(0)).merge(info);
605                updateRequestContextInfo();
606            }
607        }
608    }
609
610    /**
611     * Adds another flex request/response pair to the stack.<p>
612     * 
613     * @param req the request to add
614     * @param res the response to add
615     */
616    public void push(CmsFlexRequest req, CmsFlexResponse res) {
617
618        m_flexRequestList.add(req);
619        m_flexResponseList.add(res);
620        m_flexContextInfoList.add(new CmsFlexRequestContextInfo());
621        updateRequestContextInfo();
622    }
623
624    /**
625     * Removes request attributes which shouldn't be cached in flex cache entries from a map.<p>
626     * 
627     * @param attributeMap the map of attributes 
628     */
629    public void removeUncacheableAttributes(Map<String, Object> attributeMap) {
630
631        for (String uncacheableAttribute : uncacheableAttributes) {
632            attributeMap.remove(uncacheableAttribute);
633        }
634        attributeMap.remove(CmsFlexController.ATTRIBUTE_NAME);
635        attributeMap.remove(CmsDetailPageResourceHandler.ATTR_DETAIL_CONTENT_RESOURCE);
636    }
637
638    /**
639     * Sets the value of the "forward mode" flag.<p>
640     *
641     * @param value the forward mode to set
642     */
643    public void setForwardMode(boolean value) {
644
645        m_forwardMode = value;
646    }
647
648    /**
649     * Sets an exception (Throwable) that was caught during inclusion of sub elements.<p>
650     * 
651     * If another exception is already set in this controller, then the additional exception
652     * is ignored.<p>
653     * 
654     * @param throwable the exception (Throwable) to set
655     * @param resource the URI of the VFS resource the error occurred on (might be <code>null</code> if unknown)
656     * 
657     * @return the exception stored in the controller
658     */
659    public Throwable setThrowable(Throwable throwable, String resource) {
660
661        if (m_throwable == null) {
662            m_throwable = throwable;
663            m_throwableResourceUri = resource;
664        } else {
665            if (LOG.isDebugEnabled()) {
666                if (resource != null) {
667                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCONTROLLER_IGNORED_EXCEPTION_1, resource));
668                } else {
669                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCONTROLLER_IGNORED_EXCEPTION_0));
670                }
671            }
672        }
673        return m_throwable;
674    }
675
676    /**
677     * Puts the response in a suspended state.<p>  
678     */
679    public void suspendFlexResponse() {
680
681        for (int i = 0; i < m_flexResponseList.size(); i++) {
682            CmsFlexResponse res = m_flexResponseList.get(i);
683            res.setSuspended(true);
684        }
685    }
686
687    /**
688     * Updates the "last modified" date and the "expires" date 
689     * for all resources read during this request with the given values.<p>
690     * 
691     * The currently stored value for "last modified" is only updated with the new value if
692     * the new value is either larger (i.e. newer) then the stored value,
693     * or if the new value is less then zero, which indicates that the "last modified"
694     * optimization can not be used because the element is dynamic.<p>
695     * 
696     * The stored "expires" value is only updated if the new value is smaller
697     * then the stored value.<p>
698     * 
699     * @param dateLastModified the value to update the "last modified" date with
700     * @param dateExpires the value to update the "expires" date with
701     */
702    public void updateDates(long dateLastModified, long dateExpires) {
703
704        int pos = m_flexContextInfoList.size() - 1;
705        if (pos < 0) {
706            // ensure a valid position is used
707            return;
708        }
709        (m_flexContextInfoList.get(pos)).updateDates(dateLastModified, dateExpires);
710    }
711
712    /**
713     * Updates the context info of the request context.<p>
714     */
715    private void updateRequestContextInfo() {
716
717        if ((m_flexContextInfoList != null) && !m_flexContextInfoList.isEmpty()) {
718            m_cmsObject.getRequestContext().setAttribute(
719                CmsRequestUtil.HEADER_LAST_MODIFIED,
720                m_flexContextInfoList.get(m_flexContextInfoList.size() - 1));
721        }
722    }
723}