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