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}