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

org.glassfish.jersey.microprofile.restclient.MethodModel Maven / Gradle / Ivy

/*
 * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019 Payara Foundation and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.microprofile.restclient;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.InterceptionType;
import jakarta.enterprise.inject.spi.Interceptor;
import jakarta.json.JsonValue;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.MatrixParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;

import org.eclipse.microprofile.rest.client.RestClientDefinitionException;
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor;
import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptorFactory;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.glassfish.jersey.internal.util.collection.ImmutableMultivaluedMap;

/**
 * Method model contains all information about method defined in rest client interface.
 *
 * @author David Kral
 * @author Patrik Dudits
 * @author Tomas Langer
 */
class MethodModel {

    private static final String INVOKED_METHOD = "org.eclipse.microprofile.rest.client.invokedMethod";

    private final InterfaceModel interfaceModel;

    private final Method method;
    private final GenericType returnType;
    private final String httpMethod;
    private final String path;
    private final String[] produces;
    private final String[] consumes;
    private final List parameterModels;
    private final List clientHeaders;
    private final List invocationInterceptors;
    private final RestClientModel subResourceModel;

    /**
     * Processes interface method and creates new instance of the model.
     *
     * @param interfaceModel
     * @param method
     * @return
     */
    static MethodModel from(InterfaceModel interfaceModel, Method method) {
        return new Builder(interfaceModel, method).build();
    }

    private MethodModel(Builder builder) {
        this.method = builder.method;
        this.interfaceModel = builder.interfaceModel;
        this.returnType = builder.returnType;
        this.httpMethod = builder.httpMethod;
        this.path = builder.pathValue;
        this.produces = builder.produces;
        this.consumes = builder.consumes;
        this.parameterModels = builder.parameterModels;
        this.clientHeaders = builder.clientHeaders;
        this.invocationInterceptors = builder.invocationInterceptors;
        if (httpMethod.isEmpty()) {
            subResourceModel = RestClientModel.from(RestClientContext.builder(returnType.getRawType())
                                                            .copyFrom(interfaceModel.context())
                                                            .build());
        } else {
            subResourceModel = null;
        }
    }

    /**
     * Returns all registered cdi interceptors to this method.
     *
     * @return registered interceptors
     */
    List getInvocationInterceptors() {
        return invocationInterceptors;
    }

    /**
     * Invokes corresponding method according to
     *
     * @param classLevelTarget
     * @param method
     * @param args
     * @return
     */
    @SuppressWarnings("unchecked")
    //I am checking the type of parameter and I know it should handle instance I am sending
    Object invokeMethod(WebTarget classLevelTarget, Method method, Object[] args) {
        WebTarget methodLevelTarget = classLevelTarget.path(path);

        AtomicReference entity = new AtomicReference<>();
        AtomicReference webTargetAtomicReference = new AtomicReference<>(methodLevelTarget);
        parameterModels.stream()
                .filter(parameterModel -> parameterModel.handles(PathParam.class))
                .forEach(parameterModel ->
                                 webTargetAtomicReference.set((WebTarget)
                                                                      parameterModel
                                                                              .handleParameter(webTargetAtomicReference.get(),
                                                                                               PathParam.class,
                                                                                               args[parameterModel
                                                                                                       .getParamPosition()])));

        parameterModels.stream()
                .filter(ParamModel::isEntity)
                .findFirst()
                .ifPresent(parameterModel -> entity.set(args[parameterModel.getParamPosition()]));

        Form form = handleForm(args);

        WebTarget webTarget = webTargetAtomicReference.get();
        if (httpMethod.isEmpty()) {
            //sub resource method
            return subResourceProxy(webTarget, returnType.getRawType());
        }
        webTarget = addQueryParams(webTarget, args);
        webTarget = addMatrixParams(webTarget, args);

        MultivaluedMap customHeaders = addCustomHeaders(args);

        Object entityToUse = entity.get();
        if (entityToUse == null && !form.asMap().isEmpty()) {
            entityToUse = form;
        }
        if (entityToUse == null) {
            customHeaders.remove(HttpHeaders.CONTENT_TYPE);
        }

        Invocation.Builder builder = webTarget
                .request(produces)
                .property(INVOKED_METHOD, method)
                .headers(customHeaders);
        builder = addCookies(builder, args);

        Object response;

        if (CompletionStage.class.isAssignableFrom(method.getReturnType())) {
            response = asynchronousCall(builder, entityToUse, method, customHeaders);
        } else {
            response = synchronousCall(builder, entityToUse, method, customHeaders);
        }
        return response;
    }

    @SuppressWarnings("unchecked")
    private Form handleForm(Object[] args) {
        final Form form = new Form();
        parameterModels.stream()
                .filter(parameterModel -> parameterModel.handles(FormParam.class))
                .forEach(parameterModel -> parameterModel.handleParameter(form,
                                                                          FormParam.class,
                                                                          args[parameterModel.getParamPosition()]));
        return form;
    }

    private Object synchronousCall(Invocation.Builder builder,
                                   Object entity,
                                   Method method,
                                   MultivaluedMap customHeaders) {
        Response response;

        if (entity != null
                && !httpMethod.equals(GET.class.getSimpleName())
                && !httpMethod.equals(DELETE.class.getSimpleName())) {
            response = builder.method(httpMethod, Entity.entity(entity, getContentType(customHeaders)));
        } else {
            response = builder.method(httpMethod);
        }

        evaluateResponse(response, method);

        if (returnType.getType().equals(Void.class)) {
            return null;
        } else if (returnType.getType().equals(Response.class)) {
            return response;
        }
        return response.readEntity(returnType);
    }

    private CompletableFuture asynchronousCall(Invocation.Builder builder,
                                               Object entity,
                                               Method method,
                                               MultivaluedMap customHeaders) {

        //AsyncInterceptors initialization
        List asyncInterceptors = interfaceModel.context().asyncInterceptorFactories().stream()
                .map(AsyncInvocationInterceptorFactory::newInterceptor)
                .collect(Collectors.toList());
        asyncInterceptors.forEach(AsyncInvocationInterceptor::prepareContext);
        ExecutorServiceWrapper.asyncInterceptors.set(asyncInterceptors);

        CompletableFuture result = new CompletableFuture<>();
        Future theFuture;
        if (entity != null
                && !httpMethod.equals(GET.class.getSimpleName())
                && !httpMethod.equals(DELETE.class.getSimpleName())) {
            theFuture = builder.async().method(httpMethod, Entity.entity(entity, getContentType(customHeaders)));
        } else {
            theFuture = builder.async().method(httpMethod);
        }

        CompletableFuture completableFuture = (CompletableFuture) theFuture;
        completableFuture.thenAccept(response -> {
            asyncInterceptors.forEach(AsyncInvocationInterceptor::removeContext);
            try {
                evaluateResponse(response, method);
                if (returnType.getType().equals(Void.class)) {
                    result.complete(null);
                } else if (returnType.getType().equals(Response.class)) {
                    result.complete(response);
                } else {
                    result.complete(response.readEntity(returnType));
                }
            } catch (Exception e) {
                result.completeExceptionally(e);
            }
        }).exceptionally(throwable -> {
            // Since it could have been the removeContext method causing exception, we need to be more careful
            // to assure, that the future completes
            asyncInterceptors.forEach(interceptor -> {
                try {
                    interceptor.removeContext();
                } catch (Throwable e) {
                    throwable.addSuppressed(e);
                }
            });
            result.completeExceptionally(throwable);
            return null;
        });

        return result;
    }

    private String getContentType(MultivaluedMap customHeaders) {
        return (String) customHeaders.getFirst(HttpHeaders.CONTENT_TYPE);
    }

    @SuppressWarnings("unchecked")
    private  T subResourceProxy(WebTarget webTarget, Class subResourceType) {
        return (T) Proxy.newProxyInstance(subResourceType.getClassLoader(),
                                          new Class[] {subResourceType},
                                          new ProxyInvocationHandler(webTarget, subResourceModel)
        );
    }

    @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending
    private WebTarget addQueryParams(WebTarget webTarget, Object[] args) {
        Map queryParams = new HashMap<>();
        WebTarget toReturn = webTarget;
        parameterModels.stream()
                .filter(parameterModel -> parameterModel.handles(QueryParam.class))
                .forEach(parameterModel -> parameterModel.handleParameter(queryParams,
                                                                          QueryParam.class,
                                                                          args[parameterModel.getParamPosition()]));

        for (Map.Entry entry : queryParams.entrySet()) {
            toReturn = toReturn.queryParam(entry.getKey(), entry.getValue());
        }
        return toReturn;
    }

    @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending
    private WebTarget addMatrixParams(WebTarget webTarget, Object[] args) {
        AtomicReference toReturn = new AtomicReference<>(webTarget);
        parameterModels.stream()
                .filter(parameterModel -> parameterModel.handles(MatrixParam.class))
                .forEach(parameterModel -> toReturn
                        .set((WebTarget) parameterModel.handleParameter(toReturn.get(),
                                                                        MatrixParam.class,
                                                                        args[parameterModel.getParamPosition()])));
        return toReturn.get();
    }

    @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending
    private Invocation.Builder addCookies(Invocation.Builder builder, Object[] args) {
        Map cookies = new HashMap<>();
        Invocation.Builder toReturn = builder;
        parameterModels.stream()
                .filter(parameterModel -> parameterModel.handles(CookieParam.class))
                .forEach(parameterModel -> parameterModel.handleParameter(cookies,
                                                                          CookieParam.class,
                                                                          args[parameterModel.getParamPosition()]));

        for (Map.Entry entry : cookies.entrySet()) {
            toReturn = toReturn.cookie(entry.getKey(), entry.getValue());
        }
        return toReturn;
    }

    private MultivaluedMap addCustomHeaders(Object[] args) {
        MultivaluedMap result = new MultivaluedHashMap<>();
        for (Map.Entry> entry : resolveCustomHeaders(args).entrySet()) {
            entry.getValue().forEach(val -> result.add(entry.getKey(), val));
        }
        for (String produce : produces) {
            result.add(HttpHeaders.ACCEPT, produce);
        }
        result.putIfAbsent(HttpHeaders.CONTENT_TYPE, Collections.singletonList(consumes[0]));
        return new MultivaluedHashMap(result);
    }

    @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending
    private MultivaluedMap resolveCustomHeaders(Object[] args) {
        MultivaluedMap customHeaders = new MultivaluedHashMap<>();
        customHeaders.putAll(createMultivaluedHeadersMap(interfaceModel.getClientHeaders()));
        customHeaders.putAll(createMultivaluedHeadersMap(clientHeaders));
        parameterModels.stream()
                .filter(parameterModel -> parameterModel.handles(HeaderParam.class))
                .forEach(parameterModel -> parameterModel.handleParameter(customHeaders,
                                                                          HeaderParam.class,
                                                                          args[parameterModel.getParamPosition()]));

        MultivaluedMap inbound = new MultivaluedHashMap<>();
        Optional headersContext = HeadersContext.get();
        headersContext.ifPresent(hc -> inbound.putAll(hc.inboundHeaders()));
        if (!headersContext.isPresent()) {
            for (InboundHeadersProvider provider : interfaceModel.context().inboundHeadersProviders()) {
                inbound.putAll(provider.inboundHeaders());
            }
        }

        ImmutableMultivaluedMap unmodif = new ImmutableMultivaluedMap<>(customHeaders);
        if (interfaceModel.getClientHeadersFactory().isPresent()) {
            ClientHeadersFactory factory = interfaceModel.getClientHeadersFactory().get();
            MultivaluedMap fromFactory = factory.update(inbound, unmodif);
            customHeaders.putAll(fromFactory);
        }
        return customHeaders;
    }

    private  MultivaluedMap createMultivaluedHeadersMap(List clientHeaders) {
        MultivaluedMap customHeaders = new MultivaluedHashMap<>();
        for (ClientHeaderParamModel clientHeaderParamModel : clientHeaders) {
            if (clientHeaderParamModel.getComputeMethod() == null) {
                customHeaders
                        .put(clientHeaderParamModel.getHeaderName(), Arrays.asList(clientHeaderParamModel.getHeaderValue()));
            } else {
                try {
                    Method method = clientHeaderParamModel.getComputeMethod();
                    if (method.isDefault()) {
                        //method is interface default
                        //we need to create instance of the interface to be able to call default method
                        T instance = (T) ReflectionUtil.createProxyInstance(interfaceModel.getRestClientClass());
                        if (method.getParameterCount() > 0) {
                            customHeaders.put(clientHeaderParamModel.getHeaderName(),
                                              createList(method.invoke(instance, clientHeaderParamModel.getHeaderName())));
                        } else {
                            customHeaders.put(clientHeaderParamModel.getHeaderName(),
                                              createList(method.invoke(instance, null)));
                        }
                    } else {
                        //Method is static
                        if (method.getParameterCount() > 0) {
                            customHeaders.put(clientHeaderParamModel.getHeaderName(),
                                              createList(method.invoke(null, clientHeaderParamModel.getHeaderName())));
                        } else {
                            customHeaders.put(clientHeaderParamModel.getHeaderName(),
                                              createList(method.invoke(null, null)));
                        }
                    }
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    if (clientHeaderParamModel.isRequired()) {
                        if (e.getCause() instanceof RuntimeException) {
                            throw (RuntimeException) e.getCause();
                        }
                        throw new RuntimeException(e.getCause());
                    }
                }
            }
        }
        return customHeaders;
    }

    private static List createList(Object value) {
        if (value instanceof String[]) {
            String[] array = (String[]) value;
            return Arrays.asList(array);
        }
        String s = (String) value;
        return Collections.singletonList(s);
    }

    /**
     * Evaluation of {@link Response} if it is applicable for any of the registered {@link ResponseExceptionMapper} providers.
     *
     * @param response obtained response
     * @param method   called method
     */
    private void evaluateResponse(Response response, Method method) {
        ResponseExceptionMapper lowestMapper = null;
        Throwable throwable = null;
        for (ResponseExceptionMapper responseExceptionMapper : interfaceModel.context().responseExceptionMappers()) {
            if (responseExceptionMapper.handles(response.getStatus(), response.getHeaders())) {
                if (lowestMapper == null
                        || throwable == null
                        || lowestMapper.getPriority() > responseExceptionMapper.getPriority()) {
                    lowestMapper = responseExceptionMapper;
                    Throwable tmp = lowestMapper.toThrowable(response);
                    if (tmp != null) {
                        throwable = tmp;
                    }
                }
            }
        }
        if (throwable != null) {
            if (throwable instanceof RuntimeException) {
                throw (RuntimeException) throwable;
            } else if (throwable instanceof Error) {
                throw (Error) throwable;
            }
            for (Class exception : method.getExceptionTypes()) {
                if (throwable.getClass().isAssignableFrom(exception)) {
                    throw new WebApplicationException(throwable);
                }
            }
        }
    }

    private static String parseHttpMethod(InterfaceModel classModel, Method method) {
        List> httpAnnotations = InterfaceUtil.getHttpAnnotations(method);
        if (httpAnnotations.size() > 1) {
            throw new RestClientDefinitionException("Method can't have more then one annotation of @HttpMethod type. "
                                                            + "See " + classModel.getRestClientClass().getName()
                                                            + "::" + method.getName());
        }
        if (httpAnnotations.isEmpty()) {
            //Sub resource method
            return "";
        }
        return httpAnnotations.get(0).getSimpleName();
    }

    private static List parameterModels(InterfaceModel classModel, Method method) {
        List parameterModels = new ArrayList<>();
        final List jerseyParameters = org.glassfish.jersey.model.Parameter
                .create(classModel.getRestClientClass(), classModel.getRestClientClass(),
                        method, false);
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            parameterModels.add(ParamModel.from(classModel, parameters[i].getType(), parameters[i], jerseyParameters.get(i), i));
        }
        return parameterModels;
    }

    private static class Builder {

        private final InterfaceModel interfaceModel;
        private final Method method;

        private GenericType returnType;
        private String httpMethod;
        private String pathValue;
        private String[] produces;
        private String[] consumes;
        private List parameterModels;
        private List clientHeaders;
        private List invocationInterceptors;

        private Builder(InterfaceModel interfaceModel, Method method) {
            this.interfaceModel = interfaceModel;
            this.method = method;
            filterAllInterceptorAnnotations();
        }

        private void filterAllInterceptorAnnotations() {
            invocationInterceptors = new ArrayList<>();
            BeanManager beanManager = interfaceModel.context().beanManager();
            if (beanManager != null) {
                Set interceptorAnnotations = new HashSet<>();
                for (Annotation annotation : method.getAnnotations()) {
                    if (beanManager.isInterceptorBinding(annotation.annotationType())) {
                        interceptorAnnotations.add(annotation);
                    }
                }
                interceptorAnnotations.addAll(interfaceModel.getInterceptorAnnotations());
                Annotation[] allInterceptorAnnotations = interceptorAnnotations.toArray(new Annotation[0]);
                if (allInterceptorAnnotations.length == 0) {
                    return;
                }
                List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE,
                                                                                    allInterceptorAnnotations);
                if (!interceptors.isEmpty()) {
                    for (Interceptor interceptor : interceptors) {
                        Object interceptorInstance = beanManager.getReference(interceptor,
                                                                              interceptor.getBeanClass(),
                                                                              interfaceModel.getCreationalContext());
                        invocationInterceptors.add(new InterceptorInvocationContext
                                .InvocationInterceptor(interceptorInstance,
                                                       interceptor));
                    }
                }
            }
        }

        /**
         * Return type of the method.
         *
         * @param returnType Method return type
         */
        private void returnType(Type returnType) {
            if (returnType instanceof ParameterizedType
                    && CompletionStage.class.isAssignableFrom((Class) ((ParameterizedType) returnType).getRawType())) {
                this.returnType = new GenericType<>(((ParameterizedType) returnType).getActualTypeArguments()[0]);
            } else {
                this.returnType = new GenericType<>(returnType);
            }
        }

        /**
         * HTTP method of the method.
         *
         * @param httpMethod HTTP method of the method
         */
        private void httpMethod(String httpMethod) {
            this.httpMethod = httpMethod;
        }

        /**
         * Path value from {@link Path} annotation. If annotation is null, empty String is set as path.
         *
         * @param path {@link Path} annotation
         */
        private void pathValue(Path path) {
            this.pathValue = path != null ? path.value() : "";
            //if only / is added to path like this "localhost:80/test" it makes invalid path "localhost:80/test/"
            this.pathValue = pathValue.equals("/") ? "" : pathValue;
        }

        /**
         * Extracts MediaTypes from {@link Produces} annotation.
         * If annotation is null, value from {@link InterfaceModel} is set.
         *
         * @param produces {@link Produces} annotation
         */
        private void produces(Produces produces) {
            this.produces = produces == null ? interfaceModel.getProduces() : produces.value();
        }

        /**
         * Extracts MediaTypes from {@link Consumes} annotation.
         * If annotation is null, value from {@link InterfaceModel} is set.
         *
         * @param consumes {@link Consumes} annotation
         */
        private void consumes(Consumes consumes) {
            this.consumes = consumes == null ? interfaceModel.getConsumes() : consumes.value();
        }

        /**
         * {@link List} of transformed method parameters.
         *
         * @param parameterModels {@link List} of parameters
         */
        private void parameters(List parameterModels) {
            this.parameterModels = parameterModels;
        }

        /**
         * Process data from {@link ClientHeaderParam} annotation to extract methods and values.
         *
         * @param clientHeaderParams {@link ClientHeaderParam} annotations
         */
        private void clientHeaders(ClientHeaderParam[] clientHeaderParams) {
            clientHeaders = Arrays.stream(clientHeaderParams)
                    .map(clientHeaderParam -> new ClientHeaderParamModel(interfaceModel.getRestClientClass(), clientHeaderParam))
                    .collect(Collectors.toList());
        }

        /**
         * Creates new MethodModel instance.
         *
         * @return new instance
         */
        MethodModel build() {
            returnType(method.getGenericReturnType());
            httpMethod(parseHttpMethod(interfaceModel, method));
            pathValue(method.getAnnotation(Path.class));
            produces(method.getAnnotation(Produces.class));
            consumes(method.getAnnotation(Consumes.class));
            parameters(parameterModels(interfaceModel, method));
            clientHeaders(method.getAnnotationsByType(ClientHeaderParam.class));

            validateParameters();
            validateHeaderDuplicityNames();

            if (isJsonValue(returnType.getType())) {
                this.produces = new String[] {MediaType.APPLICATION_JSON};
            }

            parameterModels.stream()
                    .filter(ParamModel::isEntity)
                    .map(ParamModel::getType)
                    .filter(this::isJsonValue)
                    .findFirst()
                    .ifPresent(paramModel -> this.consumes = new String[] {MediaType.APPLICATION_JSON});

            return new MethodModel(this);
        }

        private void validateParameters() {
            UriBuilder uriBuilder = UriBuilder.fromUri(interfaceModel.getPath()).path(pathValue);
            List parameters = InterfaceUtil.getAllMatchingParams(uriBuilder.toTemplate());
            List methodPathParameters = new ArrayList<>();
            List pathHandlingParams = parameterModels.stream()
                    .filter(parameterModel -> parameterModel.handles(PathParam.class))
                    .collect(Collectors.toList());
            for (ParamModel paramModel : pathHandlingParams) {
                if (paramModel instanceof PathParamModel) {
                    methodPathParameters.add(((PathParamModel) paramModel).getPathParamName());
                } else if (paramModel instanceof BeanParamModel) {
                    for (ParamModel beanPathParams : ((BeanParamModel) paramModel).getAllParamsWithType(PathParam.class)) {
                        methodPathParameters.add(((PathParamModel) beanPathParams).getPathParamName());
                    }
                }
            }
            for (String parameterName : methodPathParameters) {
                if (!parameters.contains(parameterName)) {
                    throw new RestClientDefinitionException("Parameter name " + parameterName + " on "
                                                                    + interfaceModel.getRestClientClass().getName()
                                                                    + "::" + method.getName()
                                                                    + " doesn't match any @Path variable name.");
                }
                parameters.remove(parameterName);
            }
            if (!parameters.isEmpty()) {
                throw new RestClientDefinitionException("Some variable names does not have matching @PathParam "
                                                                + "defined on method " + interfaceModel.getRestClientClass()
                        .getName()
                                                                + "::" + method.getName());
            }
            List entities = parameterModels.stream()
                    .filter(ParamModel::isEntity)
                    .collect(Collectors.toList());
            if (entities.size() > 1) {
                throw new RestClientDefinitionException("You cant have more than 1 entity method parameter! Check "
                                                                + interfaceModel.getRestClientClass().getName()
                                                                + "::" + method.getName());
            }
        }

        private void validateHeaderDuplicityNames() {
            ArrayList names = new ArrayList<>();
            for (ClientHeaderParamModel clientHeaderParamModel : clientHeaders) {
                String headerName = clientHeaderParamModel.getHeaderName();
                if (names.contains(headerName)) {
                    throw new RestClientDefinitionException("Header name cannot be registered more then once on the same target."
                                                                    + "See " + interfaceModel.getRestClientClass().getName());
                }
                names.add(headerName);
            }
        }

        private boolean isJsonValue(Type type) {
            return type instanceof Class && JsonValue.class.isAssignableFrom((Class) type);
        }
    }
}