001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2020, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.common.contenttype;
019
020
021import java.nio.charset.Charset;
022import java.text.ParseException;
023import java.util.*;
024
025
026/**
027 * Content (media) type.
028 *
029 * <p>To create a new content type {@code application/json} without character
030 * set parameter:
031 *
032 * <pre>
033 * ContentType ct = new ContentType("application", "json");
034 *
035 * // Prints out "application/json"
036 * System.out.println(ct.toString());
037 * </pre>
038 *
039 * <p>With a character set parameter {@code application/json; charset=UTF-8}:
040 *
041 * <pre>
042 * ContentType ct = new ContentType("application", "json", new ContentType.Parameter("charset", "UTF-8"));
043 *
044 * // Prints out "application/json; charset=UTF-8"
045 * System.out.println(ct.toString());
046 * </pre>
047 *
048 * <p>To parse a content type:
049 *
050 * <pre>
051 * try {
052 *         ContentType.parse("application/json; charset=UTF-8");
053 * } catch (java.text.ParseException e) {
054 *         System.err.println(e.getMessage());
055 * }
056 * </pre>
057 *
058 * <p>See RFC 2045, section 5.1.
059 *
060 * @author vd
061 */
062public final class ContentType {
063        
064        
065        /**
066         * Optional content type parameter, for example {@code charset=UTF-8}.
067         */
068        public static final class Parameter {
069                
070                
071                /**
072                 * A {@code charset=UTF-8} parameter.
073                 */
074                public static final Parameter CHARSET_UTF_8 = new Parameter("charset", "UTF-8");
075                
076                
077                /**
078                 * The parameter name.
079                 */
080                private final String name;
081                
082                
083                /**
084                 * The parameter value.
085                 */
086                private final String value;
087                
088                
089                /**
090                 * Creates a new content type parameter.
091                 *
092                 * @param name  The name. Must not be {@code null} or empty.
093                 * @param value The value. Must not be {@code null} or empty.
094                 */
095                public Parameter(final String name, final String value) {
096                        
097                        if (name == null || name.trim().isEmpty()) {
098                                throw new IllegalArgumentException("The parameter name must be specified");
099                        }
100                        this.name = name;
101                        
102                        if (value == null || value.trim().isEmpty()) {
103                                throw new IllegalArgumentException("The parameter value must be specified");
104                        }
105                        this.value = value;
106                }
107                
108                
109                /**
110                 * Returns the parameter name.
111                 *
112                 * @return The name.
113                 */
114                public String getName() {
115                        return name;
116                }
117                
118                
119                /**
120                 * Returns the parameter value.
121                 *
122                 * @return The value.
123                 */
124                public String getValue() {
125                        return value;
126                }
127                
128                
129                @Override
130                public String toString() {
131                        return name + "=" + value;
132                }
133                
134                
135                @Override
136                public boolean equals(Object o) {
137                        if (this == o) return true;
138                        if (!(o instanceof Parameter)) return false;
139                        Parameter parameter = (Parameter) o;
140                        return getName().equalsIgnoreCase(parameter.getName()) &&
141                                getValue().equalsIgnoreCase(parameter.getValue());
142                }
143                
144                
145                @Override
146                public int hashCode() {
147                        return Objects.hash(getName().toLowerCase(), getValue().toLowerCase());
148                }
149        }
150        
151        
152        /**
153         * Content type {@code application/json; charset=UTF-8}.
154         */
155        public static final ContentType APPLICATION_JSON = new ContentType("application", "json", Parameter.CHARSET_UTF_8);
156        
157        
158        /**
159         * Content type {@code application/jose; charset=UTF-8}.
160         */
161        public static final ContentType APPLICATION_JOSE = new ContentType("application", "jose", Parameter.CHARSET_UTF_8);
162        
163        
164        /**
165         * Content type {@code application/jwt; charset=UTF-8}.
166         */
167        public static final ContentType APPLICATION_JWT = new ContentType("application", "jwt", Parameter.CHARSET_UTF_8);
168        
169        
170        /**
171         * Content type {@code application/x-www-form-urlencoded; charset=UTF-8}.
172         */
173        public static final ContentType APPLICATION_URLENCODED = new ContentType("application", "x-www-form-urlencoded", Parameter.CHARSET_UTF_8);
174        
175        
176        /**
177         * The base type.
178         */
179        private final String baseType;
180        
181        
182        /**
183         * The sub type.
184         */
185        private final String subType;
186        
187        
188        /**
189         * The optional parameters.
190         */
191        private final List<Parameter> params;
192        
193        
194        /**
195         * Creates a new content type.
196         *
197         * @param baseType The type. Must not be {@code null} or empty.
198         * @param subType  The subtype. Must not be {@code null} or empty.
199         * @param param    Optional parameters.
200         */
201        public ContentType(final String baseType, final String subType, final Parameter ... param) {
202                
203                if (baseType == null || baseType.trim().isEmpty()) {
204                        throw new IllegalArgumentException("The base type must be specified");
205                }
206                this.baseType = baseType;
207                
208                if (subType == null || subType.trim().isEmpty()) {
209                        throw new IllegalArgumentException("The subtype must be specified");
210                }
211                this.subType = subType;
212                
213                
214                if (param != null && param.length > 0) {
215                        params = Collections.unmodifiableList(Arrays.asList(param));
216                } else {
217                        params = Collections.emptyList();
218                }
219        }
220        
221        
222        /**
223         * Creates a new content type with the specified character set.
224         *
225         * @param baseType The base type. Must not be {@code null} or empty.
226         * @param subType  The subtype. Must not be {@code null} or empty.
227         * @param charset  The character set to use for the {@code charset}
228         *                 parameter. Must not be {@code null}.
229         */
230        public ContentType(final String baseType, final String subType, final Charset charset) {
231                
232                this(baseType, subType, new Parameter("charset", charset.toString()));
233        }
234        
235        
236        /**
237         * Returns the base type.
238         *
239         * @return The base type.
240         */
241        public String getBaseType() {
242                return baseType;
243        }
244        
245        
246        /**
247         * Returns the subtype.
248         *
249         * @return The subtype.
250         */
251        public String getSubType() {
252                return subType;
253        }
254        
255        
256        /**
257         * Returns the optional parameters.
258         *
259         * @return The parameters, as unmodifiable list, empty list if none.
260         */
261        public List<Parameter> getParameters() {
262                return params;
263        }
264        
265        
266        /**
267         * Returns {@code true} if the types and subtypes match. The
268         * parameters, if any, are ignored.
269         *
270         * @param other The other content type, {@code null} if not specified.
271         *
272         * @return {@code true} if the types and subtypes match, else
273         *         {@code false}.
274         */
275        public boolean matches(final ContentType other) {
276                
277                return other != null
278                        && getBaseType().equalsIgnoreCase(other.getBaseType())
279                        && getSubType().equalsIgnoreCase(other.getSubType());
280        }
281        
282        
283        @Override
284        public String toString() {
285                StringBuilder sb = new StringBuilder();
286                sb.append(getBaseType());
287                sb.append("/");
288                sb.append(getSubType());
289                
290                if (! getParameters().isEmpty()) {
291                        for (Parameter p: getParameters()) {
292                                sb.append("; ");
293                                sb.append(p.getName());
294                                sb.append("=");
295                                sb.append(p.getValue());
296                        }
297                }
298                
299                return sb.toString();
300        }
301        
302        
303        @Override
304        public boolean equals(Object o) {
305                if (this == o) return true;
306                if (!(o instanceof ContentType)) return false;
307                ContentType that = (ContentType) o;
308                return getBaseType().equalsIgnoreCase(that.getBaseType()) &&
309                        getSubType().equalsIgnoreCase(that.getSubType()) &&
310                        params.equals(that.params);
311        }
312        
313        
314        @Override
315        public int hashCode() {
316                return Objects.hash(getBaseType().toLowerCase(), getSubType().toLowerCase(), params);
317        }
318        
319        
320        /**
321         * Parses a content type from the specified string.
322         *
323         * @param s The string to parse.
324         *
325         * @return The content type.
326         *
327         * @throws ParseException If parsing failed or the string is
328         *                        {@code null} or empty.
329         */
330        public static ContentType parse(final String s)
331                throws ParseException {
332                
333                if (s == null || s.trim().isEmpty()) {
334                        throw new ParseException("Null or empty content type string", 0);
335                }
336                
337                StringTokenizer st = new StringTokenizer(s, "/");
338                
339                if (! st.hasMoreTokens()) {
340                        throw new ParseException("Invalid content type string", 0);
341                }
342                
343                String type = st.nextToken().trim();
344                
345                if (type.trim().isEmpty()) {
346                        throw new ParseException("Invalid content type string", 0);
347                }
348                
349                if (! st.hasMoreTokens()) {
350                        throw new ParseException("Invalid content type string", 0);
351                }
352                
353                String subtypeWithOptParams = st.nextToken().trim();
354                
355                st = new StringTokenizer(subtypeWithOptParams, ";");
356                
357                if (! st.hasMoreTokens()) {
358                        // No params
359                        return new ContentType(type, subtypeWithOptParams.trim());
360                }
361                
362                String subtype = st.nextToken().trim();
363                
364                if (! st.hasMoreTokens()) {
365                        // No params
366                        return new ContentType(type, subtype);
367                }
368                
369                List<Parameter> params = new LinkedList<>();
370                
371                while (st.hasMoreTokens()) {
372                        
373                        String paramToken = st.nextToken().trim();
374                        
375                        StringTokenizer paramTokenizer = new StringTokenizer(paramToken, "=");
376                        
377                        if (! paramTokenizer.hasMoreTokens()) {
378                                throw new ParseException("Invalid parameter", 0);
379                        }
380                        
381                        String paramName = paramTokenizer.nextToken().trim();
382                        
383                        if (! paramTokenizer.hasMoreTokens()) {
384                                throw new ParseException("Invalid parameter", 0);
385                        }
386                        
387                        String paramValue = paramTokenizer.nextToken().trim();
388                        
389                        try {
390                                params.add(new Parameter(paramName, paramValue));
391                        } catch (IllegalArgumentException e) {
392                                throw new ParseException("Invalid parameter: " + e.getMessage(), 0);
393                        }
394                }
395                
396                return new ContentType(type, subtype, params.toArray(new Parameter[0]));
397        }
398}