001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.io.IOException;
037import java.util.Arrays;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042import java.util.Vector;
043
044import org.apache.commons.logging.Log;
045
046import jakarta.servlet.RequestDispatcher;
047import jakarta.servlet.ServletRequest;
048import jakarta.servlet.http.HttpServletRequest;
049import jakarta.servlet.http.HttpServletResponse;
050
051/**
052 * Controller for getting access to the CmsObject, should be used as a
053 * request attribute.<p>
054 *
055 * @since 6.0.0
056 */
057public class CmsFlexController {
058
059    /**
060     * Information about where to redirect to.
061     */
062    public static class RedirectInfo {
063
064        /** True if a permanent redirect should be used. */
065        private boolean m_permanent;
066
067        /** The redirect target. */
068        private String m_target;
069
070        /**
071         * Creates a new instance.
072         *
073         * @param target the redirect target
074         * @param permanent true if a permanent redirect should be used
075         */
076        public RedirectInfo(String target, boolean permanent) {
077
078            m_target = target;
079            m_permanent = permanent;
080
081        }
082
083        /**
084         * Executes the redirect on the given response with the stored information.
085         *
086         * @param res the res to use for executing the redirect
087         */
088        public void executeRedirect(HttpServletResponse res) throws IOException {
089
090            if (isPermanent()) {
091                res.setHeader(CmsRequestUtil.HEADER_LOCATION, getTarget());
092                res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
093            } else {
094                res.sendRedirect(getTarget());
095            }
096
097        }
098
099        /**
100         * Gets the redirect target.
101         *
102         * @return the redirect target
103         */
104        public String getTarget() {
105
106            return m_target;
107        }
108
109        /**
110         * Returns true if a permanent redirect should be used.
111         *
112         * @return true if a permanent redirect should be used
113         */
114        public boolean isPermanent() {
115
116            return m_permanent;
117        }
118
119    }
120
121    /** Constant for the controller request attribute name. */
122    public static final String ATTRIBUTE_NAME = "org.opencms.flex.CmsFlexController";
123
124    /** The log object for this class. */
125    private static final Log LOG = CmsLog.getLog(CmsFlexController.class);
126
127    /** Set of uncacheable attributes. */
128    private static Set<String> uncacheableAttributes = new HashSet<String>();
129
130    static {
131        for (String attr : Arrays.asList(
132            RequestDispatcher.ERROR_EXCEPTION,
133            RequestDispatcher.ERROR_EXCEPTION_TYPE,
134            RequestDispatcher.ERROR_MESSAGE,
135            RequestDispatcher.ERROR_REQUEST_URI,
136            RequestDispatcher.ERROR_SERVLET_NAME,
137            RequestDispatcher.ERROR_STATUS_CODE)) {
138
139            // global rule: don't cache error page attributes.
140            CmsFlexController.registerUncacheableAttribute(attr);
141        }
142    }
143
144    /** Fake HTTP header used to store information about the content type in a CmsFlexResponse. Should never be actually sent to the client. */
145    public static final String HEADER_OPENCMS_CONTENT_TYPE = "X-OpenCms-Content-Type";
146
147    /** The CmsFlexCache where the result will be cached in, required for the dispatcher. */
148    private CmsFlexCache m_cache;
149
150    /** The wrapped CmsObject provides JSP with access to the core system. */
151    private CmsObject m_cmsObject;
152
153    /** List of wrapped RequestContext info object. */
154    private List<CmsFlexRequestContextInfo> m_flexContextInfoList;
155
156    /** List of wrapped CmsFlexRequests. */
157    private List<CmsFlexRequest> m_flexRequestList;
158
159    /** List of wrapped CmsFlexResponses. */
160    private List<CmsFlexResponse> m_flexResponseList;
161
162    /** Indicates if this controller is currently in "forward" mode. */
163    private boolean m_forwardMode;
164
165    /** Information about where to redirect to. */
166    private RedirectInfo m_redirectInfo;
167
168    /** Wrapped top request. */
169    private HttpServletRequest m_req;
170
171    /** Wrapped top response. */
172    private HttpServletResponse m_res;
173
174    /** The CmsResource that was initialized by the original request, required for URI actions. */
175    private CmsResource m_resource;
176
177    /** Indicates if the response should be streamed. */
178    private boolean m_streaming;
179
180    /** Exception that was caught during inclusion of sub elements. */
181    private Throwable m_throwable;
182
183    /** URI of a VFS resource that caused the exception. */
184    private String m_throwableResourceUri;
185
186    /** Indicates if the request is the top request. */
187    private boolean m_top;
188
189    /**
190     * Creates a new controller form the old one, exchanging just the provided OpenCms user context.<p>
191     *
192     * @param cms the OpenCms user context for this controller
193     * @param base the base controller
194     */
195    public CmsFlexController(CmsObject cms, CmsFlexController base) {
196
197        m_cmsObject = cms;
198        m_resource = base.m_resource;
199        m_cache = base.m_cache;
200        m_req = base.m_req;
201        m_res = base.m_res;
202        m_streaming = base.m_streaming;
203        m_top = base.m_top;
204        m_flexRequestList = base.m_flexRequestList;
205        m_flexResponseList = base.m_flexResponseList;
206        m_flexContextInfoList = base.m_flexContextInfoList;
207        m_forwardMode = base.m_forwardMode;
208        m_throwableResourceUri = base.m_throwableResourceUri;
209        m_redirectInfo = base.m_redirectInfo;
210    }
211
212    /**
213     * Default constructor.<p>
214     *
215     * @param cms the initial CmsObject to wrap in the controller
216     * @param resource the file requested
217     * @param cache the instance of the flex cache
218     * @param req the current request
219     * @param res the current response
220     * @param streaming indicates if the response is streaming
221     * @param top indicates if the response is the top response
222     */
223    public CmsFlexController(
224        CmsObject cms,
225        CmsResource resource,
226        CmsFlexCache cache,
227        HttpServletRequest req,
228        HttpServletResponse res,
229        boolean streaming,
230        boolean top) {
231
232        m_cmsObject = cms;
233        m_resource = resource;
234        m_cache = cache;
235        m_req = req;
236        m_res = res;
237        m_streaming = streaming;
238        m_top = top;
239        m_flexRequestList = new Vector<CmsFlexRequest>();
240        m_flexResponseList = new Vector<CmsFlexResponse>();
241        m_flexContextInfoList = new Vector<CmsFlexRequestContextInfo>();
242        m_forwardMode = false;
243        m_throwableResourceUri = null;
244    }
245
246    /**
247     * Returns the wrapped CmsObject form the provided request, or <code>null</code> if the
248     * request is not running inside OpenCms.<p>
249     *
250     * @param req the current request
251     * @return the wrapped CmsObject
252     */
253    public static CmsObject getCmsObject(ServletRequest req) {
254
255        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
256        if (controller != null) {
257            return controller.getCmsObject();
258        } else {
259            return null;
260        }
261    }
262
263    /**
264     * Returns the controller from the given request, or <code>null</code> if the
265     * request is not running inside OpenCms.<p>
266     *
267     * @param req the request to get the controller from
268     *
269     * @return the controller from the given request, or <code>null</code> if the request is not running inside OpenCms
270     */
271    public static CmsFlexController getController(ServletRequest req) {
272
273        return (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
274    }
275
276    /**
277     * Provides access to a root cause Exception that might have occurred in a complex include scenario.<p>
278     *
279     * @param req the current request
280     *
281     * @return the root cause exception or null if no root cause exception is available
282     *
283     * @see #getThrowable()
284     */
285    public static Throwable getThrowable(ServletRequest req) {
286
287        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
288        if (controller != null) {
289            return controller.getThrowable();
290        } else {
291            return null;
292        }
293    }
294
295    /**
296     * Provides access to URI of a VFS resource that caused an exception that might have occurred in a complex include scenario.<p>
297     *
298     * @param req the current request
299     *
300     * @return to URI of a VFS resource that caused an exception, or <code>null</code>
301     *
302     * @see #getThrowableResourceUri()
303     */
304    public static String getThrowableResourceUri(ServletRequest req) {
305
306        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
307        if (controller != null) {
308            return controller.getThrowableResourceUri();
309        } else {
310            return null;
311        }
312    }
313
314    /**
315     * Checks if the provided request is running in OpenCms and the current users project is the online project.<p>
316     *
317     * @param req the current request
318     *
319     * @return <code>true</code> if the request is running in OpenCms and the current users project is
320     *      the online project, <code>false</code> otherwise
321     */
322    public static boolean isCmsOnlineRequest(ServletRequest req) {
323
324        if (req == null) {
325            return false;
326        }
327        return getController(req).getCmsObject().getRequestContext().getCurrentProject().isOnlineProject();
328    }
329
330    /**
331     * Checks if the provided request is running in OpenCms.<p>
332     *
333     * @param req the current request
334     *
335     * @return <code>true</code> if the request is running in OpenCms, <code>false</code> otherwise
336     */
337    public static boolean isCmsRequest(ServletRequest req) {
338
339        return ((req != null) && (req.getAttribute(ATTRIBUTE_NAME) != null));
340    }
341
342    /**
343     * Checks if the request has the "If-Modified-Since" header set, and if so,
344     * if the header date value is equal to the provided last modification date.<p>
345     *
346     * @param req the request to set the "If-Modified-Since" date header from
347     * @param dateLastModified the date to compare the header with
348     *
349     * @return <code>true</code> if the header is set and the header date is equal to the provided date
350     */
351    public static boolean isNotModifiedSince(HttpServletRequest req, long dateLastModified) {
352
353        // check if the request contains a last modified header
354        try {
355            long lastModifiedHeader = req.getDateHeader(CmsRequestUtil.HEADER_IF_MODIFIED_SINCE);
356            // if last modified header is set (> -1), compare it to the requested resource
357            return ((lastModifiedHeader > -1) && (((dateLastModified / 1000) * 1000) == lastModifiedHeader));
358        } catch (Exception ex) {
359            // some clients (e.g. User-Agent: BlackBerry7290/4.1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/111)
360            // send an invalid "If-Modified-Since" header (e.g. in german locale)
361            // which breaks with http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
362            // this has to be caught because the subsequent request for the 500 error handler
363            // would run into the same exception.
364            LOG.warn(
365                Messages.get().getBundle().key(
366                    Messages.ERR_HEADER_IFMODIFIEDSINCE_FORMAT_3,
367                    new Object[] {
368                        CmsRequestUtil.HEADER_IF_MODIFIED_SINCE,
369                        req.getHeader(CmsRequestUtil.HEADER_USER_AGENT),
370                        req.getHeader(CmsRequestUtil.HEADER_IF_MODIFIED_SINCE)}));
371        }
372        return false;
373    }
374
375    /**
376     * Tells the flex controller to never cache the given attribute.<p>
377     *
378     * @param attributeName the attribute which shouldn't be cached
379     */
380    public static void registerUncacheableAttribute(String attributeName) {
381
382        uncacheableAttributes.add(attributeName);
383    }
384
385    /**
386     * Removes the controller attribute from a request.<p>
387     *
388     * @param req the request to remove the controller from
389     */
390    public static void removeController(ServletRequest req) {
391
392        CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME);
393        if (controller != null) {
394            controller.clear();
395        }
396    }
397
398    /**
399     * Stores the given controller in the given request (using a request attribute).<p>
400     *
401     * @param req the request where to store the controller in
402     * @param controller the controller to store
403     */
404    public static void setController(ServletRequest req, CmsFlexController controller) {
405
406        req.setAttribute(CmsFlexController.ATTRIBUTE_NAME, controller);
407    }
408
409    /**
410     * Sets the <code>Expires</code> date header for a given http request.<p>
411     *
412     * Also sets the <code>cache-control: max-age</code> header to the time of the expiration.
413     * A certain upper limit is imposed on the expiration date parameter to ensure the resources are
414     * not cached to long in proxies. This can be controlled by the <code>maxAge</code> parameter.
415     * If <code>maxAge</code> is lower then 0, then a default max age of 86400000 msec (1 day) is used.<p>
416     *
417     * @param res the response to set the "Expires" date header for
418     * @param maxAge maximum amount of time in milliseconds the response remains valid
419     * @param dateExpires the date to set (if this is not in the future, it is ignored)
420     */
421    public static void setDateExpiresHeader(HttpServletResponse res, long dateExpires, long maxAge) {
422
423        long now = System.currentTimeMillis();
424        if ((dateExpires > now) && (dateExpires != CmsResource.DATE_EXPIRED_DEFAULT)) {
425
426            // important: many caches (browsers or proxy) use the "Expires" header
427            // to avoid re-loading of pages that are not expired
428            // while this is right in general, no changes before the expiration date
429            // will be displayed
430            // therefore it is better to not use an expiration to far in the future
431
432            // if no valid max age is set, restrict it to 24 hrs
433            if (maxAge < 0L) {
434                maxAge = 86400000;
435            }
436
437            if ((dateExpires - now) > maxAge) {
438                // set "Expires" header max one day into the future
439                dateExpires = now + maxAge;
440            }
441            res.setDateHeader(CmsRequestUtil.HEADER_EXPIRES, dateExpires);
442
443            // setting the "Expires" header only is not sufficient - even expired documents seems to be cached
444            // therefore, the "cache-control: max-age" is also set
445            res.setHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_MAX_AGE + (maxAge / 1000L));
446        }
447    }
448
449    /**
450     * Sets the "last modified" date header for a given http request.<p>
451     *
452     * @param res the response to set the "last modified" date header for
453     * @param dateLastModified the date to set (if this is lower then 0, the current time is set)
454     */
455    public static void setDateLastModifiedHeader(HttpServletResponse res, long dateLastModified) {
456
457        if (dateLastModified > -1) {
458            // set date last modified header (precision is only second, not millisecond
459            res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, (dateLastModified / 1000) * 1000);
460        } else {
461            // this resource can not be optimized for "last modified", use current time as header
462            res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis());
463            // avoiding issues with IE8+
464            res.addHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, "public, max-age=0");
465        }
466    }
467
468    /**
469     * Method overload from the standard HttpServletResponse API.<p>
470     *
471     * The method is added to the controller itself,
472     * to automatically have the correct choice for the response to set the header.
473     *
474     * @param name
475     * @param date
476     *
477     * @see jakarta.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long)
478     */
479    public void addDateHeader(String name, long date) {
480
481        getCurrentResponse().addDateHeader(name, date);
482    }
483
484    /**
485     * Method overload from the standard HttpServletResponse API.<p>
486     *
487     * The method is added to the controller itself,
488     * to automatically have the correct choice for the response to set the header.
489     *
490     * @param name
491     * @param value
492     *
493     * @see jakarta.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String)
494     */
495    public void addHeader(String name, String value) {
496
497        getCurrentResponse().addHeader(name, value);
498    }
499
500    /**
501     * Method overload from the standard HttpServletResponse API.<p>
502     *
503     * The method is added to the controller itself,
504     * to automatically have the correct choice for the response to set the header.
505     *
506     * @param name
507     * @param value
508     *
509     * @see jakarta.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int)
510     */
511    public void addIntHeader(String name, int value) {
512
513        getCurrentResponse().addIntHeader(name, value);
514    }
515
516    /**
517     * Clears all data of this controller.<p>
518     */
519    public void clear() {
520
521        if (m_flexRequestList != null) {
522            m_flexRequestList.clear();
523        }
524        m_flexRequestList = null;
525        if (m_flexResponseList != null) {
526            m_flexResponseList.clear();
527        }
528        m_flexResponseList = null;
529        if (m_req != null) {
530            m_req.removeAttribute(ATTRIBUTE_NAME);
531        }
532        m_req = null;
533        m_res = null;
534        m_cmsObject = null;
535        m_resource = null;
536        m_cache = null;
537        m_throwable = null;
538    }
539
540    /**
541     * Returns the CmsFlexCache instance where all results from this request will be cached in.<p>
542     *
543     * This is public so that pages like the Flex Cache Administration page
544     * have a way to access the cache object.<p>
545     *
546     * @return the CmsFlexCache instance where all results from this request will be cached in
547     */
548    public CmsFlexCache getCmsCache() {
549
550        return m_cache;
551    }
552
553    /**
554     * Returns the wrapped CmsObject.<p>
555     *
556     * @return the wrapped CmsObject
557     */
558    public CmsObject getCmsObject() {
559
560        return m_cmsObject;
561    }
562
563    /**
564     * This method provides access to the top-level CmsResource of the request
565     * which is of a type that supports the FlexCache,
566     * i.e. usually the CmsFile that is identical to the file uri requested by the user,
567     * not he current included element.<p>
568     *
569     * @return the requested top-level CmsFile
570     */
571    public CmsResource getCmsResource() {
572
573        return m_resource;
574    }
575
576    /**
577     * Returns the current flex request.<p>
578     *
579     * @return the current flex request
580     */
581    public CmsFlexRequest getCurrentRequest() {
582
583        return m_flexRequestList.get(m_flexRequestList.size() - 1);
584    }
585
586    /**
587     * Returns the current flex response.<p>
588     *
589     * @return the current flex response
590     */
591    public CmsFlexResponse getCurrentResponse() {
592
593        return m_flexResponseList.get(m_flexResponseList.size() - 1);
594    }
595
596    /**
597     * Returns the combined "expires" date for all resources read during this request.<p>
598     *
599     * @return the combined "expires" date for all resources read during this request
600     */
601    public long getDateExpires() {
602
603        int pos = m_flexContextInfoList.size() - 1;
604        if (pos < 0) {
605            // ensure a valid position is used
606            return CmsResource.DATE_EXPIRED_DEFAULT;
607        }
608        return (m_flexContextInfoList.get(pos)).getDateExpires();
609    }
610
611    /**
612     * Returns the combined "last modified" date for all resources read during this request.<p>
613     *
614     * @return the combined "last modified" date for all resources read during this request
615     */
616    public long getDateLastModified() {
617
618        int pos = m_flexContextInfoList.size() - 1;
619        if (pos < 0) {
620            // ensure a valid position is used
621            return CmsResource.DATE_RELEASED_DEFAULT;
622        }
623        return (m_flexContextInfoList.get(pos)).getDateLastModified();
624    }
625
626    /**
627     * Gets the information about where to redirect to.
628     *
629     * @return the redirect information
630     */
631    public RedirectInfo getRedirectInfo() {
632
633        return m_redirectInfo;
634    }
635
636    /**
637     * Returns the size of the response stack.<p>
638     *
639     * @return the size of the response stack
640     */
641    public int getResponseStackSize() {
642
643        return m_flexResponseList.size();
644    }
645
646    /**
647     * Returns an exception (Throwable) that was caught during inclusion of sub elements,
648     * or null if no exceptions where thrown in sub elements.<p>
649     *
650     * @return an exception (Throwable) that was caught during inclusion of sub elements
651     */
652    public Throwable getThrowable() {
653
654        return m_throwable;
655    }
656
657    /**
658     * Returns the URI of a VFS resource that caused the exception that was caught during inclusion of sub elements,
659     * might return null if no URI information was available for the exception.<p>
660     *
661     * @return the URI of a VFS resource that caused the exception that was caught during inclusion of sub elements
662     */
663    public String getThrowableResourceUri() {
664
665        return m_throwableResourceUri;
666    }
667
668    /**
669     * Returns the current http request.<p>
670     *
671     * @return the current http request
672     */
673    public HttpServletRequest getTopRequest() {
674
675        return m_req;
676    }
677
678    /**
679     * Returns the current http response.<p>
680     *
681     * @return the current http response
682     */
683    public HttpServletResponse getTopResponse() {
684
685        return m_res;
686    }
687
688    /**
689     * Returns <code>true</code> if the controller does not yet contain any requests.<p>
690     *
691     * @return <code>true</code> if the controller does not yet contain any requests
692     */
693    public boolean isEmptyRequestList() {
694
695        return (m_flexRequestList != null) && m_flexRequestList.isEmpty();
696    }
697
698    /**
699     * Returns <code>true</code> if this controller is currently in "forward" mode.<p>
700     *
701     * @return <code>true</code> if this controller is currently in "forward" mode
702     */
703    public boolean isForwardMode() {
704
705        return m_forwardMode;
706    }
707
708    /**
709     * Returns <code>true</code> if the generated output of the response should
710     * be written to the stream directly.<p>
711     *
712     * @return <code>true</code> if the generated output of the response should be written to the stream directly
713     */
714    public boolean isStreaming() {
715
716        return m_streaming;
717    }
718
719    /**
720     * Returns <code>true</code> if this controller was generated as top level controller.<p>
721     *
722     * If a resource (e.g. a JSP) is processed and it's content is included in
723     * another resource, then this will be <code>false</code>.
724     *
725     * @return <code>true</code> if this controller was generated as top level controller
726     *
727     * @see org.opencms.loader.I_CmsResourceLoader#dump(CmsObject, CmsResource, String, java.util.Locale, HttpServletRequest, HttpServletResponse)
728     * @see org.opencms.jsp.CmsJspActionElement#getContent(String)
729     */
730    public boolean isTop() {
731
732        return m_top;
733    }
734
735    /**
736     * Removes the topmost request/response pair from the stack.<p>
737     */
738    public void pop() {
739
740        if ((m_flexRequestList != null) && !m_flexRequestList.isEmpty()) {
741            m_flexRequestList.remove(m_flexRequestList.size() - 1);
742        }
743        if ((m_flexResponseList != null) && !m_flexRequestList.isEmpty()) {
744            m_flexResponseList.remove(m_flexResponseList.size() - 1);
745        }
746        if ((m_flexContextInfoList != null) && !m_flexContextInfoList.isEmpty()) {
747            CmsFlexRequestContextInfo info = m_flexContextInfoList.remove(m_flexContextInfoList.size() - 1);
748            if (m_flexContextInfoList.size() > 0) {
749                (m_flexContextInfoList.get(0)).merge(info);
750                updateRequestContextInfo();
751            }
752        }
753    }
754
755    /**
756     * Adds another flex request/response pair to the stack.<p>
757     *
758     * @param req the request to add
759     * @param res the response to add
760     */
761    public void push(CmsFlexRequest req, CmsFlexResponse res) {
762
763        m_flexRequestList.add(req);
764        m_flexResponseList.add(res);
765        m_flexContextInfoList.add(new CmsFlexRequestContextInfo());
766        updateRequestContextInfo();
767    }
768
769    /**
770     * Removes request attributes which shouldn't be cached in flex cache entries from a map.<p>
771     *
772     * @param attributeMap the map of attributes
773     */
774    public void removeUncacheableAttributes(Map<String, Object> attributeMap) {
775
776        for (String uncacheableAttribute : uncacheableAttributes) {
777            attributeMap.remove(uncacheableAttribute);
778        }
779        attributeMap.remove(CmsFlexController.ATTRIBUTE_NAME);
780        attributeMap.remove(CmsDetailPageResourceHandler.ATTR_DETAIL_CONTENT_RESOURCE);
781        attributeMap.remove(CmsDetailPageResourceHandler.ATTR_DETAIL_FUNCTION_PAGE);
782    }
783
784    /**
785     * Special method for setting the content type from a JSP in a way that is compatible with both Tomcat and Jetty and also works with the Flex Cache.
786     *
787     * <p>This is implemented as setting a fake HTTP header which is then translated back to a call to setContentType() in CmsJspLoader.dispatchJsp().
788     *
789     * @param contentType the content type to set
790     */
791    public void setContentType(String contentType) {
792
793        getCurrentResponse().setHeader(HEADER_OPENCMS_CONTENT_TYPE, contentType);
794    }
795
796    /**
797     * Method overload from the standard HttpServletResponse API.<p>
798     *
799     * The method is added to the controller itself,
800     * to automatically have the correct choice for the response to set the header.
801     *
802     * @param name
803     * @param date
804     *
805     * @see jakarta.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long)
806     */
807    public void setDateHeader(String name, long date) {
808
809        getCurrentResponse().setDateHeader(name, date);
810    }
811
812    /**
813     * Sets the value of the "forward mode" flag.<p>
814     *
815     * @param value the forward mode to set
816     */
817    public void setForwardMode(boolean value) {
818
819        m_forwardMode = value;
820    }
821
822    /**
823     * Method overload from the standard HttpServletResponse API.<p>
824     *
825     * The method is added to the controller itself,
826     * to automatically have the correct choice for the response to set the header.
827     *
828     * @param name
829     * @param value
830     *
831     * @see jakarta.servlet.http.HttpServletResponse#setHeader(java.lang.String, java.lang.String)
832     */
833    public void setHeader(String name, String value) {
834
835        getCurrentResponse().setHeader(name, value);
836    }
837
838    /**
839     * Method overload from the standard HttpServletResponse API.<p>
840     *
841     * The method is added to the controller itself,
842     * to automatically have the correct choice for the response to set the header.
843     *
844     * @param name
845     * @param value
846     *
847     * @see jakarta.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int)
848     */
849    public void setIntHeader(String name, int value) {
850
851        getCurrentResponse().setIntHeader(name, value);
852    }
853
854    /**
855     * Sets the information about where to redirect to.
856     *
857     * @param redirectInfo the redirect information
858     */
859    public void setRedirectInfo(RedirectInfo redirectInfo) {
860
861        m_redirectInfo = redirectInfo;
862    }
863
864    /**
865     * Sets an exception (Throwable) that was caught during inclusion of sub elements.<p>
866     *
867     * If another exception is already set in this controller, then the additional exception
868     * is ignored.<p>
869     *
870     * @param throwable the exception (Throwable) to set
871     * @param resource the URI of the VFS resource the error occurred on (might be <code>null</code> if unknown)
872     *
873     * @return the exception stored in the controller
874     */
875    public Throwable setThrowable(Throwable throwable, String resource) {
876
877        if (m_throwable == null) {
878            m_throwable = throwable;
879            m_throwableResourceUri = resource;
880        } else {
881            if (LOG.isDebugEnabled()) {
882                if (resource != null) {
883                    LOG.debug(
884                        Messages.get().getBundle().key(Messages.LOG_FLEXCONTROLLER_IGNORED_EXCEPTION_1, resource));
885                } else {
886                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCONTROLLER_IGNORED_EXCEPTION_0));
887                }
888            }
889        }
890        return m_throwable;
891    }
892
893    /**
894     * Puts the response in a suspended state.<p>
895     */
896    public void suspendFlexResponse() {
897
898        for (int i = 0; i < m_flexResponseList.size(); i++) {
899            CmsFlexResponse res = m_flexResponseList.get(i);
900            res.setSuspended(true);
901        }
902    }
903
904    /**
905     * Updates the "last modified" date and the "expires" date
906     * for all resources read during this request with the given values.<p>
907     *
908     * The currently stored value for "last modified" is only updated with the new value if
909     * the new value is either larger (i.e. newer) then the stored value,
910     * or if the new value is less then zero, which indicates that the "last modified"
911     * optimization can not be used because the element is dynamic.<p>
912     *
913     * The stored "expires" value is only updated if the new value is smaller
914     * then the stored value.<p>
915     *
916     * @param dateLastModified the value to update the "last modified" date with
917     * @param dateExpires the value to update the "expires" date with
918     */
919    public void updateDates(long dateLastModified, long dateExpires) {
920
921        int pos = m_flexContextInfoList.size() - 1;
922        if (pos < 0) {
923            // ensure a valid position is used
924            return;
925        }
926        (m_flexContextInfoList.get(pos)).updateDates(dateLastModified, dateExpires);
927    }
928
929    /**
930     * Updates the context info of the request context.<p>
931     */
932    private void updateRequestContextInfo() {
933
934        if ((m_flexContextInfoList != null) && !m_flexContextInfoList.isEmpty()) {
935            m_cmsObject.getRequestContext().setAttribute(
936                CmsRequestUtil.HEADER_LAST_MODIFIED,
937                m_flexContextInfoList.get(m_flexContextInfoList.size() - 1));
938        }
939    }
940}