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