View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *  http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package org.apache.any23.servlet.conneg;
    19
    20  import java.util.ArrayList;
    21  import java.util.Collection;
    22  import java.util.Collections;
    23  import java.util.Iterator;
    24  import java.util.List;
    25  import java.util.regex.Pattern;
    26
    27  /**
    28   * This class defines a negotiator for content types based on scoring.
    29   */
    30  public class ContentTypeNegotiator {
    31
    32      private List<VariantSpec> variantSpecs = new ArrayList<>();
    33
    34      private List<MediaRangeSpec> defaultAcceptRanges = Collections.singletonList(MediaRangeSpec.parseRange("*/*"));
    35
    36      private Collection<AcceptHeaderOverride> userAgentOverrides = new ArrayList<>();
    37
    38      protected ContentTypeNegotiator(){}
    39
    40      /**
    41       * Returns the {@link MediaRangeSpec}
    42       * associated to the given <i>accept</i> type.
    43       *
    44       * @param accept a provided <i>accept</i> type
    45       * @return a {@link MediaRangeSpec} associated to the accept parameter
    46       */
    47      public MediaRangeSpec getBestMatch(String accept) {
    48          return getBestMatch(accept, null);
    49      }
    50
    51      /**
    52       * Returns the {@link MediaRangeSpec}
    53       * associated to the given <i>accept</i> type and <i>userAgent</i>.
    54       *
    55       * @param accept a provided <i>accept</i> type
    56       * @param userAgent use agent associated with the request
    57       * @return the {@link MediaRangeSpec}
    58       * associated to the given <i>accept</i> type and <i>userAgent</i>.
    59       */
    60      public MediaRangeSpec getBestMatch(String accept, String userAgent) {
    61          if (userAgent == null) {
    62              userAgent = "";
    63          }
    64          Iterator<AcceptHeaderOverride> it = userAgentOverrides.iterator();
    65          String overriddenAccept = accept;
    66          while (it.hasNext()) {
    67              AcceptHeaderOverride override = it.next();
    68              if (override.matches(accept, userAgent)) {
    69                  overriddenAccept = override.getReplacement();
    70              }
    71          }
    72          return new Negotiation(toAcceptRanges(overriddenAccept)).negotiate();
    73      }
    74
    75      protected VariantSpec addVariant(String mediaType) {
    76          VariantSpec result = new VariantSpec(mediaType);
    77          variantSpecs.add(result);
    78          return result;
    79      }
    80
    81      /**
    82       * Sets an Accept header to be used as the default if a client does
    83       * not send an Accept header, or if the Accept header cannot be parsed.
    84       * Defaults to "* / *".
    85       * @param accept a default <i>accept</i> type
    86       */
    87      protected void setDefaultAccept(String accept) {
    88          this.defaultAcceptRanges = MediaRangeSpec.parseAccept(accept);
    89      }
    90
    91      /**
    92       * Overrides the Accept header for certain user agents. This can be
    93       * used to implement special-case handling for user agents that send
    94       * faulty Accept headers.
    95       *
    96       * @param userAgentString      A pattern to be matched against the User-Agent header;
    97       *                             <tt>null</tt> means regardless of User-Agent
    98       * @param originalAcceptHeader Only override the Accept header if the user agent
    99       *                             sends this header; <tt>null</tt> means always override
    100      * @param newAcceptHeader      The Accept header to be used instead
    101      */
    102     protected void addUserAgentOverride(
    103          Pattern userAgentString,
    104          String originalAcceptHeader,
    105          String newAcceptHeader
    106     ) {
    107         this.userAgentOverrides.add(
    108             new AcceptHeaderOverride(userAgentString, originalAcceptHeader, newAcceptHeader)
    109         );
    110     }
    111
    112     private List<MediaRangeSpec> toAcceptRanges(String accept) {
    113         if (accept == null) {
    114             return defaultAcceptRanges;
    115         }
    116         List<MediaRangeSpec> result = MediaRangeSpec.parseAccept(accept);
    117         if (result.isEmpty()) {
    118             return defaultAcceptRanges;
    119         }
    120         return result;
    121     }
    122
    123     protected class VariantSpec {
    124
    125         private MediaRangeSpec type;
    126         private List<MediaRangeSpec> aliases = new ArrayList<>();
    127         private boolean isDefault = false;
    128
    129         public VariantSpec(String mediaType) {
    130             type = MediaRangeSpec.parseType(mediaType);
    131         }
    132
    133         public VariantSpec addAliasMediaType(String mediaType) {
    134             aliases.add(MediaRangeSpec.parseType(mediaType));
    135             return this;
    136         }
    137
    138         public void makeDefault() {
    139             isDefault = true;
    140         }
    141
    142         public MediaRangeSpec getMediaType() {
    143             return type;
    144         }
    145
    146         public boolean isDefault() {
    147             return isDefault;
    148         }
    149
    150         public List<MediaRangeSpec> getAliases() {
    151             return aliases;
    152         }
    153     }
    154
    155     private class Negotiation {
    156
    157         private final List<MediaRangeSpec> ranges;
    158         private MediaRangeSpec bestMatchingVariant = null;
    159         private MediaRangeSpec bestDefaultVariant = null;
    160         private double bestMatchingQuality = 0;
    161         private double bestDefaultQuality = 0;
    162
    163         Negotiation(List<MediaRangeSpec> ranges) {
    164             this.ranges = ranges;
    165         }
    166
    167         MediaRangeSpec negotiate() {
    168             Iterator<VariantSpec> it = variantSpecs.iterator();
    169             while (it.hasNext()) {
    170                 VariantSpec variant = it.next();
    171                 if (variant.isDefault) {
    172                     evaluateDefaultVariant(variant.getMediaType());
    173                 }
    174                 evaluateVariant(variant.getMediaType());
    175                 Iterator<MediaRangeSpec> aliasIt = variant.getAliases().iterator();
    176                 while (aliasIt.hasNext()) {
    177                     MediaRangeSpec alias = aliasIt.next();
    178                     evaluateVariantAlias(alias, variant.getMediaType());
    179                 }
    180             }
    181             return (bestMatchingVariant == null) ? bestDefaultVariant : bestMatchingVariant;
    182         }
    183
    184         private void evaluateVariantAlias(MediaRangeSpec./../../org/apache/any23/servlet/conneg/MediaRangeSpec.html#MediaRangeSpec">MediaRangeSpec variant, MediaRangeSpec isAliasFor) {
    185             if (variant.getBestMatch(ranges) == null)
    186               return;
    187             double q = variant.getBestMatch(ranges).getQuality();
    188             if (q * variant.getQuality() > bestMatchingQuality) {
    189                 bestMatchingVariant = isAliasFor;
    190                 bestMatchingQuality = q * variant.getQuality();
    191             }
    192         }
    193
    194         private void evaluateVariant(MediaRangeSpec variant) {
    195             evaluateVariantAlias(variant, variant);
    196         }
    197
    198         private void evaluateDefaultVariant(MediaRangeSpec variant) {
    199             if (variant.getQuality() > bestDefaultQuality) {
    200                 bestDefaultVariant = variant;
    201                 bestDefaultQuality = 0.00001 * variant.getQuality();
    202             }
    203         }
    204
    205     }
    206
    207     private class AcceptHeaderOverride {
    208
    209         private Pattern userAgentPattern;
    210         private String original;
    211         private String replacement;
    212
    213         AcceptHeaderOverride(Pattern userAgentPattern, String original, String replacement) {
    214             this.userAgentPattern = userAgentPattern;
    215             this.original = original;
    216             this.replacement = replacement;
    217         }
    218
    219         boolean matches(String acceptHeader, String userAgentHeader) {
    220             return (userAgentPattern == null
    221                     || userAgentPattern.matcher(userAgentHeader).find())
    222                     && (original == null || original.equals(acceptHeader));
    223         }
    224
    225         String getReplacement() {
    226             return replacement;
    227         }
    228     }
    229
    230 }