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}