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 * The base type. 178 */ 179 private final String baseType; 180 181 182 /** 183 * The sub type. 184 */ 185 private final String subType; 186 187 188 /** 189 * The optional parameters. 190 */ 191 private final List<Parameter> params; 192 193 194 /** 195 * Creates a new content type. 196 * 197 * @param baseType The type. Must not be {@code null} or empty. 198 * @param subType The subtype. Must not be {@code null} or empty. 199 * @param param Optional parameters. 200 */ 201 public ContentType(final String baseType, final String subType, final Parameter ... param) { 202 203 if (baseType == null || baseType.trim().isEmpty()) { 204 throw new IllegalArgumentException("The base type must be specified"); 205 } 206 this.baseType = baseType; 207 208 if (subType == null || subType.trim().isEmpty()) { 209 throw new IllegalArgumentException("The subtype must be specified"); 210 } 211 this.subType = subType; 212 213 214 if (param != null && param.length > 0) { 215 params = Collections.unmodifiableList(Arrays.asList(param)); 216 } else { 217 params = Collections.emptyList(); 218 } 219 } 220 221 222 /** 223 * Creates a new content type with the specified character set. 224 * 225 * @param baseType The base type. Must not be {@code null} or empty. 226 * @param subType The subtype. Must not be {@code null} or empty. 227 * @param charset The character set to use for the {@code charset} 228 * parameter. Must not be {@code null}. 229 */ 230 public ContentType(final String baseType, final String subType, final Charset charset) { 231 232 this(baseType, subType, new Parameter("charset", charset.toString())); 233 } 234 235 236 /** 237 * Returns the base type. 238 * 239 * @return The base type. 240 */ 241 public String getBaseType() { 242 return baseType; 243 } 244 245 246 /** 247 * Returns the subtype. 248 * 249 * @return The subtype. 250 */ 251 public String getSubType() { 252 return subType; 253 } 254 255 256 /** 257 * Returns the optional parameters. 258 * 259 * @return The parameters, as unmodifiable list, empty list if none. 260 */ 261 public List<Parameter> getParameters() { 262 return params; 263 } 264 265 266 /** 267 * Returns {@code true} if the types and subtypes match. The 268 * parameters, if any, are ignored. 269 * 270 * @param other The other content type, {@code null} if not specified. 271 * 272 * @return {@code true} if the types and subtypes match, else 273 * {@code false}. 274 */ 275 public boolean matches(final ContentType other) { 276 277 return other != null 278 && getBaseType().equalsIgnoreCase(other.getBaseType()) 279 && getSubType().equalsIgnoreCase(other.getSubType()); 280 } 281 282 283 @Override 284 public String toString() { 285 StringBuilder sb = new StringBuilder(); 286 sb.append(getBaseType()); 287 sb.append("/"); 288 sb.append(getSubType()); 289 290 if (! getParameters().isEmpty()) { 291 for (Parameter p: getParameters()) { 292 sb.append("; "); 293 sb.append(p.getName()); 294 sb.append("="); 295 sb.append(p.getValue()); 296 } 297 } 298 299 return sb.toString(); 300 } 301 302 303 @Override 304 public boolean equals(Object o) { 305 if (this == o) return true; 306 if (!(o instanceof ContentType)) return false; 307 ContentType that = (ContentType) o; 308 return getBaseType().equalsIgnoreCase(that.getBaseType()) && 309 getSubType().equalsIgnoreCase(that.getSubType()) && 310 params.equals(that.params); 311 } 312 313 314 @Override 315 public int hashCode() { 316 return Objects.hash(getBaseType().toLowerCase(), getSubType().toLowerCase(), params); 317 } 318 319 320 /** 321 * Parses a content type from the specified string. 322 * 323 * @param s The string to parse. 324 * 325 * @return The content type. 326 * 327 * @throws ParseException If parsing failed or the string is 328 * {@code null} or empty. 329 */ 330 public static ContentType parse(final String s) 331 throws ParseException { 332 333 if (s == null || s.trim().isEmpty()) { 334 throw new ParseException("Null or empty content type string", 0); 335 } 336 337 StringTokenizer st = new StringTokenizer(s, "/"); 338 339 if (! st.hasMoreTokens()) { 340 throw new ParseException("Invalid content type string", 0); 341 } 342 343 String type = st.nextToken().trim(); 344 345 if (type.trim().isEmpty()) { 346 throw new ParseException("Invalid content type string", 0); 347 } 348 349 if (! st.hasMoreTokens()) { 350 throw new ParseException("Invalid content type string", 0); 351 } 352 353 String subtypeWithOptParams = st.nextToken().trim(); 354 355 st = new StringTokenizer(subtypeWithOptParams, ";"); 356 357 if (! st.hasMoreTokens()) { 358 // No params 359 return new ContentType(type, subtypeWithOptParams.trim()); 360 } 361 362 String subtype = st.nextToken().trim(); 363 364 if (! st.hasMoreTokens()) { 365 // No params 366 return new ContentType(type, subtype); 367 } 368 369 List<Parameter> params = new LinkedList<>(); 370 371 while (st.hasMoreTokens()) { 372 373 String paramToken = st.nextToken().trim(); 374 375 StringTokenizer paramTokenizer = new StringTokenizer(paramToken, "="); 376 377 if (! paramTokenizer.hasMoreTokens()) { 378 throw new ParseException("Invalid parameter", 0); 379 } 380 381 String paramName = paramTokenizer.nextToken().trim(); 382 383 if (! paramTokenizer.hasMoreTokens()) { 384 throw new ParseException("Invalid parameter", 0); 385 } 386 387 String paramValue = paramTokenizer.nextToken().trim(); 388 389 try { 390 params.add(new Parameter(paramName, paramValue)); 391 } catch (IllegalArgumentException e) { 392 throw new ParseException("Invalid parameter: " + e.getMessage(), 0); 393 } 394 } 395 396 return new ContentType(type, subtype, params.toArray(new Parameter[0])); 397 } 398}