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}