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}