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