    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005//     http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.http;
    014
    015import java.util.Collections;
    016import java.util.List;
    017import java.util.Map;
    018import java.util.StringTokenizer;
    019import java.util.regex.Matcher;
    020import java.util.regex.Pattern;
    021
    022import org.apache.tapestry5.commons.util.CollectionFactory;
    023import org.apache.tapestry5.http.internal.TapestryHttpInternalConstants;
    024import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    025
    026/**
    027 * Represents an HTTP content type. Allows to set various elements like the MIME type, the character set, and other
    028 * parameters. This is similar to a number of other implementations of the same concept in JAF, etc. We have created
    029 * this simple implementation to avoid including the whole libraries.
    030 *
    031 * As of Tapestry 5.4, this is now an immutable data type.
    032 */
    033public final class ContentType
    034{
    035    private final String baseType;
    036
    037    private final String subType;
    038
    039    private final Map<String, String> parameters;
    040
    041    private static final Pattern PATTERN = Pattern.compile("^(.+)/([^;]+)(;(.+=[^;]+))*$");
    042
    043    /**
    044     * Creates a new content type from the argument. The format of the argument has to be basetype/subtype(;key=value)*
    045     *
    046     * @param contentType
    047     *         the content type that needs to be represented
    048     */
    049    public ContentType(String contentType)
    050    {
    051        Matcher matcher = PATTERN.matcher(contentType);
    052
    053        if (!matcher.matches())
    054        {
    055            throw new IllegalArgumentException(String.format("Not a parseable content type '%s'.", contentType));
    056        }
    057
    058        this.baseType = matcher.group(1);
    059        this.subType = matcher.group(2);
    060        this.parameters = parseKeyValues(matcher.group(4));
    061    }
    062
    063    private ContentType(String baseType, String subType, Map<String, String> parameters)
    064    {
    065        this.baseType = baseType;
    066        this.subType = subType;
    067        this.parameters = parameters;
    068    }
    069
    070
    071    private static Map<String, String> parseKeyValues(String keyValues)
    072    {
    073        if (keyValues == null)
    074        {
    075            return Collections.emptyMap();
    076        }
    077
    078        Map<String, String> parameters = CollectionFactory.newCaseInsensitiveMap();
    079
    080        StringTokenizer tk = new StringTokenizer(keyValues, ";");
    081
    082        while (tk.hasMoreTokens())
    083        {
    084            String token = tk.nextToken();
    085            int sep = token.indexOf('=');
    086
    087            parameters.put(token.substring(0, sep), token.substring(sep + 1));
    088        }
    089
    090        return parameters;
    091    }
    092
    093    /**
    094     * Returns true only if the other object is another instance of ContentType, and has the same baseType, subType and
    095     * set of parameters.
    096     */
    097    @Override
    098    public boolean equals(Object o)
    099    {
    100        if (o == null) return false;
    101
    102        if (o.getClass() != this.getClass()) return false;
    103
    104        ContentType ct = (ContentType) o;
    105
    106        return baseType.equals(ct.baseType) && subType.equals(ct.subType) && parameters.equals(ct.parameters);
    107    }
    108
    109    /**
    110     * @return the base type of the content type
    111     */
    112    public String getBaseType()
    113    {
    114        return baseType;
    115    }
    116
    117    /**
    118     * @return the sub-type of the content type
    119     */
    120    public String getSubType()
    121    {
    122        return subType;
    123    }
    124
    125    /**
    126     * @return the MIME type of the content type (the base type and the subtype, seperated with a '/').
    127     */
    128    public String getMimeType()
    129    {
    130        return baseType + "/" + subType;
    131    }
    132
    133    /**
    134     * @return the list of names of parameters in this content type, in alphabetical order.
    135     */
    136    public List<String> getParameterNames()
    137    {
    138        return InternalUtils.sortedKeys(parameters);
    139    }
    140
    141    /**
    142     * @return the character set (the "charset" parameter) or null.
    143     */
    144    public String getCharset()
    145    {
    146        return getParameter(TapestryHttpInternalConstants.CHARSET_CONTENT_TYPE_PARAMETER);
    147    }
    148
    149    /**
    150     * @param key
    151     *         the name of the content type parameter
    152     * @return the value of the content type parameter
    153     */
    154    public String getParameter(String key)
    155    {
    156        assert key != null;
    157        return parameters.get(key);
    158    }
    159
    160    private String unparse()
    161    {
    162        StringBuilder buffer = new StringBuilder(getMimeType());
    163
    164        for (String parameterName : getParameterNames())
    165        {
    166            buffer.append(';');
    167            buffer.append(parameterName);
    168            buffer.append('=');
    169            buffer.append(parameters.get(parameterName));
    170        }
    171
    172        return buffer.toString();
    173    }
    174
    175    /**
    176     * Returns a new content type with the indicated parameter.
    177     *
    178     * @since 5.4
    179     */
    180    public ContentType withParameter(String key, String value)
    181    {
    182        assert InternalUtils.isNonBlank(key);
    183        assert InternalUtils.isNonBlank(value);
    184
    185        Map<String, String> newParameters = CollectionFactory.newCaseInsensitiveMap();
    186
    187        newParameters.putAll(parameters);
    188        newParameters.put(key, value);
    189
    190        return new ContentType(baseType, subType, newParameters);
    191    }
    192
    193    public ContentType withCharset(String charset)
    194    {
    195        return withParameter(TapestryHttpInternalConstants.CHARSET_CONTENT_TYPE_PARAMETER, charset);
    196    }
    197
    198    /**
    199     * @return the string representation of this content type.
    200     */
    201    @Override
    202    public String toString()
    203    {
    204        return unparse();
    205    }
    206
    207    /**
    208     * @return true if the content type includes parameters (such as 'charset').
    209     * @since 5.4
    210     */
    211    public boolean hasParameters()
    212    {
    213        return !parameters.isEmpty();
    214    }
    215}