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.file.CmsObject;
031import org.opencms.file.CmsPropertyDefinition;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsVfsResourceNotFoundException;
034import org.opencms.loader.I_CmsResourceLoader;
035import org.opencms.main.CmsException;
036import org.opencms.main.CmsLog;
037import org.opencms.main.OpenCms;
038
039import java.io.IOException;
040import java.util.List;
041import java.util.Map;
042
043import javax.servlet.RequestDispatcher;
044import javax.servlet.ServletException;
045import javax.servlet.ServletRequest;
046import javax.servlet.ServletResponse;
047import javax.servlet.http.HttpServletRequest;
048import javax.servlet.http.HttpServletResponse;
049
050import org.apache.commons.logging.Log;
051
052/**
053 * Implementation of the <code>{@link javax.servlet.RequestDispatcher}</code> interface to allow JSPs to be loaded
054 * from the OpenCms VFS.<p>
055 *
056 * This dispatcher will load data from 3 different data sources:
057 * <ol>
058 * <li>Form the "real" os File system (e.g. for JSP pages)
059 * <li>From the OpenCms VFS
060 * <li>From the Flex cache
061 * </ol>
062 * <p>
063 *
064 * @since 6.0.0
065 */
066public class CmsFlexRequestDispatcher implements RequestDispatcher {
067
068    /** The log object for this class. */
069    private static final Log LOG = CmsLog.getLog(CmsFlexRequestDispatcher.class);
070
071    /** The external target that will be included by the RequestDispatcher, needed if this is not a dispatcher to a cms resource. */
072    private String m_extTarget;
073
074    /** The "real" RequestDispatcher, used when a true include (to the file system) is needed. */
075    private RequestDispatcher m_rd;
076
077    /** The OpenCms VFS target that will be included by the RequestDispatcher. */
078    private String m_vfsTarget;
079
080    /**
081     * Creates a new instance of CmsFlexRequestDispatcher.<p>
082     *
083     * @param rd the "real" dispatcher, used for include call to file system
084     * @param vfs_target the cms resource that represents the external target
085     * @param ext_target the external target that the request will be dispatched to
086     */
087    public CmsFlexRequestDispatcher(RequestDispatcher rd, String vfs_target, String ext_target) {
088
089        m_rd = rd;
090        m_vfsTarget = vfs_target;
091        m_extTarget = ext_target;
092    }
093
094    /**
095     * Wrapper for the standard servlet API call.<p>
096     *
097     * Forward calls are actually NOT wrapped by OpenCms as of now.
098     * So they should not be used in JSP pages or servlets.<p>
099     *
100     * @param req the servlet request
101     * @param res the servlet response
102     * @throws ServletException in case something goes wrong
103     * @throws IOException in case something goes wrong
104     *
105     * @see javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
106     */
107    public void forward(ServletRequest req, ServletResponse res) throws ServletException, IOException {
108
109        CmsFlexController controller = CmsFlexController.getController(req);
110        controller.setForwardMode(true);
111        m_rd.forward(req, res);
112    }
113
114    /**
115     * Wrapper for dispatching to a file from the OpenCms VFS.<p>
116     *
117     * This method will dispatch to cache, to real file system or
118     * to the OpenCms VFS, whatever is needed.<p>
119     *
120     * This method is much more complex than it should be because of the internal standard
121     * buffering of JSP pages.
122     * Because of that I can not just intercept and buffer the stream, since I don't have
123     * access to it (it is wrapped internally in the JSP pages, which have their own buffer).
124     * That leads to a solution where the data is first written to the buffered stream,
125     * but without includes. Then it is parsed again later
126     * in the <code>{@link CmsFlexResponse}</code>, enriched with the
127     * included elements that have been omitted in the first case.
128     * I would love to see a simpler solution, but this works for now.<p>
129     *
130     * @param req the servlet request
131     * @param res the servlet response
132     *
133     * @throws ServletException in case something goes wrong
134     * @throws IOException in case something goes wrong
135     */
136    public void include(ServletRequest req, ServletResponse res) throws ServletException, IOException {
137
138        if (LOG.isDebugEnabled()) {
139            LOG.debug(
140                Messages.get().getBundle().key(
141                    Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDING_TARGET_2,
142                    m_vfsTarget,
143                    m_extTarget));
144        }
145
146        CmsFlexController controller = CmsFlexController.getController(req);
147        CmsResource resource = null;
148
149        if ((m_extTarget == null) && (controller != null)) {
150            // check if the file exists in the VFS, if not set external target
151            try {
152                resource = controller.getCmsObject().readResource(m_vfsTarget);
153            } catch (CmsVfsResourceNotFoundException e) {
154                // file not found in VFS, treat it as external file
155                m_extTarget = m_vfsTarget;
156            } catch (CmsException e) {
157                // if other OpenCms exception occurred we are in trouble
158                throw new ServletException(
159                    Messages.get().getBundle().key(Messages.ERR_FLEXREQUESTDISPATCHER_VFS_ACCESS_EXCEPTION_0),
160                    e);
161            }
162        }
163
164        if ((m_extTarget != null) || (controller == null)) {
165            includeExternal(req, res);
166        } else if (controller.isForwardMode()) {
167            includeInternalNoCache(req, res, controller, controller.getCmsObject(), resource);
168        } else {
169            includeInternalWithCache(req, res, controller, controller.getCmsObject(), resource);
170        }
171    }
172
173    /**
174     * Include an external (non-OpenCms) file using the standard dispatcher.<p>
175     *
176     * @param req the servlet request
177     * @param res the servlet response
178     *
179     * @throws ServletException in case something goes wrong
180     * @throws IOException in case something goes wrong
181     */
182    private void includeExternal(ServletRequest req, ServletResponse res) throws ServletException, IOException {
183
184        // This is an external include, probably to a JSP page, dispatch with system dispatcher
185        if (LOG.isInfoEnabled()) {
186            LOG.info(
187                Messages.get().getBundle().key(
188                    Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDING_EXTERNAL_TARGET_1,
189                    m_extTarget));
190        }
191        m_rd.include(req, res);
192    }
193
194    /**
195     * Includes the requested resource, ignoring the Flex cache.<p>
196     *
197     * @param req the servlet request
198     * @param res the servlet response
199     * @param controller the flex controller
200     * @param cms the cms context
201     * @param resource the requested resource (may be <code>null</code>)
202     *
203     * @throws ServletException in case something goes wrong
204     * @throws IOException in case something goes wrong
205     */
206    private void includeInternalNoCache(
207        ServletRequest req,
208        ServletResponse res,
209        CmsFlexController controller,
210        CmsObject cms,
211        CmsResource resource) throws ServletException, IOException {
212
213        // load target with the internal resource loader
214        I_CmsResourceLoader loader;
215
216        try {
217            if (resource == null) {
218                resource = cms.readResource(m_vfsTarget);
219            }
220            if (LOG.isDebugEnabled()) {
221                LOG.debug(
222                    Messages.get().getBundle().key(
223                        Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_TYPE_1,
224                        new Integer(resource.getTypeId())));
225            }
226            loader = OpenCms.getResourceManager().getLoader(resource);
227        } catch (CmsException e) {
228            // file might not exist or no read permissions
229            controller.setThrowable(e, m_vfsTarget);
230            throw new ServletException(
231                Messages.get().getBundle().key(
232                    Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_READING_RESOURCE_1,
233                    m_vfsTarget),
234                e);
235        }
236
237        if (LOG.isDebugEnabled()) {
238            LOG.debug(
239                Messages.get().getBundle().key(Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDE_RESOURCE_1, m_vfsTarget));
240        }
241        try {
242            loader.service(cms, resource, req, res);
243        } catch (CmsException e) {
244            // an error occurred during access to OpenCms
245            controller.setThrowable(e, m_vfsTarget);
246            throw new ServletException(e);
247        }
248    }
249
250    /**
251     * Includes the requested resource, ignoring the Flex cache.<p>
252     *
253     * @param req the servlet request
254     * @param res the servlet response
255     * @param controller the Flex controller
256     * @param cms the current users OpenCms context
257     * @param resource the requested resource (may be <code>null</code>)
258     *
259     * @throws ServletException in case something goes wrong
260     * @throws IOException in case something goes wrong
261     */
262    private void includeInternalWithCache(
263        ServletRequest req,
264        ServletResponse res,
265        CmsFlexController controller,
266        CmsObject cms,
267        CmsResource resource) throws ServletException, IOException {
268
269        CmsFlexCache cache = controller.getCmsCache();
270
271        // this is a request through the CMS
272        CmsFlexRequest f_req = controller.getCurrentRequest();
273        CmsFlexResponse f_res = controller.getCurrentResponse();
274
275        if (f_req.exceedsCallLimit(m_vfsTarget)) {
276            // this resource was already included earlier, so we have a (probably endless) inclusion loop
277            throw new ServletException(
278                Messages.get().getBundle().key(Messages.ERR_FLEXREQUESTDISPATCHER_INCLUSION_LOOP_1, m_vfsTarget));
279        } else {
280            f_req.addInlucdeCall(m_vfsTarget);
281        }
282
283        // do nothing if response is already finished (probably as a result of an earlier redirect)
284        if (f_res.isSuspended()) {
285            // remove this include call if response is suspended (e.g. because of redirect)
286            f_res.setCmsIncludeMode(false);
287            f_req.removeIncludeCall(m_vfsTarget);
288            return;
289        }
290
291        // indicate to response that all further output or headers are result of include calls
292        f_res.setCmsIncludeMode(true);
293
294        // create wrapper for request & response
295        CmsFlexRequest w_req = new CmsFlexRequest((HttpServletRequest)req, controller, m_vfsTarget);
296        CmsFlexResponse w_res = new CmsFlexResponse((HttpServletResponse)res, controller);
297
298        // push req/res to controller stack
299        controller.push(w_req, w_res);
300
301        // now that the req/res are on the stack, we need to make sure that they are removed later
302        // that's why we have this try { ... } finally { ... } clause here
303        try {
304            CmsFlexCacheEntry entry = null;
305            if (f_req.isCacheable()) {
306                // caching is on, check if requested resource is already in cache
307                entry = cache.get(w_req.getCmsCacheKey());
308                if (entry != null) {
309                    // the target is already in the cache
310                    try {
311                        if (LOG.isDebugEnabled()) {
312                            LOG.debug(
313                                Messages.get().getBundle().key(
314                                    Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_FROM_CACHE_1,
315                                    m_vfsTarget));
316                        }
317                        controller.updateDates(entry.getDateLastModified(), entry.getDateExpires());
318                        entry.service(w_req, w_res);
319                    } catch (CmsException e) {
320                        Throwable t;
321                        if (e.getCause() != null) {
322                            t = e.getCause();
323                        } else {
324                            t = e;
325                        }
326                        t = controller.setThrowable(e, m_vfsTarget);
327                        throw new ServletException(
328                            Messages.get().getBundle().key(
329                                Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_LOADING_RESOURCE_FROM_CACHE_1,
330                                m_vfsTarget),
331                            t);
332                    }
333                } else {
334                    // cache is on and resource is not yet cached, so we need to read the cache key for the response
335                    CmsFlexCacheKey res_key = cache.getKey(CmsFlexCacheKey.getKeyName(m_vfsTarget, w_req.isOnline()));
336                    if (res_key != null) {
337                        // key already in cache, reuse it
338                        w_res.setCmsCacheKey(res_key);
339                    } else {
340                        // cache key is unknown, read key from properties
341                        String cacheProperty = null;
342                        try {
343                            // read caching property from requested VFS resource
344                            if (resource == null) {
345                                resource = cms.readResource(m_vfsTarget);
346                            }
347                            cacheProperty = cms.readPropertyObject(
348                                resource,
349                                CmsPropertyDefinition.PROPERTY_CACHE,
350                                true).getValue();
351                            if (cacheProperty == null) {
352                                // caching property not set, use default for resource type
353                                cacheProperty = OpenCms.getResourceManager().getResourceType(
354                                    resource.getTypeId()).getCachePropertyDefault();
355                            }
356                            cache.putKey(
357                                w_res.setCmsCacheKey(
358                                    cms.getRequestContext().addSiteRoot(m_vfsTarget),
359                                    cacheProperty,
360                                    f_req.isOnline()));
361                        } catch (CmsFlexCacheException e) {
362
363                            // invalid key is ignored but logged, used key is cache=never
364                            if (LOG.isWarnEnabled()) {
365                                LOG.warn(
366                                    Messages.get().getBundle().key(
367                                        Messages.LOG_FLEXREQUESTDISPATCHER_INVALID_CACHE_KEY_2,
368                                        m_vfsTarget,
369                                        cacheProperty));
370                            }
371                            // there will be a valid key in the response ("cache=never") even after an exception
372                            cache.putKey(w_res.getCmsCacheKey());
373                        } catch (CmsException e) {
374
375                            // all other errors are not handled here
376                            controller.setThrowable(e, m_vfsTarget);
377                            throw new ServletException(
378                                Messages.get().getBundle().key(
379                                    Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_LOADING_CACHE_PROPERTIES_1,
380                                    m_vfsTarget),
381                                e);
382                        }
383                        if (LOG.isDebugEnabled()) {
384                            LOG.debug(
385                                Messages.get().getBundle().key(
386                                    Messages.LOG_FLEXREQUESTDISPATCHER_ADDING_CACHE_PROPERTIES_2,
387                                    m_vfsTarget,
388                                    cacheProperty));
389                        }
390                    }
391                }
392            }
393
394            if (entry == null) {
395                // the target is not cached (or caching off), so load it with the internal resource loader
396                I_CmsResourceLoader loader = null;
397
398                String variation = null;
399                // check cache keys to see if the result can be cached
400                if (w_req.isCacheable()) {
401                    variation = w_res.getCmsCacheKey().matchRequestKey(w_req.getCmsCacheKey());
402                }
403                // indicate to the response if caching is not required
404                w_res.setCmsCachingRequired(!controller.isForwardMode() && (variation != null));
405
406                try {
407                    if (resource == null) {
408                        resource = cms.readResource(m_vfsTarget);
409                    }
410                    if (LOG.isDebugEnabled()) {
411                        LOG.debug(
412                            Messages.get().getBundle().key(
413                                Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_TYPE_1,
414                                new Integer(resource.getTypeId())));
415                    }
416                    loader = OpenCms.getResourceManager().getLoader(resource);
417                } catch (ClassCastException e) {
418                    controller.setThrowable(e, m_vfsTarget);
419                    throw new ServletException(
420                        Messages.get().getBundle().key(
421                            Messages.ERR_FLEXREQUESTDISPATCHER_CLASSCAST_EXCEPTION_1,
422                            m_vfsTarget),
423                        e);
424                } catch (CmsException e) {
425                    // file might not exist or no read permissions
426                    controller.setThrowable(e, m_vfsTarget);
427                    throw new ServletException(
428                        Messages.get().getBundle().key(
429                            Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_READING_RESOURCE_1,
430                            m_vfsTarget),
431                        e);
432                }
433
434                if (LOG.isDebugEnabled()) {
435                    LOG.debug(
436                        Messages.get().getBundle().key(
437                            Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDE_RESOURCE_1,
438                            m_vfsTarget));
439                }
440                try {
441                    loader.service(cms, resource, w_req, w_res);
442                } catch (CmsException e) {
443                    // an error occurred during access to OpenCms
444                    controller.setThrowable(e, m_vfsTarget);
445                    throw new ServletException(e);
446                }
447
448                entry = w_res.processCacheEntry();
449                if ((entry != null) && (variation != null) && w_req.isCacheable()) {
450                    // the result can be cached
451                    if (w_res.getCmsCacheKey().getTimeout() > 0) {
452                        // cache entry has a timeout, set last modified to time of last creation
453                        entry.setDateLastModifiedToPreviousTimeout(w_res.getCmsCacheKey().getTimeout());
454                        entry.setDateExpiresToNextTimeout(w_res.getCmsCacheKey().getTimeout());
455                        controller.updateDates(entry.getDateLastModified(), entry.getDateExpires());
456                    } else {
457                        // no timeout, use last modified date from files in VFS
458                        entry.setDateLastModified(controller.getDateLastModified());
459                        entry.setDateExpires(controller.getDateExpires());
460                    }
461                    cache.put(w_res.getCmsCacheKey(), entry, variation);
462                } else {
463                    // result can not be cached, do not use "last modified" optimization
464                    controller.updateDates(-1, controller.getDateExpires());
465                }
466            }
467
468            if (f_res.hasIncludeList()) {
469                // special case: this indicates that the output was not yet displayed
470                Map<String, List<String>> headers = w_res.getHeaders();
471                byte[] result = w_res.getWriterBytes();
472                if (LOG.isDebugEnabled()) {
473                    LOG.debug(
474                        Messages.get().getBundle().key(
475                            Messages.LOG_FLEXREQUESTDISPATCHER_RESULT_1,
476                            new String(result)));
477                }
478                CmsFlexResponse.processHeaders(headers, f_res);
479                f_res.addToIncludeResults(result);
480                result = null;
481            }
482        } finally {
483            // indicate to response that include is finished
484            f_res.setCmsIncludeMode(false);
485            f_req.removeIncludeCall(m_vfsTarget);
486
487            // pop req/res from controller stack
488            controller.pop();
489        }
490    }
491}