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