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         * Content type {@code text/plain; charset=UTF-8}.
178         */
179        public static final ContentType TEXT_PLAIN = new ContentType("text", "plain", Parameter.CHARSET_UTF_8);
180        
181        
182        /**
183         * The base type.
184         */
185        private final String baseType;
186        
187        
188        /**
189         * The sub type.
190         */
191        private final String subType;
192        
193        
194        /**
195         * The optional parameters.
196         */
197        private final List<Parameter> params;
198        
199        
200        /**
201         * Creates a new content type.
202         *
203         * @param baseType The type. E.g. "application" from
204         *                 "application/json".Must not be {@code null} or
205         *                 empty.
206         * @param subType  The subtype. E.g. "json" from "application/json".
207         *                 Must not be {@code null} or empty.
208         * @param param    Optional parameters.
209         */
210        public ContentType(final String baseType, final String subType, final Parameter ... param) {
211                
212                if (baseType == null || baseType.trim().isEmpty()) {
213                        throw new IllegalArgumentException("The base type must be specified");
214                }
215                this.baseType = baseType;
216                
217                if (subType == null || subType.trim().isEmpty()) {
218                        throw new IllegalArgumentException("The subtype must be specified");
219                }
220                this.subType = subType;
221                
222                
223                if (param != null && param.length > 0) {
224                        params = Collections.unmodifiableList(Arrays.asList(param));
225                } else {
226                        params = Collections.emptyList();
227                }
228        }
229        
230        
231        /**
232         * Creates a new content type with the specified character set.
233         *
234         * @param baseType The base type. E.g. "application" from
235         *                 "application/json".Must not be {@code null} or
236         *                 empty.
237         * @param subType  The subtype. E.g. "json" from "application/json".
238         *                 Must not be {@code null} or empty.
239         * @param charset  The character set to use for the {@code charset}
240         *                 parameter. Must not be {@code null}.
241         */
242        public ContentType(final String baseType, final String subType, final Charset charset) {
243                
244                this(baseType, subType, new Parameter("charset", charset.toString()));
245        }
246        
247        
248        /**
249         * Returns the base type. E.g. "application" from "application/json".
250         *
251         * @return The base type.
252         */
253        public String getBaseType() {
254                return baseType;
255        }
256        
257        
258        /**
259         * Returns the subtype. E.g. "json" from "application/json".
260         *
261         * @return The subtype.
262         */
263        public String getSubType() {
264                return subType;
265        }
266        
267        
268        /**
269         * Returns the type. E.g. "application/json".
270         *
271         * @return The type, any optional parameters are omitted.
272         */
273        public String getType() {
274                
275                StringBuilder sb = new StringBuilder();
276                sb.append(getBaseType());
277                sb.append("/");
278                sb.append(getSubType());
279                return sb.toString();
280        }
281        
282        
283        /**
284         * Returns the optional parameters.
285         *
286         * @return The parameters, as unmodifiable list, empty list if none.
287         */
288        public List<Parameter> getParameters() {
289                return params;
290        }
291        
292        
293        /**
294         * Returns {@code true} if the types and subtypes match. The
295         * parameters, if any, are ignored.
296         *
297         * @param other The other content type, {@code null} if not specified.
298         *
299         * @return {@code true} if the types and subtypes match, else
300         *         {@code false}.
301         */
302        public boolean matches(final ContentType other) {
303                
304                return other != null
305                        && getBaseType().equalsIgnoreCase(other.getBaseType())
306                        && getSubType().equalsIgnoreCase(other.getSubType());
307        }
308        
309        
310        @Override
311        public String toString() {
312                
313                StringBuilder sb = new StringBuilder(getType());
314                
315                if (! getParameters().isEmpty()) {
316                        for (Parameter p: getParameters()) {
317                                sb.append("; ");
318                                sb.append(p.getName());
319                                sb.append("=");
320                                sb.append(p.getValue());
321                        }
322                }
323                
324                return sb.toString();
325        }
326        
327        
328        @Override
329        public boolean equals(Object o) {
330                if (this == o) return true;
331                if (!(o instanceof ContentType)) return false;
332                ContentType that = (ContentType) o;
333                return getBaseType().equalsIgnoreCase(that.getBaseType()) &&
334                        getSubType().equalsIgnoreCase(that.getSubType()) &&
335                        params.equals(that.params);
336        }
337        
338        
339        @Override
340        public int hashCode() {
341                return Objects.hash(getBaseType().toLowerCase(), getSubType().toLowerCase(), params);
342        }
343        
344        
345        /**
346         * Parses a content type from the specified string.
347         *
348         * @param s The string to parse.
349         *
350         * @return The content type.
351         *
352         * @throws ParseException If parsing failed or the string is
353         *                        {@code null} or empty.
354         */
355        public static ContentType parse(final String s)
356                throws ParseException {
357                
358                if (s == null || s.trim().isEmpty()) {
359                        throw new ParseException("Null or empty content type string", 0);
360                }
361                
362                StringTokenizer st = new StringTokenizer(s, "/");
363                
364                if (! st.hasMoreTokens()) {
365                        throw new ParseException("Invalid content type string", 0);
366                }
367                
368                String type = st.nextToken().trim();
369                
370                if (type.trim().isEmpty()) {
371                        throw new ParseException("Invalid content type string", 0);
372                }
373                
374                if (! st.hasMoreTokens()) {
375                        throw new ParseException("Invalid content type string", 0);
376                }
377                
378                String subtypeWithOptParams = st.nextToken().trim();
379                
380                st = new StringTokenizer(subtypeWithOptParams, ";");
381                
382                if (! st.hasMoreTokens()) {
383                        // No params
384                        return new ContentType(type, subtypeWithOptParams.trim());
385                }
386                
387                String subtype = st.nextToken().trim();
388                
389                if (! st.hasMoreTokens()) {
390                        // No params
391                        return new ContentType(type, subtype);
392                }
393                
394                List<Parameter> params = new LinkedList<>();
395                
396                while (st.hasMoreTokens()) {
397                        
398                        String paramToken = st.nextToken().trim();
399                        
400                        StringTokenizer paramTokenizer = new StringTokenizer(paramToken, "=");
401                        
402                        if (! paramTokenizer.hasMoreTokens()) {
403                                throw new ParseException("Invalid parameter", 0);
404                        }
405                        
406                        String paramName = paramTokenizer.nextToken().trim();
407                        
408                        if (! paramTokenizer.hasMoreTokens()) {
409                                throw new ParseException("Invalid parameter", 0);
410                        }
411                        
412                        String paramValue = paramTokenizer.nextToken().trim();
413                        
414                        try {
415                                params.add(new Parameter(paramName, paramValue));
416                        } catch (IllegalArgumentException e) {
417                                throw new ParseException("Invalid parameter: " + e.getMessage(), 0);
418                        }
419                }
420                
421                return new ContentType(type, subtype, params.toArray(new Parameter[0]));
422        }
423}