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 }