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.debug(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}