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.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(Messages.get().getBundle().key( 279 Messages.ERR_HEADER_IFMODIFIEDSINCE_FORMAT_3, 280 new Object[] { 281 CmsRequestUtil.HEADER_IF_MODIFIED_SINCE, 282 req.getHeader(CmsRequestUtil.HEADER_USER_AGENT), 283 req.getHeader(CmsRequestUtil.HEADER_IF_MODIFIED_SINCE)})); 284 } 285 return false; 286 } 287 288 /** 289 * Tells the flex controller to never cache the given attribute.<p> 290 * 291 * @param attributeName the attribute which shouldn't be cached 292 */ 293 public static void registerUncacheableAttribute(String attributeName) { 294 295 uncacheableAttributes.add(attributeName); 296 } 297 298 /** 299 * Removes the controller attribute from a request.<p> 300 * 301 * @param req the request to remove the controller from 302 */ 303 public static void removeController(ServletRequest req) { 304 305 CmsFlexController controller = (CmsFlexController)req.getAttribute(ATTRIBUTE_NAME); 306 if (controller != null) { 307 controller.clear(); 308 } 309 } 310 311 /** 312 * Stores the given controller in the given request (using a request attribute).<p> 313 * 314 * @param req the request where to store the controller in 315 * @param controller the controller to store 316 */ 317 public static void setController(ServletRequest req, CmsFlexController controller) { 318 319 req.setAttribute(CmsFlexController.ATTRIBUTE_NAME, controller); 320 } 321 322 /** 323 * Sets the <code>Expires</code> date header for a given http request.<p> 324 * 325 * Also sets the <code>cache-control: max-age</code> header to the time of the expiration. 326 * A certain upper limit is imposed on the expiration date parameter to ensure the resources are 327 * not cached to long in proxies. This can be controlled by the <code>maxAge</code> parameter. 328 * If <code>maxAge</code> is lower then 0, then a default max age of 86400000 msec (1 day) is used.<p> 329 * 330 * @param res the response to set the "Expires" date header for 331 * @param maxAge maximum amount of time in milliseconds the response remains valid 332 * @param dateExpires the date to set (if this is not in the future, it is ignored) 333 */ 334 public static void setDateExpiresHeader(HttpServletResponse res, long dateExpires, long maxAge) { 335 336 long now = System.currentTimeMillis(); 337 if ((dateExpires > now) && (dateExpires != CmsResource.DATE_EXPIRED_DEFAULT)) { 338 339 // important: many caches (browsers or proxy) use the "Expires" header 340 // to avoid re-loading of pages that are not expired 341 // while this is right in general, no changes before the expiration date 342 // will be displayed 343 // therefore it is better to not use an expiration to far in the future 344 345 // if no valid max age is set, restrict it to 24 hrs 346 if (maxAge < 0L) { 347 maxAge = 86400000; 348 } 349 350 if ((dateExpires - now) > maxAge) { 351 // set "Expires" header max one day into the future 352 dateExpires = now + maxAge; 353 } 354 res.setDateHeader(CmsRequestUtil.HEADER_EXPIRES, dateExpires); 355 356 // setting the "Expires" header only is not sufficient - even expired documents seems to be cached 357 // therefore, the "cache-control: max-age" is also set 358 res.setHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_MAX_AGE + (maxAge / 1000L)); 359 } 360 } 361 362 /** 363 * Sets the "last modified" date header for a given http request.<p> 364 * 365 * @param res the response to set the "last modified" date header for 366 * @param dateLastModified the date to set (if this is lower then 0, the current time is set) 367 */ 368 public static void setDateLastModifiedHeader(HttpServletResponse res, long dateLastModified) { 369 370 if (dateLastModified > -1) { 371 // set date last modified header (precision is only second, not millisecond 372 res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, (dateLastModified / 1000) * 1000); 373 } else { 374 // this resource can not be optimized for "last modified", use current time as header 375 res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis()); 376 // avoiding issues with IE8+ 377 res.setHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, "public, max-age=0"); 378 } 379 } 380 381 /** 382 * Clears all data of this controller.<p> 383 */ 384 public void clear() { 385 386 if (m_flexRequestList != null) { 387 m_flexRequestList.clear(); 388 } 389 m_flexRequestList = null; 390 if (m_flexResponseList != null) { 391 m_flexResponseList.clear(); 392 } 393 m_flexResponseList = null; 394 if (m_req != null) { 395 m_req.removeAttribute(ATTRIBUTE_NAME); 396 } 397 m_req = null; 398 m_res = null; 399 m_cmsObject = null; 400 m_resource = null; 401 m_cache = null; 402 m_throwable = null; 403 } 404 405 /** 406 * Returns the CmsFlexCache instance where all results from this request will be cached in.<p> 407 * 408 * This is public so that pages like the Flex Cache Administration page 409 * have a way to access the cache object.<p> 410 * 411 * @return the CmsFlexCache instance where all results from this request will be cached in 412 */ 413 public CmsFlexCache getCmsCache() { 414 415 return m_cache; 416 } 417 418 /** 419 * Returns the wrapped CmsObject.<p> 420 * 421 * @return the wrapped CmsObject 422 */ 423 public CmsObject getCmsObject() { 424 425 return m_cmsObject; 426 } 427 428 /** 429 * This method provides access to the top-level CmsResource of the request 430 * which is of a type that supports the FlexCache, 431 * i.e. usually the CmsFile that is identical to the file uri requested by the user, 432 * not he current included element.<p> 433 * 434 * @return the requested top-level CmsFile 435 */ 436 public CmsResource getCmsResource() { 437 438 return m_resource; 439 } 440 441 /** 442 * Returns the current flex request.<p> 443 * 444 * @return the current flex request 445 */ 446 public CmsFlexRequest getCurrentRequest() { 447 448 return m_flexRequestList.get(m_flexRequestList.size() - 1); 449 } 450 451 /** 452 * Returns the current flex response.<p> 453 * 454 * @return the current flex response 455 */ 456 public CmsFlexResponse getCurrentResponse() { 457 458 return m_flexResponseList.get(m_flexResponseList.size() - 1); 459 } 460 461 /** 462 * Returns the combined "expires" date for all resources read during this request.<p> 463 * 464 * @return the combined "expires" date for all resources read during this request 465 */ 466 public long getDateExpires() { 467 468 int pos = m_flexContextInfoList.size() - 1; 469 if (pos < 0) { 470 // ensure a valid position is used 471 return CmsResource.DATE_EXPIRED_DEFAULT; 472 } 473 return (m_flexContextInfoList.get(pos)).getDateExpires(); 474 } 475 476 /** 477 * Returns the combined "last modified" date for all resources read during this request.<p> 478 * 479 * @return the combined "last modified" date for all resources read during this request 480 */ 481 public long getDateLastModified() { 482 483 int pos = m_flexContextInfoList.size() - 1; 484 if (pos < 0) { 485 // ensure a valid position is used 486 return CmsResource.DATE_RELEASED_DEFAULT; 487 } 488 return (m_flexContextInfoList.get(pos)).getDateLastModified(); 489 } 490 491 /** 492 * Returns the size of the response stack.<p> 493 * 494 * @return the size of the response stack 495 */ 496 public int getResponseStackSize() { 497 498 return m_flexResponseList.size(); 499 } 500 501 /** 502 * Returns an exception (Throwable) that was caught during inclusion of sub elements, 503 * or null if no exceptions where thrown in sub elements.<p> 504 * 505 * @return an exception (Throwable) that was caught during inclusion of sub elements 506 */ 507 public Throwable getThrowable() { 508 509 return m_throwable; 510 } 511 512 /** 513 * Returns the URI of a VFS resource that caused the exception that was caught during inclusion of sub elements, 514 * might return null if no URI information was available for the exception.<p> 515 * 516 * @return the URI of a VFS resource that caused the exception that was caught during inclusion of sub elements 517 */ 518 public String getThrowableResourceUri() { 519 520 return m_throwableResourceUri; 521 } 522 523 /** 524 * Returns the current http request.<p> 525 * 526 * @return the current http request 527 */ 528 public HttpServletRequest getTopRequest() { 529 530 return m_req; 531 } 532 533 /** 534 * Returns the current http response.<p> 535 * 536 * @return the current http response 537 */ 538 public HttpServletResponse getTopResponse() { 539 540 return m_res; 541 } 542 543 /** 544 * Returns <code>true</code> if the controller does not yet contain any requests.<p> 545 * 546 * @return <code>true</code> if the controller does not yet contain any requests 547 */ 548 public boolean isEmptyRequestList() { 549 550 return (m_flexRequestList != null) && m_flexRequestList.isEmpty(); 551 } 552 553 /** 554 * Returns <code>true</code> if this controller is currently in "forward" mode.<p> 555 * 556 * @return <code>true</code> if this controller is currently in "forward" mode 557 */ 558 public boolean isForwardMode() { 559 560 return m_forwardMode; 561 } 562 563 /** 564 * Returns <code>true</code> if the generated output of the response should 565 * be written to the stream directly.<p> 566 * 567 * @return <code>true</code> if the generated output of the response should be written to the stream directly 568 */ 569 public boolean isStreaming() { 570 571 return m_streaming; 572 } 573 574 /** 575 * Returns <code>true</code> if this controller was generated as top level controller.<p> 576 * 577 * If a resource (e.g. a JSP) is processed and it's content is included in 578 * another resource, then this will be <code>false</code>. 579 * 580 * @return <code>true</code> if this controller was generated as top level controller 581 * 582 * @see org.opencms.loader.I_CmsResourceLoader#dump(CmsObject, CmsResource, String, java.util.Locale, HttpServletRequest, HttpServletResponse) 583 * @see org.opencms.jsp.CmsJspActionElement#getContent(String) 584 */ 585 public boolean isTop() { 586 587 return m_top; 588 } 589 590 /** 591 * Removes the topmost request/response pair from the stack.<p> 592 */ 593 public void pop() { 594 595 if ((m_flexRequestList != null) && !m_flexRequestList.isEmpty()) { 596 m_flexRequestList.remove(m_flexRequestList.size() - 1); 597 } 598 if ((m_flexResponseList != null) && !m_flexRequestList.isEmpty()) { 599 m_flexResponseList.remove(m_flexResponseList.size() - 1); 600 } 601 if ((m_flexContextInfoList != null) && !m_flexContextInfoList.isEmpty()) { 602 CmsFlexRequestContextInfo info = m_flexContextInfoList.remove(m_flexContextInfoList.size() - 1); 603 if (m_flexContextInfoList.size() > 0) { 604 (m_flexContextInfoList.get(0)).merge(info); 605 updateRequestContextInfo(); 606 } 607 } 608 } 609 610 /** 611 * Adds another flex request/response pair to the stack.<p> 612 * 613 * @param req the request to add 614 * @param res the response to add 615 */ 616 public void push(CmsFlexRequest req, CmsFlexResponse res) { 617 618 m_flexRequestList.add(req); 619 m_flexResponseList.add(res); 620 m_flexContextInfoList.add(new CmsFlexRequestContextInfo()); 621 updateRequestContextInfo(); 622 } 623 624 /** 625 * Removes request attributes which shouldn't be cached in flex cache entries from a map.<p> 626 * 627 * @param attributeMap the map of attributes 628 */ 629 public void removeUncacheableAttributes(Map<String, Object> attributeMap) { 630 631 for (String uncacheableAttribute : uncacheableAttributes) { 632 attributeMap.remove(uncacheableAttribute); 633 } 634 attributeMap.remove(CmsFlexController.ATTRIBUTE_NAME); 635 attributeMap.remove(CmsDetailPageResourceHandler.ATTR_DETAIL_CONTENT_RESOURCE); 636 } 637 638 /** 639 * Sets the value of the "forward mode" flag.<p> 640 * 641 * @param value the forward mode to set 642 */ 643 public void setForwardMode(boolean value) { 644 645 m_forwardMode = value; 646 } 647 648 /** 649 * Sets an exception (Throwable) that was caught during inclusion of sub elements.<p> 650 * 651 * If another exception is already set in this controller, then the additional exception 652 * is ignored.<p> 653 * 654 * @param throwable the exception (Throwable) to set 655 * @param resource the URI of the VFS resource the error occurred on (might be <code>null</code> if unknown) 656 * 657 * @return the exception stored in the controller 658 */ 659 public Throwable setThrowable(Throwable throwable, String resource) { 660 661 if (m_throwable == null) { 662 m_throwable = throwable; 663 m_throwableResourceUri = resource; 664 } else { 665 if (LOG.isDebugEnabled()) { 666 if (resource != null) { 667 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCONTROLLER_IGNORED_EXCEPTION_1, resource)); 668 } else { 669 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCONTROLLER_IGNORED_EXCEPTION_0)); 670 } 671 } 672 } 673 return m_throwable; 674 } 675 676 /** 677 * Puts the response in a suspended state.<p> 678 */ 679 public void suspendFlexResponse() { 680 681 for (int i = 0; i < m_flexResponseList.size(); i++) { 682 CmsFlexResponse res = m_flexResponseList.get(i); 683 res.setSuspended(true); 684 } 685 } 686 687 /** 688 * Updates the "last modified" date and the "expires" date 689 * for all resources read during this request with the given values.<p> 690 * 691 * The currently stored value for "last modified" is only updated with the new value if 692 * the new value is either larger (i.e. newer) then the stored value, 693 * or if the new value is less then zero, which indicates that the "last modified" 694 * optimization can not be used because the element is dynamic.<p> 695 * 696 * The stored "expires" value is only updated if the new value is smaller 697 * then the stored value.<p> 698 * 699 * @param dateLastModified the value to update the "last modified" date with 700 * @param dateExpires the value to update the "expires" date with 701 */ 702 public void updateDates(long dateLastModified, long dateExpires) { 703 704 int pos = m_flexContextInfoList.size() - 1; 705 if (pos < 0) { 706 // ensure a valid position is used 707 return; 708 } 709 (m_flexContextInfoList.get(pos)).updateDates(dateLastModified, dateExpires); 710 } 711 712 /** 713 * Updates the context info of the request context.<p> 714 */ 715 private void updateRequestContextInfo() { 716 717 if ((m_flexContextInfoList != null) && !m_flexContextInfoList.isEmpty()) { 718 m_cmsObject.getRequestContext().setAttribute( 719 CmsRequestUtil.HEADER_LAST_MODIFIED, 720 m_flexContextInfoList.get(m_flexContextInfoList.size() - 1)); 721 } 722 } 723}