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}