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.Collections;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  
27  /**
28   * This class implements the <i>HTTP header media-range specification</i>.
29   * See <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC 2616 section 14.1</a>. 
30   */
31  public class MediaRangeSpec {
32  
33      private static final Pattern tokenPattern;
34  
35      private static final Pattern parameterPattern;
36  
37  
38      private static final Pattern mediaRangePattern;
39  
40      private static final Pattern qValuePattern;
41  
42      private final String type;
43  
44      private final String subtype;
45  
46      private final List<String> parameterNames;
47  
48      private final List<String> parameterValues;
49  
50      private final String mediaType;
51      
52      private final double quality;
53      
54      static {
55  
56          // See RFC 2616, section 2.2
57          String token = "[\\x20-\\x7E&&[^()<>@,;:\\\"/\\[\\]?={} ]]+";
58          String quotedString = "\"((?:[\\x20-\\x7E\\n\\r\\t&&[^\"\\\\]]|\\\\[\\x00-\\x7F])*)\"";
59  
60          // See RFC 2616, section 3.6
61          String parameter = ";\\s*(?!q\\s*=)(" + token + ")=(?:(" + token + ")|" + quotedString + ")";
62  
63          // See RFC 2616, section 3.9
64          String qualityValue = "(?:0(?:\\.\\d{0,3})?|1(?:\\.0{0,3})?)";
65  
66          // See RFC 2616, sections 14.1
67          String quality = ";\\s*q\\s*=\\s*([^;,]*)";
68  
69          // See RFC 2616, section 3.7
70          String regex = "(" + token     + ")/(" + token + ")" +
71                  "((?:\\s*" + parameter + ")*)" +
72                  "(?:\\s*"  + quality   + ")?" +
73                  "((?:\\s*" + parameter + ")*)";
74  
75          tokenPattern      = Pattern.compile(token);
76          parameterPattern  = Pattern.compile(parameter);
77          mediaRangePattern = Pattern.compile(regex);
78          qValuePattern     = Pattern.compile(qualityValue);
79      }
80  
81      /**
82       * Parses a media type from a string such as <tt>text/html;charset=utf-8;q=0.9</tt>.
83       * @param mediaType input string from which to extract mediaType
84       * @return {@link org.apache.any23.servlet.conneg.MediaRangeSpec}
85       */
86      public static MediaRangeSpec parseType(String mediaType) {
87          MediaRangeSpec m = parseRange(mediaType);
88          if (m == null || m.isWildcardType() || m.isWildcardSubtype()) {
89              return null;
90          }
91          return m;
92      }
93  
94      /**
95       * Parses a media range from a string such as <tt>text/*;charset=utf-8;q=0.9</tt>.
96       * Unlike simple media types, media ranges may include wildcards.
97       * @param mediaRange input string from which to extract media range
98       * @return {@link org.apache.any23.servlet.conneg.MediaRangeSpec}
99       */
100     public static MediaRangeSpec parseRange(String mediaRange) {
101         Matcher m = mediaRangePattern.matcher(mediaRange);
102         if (!m.matches()) {
103             return null;
104         }
105         String type = m.group(1).toLowerCase();
106         String subtype = m.group(2).toLowerCase();
107         String unparsedParameters = m.group(3);
108         String qValue = m.group(7);
109         m = parameterPattern.matcher(unparsedParameters);
110         if ("*".equals(type) && !"*".equals(subtype)) {
111             return null;
112         }
113         List<String> parameterNames = new ArrayList<>();
114         List<String> parameterValues = new ArrayList<>();
115         while (m.find()) {
116             String name = m.group(1).toLowerCase();
117             String value = (m.group(3) == null) ? m.group(2) : unescape(m.group(3));
118             parameterNames.add(name);
119             parameterValues.add(value);
120         }
121         double quality = 1.0;
122         if (qValue != null && qValuePattern.matcher(qValue).matches()) {
123             try {
124                 quality = Double.parseDouble(qValue);
125             } catch (NumberFormatException ex) {
126                 // quality stays at default value
127             }
128         }
129         return new MediaRangeSpec(type, subtype, parameterNames, parameterValues, quality);
130     }
131 
132     /**
133      * Parses an HTTP Accept header into a List of MediaRangeSpecs
134      *
135      * @param s an HTTP accept header.
136      * @return A List of MediaRangeSpecs
137      */
138     public static List<MediaRangeSpec> parseAccept(String s) {
139         List<MediaRangeSpec> result = new ArrayList<>();
140         Matcher m = mediaRangePattern.matcher(s);
141         while (m.find()) {
142             result.add(parseRange(m.group()));
143         }
144         return result;
145     }
146 
147     private static String unescape(String s) {
148         return s.replaceAll("\\\\(.)", "$1");
149     }
150 
151     private static String escape(String s) {
152         return s.replaceAll("[\\\\\"]", "\\\\$0");
153     }
154 
155     private MediaRangeSpec(
156             String type,
157             String subtype,
158             List<String> parameterNames, List<String> parameterValues,
159             double quality
160     ) {
161         this.type = type;
162         this.subtype = subtype;
163         this.parameterNames = Collections.unmodifiableList(parameterNames);
164         this.parameterValues = parameterValues;
165         this.mediaType = buildMediaType();
166         this.quality = quality;
167     }
168 
169     private String buildMediaType() {
170         StringBuffer result = new StringBuffer();
171         result.append(type);
172         result.append("/");
173         result.append(subtype);
174         for (int i = 0; i < parameterNames.size(); i++) {
175             result.append(";");
176             result.append(parameterNames.get(i));
177             result.append("=");
178             String value = parameterValues.get(i);
179             if (tokenPattern.matcher(value).matches()) {
180                 result.append(value);
181             } else {
182                 result.append("\"");
183                 result.append(escape(value));
184                 result.append("\"");
185             }
186         }
187         return result.toString();
188     }
189 
190     public String getType() {
191         return type;
192     }
193 
194     public String getSubtype() {
195         return subtype;
196     }
197 
198     public String getMediaType() {
199         return mediaType;
200     }
201 
202     public List<String> getParameterNames() {
203         return parameterNames;
204     }
205 
206     public String getParameter(String parameterName) {
207         for (int i = 0; i < parameterNames.size(); i++) {
208             if (parameterNames.get(i).equalsIgnoreCase(parameterName)) {
209                 return parameterValues.get(i);
210             }
211         }
212         return null;
213     }
214 
215     public boolean isWildcardType() {
216         return "*".equals(type);
217     }
218 
219     public boolean isWildcardSubtype() {
220         return !isWildcardType() && "*".equals(subtype);
221     }
222 
223     public double getQuality() {
224         return quality;
225     }
226 
227     public int getPrecedence(MediaRangeSpec range) {
228         if (range.isWildcardType())
229           return 1;
230         if (!range.type.equals(type))
231           return 0;
232         if (range.isWildcardSubtype())
233           return 2;
234         if (!range.subtype.equals(subtype))
235           return 0;
236         if (range.getParameterNames().isEmpty())
237           return 3;
238         int result = 3;
239         for (int i = 0; i < range.getParameterNames().size(); i++) {
240             String name  = range.getParameterNames().get(i);
241             String value = range.getParameter(name);
242             if (!value.equals(getParameter(name)))
243               return 0;
244             result++;
245         }
246         return result;
247     }
248 
249     public MediaRangeSpec getBestMatch(List<MediaRangeSpec> mediaRanges) {
250         MediaRangeSpec result = null;
251         int bestPrecedence = 0;
252         Iterator<MediaRangeSpec> it = mediaRanges.iterator();
253         while (it.hasNext()) {
254             MediaRangeSpec range = it.next();
255             if (getPrecedence(range) > bestPrecedence) {
256                 bestPrecedence = getPrecedence(range);
257                 result = range;
258             }
259         }
260         return result;
261     }
262 
263     @Override
264     public String toString() {
265         return mediaType + ";q=" + quality;
266     }
267 }