001/* 002 * ==================================================================== 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * ==================================================================== 020 * 021 * This software consists of voluntary contributions made by many 022 * individuals on behalf of the Apache Software Foundation. For more 023 * information on the Apache Software Foundation, please see 024 * <http://www.apache.org/>. 025 * 026 */ 027 028package org.apache.http.entity; 029 030import java.io.Serializable; 031import java.nio.charset.Charset; 032import java.nio.charset.UnsupportedCharsetException; 033import java.util.ArrayList; 034import java.util.Collections; 035import java.util.HashMap; 036import java.util.LinkedHashMap; 037import java.util.List; 038import java.util.Locale; 039import java.util.Map; 040 041import org.apache.http.Consts; 042import org.apache.http.Header; 043import org.apache.http.HeaderElement; 044import org.apache.http.HttpEntity; 045import org.apache.http.NameValuePair; 046import org.apache.http.ParseException; 047import org.apache.http.annotation.Contract; 048import org.apache.http.annotation.ThreadingBehavior; 049import org.apache.http.message.BasicHeaderValueFormatter; 050import org.apache.http.message.BasicHeaderValueParser; 051import org.apache.http.message.BasicNameValuePair; 052import org.apache.http.message.ParserCursor; 053import org.apache.http.util.Args; 054import org.apache.http.util.CharArrayBuffer; 055import org.apache.http.util.TextUtils; 056 057/** 058 * Content type information consisting of a MIME type and an optional charset. 059 * <p> 060 * This class makes no attempts to verify validity of the MIME type. 061 * The input parameters of the {@link #create(String, String)} method, however, may not 062 * contain characters {@code <">, <;>, <,>} reserved by the HTTP specification. 063 * 064 * @since 4.2 065 */ 066@Contract(threading = ThreadingBehavior.IMMUTABLE) 067public final class ContentType implements Serializable { 068 069 private static final long serialVersionUID = -7768694718232371896L; 070 071 // constants 072 public static final ContentType APPLICATION_ATOM_XML = create( 073 "application/atom+xml", Consts.ISO_8859_1); 074 public static final ContentType APPLICATION_FORM_URLENCODED = create( 075 "application/x-www-form-urlencoded", Consts.ISO_8859_1); 076 public static final ContentType APPLICATION_JSON = create( 077 "application/json", Consts.UTF_8); 078 public static final ContentType APPLICATION_OCTET_STREAM = create( 079 "application/octet-stream", (Charset) null); 080 public static final ContentType APPLICATION_SVG_XML = create( 081 "application/svg+xml", Consts.ISO_8859_1); 082 public static final ContentType APPLICATION_XHTML_XML = create( 083 "application/xhtml+xml", Consts.ISO_8859_1); 084 public static final ContentType APPLICATION_XML = create( 085 "application/xml", Consts.ISO_8859_1); 086 public static final ContentType MULTIPART_FORM_DATA = create( 087 "multipart/form-data", Consts.ISO_8859_1); 088 public static final ContentType TEXT_HTML = create( 089 "text/html", Consts.ISO_8859_1); 090 public static final ContentType TEXT_PLAIN = create( 091 "text/plain", Consts.ISO_8859_1); 092 public static final ContentType TEXT_XML = create( 093 "text/xml", Consts.ISO_8859_1); 094 public static final ContentType WILDCARD = create( 095 "*/*", (Charset) null); 096 097 098 private static final Map<String, ContentType> CONTENT_TYPE_MAP; 099 static { 100 101 final ContentType[] contentTypes = { 102 APPLICATION_ATOM_XML, 103 APPLICATION_FORM_URLENCODED, 104 APPLICATION_JSON, 105 APPLICATION_SVG_XML, 106 APPLICATION_XHTML_XML, 107 APPLICATION_XML, 108 MULTIPART_FORM_DATA, 109 TEXT_HTML, 110 TEXT_PLAIN, 111 TEXT_XML }; 112 final HashMap<String, ContentType> map = new HashMap<String, ContentType>(); 113 for (final ContentType contentType: contentTypes) { 114 map.put(contentType.getMimeType(), contentType); 115 } 116 CONTENT_TYPE_MAP = Collections.unmodifiableMap(map); 117 } 118 119 // defaults 120 public static final ContentType DEFAULT_TEXT = TEXT_PLAIN; 121 public static final ContentType DEFAULT_BINARY = APPLICATION_OCTET_STREAM; 122 123 private final String mimeType; 124 private final Charset charset; 125 private final NameValuePair[] params; 126 127 ContentType( 128 final String mimeType, 129 final Charset charset) { 130 this.mimeType = mimeType; 131 this.charset = charset; 132 this.params = null; 133 } 134 135 ContentType( 136 final String mimeType, 137 final Charset charset, 138 final NameValuePair[] params) { 139 this.mimeType = mimeType; 140 this.charset = charset; 141 this.params = params; 142 } 143 144 public String getMimeType() { 145 return this.mimeType; 146 } 147 148 public Charset getCharset() { 149 return this.charset; 150 } 151 152 /** 153 * @since 4.3 154 */ 155 public String getParameter(final String name) { 156 Args.notEmpty(name, "Parameter name"); 157 if (this.params == null) { 158 return null; 159 } 160 for (final NameValuePair param: this.params) { 161 if (param.getName().equalsIgnoreCase(name)) { 162 return param.getValue(); 163 } 164 } 165 return null; 166 } 167 168 /** 169 * Generates textual representation of this content type which can be used as the value 170 * of a {@code Content-Type} header. 171 */ 172 @Override 173 public String toString() { 174 final CharArrayBuffer buf = new CharArrayBuffer(64); 175 buf.append(this.mimeType); 176 if (this.params != null) { 177 buf.append("; "); 178 BasicHeaderValueFormatter.INSTANCE.formatParameters(buf, this.params, false); 179 } else if (this.charset != null) { 180 buf.append("; charset="); 181 buf.append(this.charset.name()); 182 } 183 return buf.toString(); 184 } 185 186 private static boolean valid(final String s) { 187 for (int i = 0; i < s.length(); i++) { 188 final char ch = s.charAt(i); 189 if (ch == '"' || ch == ',' || ch == ';') { 190 return false; 191 } 192 } 193 return true; 194 } 195 196 /** 197 * Creates a new instance of {@link ContentType}. 198 * 199 * @param mimeType MIME type. It may not be {@code null} or empty. It may not contain 200 * characters {@code <">, <;>, <,>} reserved by the HTTP specification. 201 * @param charset charset. 202 * @return content type 203 */ 204 public static ContentType create(final String mimeType, final Charset charset) { 205 final String normalizedMimeType = Args.notBlank(mimeType, "MIME type").toLowerCase(Locale.ROOT); 206 Args.check(valid(normalizedMimeType), "MIME type may not contain reserved characters"); 207 return new ContentType(normalizedMimeType, charset); 208 } 209 210 /** 211 * Creates a new instance of {@link ContentType} without a charset. 212 * 213 * @param mimeType MIME type. It may not be {@code null} or empty. It may not contain 214 * characters {@code <">, <;>, <,>} reserved by the HTTP specification. 215 * @return content type 216 */ 217 public static ContentType create(final String mimeType) { 218 return create(mimeType, (Charset) null); 219 } 220 221 /** 222 * Creates a new instance of {@link ContentType}. 223 * 224 * @param mimeType MIME type. It may not be {@code null} or empty. It may not contain 225 * characters {@code <">, <;>, <,>} reserved by the HTTP specification. 226 * @param charset charset. It may not contain characters {@code <">, <;>, <,>} reserved by the HTTP 227 * specification. This parameter is optional. 228 * @return content type 229 * @throws UnsupportedCharsetException Thrown when the named charset is not available in 230 * this instance of the Java virtual machine 231 */ 232 public static ContentType create( 233 final String mimeType, final String charset) throws UnsupportedCharsetException { 234 return create(mimeType, !TextUtils.isBlank(charset) ? Charset.forName(charset) : null); 235 } 236 237 private static ContentType create(final HeaderElement helem, final boolean strict) { 238 return create(helem.getName(), helem.getParameters(), strict); 239 } 240 241 private static ContentType create(final String mimeType, final NameValuePair[] params, final boolean strict) { 242 Charset charset = null; 243 for (final NameValuePair param: params) { 244 if (param.getName().equalsIgnoreCase("charset")) { 245 final String s = param.getValue(); 246 if (!TextUtils.isBlank(s)) { 247 try { 248 charset = Charset.forName(s); 249 } catch (final UnsupportedCharsetException ex) { 250 if (strict) { 251 throw ex; 252 } 253 } 254 } 255 break; 256 } 257 } 258 return new ContentType(mimeType, charset, params != null && params.length > 0 ? params : null); 259 } 260 261 /** 262 * Creates a new instance of {@link ContentType} with the given parameters. 263 * 264 * @param mimeType MIME type. It may not be {@code null} or empty. It may not contain 265 * characters {@code <">, <;>, <,>} reserved by the HTTP specification. 266 * @param params parameters. 267 * @return content type 268 * 269 * @since 4.4 270 */ 271 public static ContentType create( 272 final String mimeType, final NameValuePair... params) throws UnsupportedCharsetException { 273 final String type = Args.notBlank(mimeType, "MIME type").toLowerCase(Locale.ROOT); 274 Args.check(valid(type), "MIME type may not contain reserved characters"); 275 return create(mimeType, params, true); 276 } 277 278 /** 279 * Parses textual representation of {@code Content-Type} value. 280 * 281 * @param s text 282 * @return content type 283 * @throws ParseException if the given text does not represent a valid 284 * {@code Content-Type} value. 285 * @throws UnsupportedCharsetException Thrown when the named charset is not available in 286 * this instance of the Java virtual machine 287 */ 288 public static ContentType parse( 289 final String s) throws ParseException, UnsupportedCharsetException { 290 Args.notNull(s, "Content type"); 291 final CharArrayBuffer buf = new CharArrayBuffer(s.length()); 292 buf.append(s); 293 final ParserCursor cursor = new ParserCursor(0, s.length()); 294 final HeaderElement[] elements = BasicHeaderValueParser.INSTANCE.parseElements(buf, cursor); 295 if (elements.length > 0) { 296 return create(elements[0], true); 297 } else { 298 throw new ParseException("Invalid content type: " + s); 299 } 300 } 301 302 /** 303 * Extracts {@code Content-Type} value from {@link HttpEntity} exactly as 304 * specified by the {@code Content-Type} header of the entity. Returns {@code null} 305 * if not specified. 306 * 307 * @param entity HTTP entity 308 * @return content type 309 * @throws ParseException if the given text does not represent a valid 310 * {@code Content-Type} value. 311 * @throws UnsupportedCharsetException Thrown when the named charset is not available in 312 * this instance of the Java virtual machine 313 */ 314 public static ContentType get( 315 final HttpEntity entity) throws ParseException, UnsupportedCharsetException { 316 if (entity == null) { 317 return null; 318 } 319 final Header header = entity.getContentType(); 320 if (header != null) { 321 final HeaderElement[] elements = header.getElements(); 322 if (elements.length > 0) { 323 return create(elements[0], true); 324 } 325 } 326 return null; 327 } 328 329 /** 330 * Extracts {@code Content-Type} value from {@link HttpEntity}. Returns {@code null} 331 * if not specified or incorrect (could not be parsed).. 332 * 333 * @param entity HTTP entity 334 * @return content type 335 * 336 * @since 4.4 337 * 338 */ 339 public static ContentType getLenient(final HttpEntity entity) { 340 if (entity == null) { 341 return null; 342 } 343 final Header header = entity.getContentType(); 344 if (header != null) { 345 try { 346 final HeaderElement[] elements = header.getElements(); 347 if (elements.length > 0) { 348 return create(elements[0], false); 349 } 350 } catch (final ParseException ex) { 351 return null; 352 } 353 } 354 return null; 355 } 356 357 /** 358 * Extracts {@code Content-Type} value from {@link HttpEntity} or returns the default value 359 * {@link #DEFAULT_TEXT} if not explicitly specified. 360 * 361 * @param entity HTTP entity 362 * @return content type 363 * @throws ParseException if the given text does not represent a valid 364 * {@code Content-Type} value. 365 * @throws UnsupportedCharsetException Thrown when the named charset is not available in 366 * this instance of the Java virtual machine 367 */ 368 public static ContentType getOrDefault( 369 final HttpEntity entity) throws ParseException, UnsupportedCharsetException { 370 final ContentType contentType = get(entity); 371 return contentType != null ? contentType : DEFAULT_TEXT; 372 } 373 374 /** 375 * Extracts {@code Content-Type} value from {@link HttpEntity} or returns the default value 376 * {@link #DEFAULT_TEXT} if not explicitly specified or incorrect (could not be parsed). 377 * 378 * @param entity HTTP entity 379 * @return content type 380 * 381 * @since 4.4 382 */ 383 public static ContentType getLenientOrDefault( 384 final HttpEntity entity) throws ParseException, UnsupportedCharsetException { 385 final ContentType contentType = get(entity); 386 return contentType != null ? contentType : DEFAULT_TEXT; 387 } 388 389 390 /** 391 * Returns {@code Content-Type} for the given MIME type. 392 * 393 * @param mimeType MIME type 394 * @return content type or {@code null} if not known. 395 * 396 * @since 4.5 397 */ 398 public static ContentType getByMimeType(final String mimeType) { 399 if (mimeType == null) { 400 return null; 401 } 402 return CONTENT_TYPE_MAP.get(mimeType); 403 } 404 405 /** 406 * Creates a new instance with this MIME type and the given Charset. 407 * 408 * @param charset charset 409 * @return a new instance with this MIME type and the given Charset. 410 * @since 4.3 411 */ 412 public ContentType withCharset(final Charset charset) { 413 return create(this.getMimeType(), charset); 414 } 415 416 /** 417 * Creates a new instance with this MIME type and the given Charset name. 418 * 419 * @param charset name 420 * @return a new instance with this MIME type and the given Charset name. 421 * @throws UnsupportedCharsetException Thrown when the named charset is not available in 422 * this instance of the Java virtual machine 423 * @since 4.3 424 */ 425 public ContentType withCharset(final String charset) { 426 return create(this.getMimeType(), charset); 427 } 428 429 /** 430 * Creates a new instance with this MIME type and the given parameters. 431 * 432 * @param params 433 * @return a new instance with this MIME type and the given parameters. 434 * @since 4.4 435 */ 436 public ContentType withParameters( 437 final NameValuePair... params) throws UnsupportedCharsetException { 438 if (params.length == 0) { 439 return this; 440 } 441 final Map<String, String> paramMap = new LinkedHashMap<String, String>(); 442 if (this.params != null) { 443 for (final NameValuePair param: this.params) { 444 paramMap.put(param.getName(), param.getValue()); 445 } 446 } 447 for (final NameValuePair param: params) { 448 paramMap.put(param.getName(), param.getValue()); 449 } 450 final List<NameValuePair> newParams = new ArrayList<NameValuePair>(paramMap.size() + 1); 451 if (this.charset != null && !paramMap.containsKey("charset")) { 452 newParams.add(new BasicNameValuePair("charset", this.charset.name())); 453 } 454 for (final Map.Entry<String, String> entry: paramMap.entrySet()) { 455 newParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); 456 } 457 return create(this.getMimeType(), newParams.toArray(new NameValuePair[newParams.size()]), true); 458 } 459 460}