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