All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.muserver.rest.ResourceMethod Maven / Gradle / Ivy

The newest version!
package io.muserver.rest;

import io.muserver.Method;
import io.muserver.openapi.*;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.core.MediaType;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.muserver.Mutils.nullOrEmpty;
import static io.muserver.openapi.HeaderObjectBuilder.headerObject;
import static io.muserver.openapi.MediaTypeObjectBuilder.mediaTypeObject;
import static io.muserver.openapi.OperationObjectBuilder.operationObject;
import static io.muserver.openapi.RequestBodyObjectBuilder.requestBodyObject;
import static io.muserver.openapi.ResponseObjectBuilder.responseObject;
import static io.muserver.openapi.ResponsesObjectBuilder.responsesObject;
import static io.muserver.openapi.SchemaObjectBuilder.schemaObject;
import static io.muserver.openapi.SchemaObjectBuilder.schemaObjectFrom;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toMap;

class ResourceMethod {
    final ResourceClass resourceClass;
    final UriPattern pathPattern;
    final java.lang.reflect.Method methodHandle;
    final Method httpMethod;
    final String pathTemplate;
    final List effectiveConsumes;
    final List directlyConsumes;
    final List directlyProduces;
    final List effectiveProduces;
    final List params;
    private final SchemaObjectCustomizer schemaObjectCustomizer;
    private final DescriptionData descriptionData;
    private final boolean isDeprecated;
    private final List> nameBindingAnnotations;
    final Annotation[] methodAnnotations; // the annotations defined on the method to be passed to the message body writers

    ResourceMethod(ResourceClass resourceClass, UriPattern pathPattern, java.lang.reflect.Method methodHandle, List params, Method httpMethod, String pathTemplate, List produces, List consumes, SchemaObjectCustomizer schemaObjectCustomizer, DescriptionData descriptionData, boolean isDeprecated, List> nameBindingAnnotations, Annotation[] methodAnnotations) {
        this.resourceClass = resourceClass;
        this.pathPattern = pathPattern;
        this.methodHandle = methodHandle;
        this.params = params;
        this.httpMethod = httpMethod;
        this.pathTemplate = pathTemplate;
        this.directlyProduces = produces;
        this.directlyConsumes = consumes;
        this.schemaObjectCustomizer = schemaObjectCustomizer;
        this.descriptionData = descriptionData;
        this.isDeprecated = isDeprecated;
        this.nameBindingAnnotations = nameBindingAnnotations;
        this.methodAnnotations = methodAnnotations;
        this.effectiveProduces = !produces.isEmpty() ? produces : (!resourceClass.produces.isEmpty() ? resourceClass.produces : RequestMatcher.WILDCARD_AS_LIST);
        this.effectiveConsumes = !consumes.isEmpty() ? consumes : (!resourceClass.consumes.isEmpty() ? resourceClass.consumes : RequestMatcher.WILDCARD_AS_LIST);
    }

    boolean hasAll(List> annotations) {
        for (Class annotation : annotations) {
            if (!nameBindingAnnotations.contains(annotation) && !resourceClass.nameBindingAnnotations.contains(annotation)) {
                return false;
            }
        }
        return true;
    }

    List paramsIncludingLocators() {
        if (resourceClass.locatorMethod == null) {
            return params;
        }
        List all = new ArrayList<>(resourceClass.locatorMethod.paramsIncludingLocators());
        all.addAll(params);
        return all;
    }

    boolean isSubResource() {
        return pathPattern != null;
    }

    boolean isSubResourceLocator() {
        return httpMethod == null;
    }

    Object invoke(Object... params) throws Exception {
        try {
            return methodHandle.invoke(resourceClass.resourceInstance, params);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof Exception) {
                throw (Exception) cause;
            }
            throw e;
        }
    }

    OperationObjectBuilder createOperationBuilder(List customSchemas) {
        List apiResponseList = getApiResponses(methodHandle);

        Map httpStatusCodes = new HashMap<>();

        for (ApiResponseObj apiResponse : apiResponseList) {
            Class responseClass = apiResponse.response;

            Stream responseTypesStream = apiResponse.contentType.length != 0 ?
                Stream.of(apiResponse.contentType).map(MediaType::valueOf)
                : "204".equals(apiResponse.code) ? null : effectiveProduces.stream();

            Map content = responseTypesStream == null ? null : responseTypesStream.collect(toMap(MediaType::toString,
                mt -> {
                    SchemaObject responseSchema;
                    Object example = nullOrEmpty(apiResponse.example) ? null : apiResponse.example;

                    if (void.class.isAssignableFrom(responseClass)) {
                        responseSchema = null;
                    } else {
                        SchemaReference schemaReference = SchemaReference.find(customSchemas, responseClass, apiResponse.genericReturnType);
                        SchemaObjectBuilder builder = schemaReference == null ? schemaObjectFrom(responseClass) : schemaReference.schema.toBuilder();
                        responseSchema = schemaObjectCustomizer.customize(builder,
                            schemaContext(SchemaObjectCustomizerTarget.RESPONSE_BODY, null, responseClass, apiResponse.genericReturnType, mt)).build();
                        if (responseSchema.example() != null) {
                            example = responseSchema.example();
                        }
                    }
                    return mediaTypeObject()
                        .withSchema(responseSchema)
                        .withExample(example)
                        .build();
                }));
            httpStatusCodes.put(apiResponse.code, responseObject()
                .withContent(content)
                .withDescription(apiResponse.message)
                .withHeaders(
                    apiResponse.responseHeaders.length == 0 ? null :
                        Stream.of(apiResponse.responseHeaders).collect(
                            toMap(ResponseHeader::name,
                                rh -> headerObject().withDescription(rh.description())
                                    .withDeprecated(rh.deprecated() ? true : null)
                                    .withExample(nullOrEmpty(rh.example()) ? null : rh.example()).build()
                            )))
                .build());
        }

        MediaType requestBodyMediaType = effectiveConsumes.get(0);
        String requestBodyMimeType = requestBodyMediaType.toString();
        RequestBodyObject requestBody = params.stream()
            .filter(p -> p instanceof ResourceMethodParam.MessageBodyParam)
            .map(ResourceMethodParam.MessageBodyParam.class::cast)
            .map(messageBodyParam -> {
                Class bodyType = messageBodyParam.parameterHandle.getType();
                Type bodyParameterizedType = messageBodyParam.parameterHandle.getParameterizedType();
                SchemaReference schemaReference = SchemaReference.find(customSchemas, bodyType, bodyParameterizedType);
                SchemaObjectBuilder builder = schemaReference != null ? schemaReference.schema.toBuilder() :
                    schemaObjectFrom(bodyType, bodyParameterizedType, messageBodyParam.isRequired)
                        .withTitle(messageBodyParam.descriptionData.summary)
                        .withDescription(messageBodyParam.descriptionData.description);
                return requestBodyObject()
                    .withContent(singletonMap(requestBodyMimeType,
                        mediaTypeObject()
                            .withSchema(
                                schemaObjectCustomizer.customize(builder,
                                    schemaContext(SchemaObjectCustomizerTarget.REQUEST_BODY, null, bodyType, bodyParameterizedType, requestBodyMediaType))
                                    .build())
                            .withExample(messageBodyParam.descriptionData.example)
                            .build()))
                    .withDescription(messageBodyParam.descriptionData.summaryAndDescription())
                    .withRequired(messageBodyParam.isRequired)
                    .build();
            })
            .findFirst().orElse(null);

        if (requestBody == null) {
            List formParams = params.stream()
                .filter(p -> p instanceof ResourceMethodParam.RequestBasedParam)
                .map(ResourceMethodParam.RequestBasedParam.class::cast)
                .filter(p -> p.source == ResourceMethodParam.ValueSource.FORM_PARAM)
                .collect(Collectors.toList());
            if (!formParams.isEmpty()) {
                List required = new ArrayList<>();
                requestBody = requestBodyObject()
                    .withContent(singletonMap(requestBodyMimeType,
                        mediaTypeObject()
                            .withSchema(
                                schemaObject()
                                    .withType("object")
                                    .withRequired(required)
                                    .withProperties(
                                        formParams.stream().collect(
                                            toMap(n -> n.key,
                                                n -> {
                                                    if (n.isRequired) {
                                                        required.add(n.key);
                                                    }
                                                    Class paramType = n.parameterHandle.getType();
                                                    Type paramParameterizedType = n.parameterHandle.getParameterizedType();

                                                    SchemaReference schemaReference = SchemaReference.find(customSchemas, paramType, paramParameterizedType);
                                                    SchemaObjectBuilder schemaObjectBuilder = schemaReference != null ? schemaReference.schema.toBuilder() : schemaObjectFrom(paramType, paramParameterizedType, n.isRequired);
                                                    schemaObjectBuilder.withDeprecated(n.isDeprecated ? true : null);
                                                    if (n.hasExplicitDefault()) {
                                                        schemaObjectBuilder.withDefaultValue(n.defaultValue());
                                                    }
                                                    if (n.descriptionData != null) {
                                                        String desc = n.descriptionData.summaryAndDescription();
                                                        schemaObjectBuilder.withExample(n.descriptionData.example)
                                                            .withDescription(n.key.equals(desc) ? null : desc);
                                                    }
                                                    return schemaObjectCustomizer
                                                        .customize(schemaObjectBuilder, schemaContext(SchemaObjectCustomizerTarget.FORM_PARAM, n.key, paramType, paramParameterizedType, requestBodyMediaType))
                                                        .build();
                                                }))
                                    )
                                    .build()
                            )
                            .build()
                    ))
                    .build();
            }
        }

        return operationObject()
            .withSummary(descriptionData.summary)
            .withDescription(descriptionData.description)
            .withExternalDocs(descriptionData.externalDocumentation)
            .withDeprecated(isDeprecated ? true : null)
            .withRequestBody(requestBody)
            .withResponses(
                responsesObject()
                    .withHttpStatusCodes(httpStatusCodes)
                    .build())
            ;
    }

    private SchemaObjectCustomizerContext schemaContext(SchemaObjectCustomizerTarget target, String parameter, Class type, Type parameterizedType, MediaType mediaType) {
        return new SchemaObjectCustomizerContext(target, type, parameterizedType, resourceClass.resourceInstance, methodHandle, parameter, mediaType);
    }

    private static List getApiResponses(java.lang.reflect.Method methodHandle) {
        ApiResponses apiResponses = methodHandle.getDeclaredAnnotation(ApiResponses.class);
        List result = new ArrayList<>();
        if (apiResponses != null) {
            for (ApiResponse ar : apiResponses.value()) {
                result.add(ApiResponseObj.fromAnnotation(ar, methodHandle));
            }
        }
        ApiResponse single = methodHandle.getDeclaredAnnotation(ApiResponse.class);
        if (single != null) {
            result.add(ApiResponseObj.fromAnnotation(single, methodHandle));
        }
        if (result.isEmpty()) {
            Class returnType = methodHandle.getReturnType();
            Type genericReturnType = methodHandle.getGenericReturnType();
            String code = void.class.isAssignableFrom(returnType) ? "204" : "200";
            result.add(new ApiResponseObj(code, "Success", new ResponseHeader[0], returnType, genericReturnType, new String[0], null));
        }
        return result;
    }

    static Method getMuMethod(java.lang.reflect.Method restMethod) {
        Annotation[] annotations = restMethod.getAnnotations();
        Method value = null;
        for (Annotation annotation : annotations) {
            Class anno = annotation.annotationType();
            HttpMethod httpMethodAnno = anno.getAnnotation(HttpMethod.class);
            if (httpMethodAnno != null) {
                if (value != null) {
                    throw new IllegalArgumentException("The method " + restMethod + " has multiple HttpMethod annotations. Only one is allowed per method.");
                }
                value = Method.valueOf(httpMethodAnno.value());
            }
        }
        return value;
    }

    @Override
    public String toString() {
        return "ResourceMethod{" + resourceClass.resourceClassName() + "#" + methodHandle.getName() + "}";
    }

    boolean canProduceFor(List clientAccepts) {
        return MediaTypeHeaderDelegate.atLeastOneCompatible(effectiveProduces, clientAccepts, null);
    }

    boolean canConsume(MediaType requestBodyMediaType) {
        return MediaTypeHeaderDelegate.atLeastOneCompatible(effectiveConsumes, Collections.singletonList(requestBodyMediaType), null);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy