001/* 002 * Copyright 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet.core.impl; 018 019import com.soklet.annotation.FormParameter; 020import com.soklet.annotation.Multipart; 021import com.soklet.annotation.PathParameter; 022import com.soklet.annotation.QueryParameter; 023import com.soklet.annotation.RequestBody; 024import com.soklet.annotation.RequestCookie; 025import com.soklet.annotation.RequestHeader; 026import com.soklet.converter.ValueConversionException; 027import com.soklet.converter.ValueConverter; 028import com.soklet.converter.ValueConverterRegistry; 029import com.soklet.core.InstanceProvider; 030import com.soklet.core.MultipartField; 031import com.soklet.core.Request; 032import com.soklet.core.RequestBodyMarshaler; 033import com.soklet.core.ResourceMethod; 034import com.soklet.core.ResourceMethodParameterProvider; 035import com.soklet.core.ResourcePath; 036import com.soklet.exception.BadRequestException; 037import com.soklet.exception.IllegalFormParameterException; 038import com.soklet.exception.IllegalMultipartFieldException; 039import com.soklet.exception.IllegalPathParameterException; 040import com.soklet.exception.IllegalQueryParameterException; 041import com.soklet.exception.IllegalRequestBodyException; 042import com.soklet.exception.IllegalRequestCookieException; 043import com.soklet.exception.IllegalRequestHeaderException; 044import com.soklet.exception.MissingFormParameterException; 045import com.soklet.exception.MissingMultipartFieldException; 046import com.soklet.exception.MissingQueryParameterException; 047import com.soklet.exception.MissingRequestBodyException; 048import com.soklet.exception.MissingRequestCookieException; 049import com.soklet.exception.MissingRequestHeaderException; 050 051import javax.annotation.Nonnull; 052import javax.annotation.Nullable; 053import javax.annotation.concurrent.NotThreadSafe; 054import javax.annotation.concurrent.ThreadSafe; 055import java.lang.annotation.Annotation; 056import java.lang.reflect.Parameter; 057import java.lang.reflect.ParameterizedType; 058import java.lang.reflect.Type; 059import java.util.ArrayList; 060import java.util.List; 061import java.util.Map; 062import java.util.Optional; 063import java.util.Set; 064 065import static com.soklet.core.Utilities.trimAggressively; 066import static com.soklet.core.Utilities.trimAggressivelyToNull; 067import static java.lang.String.format; 068import static java.util.Objects.requireNonNull; 069 070/** 071 * @author <a href="https://www.revetkn.com">Mark Allen</a> 072 */ 073@ThreadSafe 074public class DefaultResourceMethodParameterProvider implements ResourceMethodParameterProvider { 075 @Nonnull 076 private static final Map<Type, Object> DEFAULT_VALUES_BY_PRIMITIVE_TYPE; 077 078 @Nonnull 079 private final InstanceProvider instanceProvider; 080 @Nonnull 081 private final ValueConverterRegistry valueConverterRegistry; 082 @Nonnull 083 private final RequestBodyMarshaler requestBodyMarshaler; 084 085 static { 086 // See https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html 087 DEFAULT_VALUES_BY_PRIMITIVE_TYPE = Map.of( 088 byte.class, (byte) 0, 089 short.class, (short) 0, 090 int.class, 0, 091 long.class, (long) 0, 092 float.class, (float) 0, 093 double.class, (double) 0, 094 char.class, '\u0000', 095 boolean.class, false 096 ); 097 } 098 099 public DefaultResourceMethodParameterProvider() { 100 this(DefaultInstanceProvider.sharedInstance(), 101 ValueConverterRegistry.sharedInstance(), 102 DefaultRequestBodyMarshaler.sharedInstance()); 103 } 104 105 public DefaultResourceMethodParameterProvider(@Nonnull InstanceProvider instanceProvider, 106 @Nonnull ValueConverterRegistry valueConverterRegistry, 107 @Nonnull RequestBodyMarshaler requestBodyMarshaler) { 108 requireNonNull(instanceProvider); 109 requireNonNull(valueConverterRegistry); 110 requireNonNull(requestBodyMarshaler); 111 112 this.instanceProvider = instanceProvider; 113 this.valueConverterRegistry = valueConverterRegistry; 114 this.requestBodyMarshaler = requestBodyMarshaler; 115 } 116 117 @Nonnull 118 @Override 119 public List<Object> parameterValuesForResourceMethod(@Nonnull Request request, 120 @Nonnull ResourceMethod resourceMethod) { 121 requireNonNull(request); 122 requireNonNull(resourceMethod); 123 124 Parameter[] parameters = resourceMethod.getMethod().getParameters(); 125 List<Object> parametersToPass = new ArrayList<>(parameters.length); 126 127 for (int i = 0; i < parameters.length; ++i) { 128 Parameter parameter = parameters[i]; 129 130 try { 131 parametersToPass.add(extractParameterValueToPassToResourceMethod(request, resourceMethod, parameter)); 132 } catch (BadRequestException e) { 133 throw e; 134 } catch (Exception e) { 135 throw new IllegalArgumentException(format("Unable to inject parameter at index %d (%s) for resource method %s.", 136 i, parameter, resourceMethod.getMethod()), e); 137 } 138 } 139 140 return parametersToPass; 141 } 142 143 @Nullable 144 protected Object extractParameterValueToPassToResourceMethod(@Nonnull Request request, 145 @Nonnull ResourceMethod resourceMethod, 146 @Nonnull Parameter parameter) { 147 requireNonNull(request); 148 requireNonNull(resourceMethod); 149 requireNonNull(parameter); 150 151 if (parameter.getType().isAssignableFrom(Request.class)) 152 return request; 153 154 ParameterType parameterType = new ParameterType(parameter); 155 PathParameter pathParameter = parameter.getAnnotation(PathParameter.class); 156 157 if (pathParameter != null) { 158 if (parameterType.isWrappedInOptional()) 159 throw new IllegalStateException(format("@%s-annotated parameters cannot be marked %s", 160 PathParameter.class.getSimpleName(), Optional.class.getSimpleName())); 161 162 String pathParameterName = extractParameterName(resourceMethod, parameter, pathParameter, pathParameter.name()); 163 ResourcePath resourcePath = request.getResourcePath(); 164 165 Map<String, String> valuesByPathParameter = resourceMethod.getResourcePath().extractPlaceholders(resourcePath); 166 String pathParameterValue = valuesByPathParameter.get(pathParameterName); 167 168 if (pathParameterValue == null) 169 throw new IllegalStateException(format("Missing value for path parameter '%s' for resource method %s", 170 pathParameterName, resourceMethod)); 171 172 ValueConverter<Object, Object> valueConverter = getValueConverterRegistry().get(String.class, parameter.getType()).orElse(null); 173 174 if (valueConverter == null) 175 throwValueConverterMissingException(parameter, String.class, parameter.getType(), resourceMethod); 176 177 Object result; 178 179 try { 180 Optional<Object> valueConverterResult = valueConverter.convert(pathParameterValue); 181 result = valueConverterResult == null ? null : valueConverterResult.orElse(null); 182 } catch (Exception e) { 183 throw new IllegalPathParameterException(format("Illegal value '%s' was specified for path parameter '%s' (was expecting a value convertible to %s)", 184 pathParameterValue, pathParameterName, valueConverter.getToType()), e, pathParameterName, pathParameterValue); 185 } 186 187 if (result == null) 188 throw new IllegalPathParameterException(format("No value was specified for path parameter '%s' (was expecting a value convertible to %s)", 189 pathParameterName, valueConverter.getToType()), pathParameterName, pathParameterValue); 190 191 return result; 192 } 193 194 QueryParameter queryParameter = parameter.getAnnotation(QueryParameter.class); 195 196 if (queryParameter != null) 197 return extractQueryParameterValue(request, resourceMethod, parameter, queryParameter, parameterType); 198 199 FormParameter formParameter = parameter.getAnnotation(FormParameter.class); 200 201 if (formParameter != null) 202 return extractFormParameterValue(request, resourceMethod, parameter, formParameter, parameterType); 203 204 RequestHeader requestHeader = parameter.getAnnotation(RequestHeader.class); 205 206 if (requestHeader != null) 207 return extractRequestHeaderValue(request, resourceMethod, parameter, requestHeader, parameterType); 208 209 RequestCookie requestCookie = parameter.getAnnotation(RequestCookie.class); 210 211 if (requestCookie != null) 212 return extractRequestCookieValue(request, resourceMethod, parameter, requestCookie, parameterType); 213 214 Multipart multipart = parameter.getAnnotation(Multipart.class); 215 216 String multipartFieldTypeName = MultipartField.class.getTypeName(); 217 boolean isMultipartScalarType = multipartFieldTypeName.equals(parameterType.getNormalizedType().getTypeName()); 218 boolean isMultipartListType = parameterType.getListElementType().isPresent() 219 && multipartFieldTypeName.equals(parameterType.getListElementType().get().getTypeName()); 220 221 // Multipart is either indicated by @Multipart annotation or the parameter is of type MultipartField 222 if (multipart != null || (isMultipartScalarType || isMultipartListType)) 223 return extractRequestMultipartValue(request, resourceMethod, parameter, multipart, parameterType); 224 225 RequestBody requestBody = parameter.getAnnotation(RequestBody.class); 226 boolean requestBodyOptional = requestBody.optional() || parameterType.isWrappedInOptional(); 227 228 if (requestBody != null) { 229 boolean requestBodyExpectsString = String.class.equals(parameterType.getNormalizedType()); 230 boolean requestBodyExpectsByteArray = byte[].class.equals(parameterType.getNormalizedType()); 231 232 if (requestBodyExpectsString) { 233 String requestBodyAsString = request.getBodyAsString().orElse(null); 234 235 if (parameterType.isWrappedInOptional()) 236 return Optional.ofNullable(requestBodyAsString); 237 238 if (!requestBodyOptional && requestBodyAsString == null) 239 throw new MissingRequestBodyException("A request body is required for this resource."); 240 241 return requestBodyAsString; 242 } else if (requestBodyExpectsByteArray) { 243 byte[] requestBodyAsByteArray = request.getBody().orElse(null); 244 245 if (parameterType.isWrappedInOptional()) 246 return Optional.ofNullable(requestBodyAsByteArray); 247 248 if (!requestBodyOptional && requestBodyAsByteArray == null) 249 throw new MissingRequestBodyException("A request body is required for this resource."); 250 251 return requestBodyAsByteArray; 252 } else { 253 // Short circuit: optional type and no request body 254 if (parameterType.isWrappedInOptional() && request.getBody().isEmpty()) 255 return Optional.empty(); 256 257 // Short circuit: marked optional and no request body 258 if (requestBodyOptional && request.getBody().isEmpty()) 259 return defaultValueForType(parameterType.getNormalizedType()).orElse(null); 260 261 // Short circuit: not optional and no request body 262 if (!requestBodyOptional && request.getBody().isEmpty()) 263 throw new MissingRequestBodyException("A request body is required for this resource."); 264 265 // Let the request body marshaler try to handle it 266 Object requestBodyObject; 267 Type requestBodyType = parameterType.getNormalizedType(); 268 269 try { 270 Optional<Object> marshaledRequestBody = getRequestBodyMarshaler().marshalRequestBody(request, resourceMethod, parameter, requestBodyType); 271 requestBodyObject = marshaledRequestBody == null ? null : marshaledRequestBody.orElse(null); 272 } catch (IllegalRequestBodyException e) { 273 throw e; 274 } catch (Exception e) { 275 throw new IllegalRequestBodyException(format("Unable to marshal request body to %s", requestBodyType), e); 276 } 277 278 if (parameterType.isWrappedInOptional()) 279 return Optional.ofNullable(requestBodyObject); 280 281 if (!requestBodyOptional && requestBodyObject == null) 282 throw new MissingRequestBodyException("Request body is required for this resource, but it was marshaled to null"); 283 284 return requestBodyObject; 285 } 286 } 287 288 // Don't recognize what's being asked for? Have the InstanceProvider try to vend something 289 if (parameterType.isWrappedInOptional()) 290 return Optional.ofNullable(getInstanceProvider().provide(parameter.getType())); 291 else 292 return getInstanceProvider().provide(parameter.getType()); 293 } 294 295 /** 296 * What "default" value does the JDK use for an unassigned field of the given type? 297 * <p> 298 * For example, a primitive type like int defaults to 0 but a java.util.List would be null. 299 * <p> 300 * See https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html 301 * 302 * @param type the type whose default value we'd like to know 303 * @return the default value for the type, or an empty optional if no default exists (i.e. is null) 304 */ 305 @Nonnull 306 protected Optional<Object> defaultValueForType(@Nullable Type type) { 307 if (type == null) 308 return Optional.empty(); 309 310 return Optional.ofNullable(DEFAULT_VALUES_BY_PRIMITIVE_TYPE.get(type)); 311 } 312 313 @Nonnull 314 protected String extractParameterName(@Nonnull ResourceMethod resourceMethod, 315 @Nonnull Parameter parameter, 316 @Nullable Annotation annotation, 317 @Nullable String annotationValue) { 318 requireNonNull(resourceMethod); 319 requireNonNull(parameter); 320 321 String parameterName = trimAggressivelyToNull(annotationValue); 322 323 if (parameterName == null && parameter.isNamePresent()) 324 parameterName = parameter.getName(); 325 326 if (parameterName == null) { 327 String message; 328 329 if (annotation == null) 330 message = format( 331 "Unable to automatically detect resource method parameter name. " 332 + "You must compile with javac flag \"-parameters\" to preserve parameter names for reflection. Offending resource method was %s", 333 resourceMethod); 334 else 335 message = format( 336 "Unable to automatically detect resource method parameter name. " 337 + "You must either explicitly specify a @%s value for parameter %s - for example, @%s(\"name-goes-here\") - " 338 + "or compile with javac flag \"-parameters\" to preserve parameter names for reflection. Offending resource method was %s", 339 annotation.annotationType().getSimpleName(), parameter, annotation.annotationType().getSimpleName(), resourceMethod); 340 341 throw new IllegalArgumentException(message); 342 } 343 344 return parameterName; 345 } 346 347 @Nullable 348 @SuppressWarnings("unchecked") 349 protected Object extractQueryParameterValue(@Nonnull Request request, 350 @Nonnull ResourceMethod resourceMethod, 351 @Nonnull Parameter parameter, 352 @Nonnull QueryParameter queryParameter, 353 @Nonnull ParameterType parameterType) { 354 requireNonNull(request); 355 requireNonNull(resourceMethod); 356 requireNonNull(parameter); 357 requireNonNull(queryParameter); 358 requireNonNull(parameterType); 359 360 String parameterDescription = "query parameter"; 361 String parameterName = extractParameterName(resourceMethod, parameter, queryParameter, queryParameter.name()); 362 Set<String> values = request.getQueryParameters().get(parameterName); 363 364 if (values == null) 365 values = Set.of(); 366 367 RequestValueExtractionConfig<String> requestValueExtractionConfig = new RequestValueExtractionConfig.Builder<>(resourceMethod, parameter, parameterType, parameterName, parameterDescription) 368 .optional(queryParameter.optional()) 369 .values(new ArrayList<>(values)) 370 .missingExceptionProvider((message, name) -> new MissingQueryParameterException(message, parameterName)) 371 .illegalExceptionProvider((message, cause, name, value, valueMetadatum) -> new IllegalQueryParameterException(message, cause, parameterName, value)) 372 .build(); 373 374 return extractRequestValue(requestValueExtractionConfig); 375 } 376 377 @Nullable 378 @SuppressWarnings("unchecked") 379 protected Object extractFormParameterValue(@Nonnull Request request, 380 @Nonnull ResourceMethod resourceMethod, 381 @Nonnull Parameter parameter, 382 @Nonnull FormParameter formParameter, 383 @Nonnull ParameterType parameterType) { 384 requireNonNull(request); 385 requireNonNull(resourceMethod); 386 requireNonNull(parameter); 387 requireNonNull(formParameter); 388 requireNonNull(parameterType); 389 390 String parameterDescription = "form parameter"; 391 String parameterName = extractParameterName(resourceMethod, parameter, formParameter, formParameter.name()); 392 Set<String> values = request.getFormParameters().get(parameterName); 393 394 if (values == null) 395 values = Set.of(); 396 397 RequestValueExtractionConfig<String> requestValueExtractionConfig = new RequestValueExtractionConfig.Builder<>(resourceMethod, parameter, parameterType, parameterName, parameterDescription) 398 .optional(formParameter.optional()) 399 .values(new ArrayList<>(values)) 400 .missingExceptionProvider((message, name) -> new MissingFormParameterException(message, parameterName)) 401 .illegalExceptionProvider((message, cause, name, value, valueMetadatum) -> new IllegalFormParameterException(message, cause, parameterName, value)) 402 .build(); 403 404 return extractRequestValue(requestValueExtractionConfig); 405 } 406 407 @Nullable 408 @SuppressWarnings("unchecked") 409 protected Object extractRequestHeaderValue(@Nonnull Request request, 410 @Nonnull ResourceMethod resourceMethod, 411 @Nonnull Parameter parameter, 412 @Nonnull RequestHeader requestHeader, 413 @Nonnull ParameterType parameterType) { 414 requireNonNull(request); 415 requireNonNull(resourceMethod); 416 requireNonNull(parameter); 417 requireNonNull(requestHeader); 418 requireNonNull(parameterType); 419 420 String parameterDescription = "request header"; 421 String parameterName = extractParameterName(resourceMethod, parameter, requestHeader, requestHeader.name()); 422 Set<String> values = request.getHeaders().get(parameterName); 423 424 if (values == null) 425 values = Set.of(); 426 427 RequestValueExtractionConfig<String> requestValueExtractionConfig = new RequestValueExtractionConfig.Builder<>(resourceMethod, parameter, parameterType, parameterName, parameterDescription) 428 .optional(requestHeader.optional()) 429 .values(new ArrayList<>(values)) 430 .missingExceptionProvider((message, name) -> new MissingRequestHeaderException(message, parameterName)) 431 .illegalExceptionProvider((message, cause, name, value, valueMetadatum) -> new IllegalRequestHeaderException(message, cause, parameterName, value)) 432 .build(); 433 434 return extractRequestValue(requestValueExtractionConfig); 435 } 436 437 @Nullable 438 @SuppressWarnings("unchecked") 439 protected Object extractRequestCookieValue(@Nonnull Request request, 440 @Nonnull ResourceMethod resourceMethod, 441 @Nonnull Parameter parameter, 442 @Nonnull RequestCookie requestCookie, 443 @Nonnull ParameterType parameterType) { 444 requireNonNull(request); 445 requireNonNull(resourceMethod); 446 requireNonNull(parameter); 447 requireNonNull(requestCookie); 448 requireNonNull(parameterType); 449 450 String parameterDescription = "request cookie"; 451 String parameterName = extractParameterName(resourceMethod, parameter, requestCookie, requestCookie.name()); 452 Set<String> values = request.getCookies().get(parameterName); 453 454 if (values == null) 455 values = Set.of(); 456 457 RequestValueExtractionConfig<String> requestValueExtractionConfig = new RequestValueExtractionConfig.Builder<>(resourceMethod, parameter, parameterType, parameterName, parameterDescription) 458 .optional(requestCookie.optional()) 459 .values(new ArrayList<>(values)) 460 .missingExceptionProvider((message, name) -> new MissingRequestCookieException(message, parameterName)) 461 .illegalExceptionProvider((message, cause, name, value, valueMetadatum) -> new IllegalRequestCookieException(message, cause, parameterName, value)) 462 .build(); 463 464 return extractRequestValue(requestValueExtractionConfig); 465 } 466 467 @Nonnull 468 @SuppressWarnings("unchecked") 469 protected Object extractRequestMultipartValue(@Nonnull Request request, 470 @Nonnull ResourceMethod resourceMethod, 471 @Nonnull Parameter parameter, 472 @Nullable Multipart multipart, 473 @Nonnull ParameterType parameterType) { 474 requireNonNull(request); 475 requireNonNull(resourceMethod); 476 requireNonNull(parameter); 477 requireNonNull(parameterType); 478 479 String parameterDescription = "multipart field"; 480 String parameterName = extractParameterName(resourceMethod, parameter, multipart, multipart == null ? null : multipart.name()); 481 482 List<String> values = new ArrayList<>(); 483 List<MultipartField> valuesMetadata = new ArrayList<>(); 484 485 for (Map.Entry<String, Set<MultipartField>> entry : request.getMultipartFields().entrySet()) { 486 String multipartName = entry.getKey(); 487 488 if (parameterName.equals(multipartName)) { 489 Set<MultipartField> multipartFields = entry.getValue(); 490 491 for (MultipartField matchingMultipartField : multipartFields) { 492 values.add(matchingMultipartField.getDataAsString().orElse(null)); 493 valuesMetadata.add(matchingMultipartField); 494 } 495 } 496 } 497 498 ValueMetadatumConverter<MultipartField> valueMetadatumConverter = (MultipartField multipartField, Type toType, ValueConverter<Object, Object> valueConverter) -> { 499 if (toType.equals(MultipartField.class)) 500 return multipartField; 501 502 if (toType.equals(String.class)) 503 return multipartField.getDataAsString().orElse(null); 504 505 if (toType.equals(byte[].class)) 506 return multipartField.getData().orElse(null); 507 508 Optional<Object> valueConverterResult = valueConverter.convert(multipartField.getDataAsString().orElse(null)); 509 return valueConverterResult == null ? null : valueConverterResult.orElse(null); 510 }; 511 512 RequestValueExtractionConfig<MultipartField> requestValueExtractionConfig = new RequestValueExtractionConfig.Builder<>(resourceMethod, parameter, parameterType, parameterName, parameterDescription) 513 .optional(multipart == null ? false : multipart.optional()) 514 .values(new ArrayList<>(values)) 515 .valuesMetadata(valuesMetadata) 516 .valueMetadatumConverter(valueMetadatumConverter) 517 .missingExceptionProvider((message, name) -> new MissingMultipartFieldException(message, parameterName)) 518 .illegalExceptionProvider((message, cause, name, value, valueMetadatum) -> new IllegalMultipartFieldException(message, cause, ((Optional<MultipartField>) valueMetadatum).orElse(null))) 519 .build(); 520 521 return extractRequestValue(requestValueExtractionConfig); 522 } 523 524 @Nonnull 525 @SuppressWarnings("unchecked") 526 protected <T> Object extractRequestValue(@Nonnull RequestValueExtractionConfig<T> requestValueExtractionConfig) { 527 requireNonNull(requestValueExtractionConfig); 528 529 ResourceMethod resourceMethod = requestValueExtractionConfig.getResourceMethod(); 530 Parameter parameter = requestValueExtractionConfig.getParameter(); 531 ParameterType parameterType = requestValueExtractionConfig.getParameterType(); 532 String parameterName = requestValueExtractionConfig.getParameterName(); 533 String parameterDescription = requestValueExtractionConfig.getParameterDescription(); 534 boolean optional = requestValueExtractionConfig.getOptional(); 535 List<String> values = requestValueExtractionConfig.getValues(); 536 List<T> valuesMetadata = requestValueExtractionConfig.getValuesMetadata(); 537 ValueMetadatumConverter<T> valueMetadatumConverter = requestValueExtractionConfig.getValueMetadatumConverter().orElse(null); 538 MissingExceptionProvider missingExceptionProvider = requestValueExtractionConfig.getMissingExceptionProvider(); 539 IllegalExceptionProvider illegalExceptionProvider = requestValueExtractionConfig.getIllegalExceptionProvider(); 540 541 boolean returnMetadataInsteadOfValues = valueMetadatumConverter != null; 542 Type toType = parameterType.isList() ? parameterType.getListElementType().get() : parameterType.getNormalizedType(); 543 544 ValueConverter<Object, Object> valueConverter = getValueConverterRegistry().get(String.class, toType).orElse(null); 545 546 if (valueConverter == null && !returnMetadataInsteadOfValues) 547 throwValueConverterMissingException(parameter, String.class, toType, resourceMethod); 548 549 // Special handling for Lists (support for multiple query parameters/headers/cookies with the same name) 550 if (parameterType.isList()) { 551 List<Object> results = new ArrayList<>(values.size()); 552 553 if (returnMetadataInsteadOfValues) { 554 for (int i = 0; i < valuesMetadata.size(); ++i) { 555 Object valueMetadatum = valuesMetadata.get(i); 556 557 if (valueMetadatum != null) 558 try { 559 valueMetadatum = valueMetadatumConverter.convert((T) valueMetadatum, toType, valueConverter); 560 results.add(valueMetadatum); 561 } catch (ValueConversionException e) { 562 throw illegalExceptionProvider.provide( 563 format("Illegal value '%s' was specified for %s '%s' (was expecting a value convertible to %s)", valueMetadatum, 564 parameterDescription, parameterName, valueConverter.getToType()), e, parameterName, null, Optional 565 .ofNullable(valuesMetadata.size() > i ? valuesMetadata.get(i) : null)); 566 } 567 } 568 } else { 569 for (int i = 0; i < values.size(); ++i) { 570 String value = values.get(i); 571 572 if (value != null && trimAggressively(value).length() > 0) 573 try { 574 Optional<Object> valueConverterResult = valueConverter.convert(value); 575 results.add(valueConverterResult == null ? null : valueConverterResult.orElse(null)); 576 } catch (ValueConversionException e) { 577 throw illegalExceptionProvider.provide( 578 format("Illegal value '%s' was specified for %s '%s' (was expecting a value convertible to %s)", value, 579 parameterDescription, parameterName, valueConverter.getToType()), e, parameterName, value, Optional 580 .ofNullable(valuesMetadata.size() > i ? valuesMetadata.get(i) : null)); 581 } 582 } 583 } 584 585 boolean required = !parameterType.isWrappedInOptional() && !optional; 586 587 if (required && results.size() == 0) 588 throw missingExceptionProvider.provide(format("Required %s '%s' was not specified.", parameterDescription, parameterName), parameterName); 589 590 return parameterType.isWrappedInOptional() ? (results.size() == 0 ? Optional.empty() : Optional.of(results)) : results; 591 } 592 593 // Non-list support 594 Object result; 595 596 if (returnMetadataInsteadOfValues) { 597 result = valuesMetadata.size() > 0 ? valuesMetadata.get(0) : null; 598 599 if (result != null) { 600 try { 601 result = valueMetadatumConverter.convert((T) result, toType, valueConverter); 602 } catch (ValueConversionException e) { 603 throw illegalExceptionProvider.provide( 604 format("Illegal value '%s' was specified for %s '%s' (was expecting a value convertible to %s)", result, 605 parameterDescription, parameterName, valueConverter.getToType()), e, parameterName, null, Optional 606 .ofNullable(valuesMetadata.size() > 0 ? valuesMetadata.get(0) : null)); 607 } 608 } 609 610 boolean required = !parameterType.isWrappedInOptional() && !optional; 611 612 if (required && result == null) 613 throw missingExceptionProvider.provide(format("Required %s '%s' was not specified.", parameterDescription, parameterName), parameterName); 614 } else { 615 String value = values.size() > 0 ? values.get(0) : null; 616 617 if (value != null && trimAggressively(value).length() == 0) value = null; 618 619 boolean required = !parameterType.isWrappedInOptional() && !optional; 620 621 if (required && value == null) 622 throw missingExceptionProvider.provide(format("Required %s '%s' was not specified.", parameterDescription, parameterName), parameterName); 623 624 try { 625 Optional<Object> valueConverterResult = valueConverter.convert(value); 626 result = valueConverterResult == null ? null : valueConverterResult.orElse(null); 627 } catch (ValueConversionException e) { 628 throw illegalExceptionProvider.provide( 629 format("Illegal value '%s' was specified for %s '%s' (was expecting a value convertible to %s)", value, 630 parameterDescription, parameterName, valueConverter.getToType()), e, parameterName, value, Optional 631 .ofNullable(valuesMetadata.size() > 0 ? valuesMetadata.get(0) : null)); 632 } 633 } 634 635 return parameterType.isWrappedInOptional() ? Optional.ofNullable(result) : result; 636 } 637 638 protected void throwValueConverterMissingException(@Nonnull Parameter parameter, 639 @Nonnull Type fromType, 640 @Nonnull Type toType, 641 @Nonnull ResourceMethod resourceMethod) { 642 requireNonNull(parameter); 643 requireNonNull(fromType); 644 requireNonNull(toType); 645 requireNonNull(resourceMethod); 646 647 throw new IllegalArgumentException(format( 648 "No %s is registered for converting %s to %s for parameter '%s' in resource method %s ", 649 ValueConverter.class.getSimpleName(), fromType, toType, parameter, resourceMethod)); 650 } 651 652 @Nonnull 653 protected InstanceProvider getInstanceProvider() { 654 return this.instanceProvider; 655 } 656 657 @Nonnull 658 protected ValueConverterRegistry getValueConverterRegistry() { 659 return this.valueConverterRegistry; 660 } 661 662 @Nonnull 663 protected RequestBodyMarshaler getRequestBodyMarshaler() { 664 return this.requestBodyMarshaler; 665 } 666 667 @FunctionalInterface 668 protected interface MissingExceptionProvider { 669 @Nonnull 670 RuntimeException provide(@Nonnull String message, 671 @Nonnull String name); 672 } 673 674 @FunctionalInterface 675 protected interface IllegalExceptionProvider<T> { 676 @Nonnull 677 RuntimeException provide(@Nonnull String message, 678 @Nonnull Exception cause, 679 @Nonnull String name, 680 @Nullable String value, 681 @Nullable T valueMetadatum); 682 } 683 684 @FunctionalInterface 685 protected interface ValueMetadatumConverter<T> { 686 @Nonnull 687 Object convert(@Nonnull T valueMetadatum, 688 @Nonnull Type toType, 689 @Nonnull ValueConverter<Object, Object> valueConverter) throws ValueConversionException; 690 } 691 692 @NotThreadSafe 693 protected static class RequestValueExtractionConfig<T> { 694 @Nonnull 695 private final ResourceMethod resourceMethod; 696 @Nonnull 697 private final Parameter parameter; 698 @Nonnull 699 private final ParameterType parameterType; 700 @Nonnull 701 private final String parameterName; 702 @Nonnull 703 private final String parameterDescription; 704 @Nonnull 705 private final Boolean optional; 706 @Nonnull 707 private final List<String> values; 708 @Nonnull 709 private final List<T> valuesMetadata; 710 @Nullable 711 private ValueMetadatumConverter<T> valueMetadatumConverter; 712 @Nonnull 713 private final MissingExceptionProvider missingExceptionProvider; 714 @Nonnull 715 private final IllegalExceptionProvider illegalExceptionProvider; 716 717 @SuppressWarnings("unchecked") 718 protected RequestValueExtractionConfig(@Nonnull Builder builder) { 719 requireNonNull(builder); 720 721 this.resourceMethod = requireNonNull(builder.resourceMethod); 722 this.parameter = requireNonNull(builder.parameter); 723 this.parameterType = requireNonNull(builder.parameterType); 724 this.parameterName = requireNonNull(builder.parameterName); 725 this.parameterDescription = requireNonNull(builder.parameterDescription); 726 this.optional = builder.optional == null ? false : builder.optional; 727 this.values = builder.values == null ? List.of() : new ArrayList<>(builder.values); 728 this.valuesMetadata = builder.valuesMetadata == null ? List.of() : new ArrayList<>(builder.valuesMetadata); 729 this.valueMetadatumConverter = builder.valueMetadatumConverter; 730 this.missingExceptionProvider = requireNonNull(builder.missingExceptionProvider); 731 this.illegalExceptionProvider = requireNonNull(builder.illegalExceptionProvider); 732 } 733 734 @NotThreadSafe 735 protected static class Builder<T> { 736 @Nonnull 737 private final ResourceMethod resourceMethod; 738 @Nonnull 739 private final Parameter parameter; 740 @Nonnull 741 private final ParameterType parameterType; 742 @Nonnull 743 private final String parameterName; 744 @Nonnull 745 private String parameterDescription; 746 @Nullable 747 private Boolean optional; 748 @Nullable 749 private List<String> values; 750 @Nullable 751 private List<T> valuesMetadata; 752 @Nullable 753 private ValueMetadatumConverter<T> valueMetadatumConverter; 754 @Nullable 755 private MissingExceptionProvider missingExceptionProvider; 756 @Nullable 757 private IllegalExceptionProvider illegalExceptionProvider; 758 759 public Builder(@Nonnull ResourceMethod resourceMethod, 760 @Nonnull Parameter parameter, 761 @Nonnull ParameterType parameterType, 762 @Nonnull String parameterName, 763 @Nonnull String parameterDescription) { 764 requireNonNull(resourceMethod); 765 requireNonNull(parameter); 766 requireNonNull(parameterType); 767 requireNonNull(parameterName); 768 requireNonNull(parameterDescription); 769 770 this.resourceMethod = resourceMethod; 771 this.parameter = parameter; 772 this.parameterType = parameterType; 773 this.parameterName = parameterName; 774 this.parameterDescription = parameterDescription; 775 } 776 777 @Nonnull 778 public Builder optional(@Nullable Boolean optional) { 779 this.optional = optional; 780 return this; 781 } 782 783 @Nonnull 784 public Builder values(@Nullable List<String> values) { 785 this.values = values; 786 return this; 787 } 788 789 @Nonnull 790 public Builder valuesMetadata(@Nullable List<T> valuesMetadata) { 791 this.valuesMetadata = valuesMetadata; 792 return this; 793 } 794 795 @Nonnull 796 public Builder valueMetadatumConverter(@Nullable ValueMetadatumConverter<T> valueMetadatumConverter) { 797 this.valueMetadatumConverter = valueMetadatumConverter; 798 return this; 799 } 800 801 @Nonnull 802 public Builder missingExceptionProvider(@Nullable MissingExceptionProvider missingExceptionProvider) { 803 this.missingExceptionProvider = missingExceptionProvider; 804 return this; 805 } 806 807 @Nonnull 808 public Builder illegalExceptionProvider(@Nullable IllegalExceptionProvider illegalExceptionProvider) { 809 this.illegalExceptionProvider = illegalExceptionProvider; 810 return this; 811 } 812 813 @Nonnull 814 public RequestValueExtractionConfig build() { 815 return new RequestValueExtractionConfig(this); 816 } 817 } 818 819 @Nonnull 820 public ResourceMethod getResourceMethod() { 821 return this.resourceMethod; 822 } 823 824 @Nonnull 825 public Parameter getParameter() { 826 return this.parameter; 827 } 828 829 @Nonnull 830 public ParameterType getParameterType() { 831 return this.parameterType; 832 } 833 834 @Nonnull 835 public String getParameterName() { 836 return this.parameterName; 837 } 838 839 @Nonnull 840 public String getParameterDescription() { 841 return this.parameterDescription; 842 } 843 844 @Nonnull 845 public Boolean getOptional() { 846 return this.optional; 847 } 848 849 @Nonnull 850 public List<String> getValues() { 851 return this.values; 852 } 853 854 @Nonnull 855 public List<T> getValuesMetadata() { 856 return this.valuesMetadata; 857 } 858 859 @Nonnull 860 public Optional<ValueMetadatumConverter<T>> getValueMetadatumConverter() { 861 return Optional.ofNullable(this.valueMetadatumConverter); 862 } 863 864 @Nonnull 865 public MissingExceptionProvider getMissingExceptionProvider() { 866 return this.missingExceptionProvider; 867 } 868 869 @Nonnull 870 public IllegalExceptionProvider getIllegalExceptionProvider() { 871 return this.illegalExceptionProvider; 872 } 873 } 874 875 /** 876 * Given a parameter, make its "real" type a little more accessible. 877 * That means: 878 * <p> 879 * 1. If wrapped in an optional, the real type is the wrapped value 880 * 2. If it's a List type, the real type is the list element's type 881 * 3. If neither 1 nor 2 then no transformations performed 882 */ 883 @ThreadSafe 884 protected static class ParameterType { 885 @Nonnull 886 private final Type normalizedType; 887 @Nullable 888 private final Type listElementType; 889 @Nonnull 890 private final Boolean wrappedInOptional; 891 892 public ParameterType(@Nonnull Parameter parameter) { 893 requireNonNull(parameter); 894 895 Type normalizedType = parameter.getParameterizedType(); 896 Type listElementType = null; 897 boolean wrappedInOptional = false; 898 899 if (parameter.getType().isAssignableFrom(Optional.class)) { 900 normalizedType = ((ParameterizedType) parameter.getParameterizedType()).getActualTypeArguments()[0]; 901 wrappedInOptional = true; 902 } 903 904 // Special handling: determine if this property is a generic List 905 if (ParameterizedType.class.isAssignableFrom(normalizedType.getClass())) { 906 ParameterizedType parameterizedNormalizedType = (ParameterizedType) normalizedType; 907 908 if (parameterizedNormalizedType.getRawType().equals(List.class)) 909 listElementType = parameterizedNormalizedType.getActualTypeArguments()[0]; 910 } 911 912 this.normalizedType = normalizedType; 913 this.listElementType = listElementType; 914 this.wrappedInOptional = wrappedInOptional; 915 } 916 917 @Nonnull 918 public Type getNormalizedType() { 919 return this.normalizedType; 920 } 921 922 @Nonnull 923 public Optional<Type> getListElementType() { 924 return Optional.ofNullable(this.listElementType); 925 } 926 927 @Nonnull 928 public Boolean isList() { 929 return getListElementType().isPresent(); 930 } 931 932 @Nonnull 933 public Boolean isWrappedInOptional() { 934 return this.wrappedInOptional; 935 } 936 } 937}