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