1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.juneau;
18
19 import static org.apache.juneau.commons.utils.CollectionUtils.*;
20 import static org.apache.juneau.commons.utils.StringUtils.*;
21 import static org.apache.juneau.commons.utils.Utils.*;
22
23 import java.util.*;
24 import java.util.function.*;
25
26 import org.apache.http.*;
27 import org.apache.http.message.*;
28 import org.apache.juneau.annotation.*;
29 import org.apache.juneau.commons.collections.*;
30 import org.apache.juneau.commons.utils.*;
31 import org.apache.juneau.json.*;
32
33 /**
34 * Describes a single media type used in content negotiation between an HTTP client and server, as described in
35 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
36 *
37 * <h5 class='section'>See Also:</h5><ul>
38 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommonBasics">juneau-rest-common Basics</a>
39 * <li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a>
40 * </ul>
41 */
42 @BeanIgnore
43 public class MediaType implements Comparable<MediaType> {
44 /** Represents an empty media type object. */
45 public static final MediaType EMPTY = new MediaType("/*");
46
47 private static final Cache<String,MediaType> CACHE = Cache.of(String.class, MediaType.class).build();
48
49 /** Reusable predefined media type */
50 @SuppressWarnings("javadoc")
51 // @formatter:off
52 public static final MediaType
53 CSV = of("text/csv"),
54 HTML = of("text/html"),
55 JSON = of("application/json"),
56 MSGPACK = of("octal/msgpack"),
57 PLAIN = of("text/plain"),
58 UON = of("text/uon"),
59 URLENCODING = of("application/x-www-form-urlencoded"),
60 XML = of("text/xml"),
61 XMLSOAP = of("text/xml+soap"),
62
63 RDF = of("text/xml+rdf"),
64 RDFABBREV = of("text/xml+rdf+abbrev"),
65 NTRIPLE = of("text/n-triple"),
66 TURTLE = of("text/turtle"),
67 N3 = of("text/n3")
68 ;
69 // @formatter:on
70
71 /**
72 * Returns the media type for the specified string.
73 * The same media type strings always return the same objects so that these objects
74 * can be compared for equality using '=='.
75 *
76 * <h5 class='section'>Notes:</h5><ul>
77 * <li class='note'>
78 * Spaces are replaced with <js>'+'</js> characters.
79 * This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters
80 * get replaced with spaces by your browser. Since spaces aren't supported by the spec, this
81 * is doesn't break anything.
82 * <li class='note'>
83 * Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
84 * </ul>
85 *
86 * @param value
87 * The media type string.
88 * Will be lowercased.
89 * Returns <jk>null</jk> if input is null or empty.
90 * @return A cached media type object.
91 */
92 public static MediaType of(String value) {
93 return value == null ? null : CACHE.get(value, () -> new MediaType(value));
94 }
95
96 /**
97 * Same as {@link #of(String)} but allows you to specify the parameters.
98 *
99 *
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 }