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

com.microsoft.rest.v2.SwaggerMethodParser Maven / Gradle / Ivy

/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See License.txt in the project root for
 * license information.
 */

package com.microsoft.rest.v2;

import com.microsoft.rest.v2.annotations.BodyParam;
import com.microsoft.rest.v2.annotations.DELETE;
import com.microsoft.rest.v2.annotations.ExpectedResponses;
import com.microsoft.rest.v2.annotations.GET;
import com.microsoft.rest.v2.annotations.HEAD;
import com.microsoft.rest.v2.annotations.HeaderParam;
import com.microsoft.rest.v2.annotations.Headers;
import com.microsoft.rest.v2.annotations.HostParam;
import com.microsoft.rest.v2.annotations.PATCH;
import com.microsoft.rest.v2.annotations.POST;
import com.microsoft.rest.v2.annotations.PUT;
import com.microsoft.rest.v2.annotations.PathParam;
import com.microsoft.rest.v2.annotations.QueryParam;
import com.microsoft.rest.v2.annotations.ReturnValueWireType;
import com.microsoft.rest.v2.annotations.UnexpectedResponseExceptionType;
import com.microsoft.rest.v2.http.HttpHeader;
import com.microsoft.rest.v2.http.HttpHeaders;
import com.microsoft.rest.v2.http.HttpMethod;
import com.microsoft.rest.v2.protocol.SerializerAdapter;
import com.microsoft.rest.v2.util.TypeUtil;
import com.microsoft.rest.v2.util.escapers.PercentEscaper;
import com.microsoft.rest.v2.util.escapers.UrlEscapers;
import io.reactivex.Observable;
import io.reactivex.Single;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This class parses details of a specific Swagger REST API call from a provided Swagger interface
 * method.
 */
public class SwaggerMethodParser {
    private final SerializerAdapter serializer;
    private final String rawHost;
    private final String fullyQualifiedMethodName;
    private HttpMethod httpMethod;
    private String relativePath;
    private final List hostSubstitutions = new ArrayList<>();
    private final List pathSubstitutions = new ArrayList<>();
    private final List querySubstitutions = new ArrayList<>();
    private final List headerSubstitutions = new ArrayList<>();
    private final HttpHeaders headers = new HttpHeaders();
    private Integer bodyContentMethodParameterIndex;
    private String bodyContentType;
    private Type bodyJavaType;
    private int[] expectedStatusCodes;
    private Type returnType;
    private Type returnValueWireType;
    private Class exceptionType;
    private Class exceptionBodyType;

    /**
     * Create a new SwaggerMethodParser object using the provided fully qualified method name.
     * @param swaggerMethod The Swagger method to parse.
     * @param rawHost The raw host value from the @Host annotation. Before this can be used as the
     *                host value in an HTTP request, it must be processed through the possible host
     *                substitutions.
     */
    SwaggerMethodParser(Method swaggerMethod, SerializerAdapter serializer, String rawHost) {
        this.serializer = serializer;
        this.rawHost = rawHost;

        final Class swaggerInterface = swaggerMethod.getDeclaringClass();

        fullyQualifiedMethodName = swaggerInterface.getName() + "." + swaggerMethod.getName();

        if (swaggerMethod.isAnnotationPresent(GET.class)) {
            setHttpMethodAndRelativePath(HttpMethod.GET, swaggerMethod.getAnnotation(GET.class).value());
        }
        else if (swaggerMethod.isAnnotationPresent(PUT.class)) {
            setHttpMethodAndRelativePath(HttpMethod.PUT, swaggerMethod.getAnnotation(PUT.class).value());
        }
        else if (swaggerMethod.isAnnotationPresent(HEAD.class)) {
            setHttpMethodAndRelativePath(HttpMethod.HEAD, swaggerMethod.getAnnotation(HEAD.class).value());
        }
        else if (swaggerMethod.isAnnotationPresent(DELETE.class)) {
            setHttpMethodAndRelativePath(HttpMethod.DELETE, swaggerMethod.getAnnotation(DELETE.class).value());
        }
        else if (swaggerMethod.isAnnotationPresent(POST.class)) {
            setHttpMethodAndRelativePath(HttpMethod.POST, swaggerMethod.getAnnotation(POST.class).value());
        }
        else if (swaggerMethod.isAnnotationPresent(PATCH.class)) {
            setHttpMethodAndRelativePath(HttpMethod.PATCH, swaggerMethod.getAnnotation(PATCH.class).value());
        }
        else {
            final ArrayList> requiredAnnotationOptions = new ArrayList<>();
            requiredAnnotationOptions.add(GET.class);
            requiredAnnotationOptions.add(PUT.class);
            requiredAnnotationOptions.add(HEAD.class);
            requiredAnnotationOptions.add(DELETE.class);
            requiredAnnotationOptions.add(POST.class);
            requiredAnnotationOptions.add(PATCH.class);
            throw new MissingRequiredAnnotationException(requiredAnnotationOptions, swaggerMethod);
        }

        returnType = swaggerMethod.getGenericReturnType();

        final ReturnValueWireType returnValueWireTypeAnnotation = swaggerMethod.getAnnotation(ReturnValueWireType.class);
        if (returnValueWireTypeAnnotation != null) {
            Class returnValueWireType = returnValueWireTypeAnnotation.value();
            if (returnValueWireType == Base64Url.class || returnValueWireType == UnixTime.class || returnValueWireType == DateTimeRfc1123.class) {
                this.returnValueWireType = returnValueWireType;
            }
            else {
                if (TypeUtil.isTypeOrSubTypeOf(returnValueWireType, List.class)) {
                    this.returnValueWireType = returnValueWireType.getGenericInterfaces()[0];
                }
            }
        }

        if (swaggerMethod.isAnnotationPresent(Headers.class)) {
            final Headers headersAnnotation = swaggerMethod.getAnnotation(Headers.class);
            final String[] headers = headersAnnotation.value();
            for (final String header : headers) {
                final int colonIndex = header.indexOf(":");
                if (colonIndex >= 0) {
                    final String headerName = header.substring(0, colonIndex).trim();
                    if (!headerName.isEmpty()) {
                        final String headerValue = header.substring(colonIndex + 1).trim();
                        if (!headerValue.isEmpty()) {
                            this.headers.set(headerName, headerValue);
                        }
                    }
                }
            }
        }

        final ExpectedResponses expectedResponses = swaggerMethod.getAnnotation(ExpectedResponses.class);
        if (expectedResponses != null) {
            expectedStatusCodes = expectedResponses.value();
        }

        final UnexpectedResponseExceptionType unexpectedResponseExceptionType = swaggerMethod.getAnnotation(UnexpectedResponseExceptionType.class);
        if (unexpectedResponseExceptionType == null) {
            exceptionType = RestException.class;
        }
        else {
            exceptionType = unexpectedResponseExceptionType.value();
        }

        try {
            final Method exceptionBodyMethod = exceptionType.getDeclaredMethod("body");
            exceptionBodyType = exceptionBodyMethod.getReturnType();
        } catch (NoSuchMethodException e) {
            // Should always have a body() method. Register Object as a fallback plan.
            exceptionBodyType = Object.class;
        }

        final Annotation[][] allParametersAnnotations = swaggerMethod.getParameterAnnotations();
        for (int parameterIndex = 0; parameterIndex < allParametersAnnotations.length; ++parameterIndex) {
            final Annotation[] parameterAnnotations = swaggerMethod.getParameterAnnotations()[parameterIndex];
            for (final Annotation annotation : parameterAnnotations) {
                final Class annotationType = annotation.annotationType();
                if (annotationType.equals(HostParam.class)) {
                    final HostParam hostParamAnnotation = (HostParam) annotation;
                    hostSubstitutions.add(new Substitution(hostParamAnnotation.value(), parameterIndex, !hostParamAnnotation.encoded()));
                }
                else if (annotationType.equals(PathParam.class)) {
                    final PathParam pathParamAnnotation = (PathParam) annotation;
                    pathSubstitutions.add(new Substitution(pathParamAnnotation.value(), parameterIndex, !pathParamAnnotation.encoded()));
                }
                else if (annotationType.equals(QueryParam.class)) {
                    final QueryParam queryParamAnnotation = (QueryParam) annotation;
                    querySubstitutions.add(new Substitution(queryParamAnnotation.value(), parameterIndex, !queryParamAnnotation.encoded()));
                }
                else if (annotationType.equals(HeaderParam.class)) {
                    final HeaderParam headerParamAnnotation = (HeaderParam) annotation;
                    headerSubstitutions.add(new Substitution(headerParamAnnotation.value(), parameterIndex, false));
                }
                else if (annotationType.equals(BodyParam.class)) {
                    final BodyParam bodyParamAnnotation = (BodyParam) annotation;
                    bodyContentMethodParameterIndex = parameterIndex;
                    bodyContentType = bodyParamAnnotation.value();
                    bodyJavaType = swaggerMethod.getGenericParameterTypes()[parameterIndex];
                }
            }
        }
    }

    /**
     * Get the fully qualified method that was called to invoke this HTTP request.
     * @return The fully qualified method that was called to invoke this HTTP request.
     */
    public String fullyQualifiedMethodName() {
        return fullyQualifiedMethodName;
    }

    /**
     * Get the HTTP method that will be used to complete the Swagger method's request.
     * @return The HTTP method that will be used to complete the Swagger method's request.
     */
    public HttpMethod httpMethod() {
        return httpMethod;
    }

    /**
     * Get the HTTP response status codes that are expected when a request is sent out for this
     * Swagger method. If the returned int[] is null, then all status codes less than 400 are
     * allowed.
     * @return The expected HTTP response status codes for this Swagger method or null if all status
     * codes less than 400 are allowed.
     */
    public int[] expectedStatusCodes() {
        return expectedStatusCodes;
    }

    /**
     * Get the scheme to use for HTTP requests for this Swagger method.
     * @param swaggerMethodArguments The arguments to use for scheme/host substitutions.
     * @return The final host to use for HTTP requests for this Swagger method.
     */
    public String scheme(Object[] swaggerMethodArguments) {
        final String substitutedHost = applySubstitutions(rawHost, hostSubstitutions, swaggerMethodArguments, UrlEscapers.PATH_ESCAPER);
        final String[] substitutedHostParts = substitutedHost.split("://");
        return substitutedHostParts.length < 1 ? null : substitutedHostParts[0];
    }

    /**
     * Get the host to use for HTTP requests for this Swagger method.
     * @param swaggerMethodArguments The arguments to use for host substitutions.
     * @return The final host to use for HTTP requests for this Swagger method.
     */
    public String host(Object[] swaggerMethodArguments) {
        final String substitutedHost = applySubstitutions(rawHost, hostSubstitutions, swaggerMethodArguments, UrlEscapers.PATH_ESCAPER);
        final String[] substitutedHostParts = substitutedHost.split("://");
        return substitutedHostParts.length < 2 ? substitutedHost : substitutedHost.split("://")[1];
    }

    /**
     * Get the path that will be used to complete the Swagger method's request.
     * @param methodArguments The method arguments to use with the path substitutions.
     * @return The path value with its placeholders replaced by the matching substitutions.
     */
    public String path(Object[] methodArguments) {
        return applySubstitutions(relativePath, pathSubstitutions, methodArguments, UrlEscapers.PATH_ESCAPER);
    }

    /**
     * Get the encoded query parameters that have been added to this value based on the provided
     * method arguments.
     * @param swaggerMethodArguments The arguments that will be used to create the query parameters'
     *                               values.
     * @return An Iterable with the encoded query parameters.
     */
    public Iterable encodedQueryParameters(Object[] swaggerMethodArguments) {
        final List result = new ArrayList<>();
        if (querySubstitutions != null) {
            final PercentEscaper escaper = UrlEscapers.QUERY_ESCAPER;

            for (Substitution querySubstitution : querySubstitutions) {
                final int parameterIndex = querySubstitution.methodParameterIndex();
                if (0 <= parameterIndex && parameterIndex < swaggerMethodArguments.length) {
                    final Object methodArgument = swaggerMethodArguments[querySubstitution.methodParameterIndex()];
                    String parameterValue = serialize(methodArgument);
                    if (parameterValue != null) {
                        if (querySubstitution.shouldEncode() && escaper != null) {
                            parameterValue = escaper.escape(parameterValue);
                        }

                        result.add(new EncodedParameter(querySubstitution.urlParameterName(), parameterValue));
                    }
                }
            }
        }
        return result;
    }

    /**
     * Get the headers that have been added to this value based on the provided method arguments.
     * @param swaggerMethodArguments The arguments that will be used to create the headers' values.
     * @return An Iterable with the headers.
     */
    public Iterable headers(Object[] swaggerMethodArguments) {
        final HttpHeaders result = new HttpHeaders(headers);

        if (headerSubstitutions != null) {
            for (Substitution headerSubstitution : headerSubstitutions) {
                final int parameterIndex = headerSubstitution.methodParameterIndex();
                if (0 <= parameterIndex && parameterIndex < swaggerMethodArguments.length) {
                    final Object methodArgument = swaggerMethodArguments[headerSubstitution.methodParameterIndex()];
                    if (methodArgument instanceof Map) {
                        final Map headerCollection = (Map) methodArgument;
                        final String headerCollectionPrefix = headerSubstitution.urlParameterName();
                        for (final Map.Entry headerCollectionEntry : headerCollection.entrySet()) {
                            final String headerName = headerCollectionPrefix + headerCollectionEntry.getKey();
                            final String headerValue = serialize(headerCollectionEntry.getValue());
                            result.set(headerName, headerValue);
                        }
                    } else {
                        final String headerName = headerSubstitution.urlParameterName();
                        final String headerValue = serialize(methodArgument);
                        result.set(headerName, headerValue);
                    }
                }
            }
        }

        return result;
    }

    /**
     * Get the {@link Context} passed into the proxy method.
     * @param swaggerMethodArguments the arguments passed to the proxy method
     * @return the Context, or null if no Context was provided
     */
    public Context context(Object[] swaggerMethodArguments) {
        Object firstArg = swaggerMethodArguments != null && swaggerMethodArguments.length > 0 ? swaggerMethodArguments[0] : null;
        if (firstArg instanceof Context) {
            return (Context) firstArg;
        } else {
            return null;
        }
    }

    /**
     * Get whether or not the provided response status code is one of the expected status codes for
     * this Swagger method.
     * @param responseStatusCode The status code that was returned in the HTTP response.
     * @return whether or not the provided response status code is one of the expected status codes
     * for this Swagger method.
     */
    public boolean isExpectedResponseStatusCode(int responseStatusCode) {
        return isExpectedResponseStatusCode(responseStatusCode, null);
    }

    /**
     * Get whether or not the provided response status code is one of the expected status codes for
     * this Swagger method.
     * @param responseStatusCode The status code that was returned in the HTTP response.
     * @param additionalAllowedStatusCodes An additional set of allowed status codes that will be
     *                                     merged with the existing set of allowed status codes for
     *                                     this query.
     * @return whether or not the provided response status code is one of the expected status codes
     * for this Swagger method.
     */
    public boolean isExpectedResponseStatusCode(int responseStatusCode, int[] additionalAllowedStatusCodes) {
        boolean result;

        if (expectedStatusCodes == null) {
            result = (responseStatusCode < 400);
        }
        else {
            result = contains(expectedStatusCodes, responseStatusCode)
                    || contains(additionalAllowedStatusCodes, responseStatusCode);
        }

        return result;
    }

    private static boolean contains(int[] values, int searchValue) {
        boolean result = false;

        if (values != null && values.length > 0) {
            for (int value : values) {
                if (searchValue == value) {
                    result = true;
                    break;
                }
            }
        }

        return result;
    }

    /**
     * Get the type of RestException that will be thrown if the HTTP response's status code is not
     * one of the expected status codes.
     * @return The type of RestException that will be thrown if the HTTP response's status code is
     * not one of the expected status codes.
     */
    public Class exceptionType() {
        return exceptionType;
    }

    /**
     * Get the type of body Object that a thrown RestException will contain if the HTTP response's
     * status code is not one of the expected status codes.
     * @return The type of body Object that a thrown RestException will contain if the HTTP
     * response's status code is not one of the expected status codes.
     */
    public Class exceptionBodyType() {
        return exceptionBodyType;
    }

    /**
     * Get the object to be used as the body of the HTTP request.
     * @param swaggerMethodArguments The method arguments to get the body object from.
     * @return The object that will be used as the body of the HTTP request.
     */
    public Object body(Object[] swaggerMethodArguments) {
        Object result = null;

        if (bodyContentMethodParameterIndex != null
                && swaggerMethodArguments != null
                && 0 <= bodyContentMethodParameterIndex
                && bodyContentMethodParameterIndex < swaggerMethodArguments.length) {
            result = swaggerMethodArguments[bodyContentMethodParameterIndex];
        }

        return result;
    }

    /**
     * @return the Content-Type of the body of this Swagger method.
     */
    public String bodyContentType() {
        return bodyContentType;
    }

    /**
     * Get the return type for the method that this object describes.
     * @return The return type for the method that this object describes.
     */
    public Type returnType() {
        return returnType;
    }


    /**
     * Get the type of the body parameter to this method, if present.
     * @return The return type of the body parameter to this method.
     */
    public Type bodyJavaType() {
        return bodyJavaType;
    }

    /**
     * Get the type that the return value will be send across the network as. If returnValueWireType
     * is not null, then the raw HTTP response body will need to parsed to this type and then
     * converted to the actual returnType.
     * @return The type that the raw HTTP response body will be sent as.
     */
    public Type returnValueWireType() {
        return returnValueWireType;
    }

    /**
     * @return Whether or not the Swagger method expects the response to contain a body.
     */
    public boolean expectsResponseBody() {
        boolean result = true;

        if (TypeUtil.isTypeOrSubTypeOf(returnType, Void.class)) {
            result = false;
        }
        else if (TypeUtil.isTypeOrSubTypeOf(returnType, Single.class) || TypeUtil.isTypeOrSubTypeOf(returnType, Observable.class)) {
            final ParameterizedType asyncReturnType = (ParameterizedType) returnType;
            final Type syncReturnType = asyncReturnType.getActualTypeArguments()[0];
            if (TypeUtil.isTypeOrSubTypeOf(syncReturnType, Void.class)) {
                result = false;
            } else if (TypeUtil.isTypeOrSubTypeOf(syncReturnType, RestResponse.class)) {
                result = restResponseTypeExpectsBody((ParameterizedType) TypeUtil.getSuperType(syncReturnType, RestResponse.class));
            }
        } else if (TypeUtil.isTypeOrSubTypeOf(returnType, RestResponse.class)) {
            result = restResponseTypeExpectsBody((ParameterizedType) TypeUtil.getSuperType(returnType, RestResponse.class));
        }

        return result;
    }

    private static boolean restResponseTypeExpectsBody(ParameterizedType restResponseReturnType) {
        boolean result = true;

        final Type[] restResponseTypeArguments = restResponseReturnType.getActualTypeArguments();
        final Type restResponseBodyType = restResponseTypeArguments[1];
        if (restResponseBodyType == Void.class) {
            result = false;
        }

        return result;
    }

    /**
     * Set both the HTTP method and the path that will be used to complete the Swagger method's
     * request.
     * @param httpMethod The HTTP method that will be used to complete the Swagger method's request.
     * @param relativePath The path in the URL that will be used to complete the Swagger method's
     *                     request.
     */
    private void setHttpMethodAndRelativePath(HttpMethod httpMethod, String relativePath) {
        this.httpMethod = httpMethod;
        this.relativePath = relativePath;
    }

    String serialize(Object value) {
        String result = null;
        if (value != null) {
            if (value instanceof String) {
                result = (String) value;
            }
            else {
                result = serializer.serializeRaw(value);
            }
        }
        return result;
    }

    private String applySubstitutions(String originalValue, Iterable substitutions, Object[] methodArguments, PercentEscaper escaper) {
        String result = originalValue;

        if (methodArguments != null) {
            for (Substitution substitution : substitutions) {
                final int substitutionParameterIndex = substitution.methodParameterIndex();
                if (0 <= substitutionParameterIndex && substitutionParameterIndex < methodArguments.length) {
                    final Object methodArgument = methodArguments[substitutionParameterIndex];

                    String substitutionValue = serialize(methodArgument);
                    if (substitutionValue != null && !substitutionValue.isEmpty() && substitution.shouldEncode() && escaper != null) {
                        substitutionValue = escaper.escape(substitutionValue);
                    }

                    result = result.replace("{" + substitution.urlParameterName() + "}", substitutionValue);
                }
            }
        }

        return result;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy