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