001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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 & Co. KG, 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)
212    throws ServletException, IOException {
213
214        // load target with the internal resource loader
215        I_CmsResourceLoader loader;
216
217        try {
218            if (resource == null) {
219                resource = cms.readResource(m_vfsTarget);
220            }
221            if (LOG.isDebugEnabled()) {
222                LOG.debug(
223                    Messages.get().getBundle().key(
224                        Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_TYPE_1,
225                        Integer.valueOf(resource.getTypeId())));
226            }
227            loader = OpenCms.getResourceManager().getLoader(resource);
228        } catch (CmsException e) {
229            // file might not exist or no read permissions
230            controller.setThrowable(e, m_vfsTarget);
231            throw new ServletException(
232                Messages.get().getBundle().key(
233                    Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_READING_RESOURCE_1,
234                    m_vfsTarget),
235                e);
236        }
237
238        if (LOG.isDebugEnabled()) {
239            LOG.debug(
240                Messages.get().getBundle().key(Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDE_RESOURCE_1, m_vfsTarget));
241        }
242        try {
243            loader.service(cms, resource, req, res);
244        } catch (CmsException e) {
245            // an error occurred during access to OpenCms
246            controller.setThrowable(e, m_vfsTarget);
247            throw new ServletException(e);
248        }
249    }
250
251    /**
252     * Includes the requested resource, ignoring the Flex cache.<p>
253     *
254     * @param req the servlet request
255     * @param res the servlet response
256     * @param controller the Flex controller
257     * @param cms the current users OpenCms context
258     * @param resource the requested resource (may be <code>null</code>)
259     *
260     * @throws ServletException in case something goes wrong
261     * @throws IOException in case something goes wrong
262     */
263    private void includeInternalWithCache(
264        ServletRequest req,
265        ServletResponse res,
266        CmsFlexController controller,
267        CmsObject cms,
268        CmsResource resource)
269    throws ServletException, IOException {
270
271        CmsFlexCache cache = controller.getCmsCache();
272
273        // this is a request through the CMS
274        CmsFlexRequest f_req = controller.getCurrentRequest();
275        CmsFlexResponse f_res = controller.getCurrentResponse();
276
277        if (f_req.exceedsCallLimit(m_vfsTarget)) {
278            // this resource was already included earlier, so we have a (probably endless) inclusion loop
279            throw new ServletException(
280                Messages.get().getBundle().key(Messages.ERR_FLEXREQUESTDISPATCHER_INCLUSION_LOOP_1, m_vfsTarget));
281        } else {
282            f_req.addInlucdeCall(m_vfsTarget);
283        }
284
285        // do nothing if response is already finished (probably as a result of an earlier redirect)
286        if (f_res.isSuspended()) {
287            // remove this include call if response is suspended (e.g. because of redirect)
288            f_res.setCmsIncludeMode(false);
289            f_req.removeIncludeCall(m_vfsTarget);
290            return;
291        }
292
293        // indicate to response that all further output or headers are result of include calls
294        f_res.setCmsIncludeMode(true);
295
296        // create wrapper for request & response
297        CmsFlexRequest w_req = new CmsFlexRequest((HttpServletRequest)req, controller, m_vfsTarget);
298        CmsFlexResponse w_res = new CmsFlexResponse((HttpServletResponse)res, controller);
299
300        // push req/res to controller stack
301        controller.push(w_req, w_res);
302
303        // now that the req/res are on the stack, we need to make sure that they are removed later
304        // that's why we have this try { ... } finally { ... } clause here
305        try {
306            CmsFlexCacheEntry entry = null;
307            if (f_req.isCacheable()) {
308                // caching is on, check if requested resource is already in cache
309                entry = cache.get(w_req.getCmsCacheKey());
310                if (entry != null) {
311                    // the target is already in the cache
312                    try {
313                        if (LOG.isDebugEnabled()) {
314                            LOG.debug(
315                                Messages.get().getBundle().key(
316                                    Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_FROM_CACHE_1,
317                                    m_vfsTarget));
318                        }
319                        controller.updateDates(entry.getDateLastModified(), entry.getDateExpires());
320                        entry.service(w_req, w_res);
321                    } catch (CmsException e) {
322                        Throwable t;
323                        if (e.getCause() != null) {
324                            t = e.getCause();
325                        } else {
326                            t = e;
327                        }
328                        t = controller.setThrowable(e, m_vfsTarget);
329                        throw new ServletException(
330                            Messages.get().getBundle().key(
331                                Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_LOADING_RESOURCE_FROM_CACHE_1,
332                                m_vfsTarget),
333                            t);
334                    }
335                } else {
336                    // cache is on and resource is not yet cached, so we need to read the cache key for the response
337                    CmsFlexCacheKey res_key = cache.getKey(CmsFlexCacheKey.getKeyName(m_vfsTarget, w_req.isOnline()));
338                    if (res_key != null) {
339                        // key already in cache, reuse it
340                        w_res.setCmsCacheKey(res_key);
341                    } else {
342                        // cache key is unknown, read key from properties
343                        String cacheProperty = null;
344                        try {
345                            // read caching property from requested VFS resource
346                            if (resource == null) {
347                                resource = cms.readResource(m_vfsTarget);
348                            }
349                            cacheProperty = cms.readPropertyObject(
350                                resource,
351                                CmsPropertyDefinition.PROPERTY_CACHE,
352                                true).getValue();
353                            if (cacheProperty == null) {
354                                // caching property not set, use default for resource type
355                                cacheProperty = OpenCms.getResourceManager().getResourceType(
356                                    resource.getTypeId()).getCachePropertyDefault();
357                            }
358                            cache.putKey(
359                                w_res.setCmsCacheKey(
360                                    cms.getRequestContext().addSiteRoot(m_vfsTarget),
361                                    cacheProperty,
362                                    f_req.isOnline()));
363                        } catch (CmsFlexCacheException e) {
364
365                            // invalid key is ignored but logged, used key is cache=never
366                            if (LOG.isWarnEnabled()) {
367                                LOG.warn(
368                                    Messages.get().getBundle().key(
369                                        Messages.LOG_FLEXREQUESTDISPATCHER_INVALID_CACHE_KEY_2,
370                                        m_vfsTarget,
371                                        cacheProperty));
372                            }
373                            // there will be a valid key in the response ("cache=never") even after an exception
374                            cache.putKey(w_res.getCmsCacheKey());
375                        } catch (CmsException e) {
376
377                            // all other errors are not handled here
378                            controller.setThrowable(e, m_vfsTarget);
379                            throw new ServletException(
380                                Messages.get().getBundle().key(
381                                    Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_LOADING_CACHE_PROPERTIES_1,
382                                    m_vfsTarget),
383                                e);
384                        }
385                        if (LOG.isDebugEnabled()) {
386                            LOG.debug(
387                                Messages.get().getBundle().key(
388                                    Messages.LOG_FLEXREQUESTDISPATCHER_ADDING_CACHE_PROPERTIES_2,
389                                    m_vfsTarget,
390                                    cacheProperty));
391                        }
392                    }
393                }
394            }
395
396            if (entry == null) {
397                boolean ignore = (w_res.getCmsCacheKey() != null) && w_res.getCmsCacheKey().isIgnore();
398                // the target is not cached (or caching off), so load it with the internal resource loader
399                I_CmsResourceLoader loader = null;
400
401                String variation = null;
402                // check cache keys to see if the result can be cached
403                if (w_req.isCacheable()) {
404                    variation = w_res.getCmsCacheKey().matchRequestKey(w_req.getCmsCacheKey());
405                }
406                // indicate to the response if caching is not required
407                w_res.setCmsCachingRequired(!controller.isForwardMode() && (variation != null));
408
409                try {
410                    if (resource == null) {
411                        resource = cms.readResource(m_vfsTarget);
412                    }
413                    if (LOG.isDebugEnabled()) {
414                        LOG.debug(
415                            Messages.get().getBundle().key(
416                                Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_TYPE_1,
417                                Integer.valueOf(resource.getTypeId())));
418                    }
419                    loader = OpenCms.getResourceManager().getLoader(resource);
420                } catch (ClassCastException e) {
421                    controller.setThrowable(e, m_vfsTarget);
422                    throw new ServletException(
423                        Messages.get().getBundle().key(
424                            Messages.ERR_FLEXREQUESTDISPATCHER_CLASSCAST_EXCEPTION_1,
425                            m_vfsTarget),
426                        e);
427                } catch (CmsException e) {
428                    // file might not exist or no read permissions
429                    controller.setThrowable(e, m_vfsTarget);
430                    throw new ServletException(
431                        Messages.get().getBundle().key(
432                            Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_READING_RESOURCE_1,
433                            m_vfsTarget),
434                        e);
435                }
436
437                if (LOG.isDebugEnabled()) {
438                    LOG.debug(
439                        Messages.get().getBundle().key(
440                            Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDE_RESOURCE_1,
441                            m_vfsTarget));
442                }
443                try {
444                    loader.service(cms, resource, w_req, w_res);
445                } catch (Exception e) {
446                    LOG.error(e.getLocalizedMessage(), e);
447                    // an error occurred
448                    if (f_res.hasIncludeList()) {
449                        // to prevent include list and include result list indices going out of sync, add an empty byte array
450                        f_res.addToIncludeResults(new byte[] {});
451                    }
452                    controller.setThrowable(e, m_vfsTarget);
453                    throw new ServletException(e);
454                }
455
456                entry = w_res.processCacheEntry();
457                if ((entry != null) && (variation != null) && w_req.isCacheable()) {
458                    // the result can be cached
459                    if (w_res.getCmsCacheKey().getTimeout() > 0) {
460                        // cache entry has a timeout, set last modified to time of last creation
461                        entry.setDateLastModifiedToPreviousTimeout(w_res.getCmsCacheKey().getTimeout());
462                        entry.setDateExpiresToNextTimeout(w_res.getCmsCacheKey().getTimeout());
463                        // if expiration date from controller comes before timeout, don't wait until timeout
464                        entry.limitDateExpires(controller.getDateExpires());
465                        controller.updateDates(entry.getDateLastModified(), entry.getDateExpires());
466                    } else {
467                        // no timeout, use last modified date from files in VFS
468                        entry.setDateLastModified(controller.getDateLastModified());
469                        entry.setDateExpires(controller.getDateExpires());
470                    }
471                    cache.put(w_res.getCmsCacheKey(), entry, variation, w_req.getCmsCacheKey());
472                } else if (!ignore) {
473                    // result can not be cached, do not use "last modified" optimization
474                    controller.updateDates(-1, controller.getDateExpires());
475                }
476            }
477
478            if (f_res.hasIncludeList()) {
479                // special case: this indicates that the output was not yet displayed
480                Map<String, List<String>> headers = w_res.getHeaders();
481                byte[] result = w_res.getWriterBytes();
482                if (LOG.isDebugEnabled()) {
483                    LOG.debug(
484                        Messages.get().getBundle().key(
485                            Messages.LOG_FLEXREQUESTDISPATCHER_RESULT_1,
486                            new String(result)));
487                }
488                CmsFlexResponse.processHeaders(headers, f_res);
489                f_res.addToIncludeResults(result);
490                result = null;
491            }
492        } finally {
493            // indicate to response that include is finished
494            f_res.setCmsIncludeMode(false);
495            f_req.removeIncludeCall(m_vfsTarget);
496
497            // pop req/res from controller stack
498            controller.pop();
499        }
500    }
501
502}