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 }