blob: c7287365717158fcbd09ffa7f696a84a131dc99a [file] [log] [blame]
/*
* $Id$
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.tiles.extras.renderer;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.apache.tiles.Attribute;
import org.apache.tiles.ListAttribute;
import org.apache.tiles.access.TilesAccess;
import org.apache.tiles.request.ApplicationContext;
import org.apache.tiles.request.Request;
import org.apache.tiles.request.render.Renderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a custom "options" syntax for attributes.
* The first option that can be rendered is.
* Comes from <a href="http://tech.finn.no/the-ultimate-view/">The Ultimate View</a> article.<p/>
*
* Actual rendering is delegated to the TypeDetectingRenderer that's supplied in the constructor.<p/>
*
* For example:
* "/WEB-INF/tiles/fragments/${options[myoptions]}/content.jsp"
* given the myptions list-attribute is defined like:
* <pre>
&lt;put-list-attribute name="myoptions">
&lt;add-list-attribute>
&lt;add-attribute value="car"/>
&lt;add-attribute value="vechile"/>
&lt;add-attribute value="advert"/>
&lt;/add-list-attribute>
&lt;/put-list-attribute>
</pre>
* will look for content.jsp <br/>
* first in "/WEB-INF/tiles/fragments/car/" then <br/>
* second in "/WEB-INF/tiles/fragments/vechile/" and <br/>
* last in "/WEB-INF/tiles/fragments/advert".
* <p/>
* <p/>
* Currently only supports one occurrance of such an "option" pattern in the attribute's value.
* <p/>
* Limitation: "looking" for templates is implemented using applicationContext.getResource(..)
* therefore the option values in the options list need to be visible as applicationResources.
* <p/>
* The attribute found and rendered is cached so to improve performance on subsequent lookups.
* The default cache time-to-live is {@value #DEFAULT_CACHE_LIFE}, specified by {@link #DEFAULT_CACHE_LIFE}.
* It can be customised by setting the system property {@value #CACHE_LIFE_PROPERTY}, see {@link #CACHE_LIFE_PROPERTY}.
* Setting it to zero will disable the cache.
*/
public final class OptionsRenderer implements Renderer {
public static final String CACHE_LIFE_PROPERTY = OptionsRenderer.class.getName() + ".cache_ttl_ms";
public static final long DEFAULT_CACHE_LIFE = 1000 * 60 * 5;
public static final Pattern OPTIONS_PATTERN
= Pattern.compile(Pattern.quote("{options[") + "(.+)" + Pattern.quote("]}"));
private static final Logger LOG = LoggerFactory.getLogger(OptionsRenderer.class);
private final ApplicationContext applicationContext;
private final Renderer renderer;
public OptionsRenderer(final ApplicationContext applicationContext, final Renderer renderer) {
this.applicationContext = applicationContext;
this.renderer = renderer;
}
@Override
public boolean isRenderable(final String path, final Request request) {
return renderer.isRenderable(path, request);
}
@Override
public void render(final String path, final Request request) throws IOException {
Matcher matcher = OPTIONS_PATTERN.matcher((String) path);
if (null != matcher && matcher.find()) {
boolean done = false;
String match = matcher.group(1);
ListAttribute fallbacks = (ListAttribute) TilesAccess
.getCurrentContainer(request)
.getAttributeContext(request)
.getAttribute(match);
if (null == fallbacks) {
throw new IllegalStateException("A matching list-attribute name=\"" + match + "\" must be defined.");
} else if (fallbacks.getValue().isEmpty()) {
throw new IllegalStateException(
"list-attribute name=\"" + match + "\" must have minimum one attribute");
}
for (Attribute option : (List<Attribute>) fallbacks.getValue()) {
String template = path.replaceFirst(Pattern.quote(matcher.group()), (String) option.getValue());
done = renderAttempt(template, request);
if (done) { break; }
}
if (!done) {
throw new IOException("None of the options existed for " + path);
}
} else {
renderer.render(path, request);
}
}
private boolean renderAttempt(final String template, final Request request) throws IOException {
boolean result = false;
if (Cache.attemptTemplate(template)) {
try {
if (null != applicationContext.getResource(template)) {
renderer.render(template, request);
result = true;
}
} catch (IOException ex) {
if (ex.getMessage().contains(template)) {
// expected outcome. continue loop.
LOG.trace(ex.getMessage());
} else {
// comes from an inner templateAttribute.render(..) so throw on
throw ex;
}
} catch (RuntimeException ex) {
if (ex.getMessage().contains(template)) {
// expected outcome. continue loop.
LOG.trace(ex.getMessage());
} else {
// comes from an inner templateAttribute.render(..) so throw on
throw ex;
}
}
Cache.update(template, result);
}
return result;
}
private static final class Cache {
private static final long CACHE_LIFE = Long.getLong(CACHE_LIFE_PROPERTY, DEFAULT_CACHE_LIFE);
/** It takes CACHE_LIFE milliseconds for any hot deployments to register.
*/
private static final ConcurrentMap<String,Boolean> TEMPLATE_EXISTS;
static {
LOG.info("cache_ttl_ms=" + CACHE_LIFE);
LoadingCache<String,Boolean> builder = CacheBuilder
.newBuilder()
.expireAfterWrite(CACHE_LIFE, TimeUnit.MILLISECONDS)
.build(
new CacheLoader<String, Boolean>() {
@Override
public Boolean load(String key) {
throw new UnsupportedOperationException(
"illegal TEMPLATE_EXISTS.get(\"" + key
+ "\") before TEMPLATE_EXISTS.containsKey(\"" + key + "\")");
}
});
TEMPLATE_EXISTS = builder.asMap();
}
static boolean attemptTemplate(final String template) {
return !TEMPLATE_EXISTS.containsKey(template) || TEMPLATE_EXISTS.get(template);
}
static void update(final String template, final boolean found) {
TEMPLATE_EXISTS.putIfAbsent(template, found);
}
private Cache() {}
}
}