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.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.util.*; 019import java.util.concurrent.*; 020 021import org.apache.juneau.annotation.*; 022import org.apache.juneau.internal.*; 023import org.apache.juneau.json.*; 024 025 026/** 027 * Describes a single media type used in content negotiation between an HTTP client and server, as described in 028 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification). 029 * 030 * <h5 class='section'>See Also:</h5> 031 * <ul class='doctree'> 032 * <li class='extlink'>{@doc RFC2616} 033 * </ul> 034 */ 035@BeanIgnore 036public class MediaType implements Comparable<MediaType> { 037 038 private static final boolean NOCACHE = Boolean.getBoolean("juneau.nocache"); 039 private static final ConcurrentHashMap<String,MediaType> CACHE = new ConcurrentHashMap<>(); 040 041 /** Reusable predefined media type */ 042 @SuppressWarnings("javadoc") 043 public static final MediaType 044 CSV = forString("text/csv"), 045 HTML = forString("text/html"), 046 JSON = forString("application/json"), 047 MSGPACK = forString("octal/msgpack"), 048 PLAIN = forString("text/plain"), 049 UON = forString("text/uon"), 050 URLENCODING = forString("application/x-www-form-urlencoded"), 051 XML = forString("text/xml"), 052 XMLSOAP = forString("text/xml+soap"), 053 054 RDF = forString("text/xml+rdf"), 055 RDFABBREV = forString("text/xml+rdf+abbrev"), 056 NTRIPLE = forString("text/n-triple"), 057 TURTLE = forString("text/turtle"), 058 N3 = forString("text/n3") 059 ; 060 061 private final String mediaType; 062 private final String type; // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset) 063 private final String subType; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset) 064 private final String[] subTypes; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset) 065 private final String[] subTypesSorted; // Same as subTypes, but sorted so that it can be used for comparison. 066 private final List<String> subTypesList; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset) 067 private final Map<String,Set<String>> parameters; // The media type parameters (e.g. "text/html;level=1"). Does not include q! 068 private final boolean hasSubtypeMeta; // The media subtype contains meta-character '*'. 069 070 /** 071 * Returns the media type for the specified string. 072 * The same media type strings always return the same objects so that these objects 073 * can be compared for equality using '=='. 074 * 075 * <h5 class='section'>Notes:</h5> 076 * <ul class='spaced-list'> 077 * <li> 078 * Spaces are replaced with <js>'+'</js> characters. 079 * This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters 080 * get replaced with spaces by your browser. Since spaces aren't supported by the spec, this 081 * is doesn't break anything. 082 * <li> 083 * Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>). 084 * </ul> 085 * 086 * @param s 087 * The media type string. 088 * Will be lowercased. 089 * Returns <jk>null</jk> if input is null or empty. 090 * @return A cached media type object. 091 */ 092 public static MediaType forString(String s) { 093 if (isEmpty(s)) 094 return null; 095 MediaType mt = CACHE.get(s); 096 if (mt != null) 097 return mt; 098 mt = new MediaType(s); 099 if (NOCACHE) 100 return mt; 101 CACHE.putIfAbsent(s, mt); 102 return CACHE.get(s); 103 } 104 105 /** 106 * Same as {@link #forString(String)} but allows you to construct an array of <code>MediaTypes</code> from an 107 * array of strings. 108 * 109 * @param s 110 * The media type strings. 111 * @return 112 * An array of <code>MediaType</code> objects. 113 * <br>Always the same length as the input string array. 114 */ 115 public static MediaType[] forStrings(String...s) { 116 MediaType[] mt = new MediaType[s.length]; 117 for (int i = 0; i < s.length; i++) 118 mt[i] = forString(s[i]); 119 return mt; 120 } 121 122 MediaType(String mt) { 123 Builder b = new Builder(mt); 124 this.mediaType = b.mediaType; 125 this.type = b.type; 126 this.subType = b.subType; 127 this.subTypes = b.subTypes; 128 this.subTypesSorted = b.subTypesSorted; 129 this.subTypesList = immutableList(subTypes); 130 this.parameters = unmodifiableMap(b.parameters); 131 this.hasSubtypeMeta = b.hasSubtypeMeta; 132 } 133 134 static final class Builder { 135 String mediaType, type, subType; 136 String[] subTypes, subTypesSorted; 137 Map<String,Set<String>> parameters; 138 boolean hasSubtypeMeta; 139 140 Builder(String mt) { 141 mt = mt.trim(); 142 int i = mt.indexOf(','); 143 if (i != -1) 144 mt = mt.substring(0, i); 145 146 i = mt.indexOf(';'); 147 if (i == -1) { 148 this.parameters = Collections.EMPTY_MAP; 149 } else { 150 this.parameters = new TreeMap<>(); 151 String[] tokens = mt.substring(i+1).split(";"); 152 153 for (int j = 0; j < tokens.length; j++) { 154 String[] parm = tokens[j].split("="); 155 if (parm.length == 2) { 156 String k = parm[0].trim(), v = parm[1].trim(); 157 if (! parameters.containsKey(k)) 158 parameters.put(k, new TreeSet<String>()); 159 parameters.get(k).add(v); 160 } 161 } 162 163 mt = mt.substring(0, i); 164 } 165 166 this.mediaType = mt; 167 if (mt != null) { 168 mt = mt.replace(' ', '+'); 169 i = mt.indexOf('/'); 170 type = (i == -1 ? mt : mt.substring(0, i)); 171 subType = (i == -1 ? "*" : mt.substring(i+1)); 172 } 173 this.subTypes = StringUtils.split(subType, '+'); 174 this.subTypesSorted = Arrays.copyOf(subTypes, subTypes.length); 175 Arrays.sort(this.subTypesSorted); 176 hasSubtypeMeta = ArrayUtils.contains("*", this.subTypes); 177 } 178 } 179 180 /** 181 * Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string. 182 * 183 * @return The media type. 184 */ 185 public final String getType() { 186 return type; 187 } 188 189 /** 190 * Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string. 191 * 192 * @return The media subtype. 193 */ 194 public final String getSubType() { 195 return subType; 196 } 197 198 /** 199 * Returns <jk>true</jk> if the subtype contains the specified <js>'+'</js> delimited subtype value. 200 * 201 * @param st 202 * The subtype string. 203 * Case is ignored. 204 * @return <jk>true</jk> if the subtype contains the specified subtype string. 205 */ 206 public final boolean hasSubType(String st) { 207 if (st != null) 208 for (String s : subTypes) 209 if (st.equalsIgnoreCase(s)) 210 return true; 211 return false; 212 } 213 214 /** 215 * Returns the subtypes broken down by fragments delimited by <js>"'"</js>. 216 * 217 * <P> 218 * For example, the media type <js>"text/foo+bar"</js> will return a list of 219 * <code>[<js>'foo'</js>,<js>'bar'</js>]</code> 220 * 221 * @return An unmodifiable list of subtype fragments. Never <jk>null</jk>. 222 */ 223 public final List<String> getSubTypes() { 224 return subTypesList; 225 } 226 227 /** 228 * Returns <jk>true</jk> if this media type contains the <js>'*'</js> meta character. 229 * 230 * @return <jk>true</jk> if this media type contains the <js>'*'</js> meta character. 231 */ 232 public final boolean isMeta() { 233 return hasSubtypeMeta; 234 } 235 236 /** 237 * Returns a match metric against the specified media type where a larger number represents a better match. 238 * 239 * <p> 240 * This media type can contain <js>'*'</js> metacharacters. 241 * <br>The comparison media type must not. 242 * 243 * <ul> 244 * <li>Exact matches (e.g. <js>"text/json"<js>/</js>"text/json"</js>) should match 245 * better than meta-character matches (e.g. <js>"text/*"<js>/</js>"text/json"</js>) 246 * <li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>) 247 * that will not prevent a match if the <code>allowExtraSubTypes</code> flag is set. 248 * The reverse is not true, e.g. the comparison media type must contain all subtype tokens found in the 249 * comparing media type. 250 * <ul> 251 * <li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>. 252 * <li>We want to make sure {@link org.apache.juneau.json.SimpleJsonSerializer} (<js>"text/json+simple"</js>) does not handle 253 * requests for <js>"text/json"</js>. 254 * </ul> 255 * More token matches should result in a higher match number. 256 * </ul> 257 * 258 * The formula is as follows for <code>type/subTypes</code>: 259 * <ul> 260 * <li>An exact match is <code>100,000</code>. 261 * <li>Add the following for type (assuming subtype match is <0): 262 * <ul> 263 * <li><code>10,000</code> for an exact match (e.g. <js>"text"</js>==<js>"text"</js>). 264 * <li><code>5,000</code> for a meta match (e.g. <js>"*"</js>==<js>"text"</js>). 265 * </ul> 266 * <li>Add the following for subtype (assuming type match is <0): 267 * <ul> 268 * <li><code>7,500</code> for an exact match (e.g. <js>"json+foo"</js>==<js>"json+foo"</js> or <js>"json+foo"</js>==<js>"foo+json"</js>) 269 * <li><code>100</code> for every subtype entry match (e.g. <js>"json"</js>/<js>"json+foo"</js>) 270 * </ul> 271 * </ul> 272 * 273 * @param o The media type to compare with. 274 * @param allowExtraSubTypes If <jk>true</jk>, 275 * @return <jk>true</jk> if the media types match. 276 */ 277 public final int match(MediaType o, boolean allowExtraSubTypes) { 278 279 // Perfect match 280 if (this == o || (type.equals(o.type) && subType.equals(o.subType))) 281 return 100000; 282 283 int c = 0; 284 285 if (type.equals(o.type)) 286 c += 10000; 287 else if ("*".equals(type) || "*".equals(o.type)) 288 c += 5000; 289 290 if (c == 0) 291 return 0; 292 293 // Subtypes match but are ordered different 294 if (ArrayUtils.equals(subTypesSorted, o.subTypesSorted)) 295 return c + 7500; 296 297 for (String st1 : subTypes) { 298 if ("*".equals(st1)) 299 c += 0; 300 else if (ArrayUtils.contains(st1, o.subTypes)) 301 c += 100; 302 else if (o.hasSubtypeMeta) 303 c += 0; 304 else 305 return 0; 306 } 307 for (String st2 : o.subTypes) { 308 if ("*".equals(st2)) 309 c += 0; 310 else if (ArrayUtils.contains(st2, subTypes)) 311 c += 100; 312 else if (hasSubtypeMeta) 313 c += 0; 314 else if (! allowExtraSubTypes) 315 return 0; 316 else 317 c += 10; 318 } 319 320 return c; 321 } 322 323 /** 324 * Returns the additional parameters on this media type. 325 * 326 * <p> 327 * For example, given the media type string <js>"text/html;level=1"</js>, will return a map 328 * with the single entry <code>{level:[<js>'1'</js>]}</code>. 329 * 330 * @return The map of additional parameters, or an empty map if there are no parameters. 331 */ 332 public final Map<String,Set<String>> getParameters() { 333 return parameters; 334 } 335 336 @Override /* Object */ 337 public final String toString() { 338 if (parameters.isEmpty()) 339 return mediaType; 340 StringBuilder sb = new StringBuilder(mediaType); 341 for (Map.Entry<String,Set<String>> e : parameters.entrySet()) 342 for (String value : e.getValue()) 343 sb.append(';').append(e.getKey()).append('=').append(value); 344 return sb.toString(); 345 } 346 347 @Override /* Object */ 348 public final int hashCode() { 349 return mediaType.hashCode(); 350 } 351 352 @Override /* Object */ 353 public final boolean equals(Object o) { 354 return this == o; 355 } 356 357 @Override 358 public final int compareTo(MediaType o) { 359 return mediaType.compareTo(o.mediaType); 360 } 361}