blob: e9ddf4201f8828fc3fd89598ed58da9c9aafefcb [file] [log] [blame]
/*
* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache" and "Apache Software Foundation" and
* "Apache Tapestry" must not be used to endorse or promote products
* derived from this software without prior written permission. For
* written permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* "Apache Tapestry", nor may "Apache" appear in their name, without
* prior written permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
package net.sf.tapestry.engine;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.sf.tapestry.ApplicationRuntimeException;
import net.sf.tapestry.IAsset;
import net.sf.tapestry.IComponent;
import net.sf.tapestry.IMarkupWriter;
import net.sf.tapestry.IRenderDescription;
import net.sf.tapestry.IRequestCycle;
import net.sf.tapestry.IResourceResolver;
import net.sf.tapestry.ITemplateSource;
import net.sf.tapestry.NoSuchComponentException;
import net.sf.tapestry.Tapestry;
import net.sf.tapestry.parse.ComponentTemplate;
import net.sf.tapestry.parse.ITemplateParserDelegate;
import net.sf.tapestry.parse.TemplateParseException;
import net.sf.tapestry.parse.TemplateParser;
import net.sf.tapestry.parse.TemplateToken;
import net.sf.tapestry.spec.ComponentSpecification;
import net.sf.tapestry.util.MultiKey;
/**
* 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);
// 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 HashMap();
// Previously read templates; key is the HTML resource path, value
// is the ComponentTemplate.
private Map _templates = new HashMap();
/**
* Number of tokens (each template contains multiple tokens).
*
**/
private int _tokenCount;
private static final int BUFFER_SIZE = 2000;
private IResourceResolver _resolver;
private TemplateParser _parser;
private static class ParserDelegate implements ITemplateParserDelegate
{
IComponent _component;
ParserDelegate(IComponent component)
{
_component = component;
}
public boolean getKnownComponent(String componentId)
{
try
{
_component.getComponent(componentId);
return true;
}
catch (NoSuchComponentException ex)
{
return false;
}
}
public boolean getAllowBody(String componentId)
{
return _component.getComponent(componentId).getSpecification().getAllowBody();
}
}
public DefaultTemplateSource(IResourceResolver resolver)
{
_resolver = resolver;
}
/**
* Clears the template cache. This is used during debugging.
*
**/
public void reset()
{
_cache = null;
_templates.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)
{
ComponentSpecification specification = component.getSpecification();
String specificationResourcePath = specification.getSpecificationResourcePath();
Locale locale = component.getPage().getLocale();
Object key = new MultiKey(new Object[] { specificationResourcePath, locale }, false);
ComponentTemplate result = searchCache(key);
if (result != null)
return result;
result = findTemplate(cycle, specificationResourcePath, component, locale);
if (result == null)
{
String stringKey =
(locale == null) ? "DefaultTemplateSource.no-template" : "DefaultTemplateSource.no-template-in-locale";
throw new ApplicationRuntimeException(Tapestry.getString(stringKey, component.getExtendedId(), locale));
}
saveToCache(key, result);
return result;
}
private synchronized ComponentTemplate searchCache(Object key)
{
if (_cache == null)
return null;
return (ComponentTemplate) _cache.get(key);
}
private synchronized void saveToCache(Object key, ComponentTemplate template)
{
if (_cache == null)
_cache = new HashMap();
_cache.put(key, template);
}
private synchronized ComponentTemplate findTemplate(
IRequestCycle cycle,
String specificationResourcePath,
IComponent component,
Locale locale)
{
IAsset templateAsset = component.getAsset(TEMPLATE_ASSET_NAME);
if (templateAsset != null)
return readTemplateFromAsset(cycle, component, templateAsset, locale);
return findStandardTemplate(specificationResourcePath, component, locale);
}
/**
* Reads an asset to get the template.
*
**/
private synchronized ComponentTemplate readTemplateFromAsset(IRequestCycle cycle, IComponent component, IAsset asset, Locale locale)
{
InputStream stream = asset.getResourceAsStream(cycle, locale);
char[] templateData = null;
try
{
templateData = readTemplateStream(stream);
stream.close();
}
catch (IOException ex)
{
throw new ApplicationRuntimeException(
Tapestry.getString("DefaultTemplateSource.unable-to-read-template", asset),
ex);
}
return constructTokens(templateData, asset.toString(), 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.
*
**/
private synchronized ComponentTemplate findStandardTemplate(
String specificationResourcePath,
IComponent component,
Locale locale)
{
String candidatePath = null;
String language = null;
String country = null;
if (LOG.isDebugEnabled())
LOG.debug(
"Searching for localized version of template for "
+ specificationResourcePath
+ " in locale "
+ locale.getDisplayName());
int dotx = specificationResourcePath.lastIndexOf('.');
StringBuffer buffer = new StringBuffer(dotx + 20);
buffer.append(specificationResourcePath.substring(0, dotx));
int rawLength = buffer.length();
int start = 2;
if (locale != null)
{
country = locale.getCountry();
if (country.length() > 0)
start--;
// This assumes that you never have the case where there's
// a null language code and a non-null country code.
language = locale.getLanguage();
if (language.length() > 0)
start--;
}
ComponentTemplate result = null;
// On pass #0, we use language code and country code
// On pass #1, we use language code
// On pass #2, we use neither.
// We skip pass #0 or #1 depending on whether the language code
// and/or country code is null.
for (int i = start; i < 3; i++)
{
buffer.setLength(rawLength);
if (i < 2)
{
buffer.append('_');
buffer.append(language);
}
if (i == 0)
{
buffer.append('_');
buffer.append(country);
}
buffer.append(".html");
candidatePath = buffer.toString();
// See if it's been parsed before
result = (ComponentTemplate) _templates.get(candidatePath);
if (result != null)
break;
// Ok, see if it exists.
result = parseTemplate(candidatePath, component);
if (result != null)
{
_templates.put(candidatePath, result);
break;
}
}
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(String resourceName, IComponent component)
{
char[] templateData = readTemplate(resourceName);
if (templateData == null)
return null;
return constructTokens(templateData, resourceName, component);
}
private ComponentTemplate constructTokens(char[] templateData, String resourceName, IComponent component)
{
if (_parser == null)
_parser = new TemplateParser();
ITemplateParserDelegate delegate = new ParserDelegate(component);
TemplateToken[] tokens;
try
{
tokens = _parser.parse(templateData, delegate, resourceName);
}
catch (TemplateParseException ex)
{
throw new ApplicationRuntimeException(
Tapestry.getString("DefaultTemplateSource.unable-to-parse-template", resourceName),
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(String resourceName)
{
URL url;
InputStream stream = null;
url = _resolver.getResource(resourceName);
if (url == null)
return null;
if (LOG.isDebugEnabled())
LOG.debug("Reading template " + resourceName + " from " + url);
try
{
stream = url.openStream();
return readTemplateStream(stream);
}
catch (IOException ex)
{
throw new ApplicationRuntimeException(
Tapestry.getString("DefaultTemplateSource.unable-to-read-template", resourceName),
ex);
}
finally
{
try
{
if (stream != null)
stream.close();
}
catch (IOException e)
{
// Ignore it!
}
}
}
/**
* Reads a Stream into memory as an array of characters.
*
**/
private char[] readTemplateStream(InputStream stream) throws IOException
{
char[] charBuffer = new char[BUFFER_SIZE];
StringBuffer buffer = new StringBuffer();
InputStreamReader reader = new InputStreamReader(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 synchronized String toString()
{
StringBuffer buffer = new StringBuffer("DefaultTemplateSource@");
buffer.append(Integer.toHexString(hashCode()));
buffer.append('[');
if (_cache != null)
buffer.append(_cache.keySet());
if (_tokenCount > 0)
{
buffer.append(", ");
buffer.append(_tokenCount);
buffer.append(" tokens");
}
buffer.append(']');
return buffer.toString();
}
/** @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("]");
}
}