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}