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