Coverage Report - com.mattunderscore.filter.contentnegotiation.ContentNegotiationFilter
 
Classes in this File Line Coverage Branch Coverage Complexity
ContentNegotiationFilter
69%
133/191
64%
57/88
6.25
ContentNegotiationFilter$BadSubstitution
0%
0/3
N/A
6.25
ContentNegotiationFilter$NoMatchingRuleException
0%
0/3
N/A
6.25
 
 1  
 /* Copyright © 2012, 2013 Matthew Champion
 2  
 All rights reserved.
 3  
 
 4  
 Redistribution and use in source and binary forms, with or without
 5  
 modification, are permitted provided that the following conditions are met:
 6  
  * Redistributions of source code must retain the above copyright
 7  
       notice, this list of conditions and the following disclaimer.
 8  
  * Redistributions in binary form must reproduce the above copyright
 9  
       notice, this list of conditions and the following disclaimer in the
 10  
       documentation and/or other materials provided with the distribution.
 11  
  * Neither the name of mattunderscore.com nor the
 12  
       names of its contributors may be used to endorse or promote products
 13  
       derived from this software without specific prior written permission.
 14  
 
 15  
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 16  
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 17  
 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 18  
 DISCLAIMED. IN NO EVENT SHALL MATTHEW CHAMPION BE LIABLE FOR ANY
 19  
 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 20  
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 21  
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 22  
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 23  
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 24  
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 25  
 
 26  
 package com.mattunderscore.filter.contentnegotiation;
 27  
 
 28  
 import java.io.IOException;
 29  
 import java.util.ArrayList;
 30  
 import java.util.Enumeration;
 31  
 import java.util.HashMap;
 32  
 import java.util.List;
 33  
 import java.util.Map;
 34  
 import java.util.logging.Level;
 35  
 import java.util.logging.Logger;
 36  
 import java.util.regex.Matcher;
 37  
 import java.util.regex.Pattern;
 38  
 
 39  
 import javax.servlet.FilterChain;
 40  
 import javax.servlet.ServletException;
 41  
 import javax.servlet.http.HttpServletResponse;
 42  
 import javax.servlet.http.HttpServletRequest;
 43  
 import javax.servlet.RequestDispatcher;
 44  
 
 45  
 import com.mattunderscore.http.headers.UnParsableHeaderException;
 46  
 import com.mattunderscore.http.headers.content.type.QContentType;
 47  
 import com.mattunderscore.http.headers.language.ILanguage;
 48  
 import com.mattunderscore.http.headers.language.QLanguage;
 49  
 import com.mattunderscore.filter.GenericHTTPFilter;
 50  
 import com.mattunderscore.filter.contentnegotiation.parser.CrossProduct;
 51  
 import com.mattunderscore.filter.contentnegotiation.parser.Header;
 52  
 import com.mattunderscore.filter.contentnegotiation.parser.HeaderPart;
 53  
 import com.mattunderscore.filter.contentnegotiation.parser.Variant;
 54  
 import com.mattunderscore.filter.contentnegotiation.parser.Redirect;
 55  
 import com.mattunderscore.filter.contentnegotiation.parser.Output;
 56  
 import com.mattunderscore.filter.contentnegotiation.variantsource.CachedVariantSource;
 57  
 import com.mattunderscore.filter.contentnegotiation.variantsource.ConfigurableVariantSource;
 58  
 import com.mattunderscore.filter.contentnegotiation.variantsource.VariantSource;
 59  
 import com.mattunderscore.filter.contentnegotiation.variantsource.VariantSourceForFilter;
 60  
 
 61  
 /**
 62  
  * A {@link javax.servlet.Filter} that can be used for content negotiation of HTTP requests. Loads
 63  
  * the filter configuration from the file negotiation.xml. Supports the accept header and the
 64  
  * language header.
 65  
  * 
 66  
  * @author Matt Champion
 67  
  * @since 0.0.13
 68  
  */
 69  
 public final class ContentNegotiationFilter extends GenericHTTPFilter
 70  
 {
 71  
     /**
 72  
      * The parameter name of the variant source class parameter.
 73  
      * @since 0.2.4
 74  
      */
 75  
     public static final String VARIANT_SOURCE_PARAMETER = "variantSource";
 76  
 
 77  
     private static final String DEFAULT_VARIANT_CLASS =
 78  
             "com.mattunderscore.filter.contentnegotiation.variantsource.FilterVariantXMLSource";
 79  
 
 80  
     /**
 81  
      * Logger used of information or warnings
 82  
      */
 83  2
     private static final Logger log = Logger.getLogger("com.mattunderscore.filter.contentnegotiation");
 84  
     /**
 85  
      * List of variants constructed from the negotiation.xml
 86  
      */
 87  22
     private final Map<String, String> parameterMap = new HashMap<String, String>();
 88  22
     private String variantSourceClass = DEFAULT_VARIANT_CLASS;
 89  22
     private VariantSource source = null;
 90  22
     private boolean validConfiguration = false;
 91  
 
 92  
     public ContentNegotiationFilter()
 93  22
     {
 94  22
     }
 95  
 
 96  
     @Override
 97  
     public void configureFilter()
 98  
     {
 99  20
         if (validConfiguration)
 100  
         {
 101  0
             log.warning("Unable to reconfigure filter");
 102  
         }
 103  
         try
 104  
         {
 105  
             @SuppressWarnings("unchecked")
 106  20
             final Enumeration<String> parameterNames = filterConfig.getInitParameterNames();
 107  34
             while (parameterNames.hasMoreElements())
 108  
             {
 109  16
                 final String parameterName = parameterNames.nextElement();
 110  16
                 if (VARIANT_SOURCE_PARAMETER.equals(parameterName))
 111  
                 {
 112  10
                     variantSourceClass = filterConfig.getInitParameter(parameterName);
 113  
                 }
 114  16
                 parameterMap.put(parameterName, filterConfig.getInitParameter(parameterName));
 115  32
                 log.info("Config: " + parameterName + ": "
 116  16
                         + filterConfig.getInitParameter(parameterName));
 117  16
             }
 118  18
             final Class<?> sourceClass = Class.forName(variantSourceClass);
 119  18
             final Object sourceObject = sourceClass.newInstance();
 120  18
             if (sourceObject instanceof VariantSourceForFilter)
 121  
             {
 122  8
                 final VariantSourceForFilter confSource = (VariantSourceForFilter) sourceObject;
 123  8
                 confSource.configureSource(filterConfig);
 124  0
                 source = new CachedVariantSource(confSource);
 125  0
                 validConfiguration = true;
 126  0
             }
 127  10
             else if (sourceObject instanceof ConfigurableVariantSource)
 128  
             {
 129  0
                 final ConfigurableVariantSource confSource = (ConfigurableVariantSource) sourceObject;
 130  0
                 confSource.configureSource(parameterMap);
 131  0
                 source = new CachedVariantSource(confSource);
 132  0
                 validConfiguration = true;
 133  0
             }
 134  10
             else if (sourceObject instanceof VariantSource)
 135  
             {
 136  8
                 source = new CachedVariantSource((VariantSource) sourceObject);
 137  8
                 validConfiguration = true;
 138  
             }
 139  
             else
 140  
             {
 141  2
                 log.warning("No source configured.");
 142  2
                 validConfiguration = false;
 143  
             }
 144  
         }
 145  10
         catch (Throwable t)
 146  
         {
 147  10
             log.log(Level.WARNING, "ContentNegotiationFilter configuration error", t);
 148  10
             validConfiguration = false;
 149  10
         }
 150  20
     }
 151  
 
 152  
     @Override
 153  
     public void doHTTPFilter(HttpServletRequest request, HttpServletResponse response,
 154  
             FilterChain chain) throws IOException, ServletException
 155  
     {
 156  16
         if (!validConfiguration)
 157  
         {
 158  8
             throw new ServletException("Filter not correctly configured");
 159  
         }
 160  
         try
 161  
         {
 162  8
             final List<Variant> variants = source.getVariants();
 163  8
             if (variants != null)
 164  
             {
 165  8
                 final String servletPath = request.getServletPath();
 166  8
                 final List<Variant> matchedVariants = getUrlVariants(variants, servletPath);
 167  8
                 if (matchedVariants.size() > 0)
 168  
                 {
 169  
                     try
 170  
                     {
 171  6
                         final Variant bestVariant = getBestVariant(request, matchedVariants);
 172  6
                         handleOutput(request, response, bestVariant);
 173  6
                         return;
 174  
                     }
 175  0
                     catch (final UnParsableHeaderException e)
 176  
                     {
 177  0
                         log.log(Level.WARNING, e.getMessage(), e);
 178  
                     }
 179  0
                     catch (final NoMatchingRuleException e)
 180  
                     {
 181  0
                         log.info(e.getMessage());
 182  0
                     }
 183  
                 }
 184  
                 else
 185  
                 {
 186  2
                     log.fine("No Variants on path: " + servletPath);
 187  
                 }
 188  
             }
 189  2
             chain.doFilter(request, response);
 190  
         }
 191  0
         catch (final Throwable t)
 192  
         {
 193  0
             final StringBuilder sb = new StringBuilder(100);
 194  0
             sb.append("Request failed:\n");
 195  0
             for (final String key : parameterMap.keySet())
 196  
             {
 197  0
                 sb.append("conf: ");
 198  0
                 sb.append(key);
 199  0
                 sb.append(", ");
 200  0
                 sb.append(parameterMap.get(key));
 201  0
                 sb.append("\n");
 202  0
             }
 203  0
             log.log(Level.WARNING, sb.toString(), t);
 204  0
             throw new ServletException("Request failed",t);
 205  2
         }
 206  2
     }
 207  
 
 208  
     /**
 209  
      * Get the variants that match the URL pattern.
 210  
      * 
 211  
      * @param variants
 212  
      *            The list of all Variants
 213  
      * @param path
 214  
      *            The HTTP request path
 215  
      * @return The list of Variants that the request might match
 216  
      * @since 0.0.13
 217  
      */
 218  
     private List<Variant> getUrlVariants(final List<Variant> variants, final String path)
 219  
     {
 220  8
         final List<Variant> matchedVariants = new ArrayList<Variant>();
 221  8
         for (final Variant variant : variants)
 222  
         {
 223  8
             if (variant.requestPathMatchesVariant(path))
 224  
             {
 225  8
                 matchedVariants.add(variant);
 226  
             }
 227  8
         }
 228  8
         return matchedVariants;
 229  
     }
 230  
 
 231  
     /**
 232  
      * Create the cross products that will be used to find the best content type.
 233  
      * 
 234  
      * @param contentTypes
 235  
      *            The content types of the accept header of the HTTP request
 236  
      * @param variants
 237  
      *            The Variants that might apply to the request
 238  
      * @return The list of CrossProducts for the variants and the content types
 239  
      * @throws UnParsableHeaderException
 240  
      * @since 0.0.13
 241  
      */
 242  
     private List<CrossProduct> createContentTypeCrossProducts(List<QContentType> contentTypes,
 243  
             List<Variant> variants) throws UnParsableHeaderException
 244  
     {
 245  6
         final List<CrossProduct> products = new ArrayList<CrossProduct>();
 246  6
         for (final QContentType contentType : contentTypes)
 247  
         {
 248  6
             for (final Variant variant : variants)
 249  
             {
 250  8
                 for (final QContentType variantType : variant.getQContentTypes())
 251  
                 {
 252  8
                     if (contentType.sameType(variantType))
 253  
                     {
 254  8
                         products.add(new CrossProduct(contentType, variantType, variant));
 255  
                     }
 256  8
                 }
 257  8
             }
 258  6
         }
 259  6
         return products;
 260  
     }
 261  
 
 262  
     /**
 263  
      * Create the cross products that will be used to find the best language.
 264  
      * 
 265  
      * @param langs
 266  
      *            The languages of the accept-language header of the HTTP request
 267  
      * @param variants
 268  
      *            The Variants that might apply to the request
 269  
      * @return The list of cross products between the variants and the languages.
 270  
      * @throws UnParsableHeaderException
 271  
      * @since 0.0.13
 272  
      */
 273  
     private List<CrossProduct> createLanguageCrossProducts(List<ILanguage> langs,
 274  
             List<Variant> variants) throws UnParsableHeaderException
 275  
     {
 276  2
         final List<CrossProduct> products = new ArrayList<CrossProduct>();
 277  2
         for (final ILanguage lang : langs)
 278  
         {
 279  2
             for (final Variant variant : variants)
 280  
             {
 281  4
                 for (final ILanguage variantLang : variant.getQLanguages())
 282  
                 {
 283  4
                     if (lang.sameLangauge(variantLang))
 284  
                     {
 285  2
                         products.add(new CrossProduct(lang, variantLang, variant));
 286  
                     }
 287  4
                 }
 288  4
             }
 289  2
         }
 290  2
         return products;
 291  
     }
 292  
 
 293  
     /**
 294  
      * Returns the variants that have the best cross product.
 295  
      * 
 296  
      * @param cps
 297  
      *            List of CrossProducts
 298  
      * @return List of Variants
 299  
      * @since 0.0.13
 300  
      */
 301  
     private List<Variant> getBestVariants(List<CrossProduct> cps)
 302  
     {
 303  8
         List<Variant> bvs = new ArrayList<Variant>();
 304  8
         double qualifier = 0.0;
 305  8
         for (final CrossProduct cp : cps)
 306  
         {
 307  10
             if (cp.getQualifier() > qualifier)
 308  
             {
 309  8
                 bvs = new ArrayList<Variant>();
 310  8
                 bvs.add(cp.getVariant());
 311  8
                 qualifier = cp.getQualifier();
 312  
             }
 313  2
             else if (cp.getQualifier() == qualifier && qualifier > 0.0)
 314  
             {
 315  2
                 bvs.add(cp.getVariant());
 316  
             }
 317  10
         }
 318  8
         return bvs;
 319  
     }
 320  
 
 321  
     /**
 322  
      * Return the best variant of a list of variants.
 323  
      * 
 324  
      * @param request
 325  
      *            The servlet request, parsed to get requirements of request.
 326  
      * @param variants
 327  
      *            The list of Variants that might be used to resolve the request.
 328  
      * @return The best variant for the request.
 329  
      * @throws NoMatchingRuleException
 330  
      * @throws UnParsableHeaderException
 331  
      * @throws IOException
 332  
      * @since 0.0.13
 333  
      */
 334  
     private Variant getBestVariant(HttpServletRequest request, List<Variant> variants)
 335  
             throws NoMatchingRuleException, UnParsableHeaderException, IOException
 336  
     {
 337  6
         final List<QContentType> contentTypes = QContentType.getRequestContentTypes(request);
 338  6
         final List<CrossProduct> contentTypesCrossProduct = createContentTypeCrossProducts(contentTypes,
 339  
                 variants);
 340  6
         final List<Variant> bestContentTypeVariants = getBestVariants(contentTypesCrossProduct);
 341  6
         if (bestContentTypeVariants.size() == 0)
 342  
         {
 343  0
             throw new NoMatchingRuleException();
 344  
         }
 345  6
         else if (bestContentTypeVariants.size() == 1)
 346  
         {
 347  
             // Return the only acceptable variant
 348  4
             return bestContentTypeVariants.get(0);
 349  
         }
 350  2
         final List<ILanguage> languages = QLanguage.getRequestLanguages(request);
 351  2
         final List<CrossProduct> languagesCrossProducts = createLanguageCrossProducts(languages,
 352  
                 bestContentTypeVariants);
 353  2
         final List<Variant> bestLanguageVariants = getBestVariants(languagesCrossProducts);
 354  2
         if (bestLanguageVariants.size() == 0)
 355  
         {
 356  
             // Return an acceptable content type variant
 357  0
             return bestContentTypeVariants.get(0);
 358  
         }
 359  2
         else if (bestLanguageVariants.size() == 1)
 360  
         {
 361  
             // Return the only acceptable variant
 362  2
             return bestLanguageVariants.get(0);
 363  
         }
 364  
         // Return first of the remaining variants
 365  0
         return bestLanguageVariants.get(0);
 366  
     }
 367  
 
 368  
     /**
 369  
      * Perform substitution of submatches for wildcards on redirect.
 370  
      * 
 371  
      * @param inPath
 372  
      *            The path of the request
 373  
      * @param stringPattern
 374  
      *            The URL pattern of the variant, with wild cards
 375  
      * @param outPath
 376  
      *            The destination path, with back references
 377  
      * @return The destination path with back references replaced with the wild card values
 378  
      * @throws BadSubstitution
 379  
      * @since 0.0.13
 380  
      */
 381  
     private String substitueMatch(String inPath, String stringPattern, String outPath)
 382  
             throws BadSubstitution
 383  
     {
 384  2
         final Pattern pathPattern = Pattern.compile(stringPattern);
 385  2
         final Matcher pathMatcher = pathPattern.matcher(inPath);
 386  2
         if (pathMatcher.find())
 387  
         {
 388  2
             if (pathMatcher.groupCount() < 1)
 389  
             {
 390  2
                 return outPath;
 391  
             }
 392  
             else
 393  
             {
 394  0
                 final Pattern backRefPattern = Pattern.compile("\\$([0-9])");
 395  0
                 final Matcher backRefMatcher = backRefPattern.matcher(outPath);
 396  0
                 String newString = outPath;
 397  0
                 while (backRefMatcher.find())
 398  
                 {
 399  0
                     String stringIndex = backRefMatcher.group(1);
 400  0
                     int index = Integer.parseInt(stringIndex);
 401  0
                     if (index > pathMatcher.groupCount())
 402  
                     {
 403  0
                         throw new BadSubstitution();
 404  
                     }
 405  0
                     String subValue = pathMatcher.group(index);
 406  0
                     newString = newString.replace(backRefMatcher.group(), subValue);
 407  0
                 }
 408  0
                 return newString;
 409  
             }
 410  
         }
 411  
         else
 412  
         {
 413  0
             throw new BadSubstitution();
 414  
         }
 415  
     }
 416  
 
 417  
     /**
 418  
      * Perform the output behaviour of the variant. This forwards the request or flushes the buffer.
 419  
      * 
 420  
      * @param request
 421  
      *            The servlet request, used for HTTP information
 422  
      * @param response
 423  
      *            The servlet response, used to write the response
 424  
      * @param variant
 425  
      *            The variant, used to determine the response
 426  
      * @throws IOException
 427  
      * @throws ServletException
 428  
      * @since 0.0.13
 429  
      */
 430  
     private void handleOutput(HttpServletRequest request, HttpServletResponse response,
 431  
             Variant variant) throws IOException, ServletException
 432  
     {
 433  6
         boolean varyContentType = false;
 434  6
         boolean varyLanguage = false;
 435  6
         if (variant.getContentType() != null)
 436  
         {
 437  6
             varyContentType = true;
 438  
         }
 439  6
         if (variant.getLanguage() != null)
 440  
         {
 441  6
             varyLanguage = true;
 442  
         }
 443  6
         if (varyContentType && varyLanguage)
 444  
         {
 445  6
             response.addHeader("Vary", "Accept,Accept-Language");
 446  
         }
 447  0
         else if (varyContentType)
 448  
         {
 449  0
             response.addHeader("Vary", "Accept");
 450  
         }
 451  0
         else if (varyLanguage)
 452  
         {
 453  0
             response.addHeader("Vary", "Accept-Language");
 454  
         }
 455  6
         final Output output = variant.getOutput();
 456  6
         if (output.getForward() != null)
 457  
         {
 458  2
             final RequestDispatcher rd = request.getRequestDispatcher(output.getForward());
 459  2
             log.fine("Forwading request to " + output.getForward());
 460  2
             rd.forward(request, response);
 461  2
             return;
 462  
         }
 463  4
         else if (output.getHeader() != null)
 464  
         {
 465  0
             final Header header = output.getHeader();
 466  0
             response.setStatus(header.getCode());
 467  0
             for (final HeaderPart part : header.getPart())
 468  
             {
 469  0
                 final String value = request.getContextPath() + part.getValue();
 470  0
                 response.addHeader(part.getName(), value);
 471  0
             }
 472  0
             log.fine("Returning custom header with code: " + header.getCode());
 473  0
             response.flushBuffer();
 474  0
             return;
 475  
         }
 476  4
         else if (output.getRedirect() != null)
 477  
         {
 478  2
             final Redirect redirect = output.getRedirect();
 479  2
             response.setStatus(redirect.getCode());
 480  
             String newLocation;
 481  
             try
 482  
             {
 483  4
                 newLocation = substitueMatch(request.getServletPath(), variant.getUrlPattern(),
 484  2
                         request.getContextPath() + redirect.getLocation());
 485  
             }
 486  0
             catch (BadSubstitution ex)
 487  
             {
 488  0
                 newLocation = request.getContextPath() + redirect.getLocation();
 489  2
             }
 490  4
             log.fine("Redirecting " + request.getServletPath() + " to " + newLocation + " with "
 491  2
                     + redirect.getCode());
 492  2
             response.addHeader("location", request.getContextPath() + newLocation);
 493  2
             response.flushBuffer();
 494  2
             return;
 495  
         }
 496  2
         else if (output.isFail() != null)
 497  
         {
 498  2
             if (output.isFail())
 499  
             {
 500  2
                 log.fine("Returning 406 header");
 501  2
                 response.setStatus(406);
 502  2
                 response.flushBuffer();
 503  2
                 return;
 504  
             }
 505  
         }
 506  0
     }
 507  
 
 508  
     /**
 509  
      * When there is no Variant that can be matched to the request. This should not be exposed
 510  
      * outside of this class, handling of this exception should be done in this class.
 511  
      * 
 512  
      * @author Matt Champion
 513  
      * @since 0.0.13
 514  
      */
 515  
     @SuppressWarnings("serial")
 516  
     private class NoMatchingRuleException extends Exception
 517  
     {
 518  
         public NoMatchingRuleException()
 519  0
         {
 520  0
             super("No matching Variant");
 521  0
         }
 522  
     }
 523  
 
 524  
     /**
 525  
      * When there is a problem with the substitution of wild cards and back references. This should
 526  
      * not be exposed outside of this class, handling of this exception should be done in this
 527  
      * class.
 528  
      * 
 529  
      * @author Matt Champion
 530  
      * @since 0.0.13
 531  
      */
 532  
     @SuppressWarnings("serial")
 533  
     private class BadSubstitution extends Exception
 534  
     {
 535  
         public BadSubstitution()
 536  0
         {
 537  0
             super("The back reference cannot be substituted");
 538  0
         }
 539  
     }
 540  
 }