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