Project Tiles has retired. For details please refer to its Attic page.
OptionsRenderer xref
View Javadoc

1   /*
2    * $Id: OptionsRenderer.java 1486577 2013-05-27 11:16:52Z mck $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.tiles.extras.renderer;
22  
23  import java.io.IOException;
24  import java.util.List;
25  import java.util.concurrent.ConcurrentMap;
26  import java.util.concurrent.TimeUnit;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import com.google.common.cache.CacheBuilder;
31  import com.google.common.cache.CacheLoader;
32  import com.google.common.cache.LoadingCache;
33  import org.apache.tiles.Attribute;
34  import org.apache.tiles.ListAttribute;
35  import org.apache.tiles.access.TilesAccess;
36  import org.apache.tiles.request.ApplicationContext;
37  import org.apache.tiles.request.Request;
38  import org.apache.tiles.request.render.Renderer;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
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          &lt;put-list-attribute name="myoptions">
54              &lt;add-list-attribute>
55                  &lt;add-attribute value="car"/>
56                  &lt;add-attribute value="vechile"/>
57                  &lt;add-attribute value="advert"/>
58              &lt;/add-list-attribute>
59          &lt;/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   */
77  public final class OptionsRenderer implements Renderer {
78  
79      public static final String CACHE_LIFE_PROPERTY = OptionsRenderer.class.getName() + ".cache_ttl_ms";
80  
81      public static final long DEFAULT_CACHE_LIFE = 1000 * 60 * 5;
82  
83      public static final Pattern OPTIONS_PATTERN
84              = Pattern.compile(Pattern.quote("{options[") + "(.+)" + Pattern.quote("]}"));
85  
86      private static final Logger LOG = LoggerFactory.getLogger(OptionsRenderer.class);
87  
88      private final ApplicationContext applicationContext;
89      private final Renderer renderer;
90  
91      public OptionsRenderer(final ApplicationContext applicationContext, final Renderer renderer) {
92          this.applicationContext = applicationContext;
93          this.renderer = renderer;
94      }
95  
96      @Override
97      public boolean isRenderable(final String path, final Request request) {
98          return renderer.isRenderable(path, request);
99      }
100 
101     @Override
102     public void render(final String path, final Request request) throws IOException {
103 
104         Matcher matcher =  OPTIONS_PATTERN.matcher((String) path);
105 
106         if (null != matcher && matcher.find()) {
107             boolean done = false;
108             String match = matcher.group(1);
109             ListAttribute fallbacks = (ListAttribute) TilesAccess
110                     .getCurrentContainer(request)
111                     .getAttributeContext(request)
112                     .getAttribute(match);
113 
114             if (null == fallbacks) {
115                 throw new IllegalStateException("A matching list-attribute name=\"" + match + "\" must be defined.");
116             } else if (fallbacks.getValue().isEmpty()) {
117                 throw new IllegalStateException(
118                         "list-attribute name=\"" + match + "\" must have minimum one attribute");
119             }
120 
121             for (Attribute option : (List<Attribute>) fallbacks.getValue()) {
122                 String template = path.replaceFirst(Pattern.quote(matcher.group()), (String) option.getValue());
123                 done = renderAttempt(template, request);
124                 if (done) { break; }
125             }
126             if (!done) {
127                 throw new IOException("None of the options existed for " + path);
128             }
129         } else {
130             renderer.render(path, request);
131         }
132     }
133 
134     private boolean renderAttempt(final String template, final Request request) throws IOException {
135         boolean result = false;
136         if (Cache.attemptTemplate(template)) {
137             try {
138                 if (null != applicationContext.getResource(template)) {
139                     renderer.render(template, request);
140                     result = true;
141                 }
142             } catch (IOException ex) {
143                 if (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 on
148                     throw ex;
149                 }
150             } catch (RuntimeException ex) {
151                 if (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 on
156                     throw ex;
157                 }
158             }
159             Cache.update(template, result);
160         }
161         return result;
162     }
163 
164     private static final class Cache {
165 
166         private static final long CACHE_LIFE = Long.getLong(CACHE_LIFE_PROPERTY, DEFAULT_CACHE_LIFE);
167 
168         /** It takes CACHE_LIFE milliseconds for any hot deployments to register.
169          */
170         private static final ConcurrentMap<String,Boolean> TEMPLATE_EXISTS;
171         
172         static {
173             LOG.info("cache_ttl_ms=" + CACHE_LIFE);
174 
175             LoadingCache<String,Boolean> builder = CacheBuilder
176                     .newBuilder()
177                     .expireAfterWrite(CACHE_LIFE, TimeUnit.MILLISECONDS)
178                     .build(
179                         new CacheLoader<String, Boolean>() {
180                             @Override
181                             public Boolean load(String key) {
182                                 throw new UnsupportedOperationException(
183                                         "illegal TEMPLATE_EXISTS.get(\"" + key
184                                         + "\") before TEMPLATE_EXISTS.containsKey(\"" + key + "\")");
185                             }
186                         });
187 
188             TEMPLATE_EXISTS = builder.asMap();
189         }
190 
191 
192         static boolean attemptTemplate(final String template) {
193             return !TEMPLATE_EXISTS.containsKey(template) || TEMPLATE_EXISTS.get(template);
194         }
195 
196         static void update(final String template, final boolean found) {
197             TEMPLATE_EXISTS.putIfAbsent(template, found);
198         }
199 
200         private Cache() {}
201     }
202 }