001 // Copyright 2004, 2005 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry.services.impl; 016 017 import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap; 018 import org.apache.commons.logging.Log; 019 import org.apache.hivemind.ApplicationRuntimeException; 020 import org.apache.hivemind.Resource; 021 import org.apache.tapestry.*; 022 import org.apache.tapestry.engine.ITemplateSourceDelegate; 023 import org.apache.tapestry.event.ReportStatusEvent; 024 import org.apache.tapestry.event.ReportStatusListener; 025 import org.apache.tapestry.event.ResetEventListener; 026 import org.apache.tapestry.l10n.ResourceLocalizer; 027 import org.apache.tapestry.parse.*; 028 import org.apache.tapestry.resolver.ComponentSpecificationResolver; 029 import org.apache.tapestry.resolver.IComponentResourceResolver; 030 import org.apache.tapestry.services.ComponentPropertySource; 031 import org.apache.tapestry.services.TemplateSource; 032 import org.apache.tapestry.spec.IComponentSpecification; 033 import org.apache.tapestry.util.MultiKey; 034 035 import java.io.BufferedInputStream; 036 import java.io.IOException; 037 import java.io.InputStream; 038 import java.io.InputStreamReader; 039 import java.net.URL; 040 import java.util.Iterator; 041 import java.util.Locale; 042 import java.util.Map; 043 044 /** 045 * Implementation of {@link org.apache.tapestry.services.TemplateSource}. Templates, once parsed, 046 * stay in memory until explicitly cleared. 047 * 048 * @author Howard Lewis Ship 049 */ 050 051 public class TemplateSourceImpl implements TemplateSource, ResetEventListener, ReportStatusListener 052 { 053 054 // The name of the component/application/etc property that will be used to 055 // determine the encoding to use when loading the template 056 057 public static final String TEMPLATE_ENCODING_PROPERTY_NAME = "org.apache.tapestry.template-encoding"; 058 059 private static final int BUFFER_SIZE = 2000; 060 061 private String _serviceId; 062 063 private Log _log; 064 065 // Cache of previously retrieved templates. Key is a multi-key of 066 // specification resource path and locale (local may be null), value 067 // is the ComponentTemplate. 068 069 private Map _cache = new ConcurrentHashMap(); 070 071 // Previously read templates; key is the Resource, value 072 // is the ComponentTemplate. 073 074 private Map _templates = new ConcurrentHashMap(); 075 076 private ITemplateParser _parser; 077 078 /** @since 2.2 */ 079 080 private Resource _contextRoot; 081 082 /** @since 3.0 */ 083 084 private ITemplateSourceDelegate _delegate; 085 086 /** @since 4.0 */ 087 088 private ComponentSpecificationResolver _componentSpecificationResolver; 089 090 /** @since 4.0 */ 091 092 private ComponentPropertySource _componentPropertySource; 093 094 /** @since 4.0 */ 095 096 private ResourceLocalizer _localizer; 097 098 /** @since 4.1.2 */ 099 100 private IComponentResourceResolver _resourceResolver; 101 102 /** 103 * Clears the template cache. This is used during debugging. 104 */ 105 106 public void resetEventDidOccur() 107 { 108 _cache.clear(); 109 _templates.clear(); 110 } 111 112 public void reportStatus(ReportStatusEvent event) 113 { 114 event.title(_serviceId); 115 116 int templateCount = 0; 117 int tokenCount = 0; 118 int characterCount = 0; 119 120 Iterator i = _templates.values().iterator(); 121 122 while (i.hasNext()) 123 { 124 ComponentTemplate template = (ComponentTemplate) i.next(); 125 126 templateCount++; 127 128 int count = template.getTokenCount(); 129 130 tokenCount += count; 131 132 for (int j = 0; j < count; j++) 133 { 134 TemplateToken token = template.getToken(j); 135 136 if (token.getType() == TokenType.TEXT) 137 { 138 TextToken tt = (TextToken) token; 139 140 characterCount += tt.getLength(); 141 } 142 } 143 } 144 145 event.property("parsed templates", templateCount); 146 event.property("total template tokens", tokenCount); 147 event.property("total template characters", characterCount); 148 149 event.section("Parsed template token counts"); 150 151 i = _templates.entrySet().iterator(); 152 153 while (i.hasNext()) 154 { 155 Map.Entry entry = (Map.Entry) i.next(); 156 157 String key = entry.getKey().toString(); 158 159 ComponentTemplate template = (ComponentTemplate) entry.getValue(); 160 161 event.property(key, template.getTokenCount()); 162 } 163 } 164 165 /** 166 * Reads the template for the component. 167 */ 168 169 public ComponentTemplate getTemplate(IRequestCycle cycle, IComponent component) 170 { 171 IComponentSpecification specification = component.getSpecification(); 172 Resource resource = specification.getSpecificationLocation(); 173 174 Locale locale = component.getPage().getLocale(); 175 176 Object key = new MultiKey(new Object[] { resource, locale }, false); 177 178 ComponentTemplate result = searchCache(key); 179 if (result != null) 180 return result; 181 182 result = findTemplate(cycle, resource, component, locale); 183 184 if (result == null) 185 { 186 result = _delegate.findTemplate(cycle, component, locale); 187 188 if (result != null) 189 return result; 190 191 String message = component.getSpecification().isPageSpecification() ? ImplMessages 192 .noTemplateForPage(component.getExtendedId(), locale) : ImplMessages 193 .noTemplateForComponent(component.getExtendedId(), locale); 194 195 throw new ApplicationRuntimeException(message, component, component.getLocation(), null); 196 } 197 198 saveToCache(key, result); 199 200 return result; 201 } 202 203 private ComponentTemplate searchCache(Object key) 204 { 205 return (ComponentTemplate) _cache.get(key); 206 } 207 208 private void saveToCache(Object key, ComponentTemplate template) 209 { 210 _cache.put(key, template); 211 212 } 213 214 /** 215 * Finds the template for the given component, using the following rules: 216 * <ul> 217 * <li>If the component has a $template asset, use that 218 * <li>Look for a template in the same folder as the component 219 * <li>If a page in the application namespace, search in the application root 220 * <li>Fail! 221 * </ul> 222 * 223 * @return the template, or null if not found 224 */ 225 226 private ComponentTemplate findTemplate(IRequestCycle cycle, Resource resource, 227 IComponent component, Locale locale) 228 { 229 IAsset templateAsset = component.getAsset(TEMPLATE_ASSET_NAME); 230 231 if (templateAsset != null && templateAsset.getResourceLocation() != null && templateAsset.getResourceLocation().getResourceURL() != null) 232 return readTemplateFromAsset(cycle, component, templateAsset.getResourceLocation()); 233 234 String name = resource.getName(); 235 int dotx = name.lastIndexOf('.'); 236 String templateExtension = getTemplateExtension(component); 237 String templateBaseName = name.substring(0, dotx + 1) + templateExtension; 238 239 ComponentTemplate result = findStandardTemplate( 240 cycle, 241 resource, 242 component, 243 templateBaseName, 244 locale); 245 246 if (result == null && component.getSpecification().isPageSpecification() 247 && component.getNamespace().isApplicationNamespace()) 248 result = findPageTemplateInApplicationRoot( 249 cycle, 250 (IPage) component, 251 templateExtension, 252 locale); 253 254 if (result == null) { 255 256 Resource template = _resourceResolver.findComponentResource(component, cycle, null, "." + templateExtension, locale); 257 258 if (template != null && template.getResourceURL() != null) 259 return readTemplateFromAsset(cycle, component, template); 260 } 261 262 return result; 263 } 264 265 private ComponentTemplate findPageTemplateInApplicationRoot(IRequestCycle cycle, IPage page, 266 String templateExtension, Locale locale) 267 { 268 // Note: a subtle change from release 3.0 to 4.0. 269 // In release 3.0, you could use a <page> element to define a page named Foo whose 270 // specification was Bar.page. We would then search for /Bar.page. Confusing? Yes. 271 // In 4.0, we are more reliant on the page name, which may include a folder prefix (i.e., 272 // "admin/EditUser", so when we search it is based on the page name and not the 273 // specification resource file name. We would search for Foo.html. Moral of the 274 // story is to use the page name for the page specifiation and the template. 275 276 String templateBaseName = page.getPageName() + "." + templateExtension; 277 278 if (_log.isDebugEnabled()) 279 _log.debug("Checking for " + templateBaseName + " in application root"); 280 281 Resource baseLocation = _contextRoot.getRelativeResource(templateBaseName); 282 Resource localizedLocation = _localizer.findLocalization(baseLocation, locale); 283 284 if (localizedLocation == null) 285 return null; 286 287 return getOrParseTemplate(cycle, localizedLocation, page); 288 } 289 290 291 292 /** 293 * Reads an asset to get the template. 294 */ 295 296 private ComponentTemplate readTemplateFromAsset(IRequestCycle cycle, IComponent component, 297 Resource asset) 298 { 299 InputStream stream = null; 300 301 char[] templateData = null; 302 303 try 304 { 305 stream = asset.getResourceURL().openStream(); 306 307 String encoding = getTemplateEncoding(component, null); 308 309 templateData = readTemplateStream(stream, encoding); 310 311 stream.close(); 312 } 313 catch (IOException ex) 314 { 315 throw new ApplicationRuntimeException(ImplMessages.unableToReadTemplate(asset), ex); 316 } 317 318 return constructTemplateInstance(cycle, templateData, asset, component); 319 } 320 321 /** 322 * Search for the template corresponding to the resource and the locale. This may be in the 323 * template map already, or may involve reading and parsing the template. 324 * 325 * @return the template, or null if not found. 326 */ 327 328 private ComponentTemplate findStandardTemplate(IRequestCycle cycle, Resource resource, 329 IComponent component, String templateBaseName, Locale locale) 330 { 331 if (_log.isDebugEnabled()) 332 _log.debug("Searching for localized version of template for " + resource 333 + " in locale " + locale.getDisplayName()); 334 335 Resource baseTemplateLocation = resource.getRelativeResource(templateBaseName); 336 Resource localizedTemplateLocation = _localizer.findLocalization(baseTemplateLocation, locale); 337 338 if (localizedTemplateLocation == null) 339 return null; 340 341 return getOrParseTemplate(cycle, localizedTemplateLocation, component); 342 343 } 344 345 /** 346 * Returns a previously parsed template at the specified location (which must already be 347 * localized). If not already in the template Map, then the location is parsed and stored into 348 * the templates Map, then returned. 349 */ 350 351 private ComponentTemplate getOrParseTemplate(IRequestCycle cycle, Resource resource, 352 IComponent component) 353 { 354 355 ComponentTemplate result = (ComponentTemplate) _templates.get(resource); 356 if (result != null) 357 return result; 358 359 // Ok, see if it exists. 360 361 result = parseTemplate(cycle, resource, component); 362 363 if (result != null) 364 _templates.put(resource, result); 365 366 return result; 367 } 368 369 /** 370 * Reads the template for the given resource; returns null if the resource doesn't exist. Note 371 * that this method is only invoked from a synchronized block, so there shouldn't be threading 372 * issues here. 373 */ 374 375 private ComponentTemplate parseTemplate(IRequestCycle cycle, Resource resource, 376 IComponent component) 377 { 378 String encoding = getTemplateEncoding(component, resource.getLocale()); 379 380 char[] templateData = readTemplate(resource, encoding); 381 if (templateData == null) 382 return null; 383 384 return constructTemplateInstance(cycle, templateData, resource, component); 385 } 386 387 /** 388 * This method is currently synchronized, because {@link org.apache.tapestry.parse.TemplateParser} is not threadsafe. 389 * Another good candidate for a pooling mechanism, especially because parsing a template may 390 * take a while. 391 */ 392 393 private synchronized ComponentTemplate constructTemplateInstance(IRequestCycle cycle, 394 char[] templateData, Resource resource, IComponent component) 395 { 396 String componentAttributeName = _componentPropertySource.getComponentProperty( 397 component, 398 "org.apache.tapestry.jwcid-attribute-name"); 399 400 ITemplateParserDelegate delegate = new DefaultParserDelegate(component, 401 componentAttributeName, cycle, _componentSpecificationResolver); 402 403 TemplateToken[] tokens; 404 405 try 406 { 407 tokens = _parser.parse(templateData, delegate, resource); 408 } 409 catch (TemplateParseException ex) 410 { 411 throw new ApplicationRuntimeException(ImplMessages.unableToParseTemplate(resource), ex); 412 } 413 414 if (_log.isDebugEnabled()) 415 _log.debug("Parsed " + tokens.length + " tokens from template"); 416 417 return new ComponentTemplate(templateData, tokens); 418 } 419 420 /** 421 * Reads the template, given the complete path to the resource. Returns null if the resource 422 * doesn't exist. 423 */ 424 425 private char[] readTemplate(Resource resource, String encoding) 426 { 427 if (_log.isDebugEnabled()) 428 _log.debug("Reading template " + resource); 429 430 URL url = resource.getResourceURL(); 431 432 if (url == null) 433 { 434 if (_log.isDebugEnabled()) 435 _log.debug("Template does not exist."); 436 437 return null; 438 } 439 440 if (_log.isDebugEnabled()) 441 _log.debug("Reading template from URL " + url); 442 443 InputStream stream = null; 444 445 try 446 { 447 stream = url.openStream(); 448 449 return readTemplateStream(stream, encoding); 450 } 451 catch (IOException ex) 452 { 453 throw new ApplicationRuntimeException(ImplMessages.unableToReadTemplate(resource), ex); 454 } 455 finally 456 { 457 Tapestry.close(stream); 458 } 459 460 } 461 462 /** 463 * Reads a Stream into memory as an array of characters. 464 */ 465 466 private char[] readTemplateStream(InputStream stream, String encoding) throws IOException 467 { 468 char[] charBuffer = new char[BUFFER_SIZE]; 469 StringBuffer buffer = new StringBuffer(); 470 471 InputStreamReader reader; 472 if (encoding != null) 473 reader = new InputStreamReader(new BufferedInputStream(stream), encoding); 474 else 475 reader = new InputStreamReader(new BufferedInputStream(stream)); 476 477 try 478 { 479 while (true) 480 { 481 int charsRead = reader.read(charBuffer, 0, BUFFER_SIZE); 482 483 if (charsRead <= 0) 484 break; 485 486 buffer.append(charBuffer, 0, charsRead); 487 } 488 } 489 finally 490 { 491 reader.close(); 492 } 493 494 // OK, now reuse the charBuffer variable to 495 // produce the final result. 496 497 int length = buffer.length(); 498 499 charBuffer = new char[length]; 500 501 // Copy the character out of the StringBuffer and into the 502 // array. 503 504 buffer.getChars(0, length, charBuffer, 0); 505 506 return charBuffer; 507 } 508 509 /** 510 * Checks for the {@link Tapestry#TEMPLATE_EXTENSION_PROPERTY}in the component's specification, 511 * then in the component's namespace's specification. Returns 512 * {@link Tapestry#TEMPLATE_EXTENSION_PROPERTY} if not otherwise overriden. 513 */ 514 515 private String getTemplateExtension(IComponent component) 516 { 517 return _componentPropertySource.getComponentProperty( 518 component, 519 Tapestry.TEMPLATE_EXTENSION_PROPERTY); 520 } 521 522 private String getTemplateEncoding(IComponent component, Locale locale) 523 { 524 return _componentPropertySource.getLocalizedComponentProperty( 525 component, 526 locale, 527 TEMPLATE_ENCODING_PROPERTY_NAME); 528 } 529 530 /** @since 4.0 */ 531 532 public void setParser(ITemplateParser parser) 533 { 534 _parser = parser; 535 } 536 537 /** @since 4.0 */ 538 539 public void setLog(Log log) 540 { 541 _log = log; 542 } 543 544 /** @since 4.0 */ 545 546 public void setDelegate(ITemplateSourceDelegate delegate) 547 { 548 _delegate = delegate; 549 } 550 551 /** @since 4.0 */ 552 553 public void setComponentSpecificationResolver(ComponentSpecificationResolver resolver) 554 { 555 _componentSpecificationResolver = resolver; 556 } 557 558 /** @since 4.0 */ 559 public void setContextRoot(Resource contextRoot) 560 { 561 _contextRoot = contextRoot; 562 } 563 564 /** @since 4.0 */ 565 public void setComponentPropertySource(ComponentPropertySource componentPropertySource) 566 { 567 _componentPropertySource = componentPropertySource; 568 } 569 570 /** @since 4.0 */ 571 public void setServiceId(String serviceId) 572 { 573 _serviceId = serviceId; 574 } 575 576 /** @since 4.0 */ 577 public void setLocalizer(ResourceLocalizer localizer) 578 { 579 _localizer = localizer; 580 } 581 582 public void setComponentResourceResolver(IComponentResourceResolver resourceResolver) 583 { 584 _resourceResolver = resourceResolver; 585 } 586 }