| // Copyright 2004 The Apache Software Foundation |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry.engine; |
| |
| import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap; |
| import edu.emory.mathcs.backport.java.util.concurrent.locks.ReentrantLock; |
| import org.apache.commons.lang.builder.ToStringBuilder; |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.tapestry.*; |
| import org.apache.tapestry.parse.*; |
| import org.apache.tapestry.spec.IApplicationSpecification; |
| import org.apache.tapestry.spec.IComponentSpecification; |
| import org.apache.tapestry.util.*; |
| |
| import java.io.BufferedInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.net.URL; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * Default implementation of {@link ITemplateSource}. Templates, once parsed, |
| * stay in memory until explicitly cleared. |
| * |
| * <p>An instance of this class acts as a singleton shared by all sessions, so it |
| * must be threadsafe. |
| * |
| * @author Howard Lewis Ship |
| * @version $Id$ |
| * |
| **/ |
| |
| public class DefaultTemplateSource implements ITemplateSource, IRenderDescription |
| { |
| private static final Log LOG = LogFactory.getLog(DefaultTemplateSource.class); |
| |
| |
| // The name of the component/application/etc property that will be used to |
| // determine the encoding to use when loading the template |
| |
| private static final String TEMPLATE_ENCODING_PROPERTY_NAME = "org.apache.tapestry.template-encoding"; |
| |
| // Cache of previously retrieved templates. Key is a multi-key of |
| // specification resource path and locale (local may be null), value |
| // is the ComponentTemplate. |
| |
| private Map _cache = new ConcurrentHashMap();//Collections.synchronizedMap(new HashMap()); |
| |
| // Previously read templates; key is the IResourceLocation, value |
| // is the ComponentTemplate. |
| |
| private Map _templates = new ConcurrentHashMap(); //Collections.synchronizedMap(new HashMap()); |
| |
| // Used to synchronize access to specific templates |
| private ConcurrentHashMap _lockCache = new ConcurrentHashMap(); |
| |
| /** |
| * Number of tokens (each template contains multiple tokens). |
| * |
| **/ |
| |
| private int _tokenCount; |
| |
| private static final int BUFFER_SIZE = 2000; |
| |
| private TemplateParser _parser; |
| |
| /** @since 2.2 **/ |
| |
| private IResourceLocation _applicationRootLocation; |
| |
| /** @since 3.0 **/ |
| |
| private ITemplateSourceDelegate _delegate; |
| |
| /** |
| * Clears the template cache. This is used during debugging. |
| * |
| **/ |
| |
| public void reset() |
| { |
| _cache.clear(); |
| _templates.clear(); |
| _lockCache.clear(); |
| |
| _tokenCount = 0; |
| } |
| |
| /** |
| * Reads the template for the component. |
| * |
| * <p>Returns null if the template can't be found. |
| * |
| **/ |
| |
| public ComponentTemplate getTemplate(IRequestCycle cycle, IComponent component) |
| { |
| IComponentSpecification specification = component.getSpecification(); |
| IResourceLocation specificationLocation = specification.getSpecificationLocation(); |
| |
| Locale locale = component.getPage().getLocale(); |
| |
| Object key = new MultiKey(new Object[] { specificationLocation, locale }, false); |
| |
| ComponentTemplate result = searchCache(key); |
| if (result != null) |
| return result; |
| |
| _lockCache.putIfAbsent(key, new ReentrantLock()); |
| |
| ReentrantLock lock = (ReentrantLock)_lockCache.get(key); |
| |
| lock.lock(); |
| try |
| { |
| result = searchCache(key); |
| if (result != null) |
| return result; |
| |
| result = findTemplate(cycle, specificationLocation, component, locale); |
| |
| if (result == null) |
| { |
| result = getTemplateFromDelegate(cycle, component, locale); |
| |
| if (result != null) |
| return result; |
| |
| String stringKey = |
| component.getSpecification().isPageSpecification() |
| ? "DefaultTemplateSource.no-template-for-page" |
| : "DefaultTemplateSource.no-template-for-component"; |
| |
| throw new ApplicationRuntimeException( |
| Tapestry.format(stringKey, component.getExtendedId(), locale), |
| component, |
| component.getLocation(), |
| null); |
| } |
| |
| saveToCache(key, result); |
| } finally |
| { |
| lock.unlock(); |
| } |
| return result; |
| } |
| |
| private ComponentTemplate searchCache(Object key) |
| { |
| return (ComponentTemplate) _cache.get(key); |
| } |
| |
| private void saveToCache(Object key, ComponentTemplate template) |
| { |
| _cache.put(key, template); |
| |
| } |
| |
| private ComponentTemplate getTemplateFromDelegate( |
| IRequestCycle cycle, |
| IComponent component, |
| Locale locale) |
| { |
| if (_delegate == null) |
| { |
| IEngine engine = cycle.getEngine(); |
| IApplicationSpecification spec = engine.getSpecification(); |
| |
| if (spec.checkExtension(Tapestry.TEMPLATE_SOURCE_DELEGATE_EXTENSION_NAME)) |
| _delegate = |
| (ITemplateSourceDelegate) spec.getExtension( |
| Tapestry.TEMPLATE_SOURCE_DELEGATE_EXTENSION_NAME, |
| ITemplateSourceDelegate.class); |
| else |
| _delegate = NullTemplateSourceDelegate.getSharedInstance(); |
| |
| } |
| |
| return _delegate.findTemplate(cycle, component, locale); |
| } |
| |
| /** |
| * Finds the template for the given component, using the following rules: |
| * <ul> |
| * <li>If the component has a $template asset, use that |
| * <li>Look for a template in the same folder as the component |
| * <li>If a page in the application namespace, search in the application root |
| * <li>Fail! |
| * </ul> |
| * |
| * @return the template, or null if not found |
| * |
| **/ |
| |
| private ComponentTemplate findTemplate( |
| IRequestCycle cycle, |
| IResourceLocation location, |
| IComponent component, |
| Locale locale) |
| { |
| IAsset templateAsset = component.getAsset(TEMPLATE_ASSET_NAME); |
| |
| if (templateAsset != null) |
| return readTemplateFromAsset(cycle, component, templateAsset); |
| |
| String name = location.getName(); |
| int dotx = name.lastIndexOf('.'); |
| String templateBaseName = name.substring(0, dotx + 1) + getTemplateExtension(component); |
| |
| ComponentTemplate result = |
| findStandardTemplate(cycle, location, component, templateBaseName, locale); |
| |
| if (result == null |
| && component.getSpecification().isPageSpecification() |
| && component.getNamespace().isApplicationNamespace()) |
| result = findPageTemplateInApplicationRoot(cycle, component, templateBaseName, locale); |
| |
| return result; |
| } |
| |
| private ComponentTemplate findPageTemplateInApplicationRoot( |
| IRequestCycle cycle, |
| IComponent component, |
| String templateBaseName, |
| Locale locale) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Checking for " + templateBaseName + " in application root"); |
| |
| if (_applicationRootLocation == null) |
| _applicationRootLocation = Tapestry.getApplicationRootLocation(cycle); |
| |
| IResourceLocation baseLocation = |
| _applicationRootLocation.getRelativeLocation(templateBaseName); |
| IResourceLocation localizedLocation = baseLocation.getLocalization(locale); |
| |
| if (localizedLocation == null) |
| return null; |
| |
| return getOrParseTemplate(cycle, localizedLocation, component); |
| } |
| |
| /** |
| * Reads an asset to get the template. |
| * |
| **/ |
| |
| private ComponentTemplate readTemplateFromAsset( |
| IRequestCycle cycle, |
| IComponent component, |
| IAsset asset) |
| { |
| InputStream stream = asset.getResourceAsStream(cycle); |
| |
| char[] templateData = null; |
| |
| try |
| { |
| String encoding = getTemplateEncoding(cycle, component, null); |
| |
| templateData = readTemplateStream(stream, encoding); |
| |
| stream.close(); |
| } |
| catch (IOException ex) |
| { |
| throw new ApplicationRuntimeException( |
| Tapestry.format("DefaultTemplateSource.unable-to-read-template", asset), |
| ex); |
| } |
| |
| IResourceLocation resourceLocation = asset.getResourceLocation(); |
| |
| return constructTemplateInstance(cycle, templateData, resourceLocation, component); |
| } |
| |
| /** |
| * Search for the template corresponding to the resource and the locale. |
| * This may be in the template map already, or may involve reading and |
| * parsing the template. |
| * |
| * @return the template, or null if not found. |
| * |
| **/ |
| |
| private ComponentTemplate findStandardTemplate( |
| IRequestCycle cycle, |
| IResourceLocation location, |
| IComponent component, |
| String templateBaseName, |
| Locale locale) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug( |
| "Searching for localized version of template for " |
| + location |
| + " in locale " |
| + locale.getDisplayName()); |
| |
| IResourceLocation baseTemplateLocation = location.getRelativeLocation(templateBaseName); |
| |
| IResourceLocation localizedTemplateLocation = baseTemplateLocation.getLocalization(locale); |
| |
| if (localizedTemplateLocation == null) |
| return null; |
| |
| return getOrParseTemplate(cycle, localizedTemplateLocation, component); |
| |
| } |
| |
| /** |
| * Returns a previously parsed template at the specified location (which must already |
| * be localized). If not already in the template Map, then the |
| * location is parsed and stored into the templates Map, then returned. |
| * |
| **/ |
| |
| private ComponentTemplate getOrParseTemplate( |
| IRequestCycle cycle, |
| IResourceLocation location, |
| IComponent component) |
| { |
| |
| ComponentTemplate result = (ComponentTemplate) _templates.get(location); |
| if (result != null) |
| return result; |
| |
| // Ok, see if it exists. |
| |
| result = parseTemplate(cycle, location, component); |
| |
| if (result != null) |
| _templates.put(location, result); |
| |
| return result; |
| } |
| |
| /** |
| * Reads the template for the given resource; returns null if the |
| * resource doesn't exist. Note that this method is only invoked |
| * from a synchronized block, so there shouldn't be threading |
| * issues here. |
| * |
| **/ |
| |
| private ComponentTemplate parseTemplate( |
| IRequestCycle cycle, |
| IResourceLocation location, |
| IComponent component) |
| { |
| String encoding = getTemplateEncoding(cycle, component, location.getLocale()); |
| |
| char[] templateData = readTemplate(location, encoding); |
| if (templateData == null) |
| return null; |
| |
| return constructTemplateInstance(cycle, templateData, location, component); |
| } |
| |
| /** |
| * This method is currently synchronized, because |
| * {@link TemplateParser} is not threadsafe. Another good candidate |
| * for a pooling mechanism, especially because parsing a template |
| * may take a while. |
| * |
| **/ |
| |
| private synchronized ComponentTemplate constructTemplateInstance( |
| IRequestCycle cycle, |
| char[] templateData, |
| IResourceLocation location, |
| IComponent component) |
| { |
| if (_parser == null) |
| _parser = new TemplateParser(); |
| |
| ITemplateParserDelegate delegate = new TemplateParserDelegateImpl(component, cycle); |
| |
| TemplateToken[] tokens; |
| |
| try |
| { |
| tokens = _parser.parse(templateData, delegate, location); |
| } |
| catch (TemplateParseException ex) |
| { |
| throw new ApplicationRuntimeException( |
| Tapestry.format("DefaultTemplateSource.unable-to-parse-template", location), |
| ex); |
| } |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Parsed " + tokens.length + " tokens from template"); |
| |
| _tokenCount += tokens.length; |
| |
| return new ComponentTemplate(templateData, tokens); |
| } |
| |
| /** |
| * Reads the template, given the complete path to the |
| * resource. Returns null if the resource doesn't exist. |
| * |
| **/ |
| |
| private char[] readTemplate(IResourceLocation location, String encoding) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Reading template " + location); |
| |
| URL url = location.getResourceURL(); |
| |
| if (url == null) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Template does not exist."); |
| |
| return null; |
| } |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Reading template from URL " + url); |
| |
| InputStream stream = null; |
| |
| try |
| { |
| stream = url.openStream(); |
| |
| return readTemplateStream(stream, encoding); |
| } |
| catch (IOException ex) |
| { |
| throw new ApplicationRuntimeException( |
| Tapestry.format("DefaultTemplateSource.unable-to-read-template", location), |
| ex); |
| } |
| finally |
| { |
| Tapestry.close(stream); |
| } |
| |
| } |
| |
| /** |
| * Reads a Stream into memory as an array of characters. |
| * |
| **/ |
| |
| private char[] readTemplateStream(InputStream stream, String encoding) throws IOException |
| { |
| char[] charBuffer = new char[BUFFER_SIZE]; |
| StringBuffer buffer = new StringBuffer(); |
| |
| InputStreamReader reader; |
| if (encoding != null) |
| reader = new InputStreamReader(new BufferedInputStream(stream), encoding); |
| else |
| reader = new InputStreamReader(new BufferedInputStream(stream)); |
| |
| try |
| { |
| while (true) |
| { |
| int charsRead = reader.read(charBuffer, 0, BUFFER_SIZE); |
| |
| if (charsRead <= 0) |
| break; |
| |
| buffer.append(charBuffer, 0, charsRead); |
| } |
| } |
| finally |
| { |
| reader.close(); |
| } |
| |
| // OK, now reuse the charBuffer variable to |
| // produce the final result. |
| |
| int length = buffer.length(); |
| |
| charBuffer = new char[length]; |
| |
| // Copy the character out of the StringBuffer and into the |
| // array. |
| |
| buffer.getChars(0, length, charBuffer, 0); |
| |
| return charBuffer; |
| } |
| |
| public String toString() |
| { |
| ToStringBuilder builder = new ToStringBuilder(this); |
| |
| builder.append("tokenCount", _tokenCount); |
| |
| builder.append("templates", _templates.keySet()); |
| |
| return builder.toString(); |
| } |
| |
| /** |
| * Checks for the {@link Tapestry#TEMPLATE_EXTENSION_PROPERTY} in the component's |
| * specification, then in the component's namespace's specification. Returns |
| * {@link Tapestry#DEFAULT_TEMPLATE_EXTENSION} if not otherwise overriden. |
| * |
| **/ |
| |
| private String getTemplateExtension(IComponent component) |
| { |
| String extension = |
| component.getSpecification().getProperty(Tapestry.TEMPLATE_EXTENSION_PROPERTY); |
| |
| if (extension != null) |
| return extension; |
| |
| extension = |
| component.getNamespace().getSpecification().getProperty( |
| Tapestry.TEMPLATE_EXTENSION_PROPERTY); |
| |
| if (extension != null) |
| return extension; |
| |
| return Tapestry.DEFAULT_TEMPLATE_EXTENSION; |
| } |
| |
| /** @since 1.0.6 **/ |
| |
| public synchronized void renderDescription(IMarkupWriter writer) |
| { |
| writer.print("DefaultTemplateSource["); |
| |
| if (_tokenCount > 0) |
| { |
| writer.print(_tokenCount); |
| writer.print(" tokens"); |
| } |
| |
| if (_cache != null) |
| { |
| boolean first = true; |
| Iterator i = _cache.entrySet().iterator(); |
| |
| while (i.hasNext()) |
| { |
| if (first) |
| { |
| writer.begin("ul"); |
| first = false; |
| } |
| |
| Map.Entry e = (Map.Entry) i.next(); |
| Object key = e.getKey(); |
| ComponentTemplate template = (ComponentTemplate) e.getValue(); |
| |
| writer.begin("li"); |
| writer.print(key.toString()); |
| writer.print(" ("); |
| writer.print(template.getTokenCount()); |
| writer.print(" tokens)"); |
| writer.println(); |
| writer.end(); |
| } |
| |
| if (!first) |
| { |
| writer.end(); // <ul> |
| writer.beginEmpty("br"); |
| } |
| } |
| |
| writer.print("]"); |
| |
| } |
| |
| private String getTemplateEncoding(IRequestCycle cycle, IComponent component, Locale locale) |
| { |
| IPropertySource source = getComponentPropertySource(cycle, component); |
| |
| if (locale != null) |
| source = new LocalizedPropertySource(locale, source); |
| |
| return getTemplateEncodingProperty(source); |
| } |
| |
| private IPropertySource getComponentPropertySource(IRequestCycle cycle, IComponent component) |
| { |
| DelegatingPropertySource source = new DelegatingPropertySource(); |
| |
| // Search for the encoding property in the following order: |
| // First search the component specification |
| source.addSource(new PropertyHolderPropertySource(component.getSpecification())); |
| |
| // Then search its library specification |
| source.addSource(new PropertyHolderPropertySource(component.getNamespace().getSpecification())); |
| |
| // Then search the rest of the standard path |
| source.addSource(cycle.getEngine().getPropertySource()); |
| |
| return source; |
| } |
| |
| private String getTemplateEncodingProperty(IPropertySource source) |
| { |
| return source.getPropertyValue(TEMPLATE_ENCODING_PROPERTY_NAME); |
| } |
| |
| } |