001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.http; 014 015import static org.apache.juneau.internal.CollectionUtils.*; 016 017import java.util.*; 018import java.util.Map.*; 019import java.util.concurrent.*; 020 021import org.apache.juneau.annotation.*; 022import org.apache.juneau.internal.*; 023 024/** 025 * Describes a single type used in content negotiation between an HTTP client and server, as described in 026 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification). 027 * 028 * <h5 class='section'>See Also:</h5> 029 * <ul class='doctree'> 030 * <li class='extlink'>{@doc RFC2616} 031 * </ul> 032 */ 033@BeanIgnore 034public final class MediaTypeRange implements Comparable<MediaTypeRange> { 035 036 private static final MediaTypeRange[] DEFAULT = new MediaTypeRange[]{new MediaTypeRange("*/*")}; 037 private static final boolean NOCACHE = Boolean.getBoolean("juneau.nocache"); 038 private static final ConcurrentHashMap<String,MediaTypeRange[]> CACHE = new ConcurrentHashMap<>(); 039 040 private final MediaType mediaType; 041 private final Float qValue; 042 private final Map<String,Set<String>> extensions; 043 044 /** 045 * Parses an <code>Accept</code> header value into an array of media ranges. 046 * 047 * <p> 048 * The returned media ranges are sorted such that the most acceptable media is available at ordinal position 049 * <js>'0'</js>, and the least acceptable at position n-1. 050 * 051 * <p> 052 * The syntax expected to be found in the referenced <code>value</code> complies with the syntax described in 053 * RFC2616, Section 14.1, as described below: 054 * <p class='bcode w800'> 055 * Accept = "Accept" ":" 056 * #( media-range [ accept-params ] ) 057 * 058 * media-range = ( "*\/*" 059 * | ( type "/" "*" ) 060 * | ( type "/" subtype ) 061 * ) *( ";" parameter ) 062 * accept-params = ";" "q" "=" qvalue *( accept-extension ) 063 * accept-extension = ";" token [ "=" ( token | quoted-string ) ] 064 * </p> 065 * 066 * @param value 067 * The value to parse. 068 * If <jk>null</jk> or empty, returns a single <code>MediaTypeRange</code> is returned that represents all types. 069 * @return 070 * The media ranges described by the string. 071 * The ranges are sorted such that the most acceptable media is available at ordinal position <js>'0'</js>, and 072 * the least acceptable at position n-1. 073 */ 074 public static MediaTypeRange[] parse(String value) { 075 076 if (value == null || value.length() == 0) 077 return DEFAULT; 078 079 MediaTypeRange[] mtr = CACHE.get(value); 080 if (mtr != null) 081 return mtr; 082 083 if (value.indexOf(',') == -1) { 084 mtr = new MediaTypeRange[]{new MediaTypeRange(value)}; 085 } else { 086 Set<MediaTypeRange> ranges = new TreeSet<>(); 087 for (String r : StringUtils.split(value)) { 088 r = r.trim(); 089 if (r.isEmpty()) 090 continue; 091 ranges.add(new MediaTypeRange(r)); 092 } 093 mtr = ranges.toArray(new MediaTypeRange[ranges.size()]); 094 } 095 if (NOCACHE) 096 return mtr; 097 CACHE.putIfAbsent(value, mtr); 098 return CACHE.get(value); 099 } 100 101 private MediaTypeRange(String token) { 102 Builder b = new Builder(token); 103 this.mediaType = b.mediaType; 104 this.qValue = b.qValue; 105 this.extensions = unmodifiableMap(b.extensions); 106 } 107 108 static final class Builder { 109 MediaType mediaType; 110 Float qValue = 1f; 111 Map<String,Set<String>> extensions; 112 113 Builder(String token) { 114 115 token = token.trim(); 116 117 int i = token.indexOf(";q="); 118 119 if (i == -1) { 120 mediaType = MediaType.forString(token); 121 return; 122 } 123 124 mediaType = MediaType.forString(token.substring(0, i)); 125 126 String[] tokens = token.substring(i+1).split(";"); 127 128 // Only the type of the range is specified 129 if (tokens.length > 0) { 130 boolean isInExtensions = false; 131 for (int j = 0; j < tokens.length; j++) { 132 String[] parm = tokens[j].split("="); 133 if (parm.length == 2) { 134 String k = parm[0], v = parm[1]; 135 if (isInExtensions) { 136 if (extensions == null) 137 extensions = new TreeMap<>(); 138 if (! extensions.containsKey(k)) 139 extensions.put(k, new TreeSet<String>()); 140 extensions.get(k).add(v); 141 } else if (k.equals("q")) { 142 qValue = new Float(v); 143 isInExtensions = true; 144 } 145 } 146 } 147 } 148 } 149 } 150 151 /** 152 * Returns the media type enclosed by this media range. 153 * 154 * <h5 class='section'>Examples:</h5> 155 * <ul> 156 * <li><js>"text/html"</js> 157 * <li><js>"text/*"</js> 158 * <li><js>"*\/*"</js> 159 * </ul> 160 * 161 * @return The media type of this media range, lowercased, never <jk>null</jk>. 162 */ 163 public MediaType getMediaType() { 164 return mediaType; 165 } 166 167 /** 168 * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616. 169 * 170 * <p> 171 * The quality value is a float between <code>0.0</code> (unacceptable) and <code>1.0</code> (most acceptable). 172 * 173 * <p> 174 * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js> 175 * header, as opposed to <js>"accept-*"</js> header, its value will always be <js>"1"</js>. 176 * 177 * @return The 'q' value for this type, never <jk>null</jk>. 178 */ 179 public Float getQValue() { 180 return qValue; 181 } 182 183 /** 184 * Returns the optional set of custom extensions defined for this type. 185 * 186 * <p> 187 * Values are lowercase and never <jk>null</jk>. 188 * 189 * @return The optional list of extensions, never <jk>null</jk>. 190 */ 191 public Map<String,Set<String>> getExtensions() { 192 return extensions; 193 } 194 195 /** 196 * Provides a string representation of this media range, suitable for use as an <code>Accept</code> header value. 197 * 198 * <p> 199 * The literal text generated will be all lowercase. 200 * 201 * @return A media range suitable for use as an Accept header value, never <code>null</code>. 202 */ 203 @Override /* Object */ 204 public String toString() { 205 StringBuffer sb = new StringBuffer().append(mediaType); 206 207 // '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue. 208 if (qValue.floatValue() == 1.0) { 209 if (! extensions.isEmpty()) { 210 sb.append(";q=").append(qValue); 211 for (Entry<String,Set<String>> e : extensions.entrySet()) { 212 String k = e.getKey(); 213 for (String v : e.getValue()) 214 sb.append(';').append(k).append('=').append(v); 215 } 216 } 217 } else { 218 sb.append(";q=").append(qValue); 219 for (Entry<String,Set<String>> e : extensions.entrySet()) { 220 String k = e.getKey(); 221 for (String v : e.getValue()) 222 sb.append(';').append(k).append('=').append(v); 223 } 224 } 225 return sb.toString(); 226 } 227 228 /** 229 * Returns <jk>true</jk> if the specified object is also a <code>MediaType</code>, and has the same qValue, type, 230 * parameters, and extensions. 231 * 232 * @return <jk>true</jk> if object is equivalent. 233 */ 234 @Override /* Object */ 235 public boolean equals(Object o) { 236 237 if (o == null || !(o instanceof MediaTypeRange)) 238 return false; 239 240 if (this == o) 241 return true; 242 243 MediaTypeRange o2 = (MediaTypeRange) o; 244 return qValue.equals(o2.qValue) 245 && mediaType.equals(o2.mediaType) 246 && extensions.equals(o2.extensions); 247 } 248 249 /** 250 * Returns a hash based on this instance's <code>media-type</code>. 251 * 252 * @return A hash based on this instance's <code>media-type</code>. 253 */ 254 @Override /* Object */ 255 public int hashCode() { 256 return mediaType.hashCode(); 257 } 258 259 /** 260 * Compares two MediaRanges for equality. 261 * 262 * <p> 263 * The values are first compared according to <code>qValue</code> values. 264 * Should those values be equal, the <code>type</code> is then lexicographically compared (case-insensitive) in 265 * ascending order, with the <js>"*"</js> type demoted last in that order. 266 * <code>MediaRanges</code> with the same type but different sub-types are compared - a more specific subtype is 267 * promoted over the 'wildcard' subtype. 268 * <code>MediaRanges</code> with the same types but with extensions are promoted over those same types with no 269 * extensions. 270 * 271 * @param o The range to compare to. Never <jk>null</jk>. 272 */ 273 @Override /* Comparable */ 274 public int compareTo(MediaTypeRange o) { 275 276 // Compare q-values. 277 int qCompare = Float.compare(o.qValue, qValue); 278 if (qCompare != 0) 279 return qCompare; 280 281 // Compare media-types. 282 // Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison. 283 int i = o.mediaType.toString().compareTo(mediaType.toString()); 284 return i; 285 } 286}