001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://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.loader; 029 030import org.opencms.configuration.CmsParameterConfiguration; 031import org.opencms.file.CmsFile; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsPropertyDefinition; 034import org.opencms.file.CmsRequestContext; 035import org.opencms.file.CmsResource; 036import org.opencms.file.CmsResourceFilter; 037import org.opencms.file.CmsVfsResourceNotFoundException; 038import org.opencms.file.history.CmsHistoryResourceHandler; 039import org.opencms.flex.CmsFlexCache; 040import org.opencms.flex.CmsFlexController; 041import org.opencms.flex.CmsFlexController.RedirectInfo; 042import org.opencms.flex.CmsFlexRequest; 043import org.opencms.flex.CmsFlexResponse; 044import org.opencms.gwt.shared.CmsGwtConstants; 045import org.opencms.i18n.CmsEncoder; 046import org.opencms.i18n.CmsMessageContainer; 047import org.opencms.jsp.CmsJspTagEnableAde; 048import org.opencms.jsp.jsonpart.CmsJsonPartFilter; 049import org.opencms.jsp.util.CmsJspLinkMacroResolver; 050import org.opencms.jsp.util.CmsJspStandardContextBean; 051import org.opencms.main.CmsEvent; 052import org.opencms.main.CmsException; 053import org.opencms.main.CmsLog; 054import org.opencms.main.I_CmsEventListener; 055import org.opencms.main.OpenCms; 056import org.opencms.monitor.CmsMemoryMonitor; 057import org.opencms.relations.CmsRelation; 058import org.opencms.relations.CmsRelationFilter; 059import org.opencms.relations.CmsRelationType; 060import org.opencms.staticexport.CmsLinkManager; 061import org.opencms.util.CmsFileUtil; 062import org.opencms.util.CmsRequestUtil; 063import org.opencms.util.CmsStringUtil; 064import org.opencms.util.I_CmsRegexSubstitution; 065import org.opencms.workplace.CmsWorkplaceManager; 066 067import java.io.File; 068import java.io.FileNotFoundException; 069import java.io.FileOutputStream; 070import java.io.IOException; 071import java.io.UnsupportedEncodingException; 072import java.io.Writer; 073import java.net.SocketException; 074import java.util.Collection; 075import java.util.Collections; 076import java.util.HashMap; 077import java.util.HashSet; 078import java.util.Iterator; 079import java.util.LinkedHashSet; 080import java.util.Locale; 081import java.util.Map; 082import java.util.Set; 083import java.util.concurrent.locks.ReentrantReadWriteLock; 084import java.util.regex.Matcher; 085import java.util.regex.Pattern; 086 087import org.apache.commons.lang3.exception.ExceptionUtils; 088import org.apache.commons.logging.Log; 089 090import com.google.common.base.Splitter; 091 092import jakarta.servlet.ServletException; 093import jakarta.servlet.ServletRequest; 094import jakarta.servlet.ServletResponse; 095import jakarta.servlet.http.HttpServletRequest; 096import jakarta.servlet.http.HttpServletResponse; 097 098/** 099 * The JSP loader which enables the execution of JSP in OpenCms.<p> 100 * 101 * Parameters supported by this loader:<dl> 102 * 103 * <dt>jsp.repository</dt><dd> 104 * (Optional) This is the root directory in the "real" file system where generated JSPs are stored. 105 * The default is the web application path, e.g. in Tomcat if your web application is 106 * names "opencms" it would be <code>${TOMCAT_HOME}/webapps/opencms/</code>. 107 * The <code>jsp.folder</code> (see below) is added to this path. 108 * Usually the <code>jsp.repository</code> is not changed. 109 * </dd> 110 * 111 * <dt>jsp.folder</dt><dd> 112 * (Optional) A path relative to the <code>jsp.repository</code> path where the 113 * JSPs generated by OpenCms are stored. The default is to store the generated JSP in 114 * <code>/WEB-INF/jsp/</code>. 115 * This works well in Tomcat 4, and the JSPs are 116 * not accessible directly from the outside this way, only through the OpenCms servlet. 117 * <i>Please note:</i> Some servlet environments (e.g. BEA Weblogic) do not permit 118 * JSPs to be stored under <code>/WEB-INF</code>. For environments like these, 119 * set the path to some place where JSPs can be accessed, e.g. <code>/jsp/</code> only. 120 * </dd> 121 * 122 * <dt>jsp.errorpage.committed</dt><dd> 123 * (Optional) This parameter controls behavior of JSP error pages 124 * i.e. <code><% page errorPage="..." %></code>. If you find that these don't work 125 * in your servlet environment, you should try to change the value here. 126 * The default <code>true</code> has been tested with Tomcat 4.1 and 5.0. 127 * Older versions of Tomcat like 4.0 require a setting of <code>false</code>.</dd> 128 * </dl> 129 * 130 * @since 6.0.0 131 * 132 * @see I_CmsResourceLoader 133 */ 134public class CmsJspLoader implements I_CmsResourceLoader, I_CmsFlexCacheEnabledLoader, I_CmsEventListener { 135 136 /** Property value for "cache" that indicates that the FlexCache should be bypassed. */ 137 public static final String CACHE_PROPERTY_BYPASS = "bypass"; 138 139 /** Property value for "cache" that indicates that the output should be streamed. */ 140 public static final String CACHE_PROPERTY_STREAM = "stream"; 141 142 /** Default jsp folder constant. */ 143 public static final String DEFAULT_JSP_FOLDER = "/WEB-INF/jsp/"; 144 145 /** Special JSP directive tag start (<code>%></code>). */ 146 public static final String DIRECTIVE_END = "%>"; 147 148 /** Special JSP directive tag start (<code><%(</code>). */ 149 public static final String DIRECTIVE_START = "<%@"; 150 151 /** Extension for JSP managed by OpenCms (<code>.jsp</code>). */ 152 public static final String JSP_EXTENSION = ".jsp"; 153 154 /** Cache max age parameter name. */ 155 public static final String PARAM_CLIENT_CACHE_MAXAGE = "client.cache.maxage"; 156 157 /** Jsp cache size parameter name. */ 158 public static final String PARAM_JSP_CACHE_SIZE = "jsp.cache.size"; 159 160 /** Error page committed parameter name. */ 161 public static final String PARAM_JSP_ERRORPAGE_COMMITTED = "jsp.errorpage.committed"; 162 163 /** Jsp folder parameter name. */ 164 public static final String PARAM_JSP_FOLDER = "jsp.folder"; 165 166 /** Jsp repository parameter name. */ 167 public static final String PARAM_JSP_REPOSITORY = "jsp.repository"; 168 169 /** The id of this loader. */ 170 public static final int RESOURCE_LOADER_ID = 6; 171 172 /** The log object for this class. */ 173 private static final Log LOG = CmsLog.getLog(CmsJspLoader.class); 174 175 /** The maximum age for delivered contents in the clients cache. */ 176 private static long m_clientCacheMaxAge; 177 178 /** Read write locks for jsp files. */ 179 private static Map<String, ReentrantReadWriteLock> m_fileLocks = CmsMemoryMonitor.createLRUCacheMap(10000); 180 181 /** The directory to store the generated JSP pages in (absolute path). */ 182 private static String m_jspRepository; 183 184 /** The directory to store the generated JSP pages in (relative path in web application). */ 185 private static String m_jspWebAppRepository; 186 187 /** The CmsFlexCache used to store generated cache entries in. */ 188 private CmsFlexCache m_cache; 189 190 /** The resource loader configuration. */ 191 private CmsParameterConfiguration m_configuration; 192 193 /** Flag to indicate if error pages are marked as "committed". */ 194 private boolean m_errorPagesAreNotCommitted; 195 196 /** The offline JSPs. */ 197 private Map<String, Boolean> m_offlineJsps; 198 199 /** The online JSPs. */ 200 private Map<String, Boolean> m_onlineJsps; 201 202 /** A map from taglib names to their URIs. */ 203 private Map<String, String> m_taglibs = new HashMap<String, String>(); 204 205 /** Lock used to prevent JSP repository from being accessed while it is purged. The read lock is needed for accessing the JSP repository, the write lock is needed for purging it. */ 206 private ReentrantReadWriteLock m_purgeLock = new ReentrantReadWriteLock(true); 207 208 /** 209 * The constructor of the class is empty, the initial instance will be 210 * created by the resource manager upon startup of OpenCms.<p> 211 * 212 * @see org.opencms.loader.CmsResourceManager 213 */ 214 public CmsJspLoader() { 215 216 m_configuration = new CmsParameterConfiguration(); 217 OpenCms.addCmsEventListener( 218 this, 219 new int[] {EVENT_CLEAR_CACHES, EVENT_CLEAR_OFFLINE_CACHES, EVENT_CLEAR_ONLINE_CACHES}); 220 m_fileLocks = CmsMemoryMonitor.createLRUCacheMap(10000); 221 initCaches(1000); 222 } 223 224 /** 225 * This method tries to determine whether an exception is thrown by the JSP compiler. 226 * 227 * @param exception the exception to check 228 * @return true if this is likely a Jasper JSP compiler exception 229 */ 230 public static boolean isJasperCompilerException(Throwable exception) { 231 232 if (exception == null) { 233 return false; 234 } 235 236 for (Throwable t : ExceptionUtils.getThrowableList(exception)) { 237 if (t.getClass().getName().equals("org.apache.jasper.JasperException")) { 238 for (StackTraceElement elem : t.getStackTrace()) { 239 if (elem.getClassName().startsWith("org.apache.jasper.compiler.")) { 240 return true; 241 } 242 } 243 } 244 } 245 return false; 246 } 247 248 /** 249 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String) 250 */ 251 public void addConfigurationParameter(String paramName, String paramValue) { 252 253 m_configuration.add(paramName, paramValue); 254 if (paramName.startsWith("taglib.")) { 255 m_taglibs.put(paramName.replaceFirst("^taglib\\.", ""), paramValue.trim()); 256 } 257 } 258 259 /** 260 * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent) 261 */ 262 public void cmsEvent(CmsEvent event) { 263 264 switch (event.getType()) { 265 case EVENT_CLEAR_CACHES: 266 m_offlineJsps.clear(); 267 m_onlineJsps.clear(); 268 return; 269 case EVENT_CLEAR_OFFLINE_CACHES: 270 m_offlineJsps.clear(); 271 return; 272 case EVENT_CLEAR_ONLINE_CACHES: 273 m_onlineJsps.clear(); 274 return; 275 default: 276 // do nothing 277 } 278 } 279 280 /** 281 * Destroy this ResourceLoder, this is a NOOP so far. 282 */ 283 public void destroy() { 284 285 // NOOP 286 } 287 288 /** 289 * @see org.opencms.loader.I_CmsResourceLoader#dump(org.opencms.file.CmsObject, org.opencms.file.CmsResource, java.lang.String, java.util.Locale, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 290 */ 291 public byte[] dump( 292 CmsObject cms, 293 CmsResource file, 294 String element, 295 Locale locale, 296 HttpServletRequest req, 297 HttpServletResponse res) 298 throws ServletException, IOException { 299 300 // get the current Flex controller 301 CmsFlexController controller = CmsFlexController.getController(req); 302 CmsFlexController oldController = null; 303 304 if (controller != null) { 305 // for dumping we must create an new "top level" controller, save the old one to be restored later 306 oldController = controller; 307 } 308 309 byte[] result = null; 310 try { 311 // now create a new, temporary Flex controller 312 controller = getController(cms, file, req, res, false, false); 313 if (element != null) { 314 // add the element parameter to the included request 315 String[] value = new String[] {element}; 316 Map<String, String[]> parameters = Collections.singletonMap( 317 I_CmsResourceLoader.PARAMETER_ELEMENT, 318 value); 319 controller.getCurrentRequest().addParameterMap(parameters); 320 } 321 Map<String, Object> attrs = controller.getCurrentRequest().addAttributeMap( 322 CmsRequestUtil.getAtrributeMap(req)); 323 // dispatch to the JSP 324 result = dispatchJsp(controller); 325 326 // the standard context bean still references the nested request, we need to reset it to the old request 327 // (using the nested request is bad because it references the flex controller that is going to be nulled out by removeController(), so operations 328 // which use the flex controller might fail). 329 330 CmsJspStandardContextBean standardContext = (CmsJspStandardContextBean)attrs.get( 331 CmsJspStandardContextBean.ATTRIBUTE_NAME); 332 CmsFlexRequest flexRequest = CmsRequestUtil.getFlexRequest(req); 333 if ((standardContext != null) && (flexRequest != null)) { 334 standardContext.updateRequestData(flexRequest); 335 } 336 // remove temporary controller 337 CmsFlexController.removeController(req); 338 } finally { 339 if ((oldController != null) && (controller != null)) { 340 // update "date last modified" 341 oldController.updateDates(controller.getDateLastModified(), controller.getDateExpires()); 342 if (controller.getRedirectInfo() != null) { 343 oldController.setRedirectInfo(controller.getRedirectInfo()); 344 } 345 // reset saved controller 346 CmsFlexController.setController(req, oldController); 347 } 348 } 349 350 return result; 351 } 352 353 /** 354 * @see org.opencms.loader.I_CmsResourceLoader#export(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 355 */ 356 public byte[] export(CmsObject cms, CmsResource resource, HttpServletRequest req, HttpServletResponse res) 357 throws ServletException, IOException { 358 359 // get the Flex controller 360 CmsFlexController controller = getController(cms, resource, req, res, false, true); 361 362 // dispatch to the JSP 363 byte[] result = dispatchJsp(controller); 364 365 // remove the controller from the request 366 CmsFlexController.removeController(req); 367 368 // return the contents 369 return result; 370 } 371 372 /** 373 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 374 */ 375 public CmsParameterConfiguration getConfiguration() { 376 377 // return the configuration in an immutable form 378 return m_configuration; 379 } 380 381 /** 382 * Returns the absolute path in the "real" file system for the JSP repository 383 * toplevel directory.<p> 384 * 385 * @return The full path to the JSP repository 386 */ 387 public String getJspRepository() { 388 389 return m_jspRepository; 390 } 391 392 /** 393 * @see org.opencms.loader.I_CmsResourceLoader#getLoaderId() 394 */ 395 public int getLoaderId() { 396 397 return RESOURCE_LOADER_ID; 398 } 399 400 /** 401 * Returns a set of root paths of files that are including the given resource using the 'link.strong' macro.<p> 402 * 403 * @param cms the current cms context 404 * @param resource the resource to check 405 * @param referencingPaths the set of already referencing paths, also return parameter 406 * 407 * @throws CmsException if something goes wrong 408 */ 409 public void getReferencingStrongLinks(CmsObject cms, CmsResource resource, Set<String> referencingPaths) 410 throws CmsException { 411 412 CmsRelationFilter filter = CmsRelationFilter.SOURCES.filterType(CmsRelationType.JSP_STRONG); 413 Iterator<CmsRelation> it = cms.getRelationsForResource(resource, filter).iterator(); 414 while (it.hasNext()) { 415 CmsRelation relation = it.next(); 416 try { 417 CmsResource source = relation.getSource(cms, CmsResourceFilter.DEFAULT); 418 // check if file was already included 419 if (referencingPaths.contains(source.getRootPath())) { 420 // no need to include this file more than once 421 continue; 422 } 423 referencingPaths.add(source.getRootPath()); 424 getReferencingStrongLinks(cms, source, referencingPaths); 425 } catch (CmsException e) { 426 if (LOG.isErrorEnabled()) { 427 LOG.error(e.getLocalizedMessage(), e); 428 } 429 } 430 } 431 } 432 433 /** 434 * Return a String describing the ResourceLoader, 435 * which is (localized to the system default locale) 436 * <code>"The OpenCms default resource loader for JSP"</code>.<p> 437 * 438 * @return a describing String for the ResourceLoader 439 */ 440 public String getResourceLoaderInfo() { 441 442 return Messages.get().getBundle().key(Messages.GUI_LOADER_JSP_DEFAULT_DESC_0); 443 } 444 445 /** 446 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration() 447 */ 448 public void initConfiguration() { 449 450 m_jspRepository = m_configuration.get(PARAM_JSP_REPOSITORY); 451 if (m_jspRepository == null) { 452 m_jspRepository = OpenCms.getSystemInfo().getWebApplicationRfsPath(); 453 } 454 m_jspWebAppRepository = m_configuration.getString(PARAM_JSP_FOLDER, DEFAULT_JSP_FOLDER); 455 if (!m_jspWebAppRepository.endsWith("/")) { 456 m_jspWebAppRepository += "/"; 457 } 458 m_jspRepository = CmsFileUtil.normalizePath(m_jspRepository + m_jspWebAppRepository); 459 460 String maxAge = m_configuration.get(PARAM_CLIENT_CACHE_MAXAGE); 461 if (maxAge == null) { 462 m_clientCacheMaxAge = -1; 463 } else { 464 m_clientCacheMaxAge = Long.parseLong(maxAge); 465 } 466 467 // get the "error pages are committed or not" flag from the configuration 468 m_errorPagesAreNotCommitted = m_configuration.getBoolean(PARAM_JSP_ERRORPAGE_COMMITTED, true); 469 470 int cacheSize = m_configuration.getInteger(PARAM_JSP_CACHE_SIZE, -1); 471 if (cacheSize > 0) { 472 initCaches(cacheSize); 473 } 474 475 // output setup information 476 if (CmsLog.INIT.isInfoEnabled()) { 477 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_JSP_REPOSITORY_ABS_PATH_1, m_jspRepository)); 478 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_WEBAPP_PATH_1, m_jspWebAppRepository)); 479 CmsLog.INIT.info( 480 Messages.get().getBundle().key( 481 Messages.INIT_JSP_REPOSITORY_ERR_PAGE_COMMOTED_1, 482 Boolean.valueOf(m_errorPagesAreNotCommitted))); 483 if (m_clientCacheMaxAge > 0) { 484 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_CLIENT_CACHE_MAX_AGE_1, maxAge)); 485 } 486 if (cacheSize > 0) { 487 CmsLog.INIT.info( 488 Messages.get().getBundle().key(Messages.INIT_JSP_CACHE_SIZE_1, String.valueOf(cacheSize))); 489 } 490 CmsLog.INIT.info( 491 Messages.get().getBundle().key(Messages.INIT_LOADER_INITIALIZED_1, this.getClass().getName())); 492 } 493 } 494 495 /** 496 * @see org.opencms.loader.I_CmsResourceLoader#isStaticExportEnabled() 497 */ 498 public boolean isStaticExportEnabled() { 499 500 return true; 501 } 502 503 /** 504 * @see org.opencms.loader.I_CmsResourceLoader#isStaticExportProcessable() 505 */ 506 public boolean isStaticExportProcessable() { 507 508 return true; 509 } 510 511 /** 512 * @see org.opencms.loader.I_CmsResourceLoader#isUsableForTemplates() 513 */ 514 public boolean isUsableForTemplates() { 515 516 return true; 517 } 518 519 /** 520 * @see org.opencms.loader.I_CmsResourceLoader#isUsingUriWhenLoadingTemplate() 521 */ 522 public boolean isUsingUriWhenLoadingTemplate() { 523 524 return false; 525 } 526 527 /** 528 * @see org.opencms.loader.I_CmsResourceLoader#load(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 529 */ 530 public void load(CmsObject cms, CmsResource file, HttpServletRequest req, HttpServletResponse res) 531 throws ServletException, IOException, CmsException { 532 533 CmsRequestContext context = cms.getRequestContext(); 534 // If we load template jsp or template-element jsp (xml contents or xml pages) don't show source (2nd test) 535 if ((CmsHistoryResourceHandler.isHistoryRequest(req)) 536 && (context.getUri().equals(context.removeSiteRoot(file.getRootPath())))) { 537 showSource(cms, file, req, res); 538 } else { 539 // load and process the JSP 540 boolean streaming = false; 541 boolean bypass = false; 542 543 // read "cache" property for requested VFS resource to check for special "stream" and "bypass" values 544 String cacheProperty = cms.readPropertyObject(file, CmsPropertyDefinition.PROPERTY_CACHE, true).getValue(); 545 if (cacheProperty != null) { 546 cacheProperty = cacheProperty.trim(); 547 if (CACHE_PROPERTY_STREAM.equals(cacheProperty)) { 548 streaming = true; 549 } else if (CACHE_PROPERTY_BYPASS.equals(cacheProperty)) { 550 streaming = true; 551 bypass = true; 552 } 553 } 554 555 // For now, disable flex caching when the __json parameter is used 556 if (CmsJsonPartFilter.isJsonRequest(req)) { 557 streaming = true; 558 bypass = true; 559 } 560 561 // get the Flex controller 562 CmsFlexController controller = getController(cms, file, req, res, streaming, true); 563 if (bypass || controller.isForwardMode()) { 564 // initialize the standard contex bean to be available for all requests 565 CmsJspStandardContextBean.getInstance(controller.getCurrentRequest()); 566 // once in forward mode, always in forward mode (for this request) 567 controller.setForwardMode(true); 568 // bypass Flex cache for this page, update the JSP first if necessary 569 String target = updateJsp(file, controller, new HashSet<String>()); 570 // dispatch to external JSP 571 req.getRequestDispatcher(target).forward(controller.getCurrentRequest(), res); 572 } else { 573 // Flex cache not bypassed, dispatch to internal JSP 574 dispatchJsp(controller); 575 } 576 577 // remove the controller from the request if not forwarding 578 if (!controller.isForwardMode()) { 579 CmsFlexController.removeController(req); 580 } 581 } 582 } 583 584 /** 585 * Replaces taglib attributes in page directives with taglib directives.<p> 586 * 587 * @param content the JSP source text 588 * 589 * @return the transformed JSP text 590 */ 591 @Deprecated 592 public String processTaglibAttributes(String content) { 593 594 // matches a whole page directive 595 final Pattern directivePattern = Pattern.compile("(?sm)<%@\\s*page.*?%>"); 596 // matches a taglibs attribute and captures its values 597 final Pattern taglibPattern = Pattern.compile("(?sm)taglibs\\s*=\\s*\"(.*?)\""); 598 final Pattern commaPattern = Pattern.compile("(?sm)\\s*,\\s*"); 599 final Set<String> taglibs = new LinkedHashSet<String>(); 600 // we insert the marker after the first page directive 601 final String marker = ":::TAGLIBS:::"; 602 I_CmsRegexSubstitution directiveSub = new I_CmsRegexSubstitution() { 603 604 private boolean m_first = true; 605 606 public String substituteMatch(String string, Matcher matcher) { 607 608 String match = string.substring(matcher.start(), matcher.end()); 609 I_CmsRegexSubstitution taglibSub = new I_CmsRegexSubstitution() { 610 611 public String substituteMatch(String string1, Matcher matcher1) { 612 613 // values of the taglibs attribute 614 String match1 = string1.substring(matcher1.start(1), matcher1.end(1)); 615 for (String taglibKey : Splitter.on(commaPattern).split(match1)) { 616 taglibs.add(taglibKey); 617 } 618 return ""; 619 } 620 }; 621 String result = CmsStringUtil.substitute(taglibPattern, match, taglibSub); 622 if (m_first) { 623 result += marker; 624 m_first = false; 625 } 626 return result; 627 } 628 }; 629 String substituted = CmsStringUtil.substitute(directivePattern, content, directiveSub); 630 // insert taglib inclusion 631 substituted = substituted.replaceAll(marker, generateTaglibInclusions(taglibs)); 632 // remove empty page directives 633 substituted = substituted.replaceAll("(?sm)<%@\\s*page\\s*%>", ""); 634 return substituted; 635 } 636 637 /** 638 * Removes the given resources from the cache.<p> 639 * 640 * @param rootPaths the set of root paths to remove 641 * @param online if online or offline 642 */ 643 public void removeFromCache(Set<String> rootPaths, boolean online) { 644 645 Map<String, Boolean> cache; 646 if (online) { 647 cache = m_onlineJsps; 648 } else { 649 cache = m_offlineJsps; 650 } 651 Iterator<String> itRemove = rootPaths.iterator(); 652 while (itRemove.hasNext()) { 653 String rootPath = itRemove.next(); 654 cache.remove(rootPath); 655 } 656 } 657 658 /** 659 * Removes a JSP from an offline project from the RFS.<p> 660 * 661 * @param resource the offline JSP resource to remove from the RFS 662 * 663 * @throws CmsLoaderException if accessing the loader fails 664 */ 665 public void removeOfflineJspFromRepository(CmsResource resource) throws CmsLoaderException { 666 667 String jspName = getJspRfsPath(resource, false); 668 Set<String> pathSet = new HashSet<String>(); 669 pathSet.add(resource.getRootPath()); 670 ReentrantReadWriteLock lock = getFileLock(jspName); 671 lock.writeLock().lock(); 672 try { 673 removeFromCache(pathSet, false); 674 File jspFile = new File(jspName); 675 jspFile.delete(); 676 } finally { 677 lock.writeLock().unlock(); 678 } 679 } 680 681 /** 682 * @see org.opencms.loader.I_CmsResourceLoader#service(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse) 683 */ 684 public void service(CmsObject cms, CmsResource resource, ServletRequest req, ServletResponse res) 685 throws ServletException, IOException, CmsLoaderException { 686 687 CmsFlexController controller = CmsFlexController.getController(req); 688 // get JSP target name on "real" file system 689 String target = updateJsp(resource, controller, new HashSet<String>(8)); 690 // important: Indicate that all output must be buffered 691 controller.getCurrentResponse().setOnlyBuffering(true); 692 // initialize the standard contex bean to be available for all requests 693 CmsJspStandardContextBean.getInstance(controller.getCurrentRequest()); 694 // dispatch to external file 695 controller.getCurrentRequest().getRequestDispatcherToExternal(cms.getSitePath(resource), target).include( 696 req, 697 res); 698 } 699 700 /** 701 * @see org.opencms.loader.I_CmsFlexCacheEnabledLoader#setFlexCache(org.opencms.flex.CmsFlexCache) 702 */ 703 public void setFlexCache(CmsFlexCache cache) { 704 705 m_cache = cache; 706 // output setup information 707 if (CmsLog.INIT.isInfoEnabled()) { 708 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_ADD_FLEX_CACHE_0)); 709 } 710 } 711 712 /** 713 * Triggers an asynchronous purge of the JSP repository.<p> 714 * 715 * @param afterPurgeAction the action to execute after purging 716 */ 717 public void triggerPurge(final Runnable afterPurgeAction) { 718 719 OpenCms.getExecutor().execute(new Runnable() { 720 721 @SuppressWarnings("synthetic-access") 722 public void run() { 723 724 try { 725 m_purgeLock.writeLock().lock(); 726 for (ReentrantReadWriteLock lock : m_fileLocks.values()) { 727 lock.writeLock().lock(); 728 } 729 doPurge(afterPurgeAction); 730 } catch (Exception e) { 731 LOG.error("Error while purging jsp repository: " + e.getLocalizedMessage(), e); 732 } finally { 733 for (ReentrantReadWriteLock lock : m_fileLocks.values()) { 734 try { 735 lock.writeLock().unlock(); 736 } catch (Exception e) { 737 LOG.warn(e.getLocalizedMessage(), e); 738 } 739 } 740 m_purgeLock.writeLock().unlock(); 741 } 742 } 743 }); 744 } 745 746 /** 747 * Updates a JSP page in the "real" file system in case the VFS resource has changed.<p> 748 * 749 * Also processes the <code><%@ cms %></code> tags before the JSP is written to the real FS. 750 * Also recursively updates all files that are referenced by a <code><%@ cms %></code> tag 751 * on this page to make sure the file actually exists in the real FS. 752 * All <code><%@ include %></code> tags are parsed and the name in the tag is translated 753 * from the OpenCms VFS path to the path in the real FS. 754 * The same is done for filenames in <code><%@ page errorPage=... %></code> tags.<p> 755 * 756 * @param resource the requested JSP file resource in the VFS 757 * @param controller the controller for the JSP integration 758 * @param updatedFiles a Set containing all JSP pages that have been already updated 759 * 760 * @return the file name of the updated JSP in the "real" FS 761 * 762 * @throws ServletException might be thrown in the process of including the JSP 763 * @throws IOException might be thrown in the process of including the JSP 764 * @throws CmsLoaderException if the resource type can not be read 765 */ 766 public String updateJsp(CmsResource resource, CmsFlexController controller, Set<String> updatedFiles) 767 throws IOException, ServletException, CmsLoaderException { 768 769 String jspVfsName = resource.getRootPath(); 770 String extension; 771 boolean isHardInclude; 772 int loaderId = OpenCms.getResourceManager().getResourceType(resource.getTypeId()).getLoaderId(); 773 if ((loaderId == CmsJspLoader.RESOURCE_LOADER_ID) && (!jspVfsName.endsWith(JSP_EXTENSION))) { 774 // this is a true JSP resource that does not end with ".jsp" 775 extension = JSP_EXTENSION; 776 isHardInclude = false; 777 } else { 778 // not a JSP resource or already ends with ".jsp" 779 extension = ""; 780 // if this is a JSP we don't treat it as hard include 781 isHardInclude = (loaderId != CmsJspLoader.RESOURCE_LOADER_ID); 782 } 783 784 String jspTargetName = CmsFileUtil.getRepositoryName( 785 m_jspWebAppRepository, 786 jspVfsName + extension, 787 controller.getCurrentRequest().isOnline()); 788 789 // check if page was already updated 790 if (updatedFiles.contains(jspTargetName)) { 791 // no need to write the already included file to the real FS more then once 792 return jspTargetName; 793 } 794 795 String jspPath = CmsFileUtil.getRepositoryName( 796 m_jspRepository, 797 jspVfsName + extension, 798 controller.getCurrentRequest().isOnline()); 799 800 File d = new File(jspPath).getParentFile(); 801 if ((d == null) || (d.exists() && !(d.isDirectory() && d.canRead()))) { 802 CmsMessageContainer message = Messages.get().container(Messages.LOG_ACCESS_DENIED_1, jspPath); 803 LOG.error(message.key()); 804 // can not continue 805 throw new ServletException(message.key()); 806 } 807 808 if (!d.exists()) { 809 // create directory structure 810 d.mkdirs(); 811 } 812 ReentrantReadWriteLock readWriteLock = getFileLock(jspVfsName); 813 try { 814 // get a read lock for this jsp 815 readWriteLock.readLock().lock(); 816 File jspFile = new File(jspPath); 817 // check if the JSP must be updated 818 boolean mustUpdate = false; 819 long jspModificationDate = 0; 820 if (!jspFile.exists()) { 821 // file does not exist in real FS 822 mustUpdate = true; 823 // make sure the parent folder exists 824 File folder = jspFile.getParentFile(); 825 if (!folder.exists()) { 826 boolean success = folder.mkdirs(); 827 if (!success) { 828 LOG.error( 829 org.opencms.db.Messages.get().getBundle().key( 830 org.opencms.db.Messages.LOG_CREATE_FOLDER_FAILED_1, 831 folder.getAbsolutePath())); 832 } 833 } 834 } else { 835 jspModificationDate = jspFile.lastModified(); 836 if (jspModificationDate < resource.getDateLastModified()) { 837 // file in real FS is older then file in VFS 838 mustUpdate = true; 839 } else if (controller.getCurrentRequest().isDoRecompile()) { 840 // recompile is forced with parameter 841 mustUpdate = true; 842 } else { 843 // check if update is needed 844 if (controller.getCurrentRequest().isOnline()) { 845 mustUpdate = !m_onlineJsps.containsKey(jspVfsName); 846 } else { 847 mustUpdate = !m_offlineJsps.containsKey(jspVfsName); 848 } 849 // check strong links only if update is needed 850 if (mustUpdate) { 851 // update strong link dependencies 852 mustUpdate = updateStrongLinks(resource, controller, updatedFiles); 853 } 854 } 855 } 856 if (mustUpdate) { 857 if (LOG.isDebugEnabled()) { 858 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WRITING_JSP_1, jspTargetName)); 859 } 860 // jsp needs updating, acquire a write lock 861 readWriteLock.readLock().unlock(); 862 readWriteLock.writeLock().lock(); 863 try { 864 // check again if updating is still necessary as this might have happened while waiting for the write lock 865 if (!jspFile.exists() || (jspModificationDate == jspFile.lastModified())) { 866 updatedFiles.add(jspTargetName); 867 byte[] contents; 868 String encoding; 869 try { 870 CmsObject cms = controller.getCmsObject(); 871 contents = cms.readFile(resource).getContents(); 872 // check the "content-encoding" property for the JSP, use system default if not found on path 873 encoding = cms.readPropertyObject( 874 resource, 875 CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, 876 true).getValue(); 877 if (encoding == null) { 878 encoding = OpenCms.getSystemInfo().getDefaultEncoding(); 879 } else { 880 encoding = CmsEncoder.lookupEncoding(encoding.trim(), encoding); 881 } 882 } catch (CmsException e) { 883 controller.setThrowable(e, jspVfsName); 884 throw new ServletException( 885 Messages.get().getBundle().key(Messages.ERR_LOADER_JSP_ACCESS_1, jspVfsName), 886 e); 887 } 888 889 try { 890 // parse the JSP and modify OpenCms critical directives 891 contents = parseJsp(contents, encoding, controller, updatedFiles, isHardInclude); 892 if (LOG.isInfoEnabled()) { 893 // check for existing file and display some debug info 894 LOG.info( 895 Messages.get().getBundle().key( 896 Messages.LOG_JSP_PERMCHECK_4, 897 new Object[] { 898 jspFile.getAbsolutePath(), 899 Boolean.valueOf(jspFile.exists()), 900 Boolean.valueOf(jspFile.isFile()), 901 Boolean.valueOf(jspFile.canWrite())})); 902 } 903 // write the parsed JSP content to the real FS 904 synchronized (CmsJspLoader.class) { 905 // this must be done only one file at a time 906 FileOutputStream fs = new FileOutputStream(jspFile); 907 fs.write(contents); 908 fs.close(); 909 910 // we set the modification date to (approximately) that of the VFS resource. This is needed because in the Online project, the old version of a JSP 911 // may be generated in the RFS JSP repository *after* the JSP has been changed, but *before* it has been published, which would lead 912 // to it not being updated after the changed JSP is published. 913 914 // Note: the RFS may only support second precision for the last modification date 915 jspFile.setLastModified((1 + (resource.getDateLastModified() / 1000)) * 1000); 916 } 917 if (controller.getCurrentRequest().isOnline()) { 918 m_onlineJsps.put(jspVfsName, Boolean.TRUE); 919 } else { 920 m_offlineJsps.put(jspVfsName, Boolean.TRUE); 921 } 922 if (LOG.isInfoEnabled()) { 923 LOG.info( 924 Messages.get().getBundle().key( 925 Messages.LOG_UPDATED_JSP_2, 926 jspTargetName, 927 jspVfsName)); 928 } 929 } catch (FileNotFoundException e) { 930 throw new ServletException( 931 Messages.get().getBundle().key(Messages.ERR_LOADER_JSP_WRITE_1, jspFile.getName()), 932 e); 933 } 934 } 935 } finally { 936 readWriteLock.readLock().lock(); 937 readWriteLock.writeLock().unlock(); 938 } 939 } 940 941 // update "last modified" and "expires" date on controller 942 controller.updateDates(jspFile.lastModified(), CmsResource.DATE_EXPIRED_DEFAULT); 943 } finally { 944 //m_processingFiles.remove(jspVfsName); 945 readWriteLock.readLock().unlock(); 946 } 947 948 return jspTargetName; 949 } 950 951 /** 952 * Updates the internal jsp repository when the servlet container 953 * tries to compile a jsp file that may not exist.<p> 954 * 955 * @param servletPath the servlet path, just to avoid unneeded recursive calls 956 * @param request the current request 957 */ 958 public void updateJspFromRequest(String servletPath, CmsFlexRequest request) { 959 960 // assemble the RFS name of the requested jsp 961 String jspUri = servletPath; 962 String pathInfo = request.getPathInfo(); 963 if (pathInfo != null) { 964 jspUri += pathInfo; 965 } 966 967 // check the file name 968 if ((jspUri == null) || !jspUri.startsWith(m_jspWebAppRepository)) { 969 // nothing to do, this kind of request are handled by the CmsJspLoader#service method 970 return; 971 } 972 973 // remove prefixes 974 jspUri = jspUri.substring(m_jspWebAppRepository.length()); 975 if (jspUri.startsWith(CmsFlexCache.REPOSITORY_ONLINE)) { 976 jspUri = jspUri.substring(CmsFlexCache.REPOSITORY_ONLINE.length()); 977 } else if (jspUri.startsWith(CmsFlexCache.REPOSITORY_OFFLINE)) { 978 jspUri = jspUri.substring(CmsFlexCache.REPOSITORY_OFFLINE.length()); 979 } else { 980 // this is not an OpenCms jsp file 981 return; 982 } 983 984 // read the resource from OpenCms 985 CmsFlexController controller = CmsFlexController.getController(request); 986 try { 987 CmsResource includeResource; 988 try { 989 // first try to read the resource assuming no additional jsp extension was needed 990 includeResource = readJspResource(controller, jspUri); 991 } catch (CmsVfsResourceNotFoundException e) { 992 // try removing the additional jsp extension 993 if (jspUri.endsWith(JSP_EXTENSION)) { 994 jspUri = jspUri.substring(0, jspUri.length() - JSP_EXTENSION.length()); 995 } 996 includeResource = readJspResource(controller, jspUri); 997 } 998 // make sure the jsp referenced file is generated 999 updateJsp(includeResource, controller, new HashSet<String>(8)); 1000 } catch (Exception e) { 1001 if (LOG.isDebugEnabled()) { 1002 LOG.debug(e.getLocalizedMessage(), e); 1003 } 1004 } 1005 } 1006 1007 /** 1008 * Dispatches the current request to the OpenCms internal JSP.<p> 1009 * 1010 * @param controller the current controller 1011 * 1012 * @return the content of the processed JSP 1013 * 1014 * @throws ServletException if inclusion does not work 1015 * @throws IOException if inclusion does not work 1016 */ 1017 protected byte[] dispatchJsp(CmsFlexController controller) throws ServletException, IOException { 1018 1019 // get request / response wrappers 1020 CmsFlexRequest f_req = controller.getCurrentRequest(); 1021 CmsFlexResponse f_res = controller.getCurrentResponse(); 1022 try { 1023 f_req.getRequestDispatcher(controller.getCmsObject().getSitePath(controller.getCmsResource())).include( 1024 f_req, 1025 f_res); 1026 } catch (SocketException e) { 1027 // uncritical, might happen if client (browser) does not wait until end of page delivery 1028 LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e); 1029 } 1030 1031 byte[] result = null; 1032 HttpServletResponse res = controller.getTopResponse(); 1033 1034 if (!controller.isStreaming() && !f_res.isSuspended()) { 1035 try { 1036 // if a JSP error page was triggered the response will be already committed here 1037 if (!res.isCommitted() || m_errorPagesAreNotCommitted) { 1038 1039 // check if the current request was done by a workplace user 1040 boolean isWorkplaceUser = CmsWorkplaceManager.isWorkplaceUser(f_req); 1041 1042 if (controller.isTop() && (controller.getRedirectInfo() != null)) { 1043 if (!controller.getCmsObject().getRequestContext().getCurrentProject().isOnlineProject()) { 1044 RedirectInfo info = controller.getRedirectInfo(); 1045 info.executeRedirect(res); 1046 return null; 1047 } else { 1048 if (LOG.isWarnEnabled()) { 1049 Exception e = new Exception(); 1050 LOG.warn( 1051 "Found redirect info in Flex controller in Online project, but response was not suspended (target = " 1052 + controller.getRedirectInfo().getTarget() 1053 + ")", 1054 e); 1055 } 1056 } 1057 } 1058 1059 // check if the content was modified since the last request 1060 if (controller.isTop() 1061 && !isWorkplaceUser 1062 && CmsFlexController.isNotModifiedSince(f_req, controller.getDateLastModified())) { 1063 if (f_req.getParameterMap().size() == 0) { 1064 // only use "expires" header on pages that have no parameters, 1065 // otherwise some browsers (e.g. IE 6) will not even try to request 1066 // updated versions of the page 1067 CmsFlexController.setDateExpiresHeader( 1068 res, 1069 controller.getDateExpires(), 1070 m_clientCacheMaxAge); 1071 } 1072 res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 1073 return null; 1074 } 1075 1076 // get the result byte array 1077 result = f_res.getWriterBytes(); 1078 HttpServletRequest req = controller.getTopRequest(); 1079 if (req.getHeader(CmsRequestUtil.HEADER_OPENCMS_EXPORT) != null) { 1080 // this is a non "on-demand" static export request, don't write to the response stream 1081 req.setAttribute( 1082 CmsRequestUtil.HEADER_OPENCMS_EXPORT, 1083 Long.valueOf(controller.getDateLastModified())); 1084 } else if (controller.isTop()) { 1085 // process headers and write output if this is the "top" request/response 1086 res.setContentLength(result.length); 1087 // check for preset error code 1088 Integer errorCode = (Integer)req.getAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE); 1089 if (errorCode == null) { 1090 // set last modified / no cache headers only if this is not an error page 1091 if (isWorkplaceUser) { 1092 res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis()); 1093 CmsRequestUtil.setNoCacheHeaders(res); 1094 } else { 1095 // set date last modified header 1096 CmsFlexController.setDateLastModifiedHeader(res, controller.getDateLastModified()); 1097 if ((f_req.getParameterMap().size() == 0) && (controller.getDateLastModified() > -1)) { 1098 // only use "expires" header on pages that have no parameters 1099 // and that are cachable (i.e. 'date last modified' is set) 1100 // otherwise some browsers (e.g. IE 6) will not even try to request 1101 // updated versions of the page 1102 CmsFlexController.setDateExpiresHeader( 1103 res, 1104 controller.getDateExpires(), 1105 m_clientCacheMaxAge); 1106 } 1107 } 1108 // set response status to "200 - OK" (required for static export "on-demand") 1109 res.setStatus(HttpServletResponse.SC_OK); 1110 } else { 1111 // set previously saved error code 1112 res.setStatus(errorCode.intValue()); 1113 } 1114 // process the headers 1115 CmsFlexResponse.processHeaders(f_res.getHeaders(), res, true); 1116 res.getOutputStream().write(result); 1117 res.getOutputStream().flush(); 1118 } 1119 } 1120 } catch (IllegalStateException e) { 1121 // uncritical, might happen if JSP error page was used 1122 LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e); 1123 } catch (SocketException e) { 1124 // uncritical, might happen if client (browser) does not wait until end of page delivery 1125 LOG.debug(Messages.get().getBundle().key(Messages.LOG_IGNORING_EXC_1, e.getClass().getName()), e); 1126 } 1127 } else if (controller.isTop() && (controller.getRedirectInfo() != null)) { 1128 RedirectInfo info = controller.getRedirectInfo(); 1129 info.executeRedirect(res); 1130 } 1131 return result; 1132 } 1133 1134 /** 1135 * Purges the JSP repository.<p> 1136 * 1137 * @param afterPurgeAction the action to execute after purging 1138 */ 1139 protected void doPurge(Runnable afterPurgeAction) { 1140 1141 if (LOG.isInfoEnabled()) { 1142 LOG.info( 1143 org.opencms.flex.Messages.get().getBundle().key( 1144 org.opencms.flex.Messages.LOG_FLEXCACHE_WILL_PURGE_JSP_REPOSITORY_0)); 1145 } 1146 1147 File d; 1148 d = new File(getJspRepository() + CmsFlexCache.REPOSITORY_ONLINE + File.separator); 1149 CmsFileUtil.purgeDirectory(d); 1150 1151 d = new File(getJspRepository() + CmsFlexCache.REPOSITORY_OFFLINE + File.separator); 1152 CmsFileUtil.purgeDirectory(d); 1153 if (afterPurgeAction != null) { 1154 afterPurgeAction.run(); 1155 } 1156 1157 if (LOG.isInfoEnabled()) { 1158 LOG.info( 1159 org.opencms.flex.Messages.get().getBundle().key( 1160 org.opencms.flex.Messages.LOG_FLEXCACHE_PURGED_JSP_REPOSITORY_0)); 1161 } 1162 1163 } 1164 1165 /** 1166 * Generates the taglib directives for a collection of taglib identifiers.<p> 1167 * 1168 * @param taglibs the taglib identifiers 1169 * 1170 * @return a string containing taglib directives 1171 */ 1172 protected String generateTaglibInclusions(Collection<String> taglibs) { 1173 1174 StringBuffer buffer = new StringBuffer(); 1175 for (String taglib : taglibs) { 1176 String uri = m_taglibs.get(taglib); 1177 if (uri != null) { 1178 buffer.append("<%@ taglib prefix=\"" + taglib + "\" uri=\"" + uri + "\" %>"); 1179 } 1180 } 1181 return buffer.toString(); 1182 } 1183 1184 /** 1185 * Delivers a Flex controller, either by creating a new one, or by re-using an existing one.<p> 1186 * 1187 * @param cms the initial CmsObject to wrap in the controller 1188 * @param resource the resource requested 1189 * @param req the current request 1190 * @param res the current response 1191 * @param streaming indicates if the response is streaming 1192 * @param top indicates if the response is the top response 1193 * 1194 * @return a Flex controller 1195 */ 1196 protected CmsFlexController getController( 1197 CmsObject cms, 1198 CmsResource resource, 1199 HttpServletRequest req, 1200 HttpServletResponse res, 1201 boolean streaming, 1202 boolean top) { 1203 1204 CmsFlexController controller = null; 1205 if (top) { 1206 // only check for existing controller if this is the "top" request/response 1207 controller = CmsFlexController.getController(req); 1208 } 1209 if (controller == null) { 1210 // create new request / response wrappers 1211 if (!cms.getRequestContext().getCurrentProject().isOnlineProject() 1212 && (CmsHistoryResourceHandler.isHistoryRequest(req) || CmsJspTagEnableAde.isDirectEditDisabled(req))) { 1213 cms.getRequestContext().setAttribute(CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT, Boolean.TRUE); 1214 } 1215 controller = new CmsFlexController(cms, resource, m_cache, req, res, streaming, top); 1216 CmsFlexController.setController(req, controller); 1217 CmsFlexRequest f_req = new CmsFlexRequest(req, controller); 1218 CmsFlexResponse f_res = new CmsFlexResponse(res, controller, streaming, true); 1219 controller.push(f_req, f_res); 1220 } else if (controller.isForwardMode()) { 1221 // reset CmsObject (because of URI) if in forward mode 1222 controller = new CmsFlexController(cms, controller); 1223 CmsFlexController.setController(req, controller); 1224 } 1225 return controller; 1226 } 1227 1228 /** 1229 * Initializes the caches.<p> 1230 * 1231 * @param cacheSize the cache size 1232 */ 1233 protected void initCaches(int cacheSize) { 1234 1235 m_offlineJsps = CmsMemoryMonitor.createLRUCacheMap(cacheSize); 1236 m_onlineJsps = CmsMemoryMonitor.createLRUCacheMap(cacheSize); 1237 } 1238 1239 /** 1240 * Parses the JSP and modifies OpenCms critical directive information.<p> 1241 * 1242 * @param byteContent the original JSP content 1243 * @param encoding the encoding to use for the JSP 1244 * @param controller the controller for the JSP integration 1245 * @param updatedFiles a Set containing all JSP pages that have been already updated 1246 * @param isHardInclude indicated if this page is actually a "hard" include with <code><%@ include file="..." ></code> 1247 * 1248 * @return the modified JSP content 1249 */ 1250 protected byte[] parseJsp( 1251 byte[] byteContent, 1252 String encoding, 1253 CmsFlexController controller, 1254 Set<String> updatedFiles, 1255 boolean isHardInclude) { 1256 1257 String content; 1258 // make sure encoding is set correctly 1259 try { 1260 content = new String(byteContent, encoding); 1261 } catch (UnsupportedEncodingException e) { 1262 // encoding property is not set correctly 1263 LOG.error( 1264 Messages.get().getBundle().key( 1265 Messages.LOG_UNSUPPORTED_ENC_1, 1266 controller.getCurrentRequest().getElementUri()), 1267 e); 1268 try { 1269 encoding = OpenCms.getSystemInfo().getDefaultEncoding(); 1270 content = new String(byteContent, encoding); 1271 } catch (UnsupportedEncodingException e2) { 1272 // should not happen since default encoding is always a valid encoding (checked during system startup) 1273 content = new String(byteContent); 1274 } 1275 } 1276 1277 // parse for special %(link:...) macros 1278 content = parseJspLinkMacros(content, controller); 1279 // parse for special <%@cms file="..." %> tag 1280 content = parseJspCmsTag(content, controller, updatedFiles); 1281 // parse for included files in tags 1282 content = parseJspIncludes(content, controller, updatedFiles); 1283 // parse for <%@page pageEncoding="..." %> tag 1284 content = parseJspEncoding(content, encoding, isHardInclude); 1285 // Processes magic taglib attributes in page directives 1286 content = processTaglibAttributes(content); 1287 // convert the result to bytes and return it 1288 try { 1289 return content.getBytes(encoding); 1290 } catch (UnsupportedEncodingException e) { 1291 // should not happen since encoding was already checked 1292 return content.getBytes(); 1293 } 1294 } 1295 1296 /** 1297 * Parses the JSP content for the special <code><%cms file="..." %></code> tag.<p> 1298 * 1299 * @param content the JSP content to parse 1300 * @param controller the current JSP controller 1301 * @param updatedFiles a set of already updated jsp files 1302 * 1303 * @return the parsed JSP content 1304 */ 1305 protected String parseJspCmsTag(String content, CmsFlexController controller, Set<String> updatedFiles) { 1306 1307 // check if a JSP directive occurs in the file 1308 int i1 = content.indexOf(DIRECTIVE_START); 1309 if (i1 < 0) { 1310 // no directive occurs 1311 return content; 1312 } 1313 1314 StringBuffer buf = new StringBuffer(content.length()); 1315 int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length(), elen = DIRECTIVE_END.length(); 1316 1317 while (i1 >= 0) { 1318 // parse the file and replace JSP filename references 1319 i2 = content.indexOf(DIRECTIVE_END, i1 + slen); 1320 if (i2 < 0) { 1321 // wrong syntax (missing end directive) - let the JSP compiler produce the error message 1322 return content; 1323 } else if (i2 > i1) { 1324 String directive = content.substring(i1 + slen, i2); 1325 if (LOG.isDebugEnabled()) { 1326 LOG.debug( 1327 Messages.get().getBundle().key( 1328 Messages.LOG_DIRECTIVE_DETECTED_3, 1329 DIRECTIVE_START, 1330 directive, 1331 DIRECTIVE_END)); 1332 } 1333 1334 int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0; 1335 while (directive.charAt(t1) == ' ') { 1336 t1++; 1337 } 1338 String argument = null; 1339 if (directive.startsWith("cms", t1)) { 1340 if (LOG.isDebugEnabled()) { 1341 LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "cms")); 1342 } 1343 t2 = directive.indexOf("file", t1 + 3); 1344 t5 = 4; 1345 } 1346 1347 if (t2 > 0) { 1348 String sub = directive.substring(t2 + t5); 1349 char c1 = sub.charAt(t3); 1350 while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) { 1351 c1 = sub.charAt(++t3); 1352 } 1353 t4 = t3; 1354 while (c1 != '"') { 1355 c1 = sub.charAt(++t4); 1356 } 1357 if (t4 > t3) { 1358 argument = sub.substring(t3, t4); 1359 } 1360 if (LOG.isDebugEnabled()) { 1361 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument)); 1362 } 1363 } 1364 1365 if (argument != null) { 1366 // try to update the referenced file 1367 String jspname = updateJsp(argument, controller, updatedFiles); 1368 if (jspname != null) { 1369 directive = jspname; 1370 if (LOG.isDebugEnabled()) { 1371 LOG.debug( 1372 Messages.get().getBundle().key( 1373 Messages.LOG_DIRECTIVE_CHANGED_3, 1374 DIRECTIVE_START, 1375 directive, 1376 DIRECTIVE_END)); 1377 } 1378 } 1379 // cms directive was found 1380 buf.append(content.substring(p0, i1)); 1381 buf.append(directive); 1382 p0 = i2 + elen; 1383 i1 = content.indexOf(DIRECTIVE_START, p0); 1384 } else { 1385 // cms directive was not found 1386 buf.append(content.substring(p0, i1 + slen)); 1387 buf.append(directive); 1388 p0 = i2; 1389 i1 = content.indexOf(DIRECTIVE_START, p0); 1390 } 1391 } 1392 } 1393 if (i2 > 0) { 1394 // the content of the JSP was changed 1395 buf.append(content.substring(p0, content.length())); 1396 content = buf.toString(); 1397 } 1398 return content; 1399 } 1400 1401 /** 1402 * Parses the JSP content for the <code><%page pageEncoding="..." %></code> tag 1403 * and ensures that the JSP page encoding is set according to the OpenCms 1404 * "content-encoding" property value of the JSP.<p> 1405 * 1406 * @param content the JSP content to parse 1407 * @param encoding the encoding to use for the JSP 1408 * @param isHardInclude indicated if this page is actually a "hard" include with <code><%@ include file="..." ></code> 1409 * 1410 * @return the parsed JSP content 1411 */ 1412 protected String parseJspEncoding(String content, String encoding, boolean isHardInclude) { 1413 1414 // check if a JSP directive occurs in the file 1415 int i1 = content.indexOf(DIRECTIVE_START); 1416 if (i1 < 0) { 1417 // no directive occurs 1418 if (isHardInclude) { 1419 return content; 1420 } 1421 } 1422 1423 StringBuffer buf = new StringBuffer(content.length() + 64); 1424 int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length(); 1425 boolean found = false; 1426 1427 if (i1 < 0) { 1428 // no directive found at all, append content to buffer 1429 buf.append(content); 1430 } 1431 1432 while (i1 >= 0) { 1433 // parse the file and set/replace page encoding 1434 i2 = content.indexOf(DIRECTIVE_END, i1 + slen); 1435 if (i2 < 0) { 1436 // wrong syntax (missing end directive) - let the JSP compiler produce the error message 1437 return content; 1438 } else if (i2 > i1) { 1439 String directive = content.substring(i1 + slen, i2); 1440 if (LOG.isDebugEnabled()) { 1441 LOG.debug( 1442 Messages.get().getBundle().key( 1443 Messages.LOG_DIRECTIVE_DETECTED_3, 1444 DIRECTIVE_START, 1445 directive, 1446 DIRECTIVE_END)); 1447 } 1448 1449 int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0; 1450 while (directive.charAt(t1) == ' ') { 1451 t1++; 1452 } 1453 String argument = null; 1454 if (directive.startsWith("page", t1)) { 1455 if (LOG.isDebugEnabled()) { 1456 LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "page")); 1457 } 1458 t2 = directive.indexOf("pageEncoding", t1 + 4); 1459 t5 = 12; 1460 if (t2 > 0) { 1461 found = true; 1462 } 1463 } 1464 1465 if (t2 > 0) { 1466 String sub = directive.substring(t2 + t5); 1467 char c1 = sub.charAt(t3); 1468 while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) { 1469 c1 = sub.charAt(++t3); 1470 } 1471 t4 = t3; 1472 while (c1 != '"') { 1473 c1 = sub.charAt(++t4); 1474 } 1475 if (t4 > t3) { 1476 argument = sub.substring(t3, t4); 1477 } 1478 if (LOG.isDebugEnabled()) { 1479 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument)); 1480 } 1481 } 1482 1483 if (argument != null) { 1484 // a pageEncoding setting was found, changes have to be made 1485 String pre = directive.substring(0, t2 + t3 + t5); 1486 String suf = directive.substring(t2 + t3 + t5 + argument.length()); 1487 // change the encoding 1488 directive = pre + encoding + suf; 1489 if (LOG.isDebugEnabled()) { 1490 LOG.debug( 1491 Messages.get().getBundle().key( 1492 Messages.LOG_DIRECTIVE_CHANGED_3, 1493 DIRECTIVE_START, 1494 directive, 1495 DIRECTIVE_END)); 1496 } 1497 } 1498 1499 buf.append(content.substring(p0, i1 + slen)); 1500 buf.append(directive); 1501 p0 = i2; 1502 i1 = content.indexOf(DIRECTIVE_START, p0); 1503 } 1504 } 1505 if (i2 > 0) { 1506 // the content of the JSP was changed 1507 buf.append(content.substring(p0, content.length())); 1508 } 1509 if (found) { 1510 content = buf.toString(); 1511 } else if (!isHardInclude) { 1512 // encoding setting was not found 1513 // if this is not a "hard" include then add the encoding to the top of the page 1514 // checking for the hard include is important to prevent errors with 1515 // multiple page encoding settings if a template is composed from several hard included elements 1516 // this is an issue in Tomcat 4.x but not 5.x 1517 StringBuffer buf2 = new StringBuffer(buf.length() + 32); 1518 buf2.append("<%@ page pageEncoding=\""); 1519 buf2.append(encoding); 1520 buf2.append("\" %>"); 1521 buf2.append(buf); 1522 content = buf2.toString(); 1523 } 1524 return content; 1525 } 1526 1527 /** 1528 * Parses the JSP content for includes and replaces all OpenCms VFS 1529 * path information with information for the real FS.<p> 1530 * 1531 * @param content the JSP content to parse 1532 * @param controller the current JSP controller 1533 * @param updatedFiles a set of already updated files 1534 * 1535 * @return the parsed JSP content 1536 */ 1537 protected String parseJspIncludes(String content, CmsFlexController controller, Set<String> updatedFiles) { 1538 1539 // check if a JSP directive occurs in the file 1540 int i1 = content.indexOf(DIRECTIVE_START); 1541 if (i1 < 0) { 1542 // no directive occurs 1543 return content; 1544 } 1545 1546 StringBuffer buf = new StringBuffer(content.length()); 1547 int p0 = 0, i2 = 0, slen = DIRECTIVE_START.length(); 1548 1549 while (i1 >= 0) { 1550 // parse the file and replace JSP filename references 1551 i2 = content.indexOf(DIRECTIVE_END, i1 + slen); 1552 if (i2 < 0) { 1553 // wrong syntax (missing end directive) - let the JSP compiler produce the error message 1554 return content; 1555 } else if (i2 > i1) { 1556 String directive = content.substring(i1 + slen, i2); 1557 if (LOG.isDebugEnabled()) { 1558 LOG.debug( 1559 Messages.get().getBundle().key( 1560 Messages.LOG_DIRECTIVE_DETECTED_3, 1561 DIRECTIVE_START, 1562 directive, 1563 DIRECTIVE_END)); 1564 } 1565 1566 int t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0; 1567 while (directive.charAt(t1) == ' ') { 1568 t1++; 1569 } 1570 String argument = null; 1571 if (directive.startsWith("include", t1)) { 1572 if (LOG.isDebugEnabled()) { 1573 LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "include")); 1574 } 1575 t2 = directive.indexOf("file", t1 + 7); 1576 t5 = 6; 1577 } else if (directive.startsWith("page", t1)) { 1578 if (LOG.isDebugEnabled()) { 1579 LOG.debug(Messages.get().getBundle().key(Messages.LOG_X_DIRECTIVE_DETECTED_1, "page")); 1580 } 1581 t2 = directive.indexOf("errorPage", t1 + 4); 1582 t5 = 11; 1583 } 1584 1585 if (t2 > 0) { 1586 String sub = directive.substring(t2 + t5); 1587 char c1 = sub.charAt(t3); 1588 while ((c1 == ' ') || (c1 == '=') || (c1 == '"')) { 1589 c1 = sub.charAt(++t3); 1590 } 1591 t4 = t3; 1592 while (c1 != '"') { 1593 c1 = sub.charAt(++t4); 1594 } 1595 if (t4 > t3) { 1596 argument = sub.substring(t3, t4); 1597 } 1598 if (LOG.isDebugEnabled()) { 1599 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DIRECTIVE_ARG_1, argument)); 1600 } 1601 } 1602 1603 if (argument != null) { 1604 // a file was found, changes have to be made 1605 String pre = directive.substring(0, t2 + t3 + t5); 1606 String suf = directive.substring(t2 + t3 + t5 + argument.length()); 1607 // now try to update the referenced file 1608 String jspname = updateJsp(argument, controller, updatedFiles); 1609 if (jspname != null) { 1610 // only change something in case no error had occurred 1611 directive = pre + jspname + suf; 1612 if (LOG.isDebugEnabled()) { 1613 LOG.debug( 1614 Messages.get().getBundle().key( 1615 Messages.LOG_DIRECTIVE_CHANGED_3, 1616 DIRECTIVE_START, 1617 directive, 1618 DIRECTIVE_END)); 1619 } 1620 } 1621 } 1622 1623 buf.append(content.substring(p0, i1 + slen)); 1624 buf.append(directive); 1625 p0 = i2; 1626 i1 = content.indexOf(DIRECTIVE_START, p0); 1627 } 1628 } 1629 if (i2 > 0) { 1630 // the content of the JSP was changed 1631 buf.append(content.substring(p0, content.length())); 1632 content = buf.toString(); 1633 } 1634 return content; 1635 } 1636 1637 /** 1638 * Parses all jsp link macros, and replace them by the right target path.<p> 1639 * 1640 * @param content the content to parse 1641 * @param controller the request controller 1642 * 1643 * @return the parsed content 1644 */ 1645 protected String parseJspLinkMacros(String content, CmsFlexController controller) { 1646 1647 CmsJspLinkMacroResolver macroResolver = new CmsJspLinkMacroResolver(controller.getCmsObject(), null, true); 1648 return macroResolver.resolveMacros(content); 1649 } 1650 1651 /** 1652 * Returns the jsp resource identified by the given name, using the controllers cms context.<p> 1653 * 1654 * @param controller the flex controller 1655 * @param jspName the name of the jsp 1656 * 1657 * @return an OpenCms resource 1658 * 1659 * @throws CmsException if something goes wrong 1660 */ 1661 protected CmsResource readJspResource(CmsFlexController controller, String jspName) throws CmsException { 1662 1663 // create an OpenCms user context that operates in the root site 1664 CmsObject cms = OpenCms.initCmsObject(controller.getCmsObject()); 1665 // we only need to change the site, but not the project, 1666 // since the request has already the right project set 1667 cms.getRequestContext().setSiteRoot(""); 1668 // try to read the resource 1669 return cms.readResource(jspName); 1670 } 1671 1672 /** 1673 * Delivers the plain uninterpreted resource with escaped XML.<p> 1674 * 1675 * This is intended for viewing historical versions.<p> 1676 * 1677 * @param cms the initialized CmsObject which provides user permissions 1678 * @param file the requested OpenCms VFS resource 1679 * @param req the servlet request 1680 * @param res the servlet response 1681 * 1682 * @throws IOException might be thrown by the servlet environment 1683 * @throws CmsException in case of errors accessing OpenCms functions 1684 */ 1685 protected void showSource(CmsObject cms, CmsResource file, HttpServletRequest req, HttpServletResponse res) 1686 throws CmsException, IOException { 1687 1688 CmsResource historyResource = (CmsResource)CmsHistoryResourceHandler.getHistoryResource(req); 1689 if (historyResource == null) { 1690 historyResource = file; 1691 } 1692 CmsFile historyFile = cms.readFile(historyResource); 1693 String content = new String(historyFile.getContents()); 1694 // change the content-type header so that browsers show plain text 1695 res.setContentLength(content.length()); 1696 res.setContentType("text/plain"); 1697 1698 Writer out = res.getWriter(); 1699 out.write(content); 1700 out.close(); 1701 } 1702 1703 /** 1704 * Updates a JSP page in the "real" file system in case the VFS resource has changed based on the resource name.<p> 1705 * 1706 * Generates a resource based on the provided name and calls {@link #updateJsp(CmsResource, CmsFlexController, Set)}.<p> 1707 * 1708 * @param vfsName the name of the JSP file resource in the VFS 1709 * @param controller the controller for the JSP integration 1710 * @param updatedFiles a Set containing all JSP pages that have been already updated 1711 * 1712 * @return the file name of the updated JSP in the "real" FS 1713 */ 1714 protected String updateJsp(String vfsName, CmsFlexController controller, Set<String> updatedFiles) { 1715 1716 String jspVfsName = CmsLinkManager.getAbsoluteUri(vfsName, controller.getCurrentRequest().getElementRootPath()); 1717 if (LOG.isDebugEnabled()) { 1718 LOG.debug(Messages.get().getBundle().key(Messages.LOG_UPDATE_JSP_1, jspVfsName)); 1719 } 1720 String jspRfsName; 1721 try { 1722 CmsResource includeResource; 1723 try { 1724 // first try a root path 1725 includeResource = readJspResource(controller, jspVfsName); 1726 } catch (CmsVfsResourceNotFoundException e) { 1727 // if fails, try a site relative path 1728 includeResource = readJspResource( 1729 controller, 1730 controller.getCmsObject().getRequestContext().addSiteRoot(jspVfsName)); 1731 } 1732 // make sure the jsp referenced file is generated 1733 jspRfsName = updateJsp(includeResource, controller, updatedFiles); 1734 if (LOG.isDebugEnabled()) { 1735 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NAME_REAL_FS_1, jspRfsName)); 1736 } 1737 } catch (Exception e) { 1738 jspRfsName = null; 1739 if (LOG.isDebugEnabled()) { 1740 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_UPDATE_1, jspVfsName), e); 1741 } 1742 } 1743 return jspRfsName; 1744 } 1745 1746 /** 1747 * Updates all jsp files that include the given jsp file using the 'link.strong' macro.<p> 1748 * 1749 * @param resource the current updated jsp file 1750 * @param controller the controller for the jsp integration 1751 * @param updatedFiles the already updated files 1752 * 1753 * @return <code>true</code> if the given JSP file should be updated due to dirty included files 1754 * 1755 * @throws ServletException might be thrown in the process of including the JSP 1756 * @throws IOException might be thrown in the process of including the JSP 1757 * @throws CmsLoaderException if the resource type can not be read 1758 */ 1759 protected boolean updateStrongLinks(CmsResource resource, CmsFlexController controller, Set<String> updatedFiles) 1760 throws CmsLoaderException, IOException, ServletException { 1761 1762 int numberOfUpdates = updatedFiles.size(); 1763 CmsObject cms = controller.getCmsObject(); 1764 CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType(CmsRelationType.JSP_STRONG); 1765 Iterator<CmsRelation> it; 1766 try { 1767 it = cms.getRelationsForResource(resource, filter).iterator(); 1768 } catch (CmsException e) { 1769 // should never happen 1770 if (LOG.isErrorEnabled()) { 1771 LOG.error(e.getLocalizedMessage(), e); 1772 } 1773 return false; 1774 } 1775 while (it.hasNext()) { 1776 CmsRelation relation = it.next(); 1777 CmsResource target = null; 1778 try { 1779 target = relation.getTarget(cms, CmsResourceFilter.DEFAULT); 1780 } catch (CmsException e) { 1781 // should never happen 1782 if (LOG.isErrorEnabled()) { 1783 LOG.error(e.getLocalizedMessage(), e); 1784 } 1785 continue; 1786 } 1787 // prevent recursive update when including the same file 1788 if (resource.equals(target)) { 1789 continue; 1790 } 1791 // update the target 1792 updateJsp(target, controller, updatedFiles); 1793 } 1794 // the current jsp file should be updated only if one of the included jsp has been updated 1795 return numberOfUpdates < updatedFiles.size(); 1796 } 1797 1798 /** 1799 * Returns the read-write-lock for the given jsp vfs name.<p> 1800 * 1801 * @param jspVfsName the jsp vfs name 1802 * 1803 * @return the read-write-lock 1804 */ 1805 private ReentrantReadWriteLock getFileLock(String jspVfsName) { 1806 1807 ReentrantReadWriteLock lock = m_fileLocks.get(jspVfsName); 1808 if (lock == null) { 1809 // acquire the purge lock before adding new file lock entries 1810 // in case of a JSP repository purge, adding new file lock entries is blocked 1811 // and all present file locks will be locked for purge 1812 // @see #triggerPurge() 1813 m_purgeLock.readLock().lock(); 1814 synchronized (m_fileLocks) { 1815 if (!m_fileLocks.containsKey(jspVfsName)) { 1816 m_fileLocks.put(jspVfsName, new ReentrantReadWriteLock(true)); 1817 } 1818 lock = m_fileLocks.get(jspVfsName); 1819 } 1820 m_purgeLock.readLock().unlock(); 1821 } 1822 return lock; 1823 } 1824 1825 /** 1826 * Returns the RFS path for a JSP resource.<p> 1827 * 1828 * This does not check whether there actually exists a file at the returned path. 1829 * 1830 * @param resource the JSP resource 1831 * @param online true if the path for the online project should be returned 1832 * 1833 * @return the RFS path for the JSP 1834 * 1835 * @throws CmsLoaderException if accessing the resource loader fails 1836 */ 1837 private String getJspRfsPath(CmsResource resource, boolean online) throws CmsLoaderException { 1838 1839 String jspVfsName = resource.getRootPath(); 1840 String extension; 1841 int loaderId = OpenCms.getResourceManager().getResourceType(resource.getTypeId()).getLoaderId(); 1842 if ((loaderId == CmsJspLoader.RESOURCE_LOADER_ID) && (!jspVfsName.endsWith(JSP_EXTENSION))) { 1843 // this is a true JSP resource that does not end with ".jsp" 1844 extension = JSP_EXTENSION; 1845 } else { 1846 // not a JSP resource or already ends with ".jsp" 1847 extension = ""; 1848 } 1849 String jspPath = CmsFileUtil.getRepositoryName(m_jspRepository, jspVfsName + extension, online); 1850 return jspPath; 1851 } 1852}