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}