Coverage Report - org.apache.tiles.extras.renderer.OptionsRenderer
 
Classes in this File Line Coverage Branch Coverage Complexity
OptionsRenderer
70%
31/44
40%
9/22
3.5
OptionsRenderer$Cache
88%
8/9
25%
1/4
3.5
OptionsRenderer$Cache$1
50%
1/2
N/A
3.5
 
 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  1
 public final class OptionsRenderer implements Renderer {
 78  
 
 79  1
     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  1
     public static final Pattern OPTIONS_PATTERN
 84  
             = Pattern.compile(Pattern.quote("{options[") + "(.+)" + Pattern.quote("]}"));
 85  
 
 86  1
     private static final Logger LOG = LoggerFactory.getLogger(OptionsRenderer.class);
 87  
 
 88  
     private final ApplicationContext applicationContext;
 89  
     private final Renderer renderer;
 90  
 
 91  2
     public OptionsRenderer(final ApplicationContext applicationContext, final Renderer renderer) {
 92  2
         this.applicationContext = applicationContext;
 93  2
         this.renderer = renderer;
 94  2
     }
 95  
 
 96  
     @Override
 97  
     public boolean isRenderable(final String path, final Request request) {
 98  1
         return renderer.isRenderable(path, request);
 99  
     }
 100  
 
 101  
     @Override
 102  
     public void render(final String path, final Request request) throws IOException {
 103  
 
 104  1
         Matcher matcher =  OPTIONS_PATTERN.matcher((String) path);
 105  
 
 106  1
         if (null != matcher && matcher.find()) {
 107  1
             boolean done = false;
 108  1
             String match = matcher.group(1);
 109  1
             ListAttribute fallbacks = (ListAttribute) TilesAccess
 110  
                     .getCurrentContainer(request)
 111  
                     .getAttributeContext(request)
 112  
                     .getAttribute(match);
 113  
 
 114  1
             if (null == fallbacks) {
 115  0
                 throw new IllegalStateException("A matching list-attribute name=\"" + match + "\" must be defined.");
 116  1
             } else if (fallbacks.getValue().isEmpty()) {
 117  0
                 throw new IllegalStateException(
 118  
                         "list-attribute name=\"" + match + "\" must have minimum one attribute");
 119  
             }
 120  
 
 121  1
             for (Attribute option : (List<Attribute>) fallbacks.getValue()) {
 122  1
                 String template = path.replaceFirst(Pattern.quote(matcher.group()), (String) option.getValue());
 123  1
                 done = renderAttempt(template, request);
 124  1
                 if (done) { break; }
 125  0
             }
 126  1
             if (!done) {
 127  0
                 throw new IOException("None of the options existed for " + path);
 128  
             }
 129  1
         } else {
 130  0
             renderer.render(path, request);
 131  
         }
 132  1
     }
 133  
 
 134  
     private boolean renderAttempt(final String template, final Request request) throws IOException {
 135  1
         boolean result = false;
 136  1
         if (Cache.attemptTemplate(template)) {
 137  
             try {
 138  1
                 if (null != applicationContext.getResource(template)) {
 139  1
                     renderer.render(template, request);
 140  1
                     result = true;
 141  
                 }
 142  0
             } catch (IOException ex) {
 143  0
                 if (ex.getMessage().contains(template)) {
 144  
                     // expected outcome. continue loop.
 145  0
                     LOG.trace(ex.getMessage());
 146  
                 } else {
 147  
                     // comes from an inner templateAttribute.render(..) so throw on
 148  0
                     throw ex;
 149  
                 }
 150  0
             } catch (RuntimeException ex) {
 151  0
                 if (ex.getMessage().contains(template)) {
 152  
                     // expected outcome. continue loop.
 153  0
                     LOG.trace(ex.getMessage());
 154  
                 } else {
 155  
                     // comes from an inner templateAttribute.render(..) so throw on
 156  0
                     throw ex;
 157  
                 }
 158  1
             }
 159  1
             Cache.update(template, result);
 160  
         }
 161  1
         return result;
 162  
     }
 163  
 
 164  
     private static final class Cache {
 165  
 
 166  1
         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  1
             LOG.info("cache_ttl_ms=" + CACHE_LIFE);
 174  
 
 175  1
             LoadingCache<String,Boolean> builder = CacheBuilder
 176  
                     .newBuilder()
 177  
                     .expireAfterWrite(CACHE_LIFE, TimeUnit.MILLISECONDS)
 178  
                     .build(
 179  1
                         new CacheLoader<String, Boolean>() {
 180  
                             @Override
 181  
                             public Boolean load(String key) {
 182  0
                                 throw new UnsupportedOperationException(
 183  
                                         "illegal TEMPLATE_EXISTS.get(\"" + key
 184  
                                         + "\") before TEMPLATE_EXISTS.containsKey(\"" + key + "\")");
 185  
                             }
 186  
                         });
 187  
 
 188  1
             TEMPLATE_EXISTS = builder.asMap();
 189  1
         }
 190  
 
 191  
 
 192  
         static boolean attemptTemplate(final String template) {
 193  1
             return !TEMPLATE_EXISTS.containsKey(template) || TEMPLATE_EXISTS.get(template);
 194  
         }
 195  
 
 196  
         static void update(final String template, final boolean found) {
 197  1
             TEMPLATE_EXISTS.putIfAbsent(template, found);
 198  1
         }
 199  
 
 200  0
         private Cache() {}
 201  
     }
 202  
 }