1/*2 * $Id: OptionsRenderer.java 1486577 2013-05-27 11:16:52Z mck $3 *4 * Licensed to the Apache Software Foundation (ASF) under one5 * or more contributor license agreements. See the NOTICE file6 * distributed with this work for additional information7 * regarding copyright ownership. The ASF licenses this file8 * to you under the Apache License, Version 2.0 (the9 * "License"); you may not use this file except in compliance10 * with the License. You may obtain a copy of the License at11 *12 * http://www.apache.org/licenses/LICENSE-2.013 *14 * Unless required by applicable law or agreed to in writing,15 * software distributed under the License is distributed on an16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY17 * KIND, either express or implied. See the License for the18 * specific language governing permissions and limitations19 * under the License.20 */21package org.apache.tiles.extras.renderer;
2223import java.io.IOException;
24import java.util.List;
25import java.util.concurrent.ConcurrentMap;
26import java.util.concurrent.TimeUnit;
27import java.util.regex.Matcher;
28import java.util.regex.Pattern;
2930import com.google.common.cache.CacheBuilder;
31import com.google.common.cache.CacheLoader;
32import com.google.common.cache.LoadingCache;
33import org.apache.tiles.Attribute;
34import org.apache.tiles.ListAttribute;
35import org.apache.tiles.access.TilesAccess;
36import org.apache.tiles.request.ApplicationContext;
37import org.apache.tiles.request.Request;
38import org.apache.tiles.request.render.Renderer;
39import org.slf4j.Logger;
40import org.slf4j.LoggerFactory;
4142/**43 * Provides a custom "options" syntax for attributes.44 * The first option that can be rendered is.45 * Comes from <a href="http://tech.finn.no/the-ultimate-view/">The Ultimate View</a> article.<p/>46 *47 * Actual rendering is delegated to the TypeDetectingRenderer that's supplied in the constructor.<p/>48 *49 * For example:50 * "/WEB-INF/tiles/fragments/${options[myoptions]}/content.jsp"51 * given the myptions list-attribute is defined like:52 * <pre>53 <put-list-attribute name="myoptions">54 <add-list-attribute>55 <add-attribute value="car"/>56 <add-attribute value="vechile"/>57 <add-attribute value="advert"/>58 </add-list-attribute>59 </put-list-attribute>60 </pre>61 * will look for content.jsp <br/>62 * first in "/WEB-INF/tiles/fragments/car/" then <br/>63 * second in "/WEB-INF/tiles/fragments/vechile/" and <br/>64 * last in "/WEB-INF/tiles/fragments/advert".65 * <p/>66 * <p/>67 * Currently only supports one occurrance of such an "option" pattern in the attribute's value.68 * <p/>69 * Limitation: "looking" for templates is implemented using applicationContext.getResource(..)70 * therefore the option values in the options list need to be visible as applicationResources.71 * <p/>72 * The attribute found and rendered is cached so to improve performance on subsequent lookups.73 * The default cache time-to-live is {@value #DEFAULT_CACHE_LIFE}, specified by {@link #DEFAULT_CACHE_LIFE}.74 * It can be customised by setting the system property {@value #CACHE_LIFE_PROPERTY}, see {@link #CACHE_LIFE_PROPERTY}.75 * Setting it to zero will disable the cache.76 */77publicfinalclassOptionsRendererimplements Renderer {
7879publicstaticfinal String CACHE_LIFE_PROPERTY = OptionsRenderer.class.getName() + ".cache_ttl_ms";
8081publicstaticfinallong DEFAULT_CACHE_LIFE = 1000 * 60 * 5;
8283publicstaticfinal Pattern OPTIONS_PATTERN
84 = Pattern.compile(Pattern.quote("{options[") + "(.+)" + Pattern.quote("]}"));
8586privatestaticfinal Logger LOG = LoggerFactory.getLogger(OptionsRenderer.class);
8788privatefinal ApplicationContext applicationContext;
89privatefinal Renderer renderer;
9091publicOptionsRenderer(final ApplicationContext applicationContext, final Renderer renderer) {
92this.applicationContext = applicationContext;
93this.renderer = renderer;
94 }
9596 @Override
97publicboolean isRenderable(final String path, final Request request) {
98return renderer.isRenderable(path, request);
99 }
100101 @Override
102publicvoid render(final String path, final Request request) throws IOException {
103104 Matcher matcher = OPTIONS_PATTERN.matcher((String) path);
105106if (null != matcher && matcher.find()) {
107boolean done = false;
108 String match = matcher.group(1);
109ListAttribute fallbacks = (ListAttribute) TilesAccess
110 .getCurrentContainer(request)
111 .getAttributeContext(request)
112 .getAttribute(match);
113114if (null == fallbacks) {
115thrownew IllegalStateException("A matching list-attribute name=\"" + match + "\" must be defined.");
116 } elseif (fallbacks.getValue().isEmpty()) {
117thrownew IllegalStateException(
118"list-attribute name=\"" + match + "\" must have minimum one attribute");
119 }
120121for (Attribute option : (List<Attribute>) fallbacks.getValue()) {
122 String template = path.replaceFirst(Pattern.quote(matcher.group()), (String) option.getValue());
123 done = renderAttempt(template, request);
124if (done) { break; }
125 }
126if (!done) {
127thrownew IOException("None of the options existed for " + path);
128 }
129 } else {
130 renderer.render(path, request);
131 }
132 }
133134privateboolean renderAttempt(final String template, final Request request) throws IOException {
135boolean result = false;
136if (Cache.attemptTemplate(template)) {
137try {
138if (null != applicationContext.getResource(template)) {
139 renderer.render(template, request);
140 result = true;
141 }
142 } catch (IOException ex) {
143if (ex.getMessage().contains(template)) {
144// expected outcome. continue loop.145 LOG.trace(ex.getMessage());
146 } else {
147// comes from an inner templateAttribute.render(..) so throw on148throw ex;
149 }
150 } catch (RuntimeException ex) {
151if (ex.getMessage().contains(template)) {
152// expected outcome. continue loop.153 LOG.trace(ex.getMessage());
154 } else {
155// comes from an inner templateAttribute.render(..) so throw on156throw ex;
157 }
158 }
159 Cache.update(template, result);
160 }
161return result;
162 }
163164privatestaticfinalclassCache {
165166privatestaticfinallong CACHE_LIFE = Long.getLong(CACHE_LIFE_PROPERTY, DEFAULT_CACHE_LIFE);
167168/** It takes CACHE_LIFE milliseconds for any hot deployments to register.169 */170privatestaticfinal ConcurrentMap<String,Boolean> TEMPLATE_EXISTS;
171172static {
173 LOG.info("cache_ttl_ms=" + CACHE_LIFE);
174175 LoadingCache<String,Boolean> builder = CacheBuilder
176 .newBuilder()
177 .expireAfterWrite(CACHE_LIFE, TimeUnit.MILLISECONDS)
178 .build(
179new CacheLoader<String, Boolean>() {
180 @Override
181public Boolean load(String key) {
182thrownew UnsupportedOperationException(
183"illegal TEMPLATE_EXISTS.get(\"" + key
184 + "\") before TEMPLATE_EXISTS.containsKey(\"" + key + "\")");
185 }
186 });
187188 TEMPLATE_EXISTS = builder.asMap();
189 }
190191192staticboolean attemptTemplate(final String template) {
193return !TEMPLATE_EXISTS.containsKey(template) || TEMPLATE_EXISTS.get(template);
194 }
195196staticvoid update(final String template, finalboolean found) {
197 TEMPLATE_EXISTS.putIfAbsent(template, found);
198 }
199200privateCache() {}
201 }
202 }