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