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 * <p>See RFC 6838, section 4.2.8.
060 *
061 * @author vd
062 */
063public final class ContentType {
064        
065        
066        /**
067         * Optional content type parameter, for example {@code charset=UTF-8}.
068         */
069        public static final class Parameter {
070                
071                
072                /**
073                 * A {@code charset=UTF-8} parameter.
074                 */
075                public static final Parameter CHARSET_UTF_8 = new Parameter("charset", "UTF-8");
076                
077                
078                /**
079                 * The parameter name.
080                 */
081                private final String name;
082                
083                
084                /**
085                 * The parameter value.
086                 */
087                private final String value;
088                
089                
090                /**
091                 * Creates a new content type parameter.
092                 *
093                 * @param name  The name. Must not be {@code null} or empty.
094                 * @param value The value. Must not be {@code null} or empty.
095                 */
096                public Parameter(final String name, final String value) {
097                        
098                        if (name == null || name.trim().isEmpty()) {
099                                throw new IllegalArgumentException("The parameter name must be specified");
100                        }
101                        this.name = name;
102                        
103                        if (value == null || value.trim().isEmpty()) {
104                                throw new IllegalArgumentException("The parameter value must be specified");
105                        }
106                        this.value = value;
107                }
108                
109                
110                /**
111                 * Returns the parameter name.
112                 *
113                 * @return The name.
114                 */
115                public String getName() {
116                        return name;
117                }
118                
119                
120                /**
121                 * Returns the parameter value.
122                 *
123                 * @return The value.
124                 */
125                public String getValue() {
126                        return value;
127                }
128                
129                
130                @Override
131                public String toString() {
132                        return name + "=" + value;
133                }
134                
135                
136                @Override
137                public boolean equals(Object o) {
138                        if (this == o) return true;
139                        if (!(o instanceof Parameter)) return false;
140                        Parameter parameter = (Parameter) o;
141                        return getName().equalsIgnoreCase(parameter.getName()) &&
142                                getValue().equalsIgnoreCase(parameter.getValue());
143                }
144                
145                
146                @Override
147                public int hashCode() {
148                        return Objects.hash(getName().toLowerCase(), getValue().toLowerCase());
149                }
150        }
151        
152        
153        /**
154         * Content type {@code application/json; charset=UTF-8}.
155         */
156        public static final ContentType APPLICATION_JSON = new ContentType("application", "json", Parameter.CHARSET_UTF_8);
157        
158        
159        /**
160         * Content type {@code application/jose; charset=UTF-8}.
161         */
162        public static final ContentType APPLICATION_JOSE = new ContentType("application", "jose", Parameter.CHARSET_UTF_8);
163        
164        
165        /**
166         * Content type {@code application/jwt; charset=UTF-8}.
167         */
168        public static final ContentType APPLICATION_JWT = new ContentType("application", "jwt", Parameter.CHARSET_UTF_8);
169        
170        
171        /**
172         * Content type {@code application/x-www-form-urlencoded; charset=UTF-8}.
173         */
174        public static final ContentType APPLICATION_URLENCODED = new ContentType("application", "x-www-form-urlencoded", Parameter.CHARSET_UTF_8);
175        
176        
177        /**
178         * Content type {@code text/plain; charset=UTF-8}.
179         */
180        public static final ContentType TEXT_PLAIN = new ContentType("text", "plain", Parameter.CHARSET_UTF_8);
181        
182        
183        /**
184         * Content type {@code image/apng}.
185         */
186        public static final ContentType IMAGE_APNG = new ContentType("image", "apng");
187        
188        
189        /**
190         * Content type {@code image/avif}.
191         */
192        public static final ContentType IMAGE_AVIF = new ContentType("image", "avif");
193        
194        
195        /**
196         * Content type {@code image/gif}.
197         */
198        public static final ContentType IMAGE_GIF = new ContentType("image", "gif");
199        
200        
201        /**
202         * Content type {@code image/jpeg}.
203         */
204        public static final ContentType IMAGE_JPEG = new ContentType("image", "jpeg");
205        
206        
207        /**
208         * Content type {@code image/png}.
209         */
210        public static final ContentType IMAGE_PNG = new ContentType("image", "png");
211        
212        
213        /**
214         * Content type {@code image/svg+xml}.
215         */
216        public static final ContentType IMAGE_SVG_XML = new ContentType("image", "svg+xml");
217        
218        
219        /**
220         * Content type {@code image/webp}.
221         */
222        public static final ContentType IMAGE_WEBP = new ContentType("image", "webp");
223        
224        
225        /**
226         * Content type {@code application/pdf}.
227         */
228        public static final ContentType APPLICATION_PDF = new ContentType("application", "pdf");
229        
230        
231        /**
232         * The base type.
233         */
234        private final String baseType;
235        
236        
237        /**
238         * The sub type.
239         */
240        private final String subType;
241        
242        
243        /**
244         * The optional parameters.
245         */
246        private final List<Parameter> params;
247        
248        
249        /**
250         * Creates a new content type.
251         *
252         * @param baseType The type. E.g. "application" from
253         *                 "application/json".Must not be {@code null} or
254         *                 empty.
255         * @param subType  The subtype. E.g. "json" from "application/json".
256         *                 Must not be {@code null} or empty.
257         * @param param    Optional parameters.
258         */
259        public ContentType(final String baseType, final String subType, final Parameter ... param) {
260                
261                if (baseType == null || baseType.trim().isEmpty()) {
262                        throw new IllegalArgumentException("The base type must be specified");
263                }
264                this.baseType = baseType;
265                
266                if (subType == null || subType.trim().isEmpty()) {
267                        throw new IllegalArgumentException("The subtype must be specified");
268                }
269                this.subType = subType;
270                
271                
272                if (param != null && param.length > 0) {
273                        params = Collections.unmodifiableList(Arrays.asList(param));
274                } else {
275                        params = Collections.emptyList();
276                }
277        }
278        
279        
280        /**
281         * Creates a new content type with the specified character set.
282         *
283         * @param baseType The base type. E.g. "application" from
284         *                 "application/json".Must not be {@code null} or
285         *                 empty.
286         * @param subType  The subtype. E.g. "json" from "application/json".
287         *                 Must not be {@code null} or empty.
288         * @param charset  The character set to use for the {@code charset}
289         *                 parameter. Must not be {@code null}.
290         */
291        public ContentType(final String baseType, final String subType, final Charset charset) {
292                
293                this(baseType, subType, new Parameter("charset", charset.toString()));
294        }
295        
296        
297        /**
298         * Returns the base type. E.g. "application" from "application/json".
299         *
300         * @return The base type.
301         */
302        public String getBaseType() {
303                return baseType;
304        }
305        
306        
307        /**
308         * Returns the subtype. E.g. "json" from "application/json".
309         *
310         * @return The subtype.
311         */
312        public String getSubType() {
313                return subType;
314        }
315
316
317        /**
318         * Returns the base sub type. E.g. "entity-statement" from
319         * "application/entity-statement+jwt".
320         *
321         * @return The base sub type or the sub type if a suffix is not
322         *         present.
323         */
324        public String getBaseSubType() {
325
326                Map.Entry<String, String> subtypeEn = splitSubtype();
327                if (subtypeEn != null) {
328                        return subtypeEn.getKey();
329                }
330                return getSubType();
331        }
332
333
334        /**
335         * Returns the sub type suffix. E.g. "jwt" from
336         * "application/entity-statement+jwt".
337         *
338         * @return The sub type suffix, {@code null} none.
339         */
340        public String getSubTypeSuffix() {
341
342                Map.Entry<String, String> subtypeEn = splitSubtype();
343                if (subtypeEn != null) {
344                        return subtypeEn.getValue();
345                }
346                return null;
347        }
348
349
350        /**
351         * Returns {@code true} if this content type has the specified sub type
352         * suffix.
353         *
354         * @param suffix The sub type suffix, {@code null} if not specified.
355         *
356         * @return {@code true} if the sub type has the specified suffix, else
357         *         {@code false}.
358         */
359        public boolean hasSubTypeSuffix(final String suffix) {
360
361                return suffix != null && suffix.equals(getSubTypeSuffix());
362        }
363
364
365        private Map.Entry<String,String> splitSubtype() {
366
367                String[] split = getSubType().split("\\+");
368                if (split.length == 2) {
369                        return new AbstractMap.SimpleEntry<>(split[0], split[1]);
370                }
371                return null;
372        }
373        
374        
375        /**
376         * Returns the type. E.g. "application/json".
377         *
378         * @return The type, any optional parameters are omitted.
379         */
380        public String getType() {
381                
382                StringBuilder sb = new StringBuilder();
383                sb.append(getBaseType());
384                sb.append("/");
385                sb.append(getSubType());
386                return sb.toString();
387        }
388        
389        
390        /**
391         * Returns the optional parameters.
392         *
393         * @return The parameters, as unmodifiable list, empty list if none.
394         */
395        public List<Parameter> getParameters() {
396                return params;
397        }
398        
399        
400        /**
401         * Returns {@code true} if the types and subtypes match. The
402         * parameters, if any, are ignored.
403         *
404         * @param other The other content type, {@code null} if not specified.
405         *
406         * @return {@code true} if the types and subtypes match, else
407         *         {@code false}.
408         */
409        public boolean matches(final ContentType other) {
410                
411                return other != null
412                        && getBaseType().equalsIgnoreCase(other.getBaseType())
413                        && getSubType().equalsIgnoreCase(other.getSubType());
414        }
415        
416        
417        @Override
418        public String toString() {
419                
420                StringBuilder sb = new StringBuilder(getType());
421                
422                if (! getParameters().isEmpty()) {
423                        for (Parameter p: getParameters()) {
424                                sb.append("; ");
425                                sb.append(p.getName());
426                                sb.append("=");
427                                sb.append(p.getValue());
428                        }
429                }
430                
431                return sb.toString();
432        }
433        
434        
435        @Override
436        public boolean equals(Object o) {
437                if (this == o) return true;
438                if (!(o instanceof ContentType)) return false;
439                ContentType that = (ContentType) o;
440                return getBaseType().equalsIgnoreCase(that.getBaseType()) &&
441                        getSubType().equalsIgnoreCase(that.getSubType()) &&
442                        params.equals(that.params);
443        }
444        
445        
446        @Override
447        public int hashCode() {
448                return Objects.hash(getBaseType().toLowerCase(), getSubType().toLowerCase(), params);
449        }
450        
451        
452        /**
453         * Parses a content type from the specified string.
454         *
455         * @param s The string to parse.
456         *
457         * @return The content type.
458         *
459         * @throws ParseException If parsing failed or the string is
460         *                        {@code null} or empty.
461         */
462        public static ContentType parse(final String s)
463                throws ParseException {
464                
465                if (s == null || s.trim().isEmpty()) {
466                        throw new ParseException("Null or empty content type string", 0);
467                }
468                
469                StringTokenizer st = new StringTokenizer(s, "/");
470                
471                if (! st.hasMoreTokens()) {
472                        throw new ParseException("Invalid content type string", 0);
473                }
474                
475                String type = st.nextToken().trim();
476                
477                if (type.trim().isEmpty()) {
478                        throw new ParseException("Invalid content type string", 0);
479                }
480                
481                if (! st.hasMoreTokens()) {
482                        throw new ParseException("Invalid content type string", 0);
483                }
484                
485                String subtypeWithOptParams = st.nextToken().trim();
486                
487                st = new StringTokenizer(subtypeWithOptParams, ";");
488                
489                if (! st.hasMoreTokens()) {
490                        // No params
491                        return new ContentType(type, subtypeWithOptParams.trim());
492                }
493                
494                String subtype = st.nextToken().trim();
495                
496                if (! st.hasMoreTokens()) {
497                        // No params
498                        return new ContentType(type, subtype);
499                }
500                
501                List<Parameter> params = new LinkedList<>();
502                
503                while (st.hasMoreTokens()) {
504                        
505                        String paramToken = st.nextToken().trim();
506                        
507                        StringTokenizer paramTokenizer = new StringTokenizer(paramToken, "=");
508                        
509                        if (! paramTokenizer.hasMoreTokens()) {
510                                throw new ParseException("Invalid parameter", 0);
511                        }
512                        
513                        String paramName = paramTokenizer.nextToken().trim();
514                        
515                        if (! paramTokenizer.hasMoreTokens()) {
516                                throw new ParseException("Invalid parameter", 0);
517                        }
518                        
519                        String paramValue = paramTokenizer.nextToken().trim();
520                        
521                        try {
522                                params.add(new Parameter(paramName, paramValue));
523                        } catch (IllegalArgumentException e) {
524                                throw new ParseException("Invalid parameter: " + e.getMessage(), 0);
525                        }
526                }
527                
528                return new ContentType(type, subtype, params.toArray(new Parameter[0]));
529        }
530}