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; 014 015import static org.apache.juneau.common.internal.StringUtils.*; 016import static org.apache.juneau.internal.CollectionUtils.*; 017import static org.apache.juneau.internal.ObjectUtils.*; 018 019import java.util.*; 020import java.util.function.*; 021 022import org.apache.http.*; 023import org.apache.http.message.*; 024import org.apache.juneau.annotation.*; 025import org.apache.juneau.common.internal.*; 026import org.apache.juneau.internal.*; 027import org.apache.juneau.json.*; 028 029 030/** 031 * Describes a single media type used in content negotiation between an HTTP client and server, as described in 032 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification). 033 * 034 * <h5 class='section'>See Also:</h5><ul> 035 * <li class='link'><a class="doclink" href="../../../index.html#juneau-rest-common">juneau-rest-common</a> 036 * <li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a> 037 * </ul> 038 */ 039@BeanIgnore 040public class MediaType implements Comparable<MediaType> { 041 042 //----------------------------------------------------------------------------------------------------------------- 043 // Static 044 //----------------------------------------------------------------------------------------------------------------- 045 046 /** Represents an empty media type object. */ 047 public static final MediaType EMPTY = new MediaType("/*"); 048 049 private static final Cache<String,MediaType> CACHE = Cache.of(String.class, MediaType.class).build(); 050 051 /** Reusable predefined media type */ 052 @SuppressWarnings("javadoc") 053 public static final MediaType 054 CSV = of("text/csv"), 055 HTML = of("text/html"), 056 JSON = of("application/json"), 057 MSGPACK = of("octal/msgpack"), 058 PLAIN = of("text/plain"), 059 UON = of("text/uon"), 060 URLENCODING = of("application/x-www-form-urlencoded"), 061 XML = of("text/xml"), 062 XMLSOAP = of("text/xml+soap"), 063 064 RDF = of("text/xml+rdf"), 065 RDFABBREV = of("text/xml+rdf+abbrev"), 066 NTRIPLE = of("text/n-triple"), 067 TURTLE = of("text/turtle"), 068 N3 = of("text/n3") 069 ; 070 071 /** 072 * Returns the media type for the specified string. 073 * The same media type strings always return the same objects so that these objects 074 * can be compared for equality using '=='. 075 * 076 * <h5 class='section'>Notes:</h5><ul> 077 * <li class='note'> 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 class='note'> 083 * Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>). 084 * </ul> 085 * 086 * @param value 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 of(String value) { 093 return value == null ? null : CACHE.get(value, ()->new MediaType(value)); 094 } 095 096 /** 097 * Same as {@link #of(String)} but allows you to specify the parameters. 098 * 099 * 100 * @param value 101 * The media type string. 102 * Will be lowercased. 103 * Returns <jk>null</jk> if input is null or empty. 104 * @param parameters The media type parameters. If <jk>null</jk>, they're pulled from the media type string. 105 * @return A new media type object, cached if parameters were not specified. 106 */ 107 public static MediaType of(String value, NameValuePair...parameters) { 108 if (parameters.length == 0) 109 return of(value); 110 return isEmpty(value) ? null : new MediaType(value, parameters); 111 } 112 113 /** 114 * Same as {@link #of(String)} but allows you to construct an array of <c>MediaTypes</c> from an 115 * array of strings. 116 * 117 * @param values 118 * The media type strings. 119 * @return 120 * An array of <c>MediaType</c> objects. 121 * <br>Always the same length as the input string array. 122 */ 123 public static MediaType[] ofAll(String...values) { 124 MediaType[] mt = new MediaType[values.length]; 125 for (int i = 0; i < values.length; i++) 126 mt[i] = of(values[i]); 127 return mt; 128 } 129 130 //----------------------------------------------------------------------------------------------------------------- 131 // Instance 132 //----------------------------------------------------------------------------------------------------------------- 133 134 private final String string; // The entire unparsed value. 135 private final String mediaType; // The "type/subtype" portion of the media type.. 136 private final String type; // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset) 137 private final String subType; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset) 138 private final String[] subTypes; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset) 139 private final String[] subTypesSorted; // Same as subTypes, but sorted so that it can be used for comparison. 140 private final boolean hasSubtypeMeta; // The media subtype contains meta-character '*'. 141 142 private final NameValuePair[] parameters; // The media type parameters (e.g. "text/html;level=1"). Does not include q! 143 144 /** 145 * Constructor. 146 * 147 * @param mt The media type string. 148 */ 149 public MediaType(String mt) { 150 this(parse(mt)); 151 } 152 153 /** 154 * Constructor. 155 * 156 * @param mt The media type string. 157 * @param parameters The media type parameters. If <jk>null</jk>, they're pulled from the media type string. 158 */ 159 public MediaType(String mt, NameValuePair[] parameters) { 160 this(parse(mt), parameters); 161 } 162 163 /** 164 * Constructor. 165 * 166 * @param e The parsed media type string. 167 */ 168 public MediaType(HeaderElement e) { 169 this(e, null); 170 } 171 172 /** 173 * Constructor. 174 * 175 * @param e The parsed media type string. 176 * @param parameters Optional parameters. 177 */ 178 public MediaType(HeaderElement e, NameValuePair[] parameters) { 179 mediaType = e.getName(); 180 181 if (parameters == null) { 182 parameters = e.getParameters(); 183 for (int i = 0; i < parameters.length; i++) { 184 if (parameters[i].getName().equals("q")) { 185 parameters = Arrays.copyOfRange(parameters, 0, i); 186 break; 187 } 188 } 189 } 190 for (int i = 0; i < parameters.length; i++) 191 parameters[i] = new BasicNameValuePair(parameters[i].getName(), parameters[i].getValue()); 192 this.parameters = parameters; 193 194 String x = mediaType.replace(' ', '+'); 195 int i = x.indexOf('/'); 196 type = (i == -1 ? x : x.substring(0, i)); 197 subType = (i == -1 ? "*" : x.substring(i+1)); 198 199 subTypes = StringUtils.split(subType, '+'); 200 subTypesSorted = Arrays.copyOf(subTypes, subTypes.length); 201 Arrays.sort(this.subTypesSorted); 202 hasSubtypeMeta = ArrayUtils.contains("*", this.subTypes); 203 204 StringBuilder sb = new StringBuilder(); 205 sb.append(mediaType); 206 for (NameValuePair p : parameters) 207 sb.append(';').append(p.getName()).append('=').append(p.getValue()); 208 this.string = sb.toString(); 209 } 210 211 /** 212 * Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string. 213 * 214 * @return The media type. 215 */ 216 public final String getType() { 217 return type; 218 } 219 220 /** 221 * Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string. 222 * 223 * @return The media subtype. 224 */ 225 public final String getSubType() { 226 return subType; 227 } 228 229 /** 230 * Returns <jk>true</jk> if the subtype contains the specified <js>'+'</js> delimited subtype value. 231 * 232 * @param st 233 * The subtype string. 234 * Case is ignored. 235 * @return <jk>true</jk> if the subtype contains the specified subtype string. 236 */ 237 public final boolean hasSubType(String st) { 238 if (st != null) 239 for (String s : subTypes) 240 if (st.equalsIgnoreCase(s)) 241 return true; 242 return false; 243 } 244 245 /** 246 * Returns the subtypes broken down by fragments delimited by <js>"'"</js>. 247 * 248 * <P> 249 * For example, the media type <js>"text/foo+bar"</js> will return a list of 250 * <code>[<js>'foo'</js>,<js>'bar'</js>]</code> 251 * 252 * @return An unmodifiable list of subtype fragments. Never <jk>null</jk>. 253 */ 254 public final List<String> getSubTypes() { 255 return ulist(subTypes); 256 } 257 258 /** 259 * Performs an action on the subtypes broken down by fragments delimited by <js>"'"</js>. 260 * 261 * @param action The action to perform. 262 * @return This object. 263 */ 264 public final MediaType forEachSubType(Consumer<String> action) { 265 for (String s : subTypes) 266 action.accept(s); 267 return this; 268 } 269 270 /** 271 * Returns <jk>true</jk> if this media type subtype contains the <js>'*'</js> meta character. 272 * 273 * @return <jk>true</jk> if this media type subtype contains the <js>'*'</js> meta character. 274 */ 275 public final boolean isMetaSubtype() { 276 return hasSubtypeMeta; 277 } 278 279 /** 280 * Returns a match metric against the specified media type where a larger number represents a better match. 281 * 282 * <p> 283 * This media type can contain <js>'*'</js> metacharacters. 284 * <br>The comparison media type must not. 285 * 286 * <ul> 287 * <li>Exact matches (e.g. <js>"text/json"</js>/</js>"text/json"</js>) should match 288 * better than meta-character matches (e.g. <js>"text/*"</js>/</js>"text/json"</js>) 289 * <li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>) 290 * that will not prevent a match if the <c>allowExtraSubTypes</c> flag is set. 291 * The reverse is not true, e.g. the comparison media type must contain all subtype tokens found in the 292 * comparing media type. 293 * <ul> 294 * <li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>. 295 * <li>We want to make sure {@link org.apache.juneau.json.Json5Serializer} (<js>"text/json5"</js>) does not handle 296 * requests for <js>"text/json"</js>. 297 * </ul> 298 * More token matches should result in a higher match number. 299 * </ul> 300 * 301 * The formula is as follows for <c>type/subTypes</c>: 302 * <ul> 303 * <li>An exact match is <c>100,000</c>. 304 * <li>Add the following for type (assuming subtype match is <0): 305 * <ul> 306 * <li><c>10,000</c> for an exact match (e.g. <js>"text"</js>==<js>"text"</js>). 307 * <li><c>5,000</c> for a meta match (e.g. <js>"*"</js>==<js>"text"</js>). 308 * </ul> 309 * <li>Add the following for subtype (assuming type match is <0): 310 * <ul> 311 * <li><c>7,500</c> for an exact match (e.g. <js>"json+foo"</js>==<js>"json+foo"</js> or <js>"json+foo"</js>==<js>"foo+json"</js>) 312 * <li><c>100</c> for every subtype entry match (e.g. <js>"json"</js>/<js>"json+foo"</js>) 313 * </ul> 314 * </ul> 315 * 316 * @param o The media type to compare with. 317 * @param allowExtraSubTypes If <jk>true</jk>, 318 * @return <jk>true</jk> if the media types match. 319 */ 320 public final int match(MediaType o, boolean allowExtraSubTypes) { 321 322 if (o == null) 323 return -1; 324 325 // Perfect match 326 if (this == o || (type.equals(o.type) && subType.equals(o.subType))) 327 return 100000; 328 329 int c = 0; 330 331 if (type.equals(o.type)) 332 c += 10000; 333 else if ("*".equals(type) || "*".equals(o.type)) 334 c += 5000; 335 336 if (c == 0) 337 return 0; 338 339 // Subtypes match but are ordered different 340 if (ArrayUtils.equals(subTypesSorted, o.subTypesSorted)) 341 return c + 7500; 342 343 for (String st1 : subTypes) { 344 if ("*".equals(st1)) 345 c += 0; 346 else if (ArrayUtils.contains(st1, o.subTypes)) 347 c += 100; 348 else if (o.hasSubtypeMeta) 349 c += 0; 350 else 351 return 0; 352 } 353 for (String st2 : o.subTypes) { 354 if ("*".equals(st2)) 355 c += 0; 356 else if (ArrayUtils.contains(st2, subTypes)) 357 c += 100; 358 else if (hasSubtypeMeta) 359 c += 0; 360 else if (! allowExtraSubTypes) 361 return 0; 362 else 363 c += 10; 364 } 365 366 return c; 367 } 368 369 /** 370 * Returns the additional parameters on this media type. 371 * 372 * <p> 373 * For example, given the media type string <js>"text/html;level=1"</js>, will return a map 374 * with the single entry <code>{level:[<js>'1'</js>]}</code>. 375 * 376 * @return The map of additional parameters, or an empty map if there are no parameters. 377 */ 378 public List<NameValuePair> getParameters() { 379 return ulist(parameters); 380 } 381 382 /** 383 * Performs an action on the additional parameters on this media type. 384 * 385 * @param action The action to perform. 386 * @return This object. 387 */ 388 public MediaType forEachParameter(Consumer<NameValuePair> action) { 389 for (NameValuePair p : parameters) 390 action.accept(p); 391 return this; 392 } 393 394 /** 395 * Returns the additional parameter on this media type. 396 * 397 * @param name The additional parameter name. 398 * @return The parameter value, or <jk>null</jk> if not found. 399 */ 400 public String getParameter(String name) { 401 for (NameValuePair p : parameters) 402 if (eq(name, p.getName())) 403 return p.getValue(); 404 return null; 405 } 406 407 /** 408 * Given a list of media types, returns the best match for this <c>Content-Type</c> header. 409 * 410 * <p> 411 * Note that fuzzy matching is allowed on the media types where the <c>Content-Types</c> header may 412 * contain additional subtype parts. 413 * <br>For example, given a <c>Content-Type</c> value of <js>"text/json+activity"</js>, 414 * the media type <js>"text/json"</js> will match if <js>"text/json+activity"</js> or <js>"text/activity+json"</js> 415 * isn't found. 416 * <br>The purpose for this is to allow parsers to match when artifacts such as <c>id</c> properties are 417 * present in the header. 418 * 419 * @param mediaTypes The media types to match against. 420 * @return The index into the array of the best match, or <c>-1</c> if no suitable matches could be found. 421 */ 422 public int match(List<MediaType> mediaTypes) { 423 int matchQuant = 0, matchIndex = -1; 424 425 for (int i = 0; i < mediaTypes.size(); i++) { 426 MediaType mt = mediaTypes.get(i); 427 int matchQuant2 = mt.match(this, true); 428 if (matchQuant2 > matchQuant) { 429 matchQuant = matchQuant2; 430 matchIndex = i; 431 } 432 } 433 return matchIndex; 434 } 435 436 private static HeaderElement parse(String value) { 437 HeaderElement[] elements = BasicHeaderValueParser.parseElements(emptyIfNull(trim(value)), null); 438 return (elements.length > 0 ? elements[0] : new BasicHeaderElement("", "")); 439 } 440 441 @Override /* Object */ 442 public String toString() { 443 return string; 444 } 445 446 @Override /* Object */ 447 public int hashCode() { 448 return string.hashCode(); 449 } 450 451 @Override /* Object */ 452 public boolean equals(Object o) { 453 return (o instanceof MediaType) && eq(this, (MediaType)o, (x,y)->eq(x.string, y.string)); 454 } 455 456 @Override 457 public final int compareTo(MediaType o) { 458 return toString().compareTo(o.toString()); 459 } 460}