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

io.quarkus.rest.client.reactive.deployment.MicroProfileRestClientEnricher Maven / Gradle / Ivy

There is a newer version: 3.15.1
Show newest version
package io.quarkus.rest.client.reactive.deployment;

import static io.quarkus.arc.processor.DotNames.STRING;
import static io.quarkus.gizmo.MethodDescriptor.ofMethod;
import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_FORM_PARAM;
import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_FORM_PARAMS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_HEADER_PARAM;
import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_HEADER_PARAMS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_QUERY_PARAM;
import static io.quarkus.rest.client.reactive.deployment.DotNames.CLIENT_QUERY_PARAMS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_CLIENT_HEADERS;
import static org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.INVOKED_METHOD_PARAMETERS_PROP;
import static org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.INVOKED_METHOD_PROP;
import static org.jboss.resteasy.reactive.common.processor.HashUtil.sha1;
import static org.objectweb.asm.Opcodes.ACC_STATIC;

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.Configurable;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;

import org.eclipse.microprofile.rest.client.RestClientDefinitionException;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.eclipse.microprofile.rest.client.ext.DefaultClientHeadersFactoryImpl;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.client.api.ClientMultipartForm;
import org.jboss.resteasy.reactive.client.impl.WebTargetImpl;
import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;
import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.gizmo.AssignableResultHandle;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.CatchBlockCreator;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.ForEachLoop;
import io.quarkus.gizmo.Gizmo;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.TryBlock;
import io.quarkus.jaxrs.client.reactive.deployment.JaxrsClientReactiveEnricher;
import io.quarkus.rest.client.reactive.ClientFormParam;
import io.quarkus.rest.client.reactive.ClientQueryParam;
import io.quarkus.rest.client.reactive.ComputedParamContext;
import io.quarkus.rest.client.reactive.HeaderFiller;
import io.quarkus.rest.client.reactive.deployment.MicroProfileRestClientEnricher.RestClientAnnotationExpressionParser.Node;
import io.quarkus.rest.client.reactive.runtime.ClientQueryParamSupport;
import io.quarkus.rest.client.reactive.runtime.ComputedParamContextImpl;
import io.quarkus.rest.client.reactive.runtime.ConfigUtils;
import io.quarkus.rest.client.reactive.runtime.ExtendedHeaderFiller;
import io.quarkus.rest.client.reactive.runtime.HeaderFillerUtil;
import io.quarkus.rest.client.reactive.runtime.MicroProfileRestClientRequestFilter;
import io.quarkus.rest.client.reactive.runtime.NoOpHeaderFiller;
import io.quarkus.runtime.util.HashUtil;

/**
 * Alters client stub generation to add MicroProfile Rest Client features.
 *
 * Used mostly to handle the `@RegisterProvider` annotation that e.g. registers filters
 * and to add support for `@ClientHeaderParam` annotations for specifying (possibly) computed headers via annotations
 */
class MicroProfileRestClientEnricher implements JaxrsClientReactiveEnricher {
    private static final Logger log = Logger.getLogger(MicroProfileRestClientEnricher.class);

    public static final String DEFAULT_HEADERS_FACTORY = DefaultClientHeadersFactoryImpl.class.getName();

    private static final AnnotationInstance[] EMPTY_ANNOTATION_INSTANCES = new AnnotationInstance[0];

    private static final MethodDescriptor INVOCATION_BUILDER_PROPERTY_METHOD = MethodDescriptor.ofMethod(
            Invocation.Builder.class,
            "property", Invocation.Builder.class, String.class, Object.class);
    private static final MethodDescriptor LIST_ADD_METHOD = MethodDescriptor.ofMethod(List.class, "add", boolean.class,
            Object.class);

    private static final MethodDescriptor STRING_BUILDER_APPEND = MethodDescriptor.ofMethod(StringBuilder.class, "append",
            StringBuilder.class,
            String.class);

    private static final MethodDescriptor STRING_LENGTH = MethodDescriptor.ofMethod(String.class, "length", int.class);
    private static final MethodDescriptor MAP_PUT_METHOD = MethodDescriptor.ofMethod(Map.class, "put", Object.class,
            Object.class, Object.class);

    private static final MethodDescriptor HEADER_FILLER_UTIL_SHOULD_ADD_HEADER = MethodDescriptor.ofMethod(
            HeaderFillerUtil.class, "shouldAddHeader",
            boolean.class, String.class, MultivaluedMap.class, ClientRequestContext.class);
    private static final MethodDescriptor WEB_TARGET_IMPL_QUERY_PARAMS = MethodDescriptor.ofMethod(WebTargetImpl.class,
            "queryParam", WebTargetImpl.class, String.class, Collection.class);

    private static final MethodDescriptor ARRAYS_AS_LIST = ofMethod(Arrays.class, "asList", List.class, Object[].class);

    private static final MethodDescriptor COMPUTER_PARAM_CONTEXT_IMPL_CTOR = MethodDescriptor.ofConstructor(
            ComputedParamContextImpl.class, String.class,
            ClientRequestContext.class);

    private static final MethodDescriptor COMPUTER_PARAM_CONTEXT_IMPL_GET_METHOD_PARAM = MethodDescriptor.ofMethod(
            ComputedParamContextImpl.class, "getMethodParameterFromContext", Object.class, ClientRequestContext.class,
            int.class);

    private static final MethodDescriptor MAP_CONTAINS_KEY_METHOD = MethodDescriptor.ofMethod(Map.class,
            "containsKey", boolean.class, Object.class);
    private static final MethodDescriptor MULTIVALUED_MAP_ADD_ALL_METHOD = MethodDescriptor.ofMethod(MultivaluedMap.class,
            "addAll", void.class, Object.class, List.class);
    private static final MethodDescriptor QUARKUS_MULTIPART_FORM_ATTRIBUTE_METHOD = MethodDescriptor.ofMethod(
            ClientMultipartForm.class,
            "attribute", ClientMultipartForm.class, String.class, String.class, String.class);

    private static final Type STRING_TYPE = Type.create(DotName.STRING_NAME, Type.Kind.CLASS);

    private final Map interfaceMocks = new HashMap<>();

    @Override
    public void forClass(MethodCreator constructor, AssignableResultHandle webTargetBase,
            ClassInfo interfaceClass, IndexView index) {

        ResultHandle clientHeadersFactory = null;

        AnnotationInstance registerClientHeaders = interfaceClass.declaredAnnotation(REGISTER_CLIENT_HEADERS);

        if (registerClientHeaders != null) {
            String headersFactoryClass = registerClientHeaders.valueWithDefault(index)
                    .asClass().name().toString();

            if (!headersFactoryClass.equals(DEFAULT_HEADERS_FACTORY)) {
                // Arc.container().instance(...).get():
                ResultHandle containerHandle = constructor
                        .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class));
                ResultHandle instanceHandle = constructor.invokeInterfaceMethod(
                        MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class,
                                Annotation[].class),
                        containerHandle, constructor.loadClassFromTCCL(headersFactoryClass),
                        constructor.newArray(Annotation.class, 0));
                clientHeadersFactory = constructor
                        .invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class),
                                instanceHandle);
            } else {
                clientHeadersFactory = constructor
                        .newInstance(MethodDescriptor.ofConstructor(DEFAULT_HEADERS_FACTORY));
            }
        } else {
            clientHeadersFactory = constructor.loadNull();
        }

        ResultHandle restClientFilter = constructor.newInstance(
                MethodDescriptor.ofConstructor(MicroProfileRestClientRequestFilter.class, ClientHeadersFactory.class),
                clientHeadersFactory);

        constructor.assign(webTargetBase, constructor.invokeInterfaceMethod(
                MethodDescriptor.ofMethod(Configurable.class, "register", Configurable.class, Object.class),
                webTargetBase, restClientFilter));
    }

    @Override
    public void forWebTarget(MethodCreator methodCreator, IndexView index, ClassInfo interfaceClass, MethodInfo method,
            AssignableResultHandle webTarget, BuildProducer generatedClasses) {

        Map queryParamsByName = new HashMap<>();
        collectClientParamData(interfaceClass, method, queryParamsByName,
                CLIENT_QUERY_PARAM, CLIENT_QUERY_PARAMS, ClientQueryParam.class.getSimpleName());

        for (var queryEntry : queryParamsByName.entrySet()) {
            addQueryParam(method, methodCreator, queryEntry.getValue(), webTarget, generatedClasses, index);
        }
    }

    @Override
    public void forSubResourceWebTarget(MethodCreator methodCreator, IndexView index, ClassInfo rootInterfaceClass,
            ClassInfo subInterfaceClass, MethodInfo rootMethod, MethodInfo subMethod,
            AssignableResultHandle webTarget, BuildProducer generatedClasses) {

        Map queryParamsByName = new HashMap<>();
        collectClientParamData(rootInterfaceClass, rootMethod, queryParamsByName,
                CLIENT_QUERY_PARAM, CLIENT_QUERY_PARAMS, ClientQueryParam.class.getSimpleName());
        collectClientParamData(subInterfaceClass, subMethod, queryParamsByName,
                CLIENT_QUERY_PARAM, CLIENT_QUERY_PARAMS, ClientQueryParam.class.getSimpleName());

        for (var headerEntry : queryParamsByName.entrySet()) {
            addQueryParam(subMethod, methodCreator, headerEntry.getValue(), webTarget, generatedClasses, index);
        }
    }

    @Override
    public AssignableResultHandle handleFormParams(MethodCreator methodCreator, IndexView index, ClassInfo interfaceClass,
            MethodInfo method, BuildProducer generatedClasses, AssignableResultHandle formParams,
            boolean multipart) {

        Map formParamsByName = new HashMap<>();
        collectClientParamData(interfaceClass, method, formParamsByName,
                CLIENT_FORM_PARAM, CLIENT_FORM_PARAMS, ClientFormParam.class.getSimpleName());

        if (!formParamsByName.isEmpty() && formParams == null) {
            formParams = createFormData(methodCreator, multipart);
        }

        for (var formEntry : formParamsByName.entrySet()) {
            addFormParam(method, methodCreator, formEntry.getValue(), generatedClasses, index, formParams, multipart);
        }

        return formParams;
    }

    @Override
    public AssignableResultHandle handleFormParamsForSubResource(MethodCreator methodCreator, IndexView index,
            ClassInfo rootInterfaceClass, ClassInfo subInterfaceClass, MethodInfo rootMethod, MethodInfo subMethod,
            AssignableResultHandle webTarget, BuildProducer generatedClasses,
            AssignableResultHandle formParams, boolean multipart) {

        Map formParamsByName = new HashMap<>();
        collectClientParamData(rootInterfaceClass, rootMethod, formParamsByName,
                CLIENT_FORM_PARAM, CLIENT_FORM_PARAMS, ClientFormParam.class.getSimpleName());
        collectClientParamData(subInterfaceClass, subMethod, formParamsByName,
                CLIENT_FORM_PARAM, CLIENT_FORM_PARAMS, ClientFormParam.class.getSimpleName());

        if (!formParamsByName.isEmpty() && formParams == null) {
            formParams = createFormData(methodCreator, multipart);
        }

        for (var formEntry : formParamsByName.entrySet()) {
            addFormParam(subMethod, methodCreator, formEntry.getValue(), generatedClasses, index, formParams, multipart);
        }

        return formParams;
    }

    private AssignableResultHandle createFormData(BytecodeCreator methodCreator, boolean multipart) {
        AssignableResultHandle formParams;
        if (multipart) {
            formParams = methodCreator.createVariable(QuarkusMultipartForm.class);
            methodCreator.assign(formParams,
                    methodCreator.newInstance(MethodDescriptor.ofConstructor(QuarkusMultipartForm.class)));
        } else {
            formParams = methodCreator.createVariable(MultivaluedMap.class);
            methodCreator.assign(formParams,
                    methodCreator.newInstance(MethodDescriptor.ofConstructor(MultivaluedHashMap.class)));
        }
        return formParams;
    }

    private void addQueryParam(MethodInfo declaringMethod, MethodCreator methodCreator,
            ParamData paramData, AssignableResultHandle webTargetImpl,
            BuildProducer generatedClasses, IndexView index) {

        String paramName = paramData.annotation.value("name").asString();

        Supplier existenceChecker = () -> methodCreator.invokeStaticMethod(
                MethodDescriptor.ofMethod(ClientQueryParamSupport.class, "isQueryParamPresent", boolean.class,
                        WebTargetImpl.class, String.class),
                webTargetImpl, methodCreator.load(paramName));

        BiConsumer paramAdder = (creator, valuesList) -> creator.assign(webTargetImpl,
                creator.invokeVirtualMethod(WEB_TARGET_IMPL_QUERY_PARAMS, webTargetImpl, methodCreator.load(paramName),
                        valuesList));

        addParam(declaringMethod, methodCreator, paramData, generatedClasses, index, CLIENT_QUERY_PARAM,
                ClientQueryParam.class.getSimpleName(), paramName, existenceChecker, paramAdder);
    }

    private void addFormParam(MethodInfo declaringMethod, MethodCreator methodCreator,
            ParamData paramData, BuildProducer generatedClasses,
            IndexView index, AssignableResultHandle formParams, boolean multipart) {

        String paramName = paramData.annotation.value("name").asString();

        Supplier existenceChecker = () -> methodCreator.invokeInterfaceMethod(MAP_CONTAINS_KEY_METHOD,
                formParams, methodCreator.load(paramName));

        BiConsumer paramAdder = (creator, valuesList) -> {
            if (multipart) {
                String filename = null;
                AnnotationInstance partFileName = declaringMethod.annotation(ResteasyReactiveDotNames.PART_FILE_NAME);
                if (partFileName != null && partFileName.value() != null) {
                    filename = partFileName.value().asString();
                }

                ForEachLoop loop = creator.forEach(valuesList);
                BytecodeCreator block = loop.block();

                block.invokeVirtualMethod(QUARKUS_MULTIPART_FORM_ATTRIBUTE_METHOD, formParams, block.load(paramName),
                        loop.element(), block.load(filename));
            } else {
                creator.invokeInterfaceMethod(
                        MULTIVALUED_MAP_ADD_ALL_METHOD, formParams, creator.load(paramName), valuesList);
            }
        };

        addParam(declaringMethod, methodCreator, paramData, generatedClasses, index, CLIENT_FORM_PARAM,
                ClientFormParam.class.getSimpleName(), paramName, existenceChecker, paramAdder);
    }

    private void collectClientParamData(ClassInfo interfaceClass, MethodInfo method,
            Map paramFillersByName,
            DotName clientParamAnnotation, DotName clientParamsAnnotation,
            String annotationName) {
        AnnotationInstance classLevelParam = interfaceClass.declaredAnnotation(clientParamAnnotation);
        if (classLevelParam != null) {
            paramFillersByName.put(classLevelParam.value("name").asString(),
                    new ParamData(classLevelParam, interfaceClass));
        }
        putAllParamAnnotations(paramFillersByName,
                interfaceClass,
                extractAnnotations(interfaceClass.declaredAnnotation(clientParamsAnnotation)), annotationName);

        Map methodLevelParamsByName = new HashMap<>();
        AnnotationInstance methodLevelParam = method.annotation(clientParamAnnotation);
        if (methodLevelParam != null) {
            methodLevelParamsByName.put(methodLevelParam.value("name").asString(),
                    new ParamData(methodLevelParam, interfaceClass));
        }
        putAllParamAnnotations(methodLevelParamsByName, interfaceClass,
                extractAnnotations(method.annotation(clientParamsAnnotation)), annotationName);

        paramFillersByName.putAll(methodLevelParamsByName);
    }

    private void putAllParamAnnotations(Map paramMap, ClassInfo interfaceClass,
            AnnotationInstance[] annotations, String annotationName) {
        for (AnnotationInstance annotation : annotations) {
            String name = annotation.value("name").asString();
            if (paramMap.put(name, new ParamData(annotation, interfaceClass)) != null) {
                throw new RestClientDefinitionException("Duplicate " + annotationName + " annotation for parameter: " + name +
                        " on " + annotation.target());
            }
        }
    }

    private void addParam(MethodInfo declaringMethod, MethodCreator methodCreator,
            ParamData paramData, BuildProducer generatedClasses,
            IndexView index, DotName clientParamAnnotation, String annotationName, String paramName,
            Supplier existenceChecker,
            BiConsumer paramAdder) {

        AnnotationInstance annotation = paramData.annotation;
        ClassInfo declaringClass = paramData.definingClass;

        ResultHandle isParamPresent = existenceChecker.get();
        BytecodeCreator creator = methodCreator.ifTrue(isParamPresent).falseBranch();

        String[] values = annotation.value().asStringArray();

        if (values.length == 0) {
            log.warnv("Ignoring {} that specifies an empty array of values for parameter {} on {}",
                    annotationName, annotation.value("name").asString(), annotation.target());
            return;
        }

        if (values.length > 1 || !(values[0].startsWith("{") && values[0].endsWith("}"))) {
            boolean required = annotation.valueWithDefault(index, "required").asBoolean();
            ResultHandle valuesList = creator.newInstance(MethodDescriptor.ofConstructor(ArrayList.class));
            for (String value : values) {
                if (value.contains("${")) {
                    ResultHandle paramValueFromConfig = creator.invokeStaticMethod(
                            MethodDescriptor.ofMethod(ConfigUtils.class, "interpolate", String.class, String.class,
                                    boolean.class),
                            creator.load(value), creator.load(required));
                    creator.ifNotNull(paramValueFromConfig)
                            .trueBranch().invokeInterfaceMethod(LIST_ADD_METHOD, valuesList, paramValueFromConfig);
                } else {
                    creator.invokeInterfaceMethod(LIST_ADD_METHOD, valuesList, creator.load(value));
                }
            }

            paramAdder.accept(creator, valuesList);
        } else { // method call :O {some.package.ClassName.methodName} or {defaultMethodWithinThisInterfaceName}
            // if `!required` an exception on param filling does not fail the invocation:
            boolean required = annotation.valueWithDefault(index, "required").asBoolean();

            BytecodeCreator methodCallCreator = creator;
            TryBlock tryBlock = null;

            if (!required) {
                tryBlock = creator.tryBlock();
                methodCallCreator = tryBlock;
            }
            String methodName = values[0].substring(1, values[0].length() - 1); // strip curly braces

            MethodInfo paramValueMethod;
            ResultHandle paramValue;
            if (methodName.contains(".")) {
                // calling a static method
                int endOfClassName = methodName.lastIndexOf('.');
                String className = methodName.substring(0, endOfClassName);
                String staticMethodName = methodName.substring(endOfClassName + 1);

                ClassInfo clazz = index.getClassByName(DotName.createSimple(className));
                if (clazz == null) {
                    throw new RestClientDefinitionException(
                            "Class " + className + " used in " + annotationName + " on " + declaringClass + " not found");
                }
                paramValueMethod = findMethod(clazz, declaringClass, staticMethodName, clientParamAnnotation.toString());

                if (paramValueMethod.parametersCount() == 0) {
                    paramValue = methodCallCreator.invokeStaticMethod(paramValueMethod);
                } else if (paramValueMethod.parametersCount() == 1 && isString(paramValueMethod.parameterType(0))) {
                    paramValue = methodCallCreator.invokeStaticMethod(paramValueMethod, methodCallCreator.load(paramName));
                } else {
                    throw new RestClientDefinitionException(
                            annotationName + " method " + declaringClass.toString() + "#" + staticMethodName
                                    + " has too many parameters, at most one parameter, param name, expected");
                }
            } else {
                // interface method
                String mockName = mockInterface(declaringClass, generatedClasses, index);
                ResultHandle interfaceMock = methodCallCreator.newInstance(MethodDescriptor.ofConstructor(mockName));

                paramValueMethod = findMethod(declaringClass, declaringClass, methodName, clientParamAnnotation.toString());

                if (paramValueMethod == null) {
                    throw new RestClientDefinitionException(
                            annotationName + " method " + methodName + " not found on " + declaringClass);
                }

                if (paramValueMethod.parametersCount() == 0) {
                    paramValue = methodCallCreator.invokeInterfaceMethod(paramValueMethod, interfaceMock);
                } else if (paramValueMethod.parametersCount() == 1 && isString(paramValueMethod.parameterType(0))) {
                    paramValue = methodCallCreator.invokeInterfaceMethod(paramValueMethod, interfaceMock,
                            methodCallCreator.load(paramName));
                } else {
                    throw new RestClientDefinitionException(
                            annotationName + " method " + declaringClass + "#" + methodName
                                    + " has too many parameters, at most one parameter, param name, expected");
                }

            }

            Type returnType = paramValueMethod.returnType();
            ResultHandle valuesList;
            if (isStringArray(returnType)) {
                // repack array to list
                valuesList = methodCallCreator.invokeStaticMethod(ARRAYS_AS_LIST, paramValue);
            } else if (isString(returnType)) {
                valuesList = methodCallCreator.newInstance(MethodDescriptor.ofConstructor(ArrayList.class));
                methodCallCreator.invokeInterfaceMethod(LIST_ADD_METHOD, valuesList, paramValue);
            } else {
                throw new RestClientDefinitionException("Method " + declaringClass.toString() + "#" + methodName
                        + " has an unsupported return type for " + annotationName + ". " +
                        "Only String and String[] return types are supported");
            }

            paramAdder.accept(methodCallCreator, valuesList);

            if (!required) {
                CatchBlockCreator catchBlock = tryBlock.addCatch(Exception.class);
                ResultHandle log = catchBlock.invokeStaticMethod(
                        MethodDescriptor.ofMethod(Logger.class, "getLogger", Logger.class, String.class),
                        catchBlock.load(declaringClass.name().toString()));
                String errorMessage = String.format(
                        "Invoking param generation method '%s' for '%s' on method '%s#%s' failed",
                        methodName, paramName, declaringClass.name(), declaringMethod.name());
                catchBlock.invokeVirtualMethod(
                        MethodDescriptor.ofMethod(Logger.class, "warn", void.class, Object.class, Throwable.class),
                        log,
                        catchBlock.load(errorMessage), catchBlock.getCaughtException());
            }
        }
    }

    @Override
    public void forSubResourceMethod(ClassCreator subClassCreator, MethodCreator subConstructor,
            MethodCreator subClinit, MethodCreator subMethodCreator, ClassInfo rootInterfaceClass,
            ClassInfo subInterfaceClass, MethodInfo subMethod, MethodInfo rootMethod,
            AssignableResultHandle invocationBuilder, // sub-level
            IndexView index, BuildProducer generatedClasses,
            int methodIndex, int subMethodIndex, FieldDescriptor javaMethodField) {

        addJavaMethodToContext(javaMethodField, subMethodCreator, invocationBuilder);

        Map headerFillersByName = new HashMap<>();
        collectHeaderFillers(rootInterfaceClass, rootMethod, headerFillersByName);
        collectHeaderFillers(subInterfaceClass, subMethod, headerFillersByName);
        String subHeaderFillerName = subInterfaceClass.name().toString() + sha1(rootInterfaceClass.name().toString()) +
                "$$" + methodIndex + "$$" + subMethodIndex;
        createAndReturnHeaderFiller(subClassCreator, subConstructor, subMethodCreator, subMethod,
                invocationBuilder, index, generatedClasses, subMethodIndex, subHeaderFillerName, headerFillersByName);
    }

    @Override
    public void forMethod(ClassCreator classCreator, MethodCreator constructor,
            MethodCreator clinit, MethodCreator methodCreator, ClassInfo interfaceClass,
            MethodInfo method, AssignableResultHandle invocationBuilder, IndexView index,
            BuildProducer generatedClasses, int methodIndex, FieldDescriptor javaMethodField) {

        addJavaMethodToContext(javaMethodField, methodCreator, invocationBuilder);

        // header filler

        Map headerFillersByName = new HashMap<>();

        collectHeaderFillers(interfaceClass, method, headerFillersByName);

        createAndReturnHeaderFiller(classCreator, constructor, methodCreator, method,
                invocationBuilder, index, generatedClasses, methodIndex,
                interfaceClass + "$$" + method.name() + "$$" + methodIndex, headerFillersByName);
    }

    private void createAndReturnHeaderFiller(ClassCreator classCreator, MethodCreator constructor,
            MethodCreator methodCreator, MethodInfo method,
            AssignableResultHandle invocationBuilder, IndexView index,
            BuildProducer generatedClasses, int methodIndex, String fillerClassName,
            Map headerFillersByName) {
        FieldDescriptor headerFillerField = FieldDescriptor.of(classCreator.getClassName(),
                "headerFiller" + methodIndex, HeaderFiller.class);
        classCreator.getFieldCreator(headerFillerField).setModifiers(Modifier.PRIVATE | Modifier.FINAL);
        ResultHandle headerFiller;
        // create header filler for this method if headerFillersByName is not empty
        if (!headerFillersByName.isEmpty()) {
            GeneratedClassGizmoAdaptor classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, true);
            try (ClassCreator headerFillerClass = ClassCreator.builder().className(fillerClassName)
                    .interfaces(ExtendedHeaderFiller.class)
                    .classOutput(classOutput)
                    .build()) {
                FieldCreator logField = headerFillerClass.getFieldCreator("log", Logger.class);
                logField.setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE);

                MethodCreator staticConstructor = headerFillerClass.getMethodCreator("", void.class);
                staticConstructor.setModifiers(ACC_STATIC);
                ResultHandle log = staticConstructor.invokeStaticMethod(
                        MethodDescriptor.ofMethod(Logger.class, "getLogger", Logger.class, String.class),
                        staticConstructor.load(fillerClassName));
                staticConstructor.writeStaticField(logField.getFieldDescriptor(), log);
                staticConstructor.returnValue(null);

                MethodCreator fillHeaders = headerFillerClass
                        .getMethodCreator(
                                MethodDescriptor.ofMethod(HeaderFiller.class, "addHeaders", void.class,
                                        MultivaluedMap.class, ResteasyReactiveClientRequestContext.class));

                for (Map.Entry headerEntry : headerFillersByName.entrySet()) {
                    addHeaderParam(method, fillHeaders, headerEntry.getValue(), generatedClasses,
                            fillerClassName, index);
                }
                fillHeaders.returnValue(null);

                headerFiller = constructor.newInstance(MethodDescriptor.ofConstructor(fillerClassName));
            }
        } else {
            headerFiller = constructor
                    .readStaticField(FieldDescriptor.of(NoOpHeaderFiller.class, "INSTANCE", NoOpHeaderFiller.class));
        }
        constructor.writeInstanceField(headerFillerField, constructor.getThis(), headerFiller);

        ResultHandle headerFillerAsObject = methodCreator.checkCast(
                methodCreator.readInstanceField(headerFillerField, methodCreator.getThis()), Object.class);
        methodCreator.assign(invocationBuilder,
                methodCreator.invokeInterfaceMethod(INVOCATION_BUILDER_PROPERTY_METHOD, invocationBuilder,
                        methodCreator.load(HeaderFiller.class.getName()), headerFillerAsObject));

        ResultHandle parametersList = null;
        if (method.parametersCount() == 0) {
            parametersList = methodCreator.invokeStaticMethod(ofMethod(
                    Collections.class, "emptyList", List.class));
        } else {
            ResultHandle parametersArray = methodCreator.newArray(Object.class,
                    method.parametersCount());
            for (int i = 0; i < method.parametersCount(); i++) {
                methodCreator.writeArrayValue(parametersArray, i, methodCreator.getMethodParam(i));
            }
            parametersList = methodCreator.invokeStaticMethod(
                    ARRAYS_AS_LIST, parametersArray);
        }
        methodCreator.assign(invocationBuilder,
                methodCreator.invokeInterfaceMethod(INVOCATION_BUILDER_PROPERTY_METHOD, invocationBuilder,
                        methodCreator.load(INVOKED_METHOD_PARAMETERS_PROP), parametersList));
    }

    private void collectHeaderFillers(ClassInfo interfaceClass, MethodInfo method,
            Map headerFillersByName) {
        AnnotationInstance classLevelHeader = interfaceClass.declaredAnnotation(CLIENT_HEADER_PARAM);
        if (classLevelHeader != null) {
            headerFillersByName.put(classLevelHeader.value("name").asString(),
                    new ParamData(classLevelHeader, interfaceClass));
        }
        putAllHeaderAnnotations(headerFillersByName,
                interfaceClass,
                extractAnnotations(interfaceClass.declaredAnnotation(CLIENT_HEADER_PARAMS)));

        Map methodLevelHeadersByName = new HashMap<>();
        AnnotationInstance methodLevelHeader = method.annotation(CLIENT_HEADER_PARAM);
        if (methodLevelHeader != null) {
            methodLevelHeadersByName.put(methodLevelHeader.value("name").asString(),
                    new ParamData(methodLevelHeader, interfaceClass));
        }
        putAllHeaderAnnotations(methodLevelHeadersByName, interfaceClass,
                extractAnnotations(method.annotation(CLIENT_HEADER_PARAMS)));

        headerFillersByName.putAll(methodLevelHeadersByName);
    }

    /**
     * create a field in the stub class to contain (interface) java.lang.reflect.Method corresponding to this method
     * MP Rest Client spec says it has to be in the request context, keeping it in a field we don't have to
     * initialize it on each call
     *
     * @param javaMethodField method reference in a static class field
     * @param methodCreator method for which we put the java.lang.reflect.Method to context (aka this method)
     * @param invocationBuilder Invocation.Builder in this method
     */
    private void addJavaMethodToContext(FieldDescriptor javaMethodField, MethodCreator methodCreator,
            AssignableResultHandle invocationBuilder) {
        ResultHandle javaMethod = methodCreator.readStaticField(javaMethodField);
        ResultHandle javaMethodAsObject = methodCreator.checkCast(javaMethod, Object.class);
        methodCreator.assign(invocationBuilder,
                methodCreator.invokeInterfaceMethod(INVOCATION_BUILDER_PROPERTY_METHOD, invocationBuilder,
                        methodCreator.load(INVOKED_METHOD_PROP), javaMethodAsObject));
    }

    private void putAllHeaderAnnotations(Map headerMap, ClassInfo interfaceClass,
            AnnotationInstance[] annotations) {
        for (AnnotationInstance annotation : annotations) {
            String headerName = annotation.value("name").asString();
            if (headerMap.put(headerName, new ParamData(annotation, interfaceClass)) != null) {
                throw new RestClientDefinitionException("Duplicate ClientHeaderParam annotation for header: " + headerName +
                        " on " + annotation.target());
            }
        }
    }

    // fillHeaders takes `MultivaluedMap` as param and modifies it
    private void addHeaderParam(MethodInfo declaringMethod, MethodCreator fillHeadersCreator,
            ParamData paramData,
            BuildProducer generatedClasses,
            String fillerClassName,
            IndexView index) {

        AnnotationInstance annotation = paramData.annotation;
        ClassInfo declaringClass = paramData.definingClass;

        String headerName = annotation.value("name").asString();

        String[] values = annotation.value().asStringArray();

        if (values.length == 0) {
            log.warnv("Ignoring ClientHeaderParam that specifies an empty array of header values for header {0} on {1}",
                    annotation.value("name").asString(), annotation.target());
            return;
        }

        ResultHandle headerMap = fillHeadersCreator.getMethodParam(0);
        ResultHandle requestContext = fillHeadersCreator.getMethodParam(1);

        // if headers are set here, they were set with @HeaderParam, which should take precedence of MP ways
        BytecodeCreator fillHeaders = fillHeadersCreator
                .ifTrue(fillHeadersCreator.invokeStaticMethod(HEADER_FILLER_UTIL_SHOULD_ADD_HEADER,
                        fillHeadersCreator.load(headerName), headerMap, requestContext))
                .trueBranch();

        if (values.length > 1) {
            // TODO: we should probably also get rid of this as some point
            boolean required = annotation.valueWithDefault(index, "required").asBoolean();
            ResultHandle headerList = Gizmo.newArrayList(fillHeaders, 1);
            for (String value : values) {
                if (value.contains("${")) {
                    ResultHandle headerValueFromConfig = fillHeaders.invokeStaticMethod(
                            MethodDescriptor.ofMethod(ConfigUtils.class, "interpolate", String.class, String.class,
                                    boolean.class),
                            fillHeaders.load(value), fillHeaders.load(required));
                    fillHeaders.ifNotNull(headerValueFromConfig)
                            .trueBranch().invokeInterfaceMethod(LIST_ADD_METHOD, headerList, headerValueFromConfig);
                } else {
                    fillHeaders.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, fillHeaders.load(value));
                }
            }

            fillHeaders.invokeInterfaceMethod(MAP_PUT_METHOD, headerMap, fillHeaders.load(headerName), headerList);
        } else {
            // if there is only one value, we support mixing verbatim values, config params method invocations and method parameter lookups
            // A method call is in the form of {some.package.ClassName.methodName} or {defaultMethodWithinThisInterfaceName}
            // A method parameter lookup is also in the form of {methodParamName} and if there are clashes with a method call, the latter takes precedence
            // An config name is in the form of ${config.name}
            List nodes = new RestClientAnnotationExpressionParser(values[0],
                    declaringMethod.declaringClass().name().toString() + "#" + declaringMethod.name()).parse();

            // if `!required` an exception on header filling does not fail the invocation:
            boolean required = annotation.valueWithDefault(index, "required").asBoolean();

            BytecodeCreator fillHeader;
            TryBlock tryBlock;
            if (required) {
                tryBlock = null;
                fillHeader = fillHeaders;
            } else {
                tryBlock = fillHeaders.tryBlock();
                fillHeader = tryBlock;
            }

            List headerFillerInfos = nodes.stream().map(n -> {
                if (n instanceof RestClientAnnotationExpressionParser.Verbatim) {

                    return new HeaderFillerInfo(STRING_TYPE, n, new Supplier() {
                        @Override
                        public ResultHandle get() {
                            return fillHeader.load(n.getValue());
                        }
                    });

                } else if (n instanceof RestClientAnnotationExpressionParser.ConfigName) {

                    return new HeaderFillerInfo(STRING_TYPE, n, new Supplier() {
                        @Override
                        public ResultHandle get() {
                            return fillHeader.invokeStaticMethod(
                                    ofMethod(ConfigUtils.class, "doGetConfigValue", String.class, String.class,
                                            boolean.class, String.class),
                                    fillHeader.load("${" + n.getValue() + "}"), fillHeader.load(required),
                                    fillHeader.load(n.getValue()));
                        }
                    });

                } else if (n instanceof RestClientAnnotationExpressionParser.Accessible) {

                    String accessibleName = n.getValue();
                    MethodInfo headerFillingMethod;
                    AccessibleType accessibleType = accessibleName.contains(".") ? AccessibleType.STATIC_METHOD
                            : AccessibleType.INTERFACE_METHOD;
                    if (accessibleType == AccessibleType.STATIC_METHOD) {
                        // calling a static method
                        int endOfClassName = accessibleName.lastIndexOf('.');
                        String className = accessibleName.substring(0, endOfClassName);
                        String staticMethodName = accessibleName.substring(endOfClassName + 1);

                        ClassInfo clazz = index.getClassByName(DotName.createSimple(className));
                        if (clazz == null) {
                            throw new RestClientDefinitionException(String.format(
                                    "Invalid %s definition, unable to determine class %s. Problematic interface: %s",
                                    CLIENT_HEADER_PARAM, className, declaringClass));
                        }
                        headerFillingMethod = findMethod(clazz, declaringClass, staticMethodName,
                                CLIENT_HEADER_PARAM.toString());
                    } else if (accessibleType == AccessibleType.INTERFACE_METHOD) {
                        headerFillingMethod = findMethod(declaringClass, declaringClass, accessibleName,
                                CLIENT_HEADER_PARAM.toString());
                    } else {
                        throw new IllegalStateException("Unknown type " + accessibleType);
                    }

                    Type valueType = null;
                    AtomicInteger parameterPosition = new AtomicInteger(-1);
                    if (headerFillingMethod == null) {
                        for (MethodParameterInfo parameter : declaringMethod.parameters()) {
                            if (!accessibleName.equals(parameter.name())) {
                                continue;
                            }
                            if (!isString(parameter.type())) {
                                throw new RestClientDefinitionException(String.format(
                                        "Invalid %s definition, method parameter %s is not of String type. Problematic interface: %s",
                                        CLIENT_HEADER_PARAM, accessibleName, declaringClass));
                            }
                            accessibleType = AccessibleType.METHOD_PARAMETER;
                            valueType = parameter.type();
                            parameterPosition.set(parameter.position());
                            break;
                        }
                        if (valueType == null) {
                            throw new RestClientDefinitionException(String.format(
                                    "Invalid %s definition, unable to determine target method '%s'. Problematic interface: %s",
                                    CLIENT_HEADER_PARAM, accessibleName, declaringClass));
                        }
                    } else {
                        valueType = headerFillingMethod.returnType();
                    }

                    Supplier supplier;
                    if (accessibleType == AccessibleType.STATIC_METHOD) {
                        supplier = new Supplier() {
                            @Override
                            public ResultHandle get() {

                                if (headerFillingMethod.parametersCount() == 0) {
                                    return fillHeader.invokeStaticMethod(headerFillingMethod);
                                } else if (headerFillingMethod.parametersCount() == 1
                                        && isString(headerFillingMethod.parameterType(0))) {
                                    return fillHeader.invokeStaticMethod(headerFillingMethod, fillHeader.load(headerName));
                                } else if (headerFillingMethod.parametersCount() == 1
                                        && isComputedParamContext(headerFillingMethod.parameterType(0))) {
                                    ResultHandle fillerParam = fillHeader
                                            .newInstance(
                                                    COMPUTER_PARAM_CONTEXT_IMPL_CTOR,
                                                    fillHeader.load(headerName), requestContext);
                                    return fillHeader.invokeStaticMethod(headerFillingMethod, fillerParam);
                                } else {
                                    throw new RestClientDefinitionException(
                                            "@ClientHeaderParam method " + headerFillingMethod.declaringClass().toString() + "#"
                                                    + headerFillingMethod.name()
                                                    + " has too many parameters, at most one parameter, header name, expected");
                                }

                            }
                        };
                    } else if (accessibleType == AccessibleType.INTERFACE_METHOD) {
                        supplier = new Supplier() {
                            @Override
                            public ResultHandle get() {

                                String mockName = mockInterface(declaringClass, generatedClasses, index);
                                ResultHandle interfaceMock = fillHeader.newInstance(MethodDescriptor.ofConstructor(mockName));

                                if (headerFillingMethod.parametersCount() == 0) {
                                    return fillHeader.invokeInterfaceMethod(headerFillingMethod, interfaceMock);
                                } else if (headerFillingMethod.parametersCount() == 1
                                        && isString(headerFillingMethod.parameterType(0))) {
                                    return fillHeader.invokeInterfaceMethod(headerFillingMethod, interfaceMock,
                                            fillHeader.load(headerName));
                                } else if (headerFillingMethod.parametersCount() == 1
                                        && isComputedParamContext(headerFillingMethod.parameterType(0))) {
                                    ResultHandle fillerParam = fillHeader
                                            .newInstance(
                                                    COMPUTER_PARAM_CONTEXT_IMPL_CTOR,
                                                    fillHeader.load(headerName), requestContext);
                                    return fillHeader.invokeInterfaceMethod(headerFillingMethod, interfaceMock,
                                            fillerParam);
                                } else {
                                    throw new RestClientDefinitionException(
                                            "@ClientHeaderParam method " + headerFillingMethod.declaringClass().toString() + "#"
                                                    + headerFillingMethod.name()
                                                    + " has too many parameters, at most one parameter, header name, expected");
                                }
                            }
                        };
                    } else if (accessibleType == AccessibleType.METHOD_PARAMETER) {
                        supplier = new Supplier() {
                            @Override
                            public ResultHandle get() {
                                return fillHeader.invokeStaticMethod(COMPUTER_PARAM_CONTEXT_IMPL_GET_METHOD_PARAM,
                                        requestContext, fillHeader.load(parameterPosition.get()));
                            }
                        };
                    } else {
                        throw new IllegalStateException("Unknown type " + accessibleType);
                    }

                    if (nodes.size() == 1) {
                        if (!isString(valueType) && !isStringArray(
                                valueType)) {
                            throw new RestClientDefinitionException("Method " + headerFillingMethod.declaringClass().toString()
                                    + "#" + headerFillingMethod.name()
                                    + " has an unsupported return type for ClientHeaderParam. " +
                                    "Only String and String[] return types are supported");
                        }
                    } else {
                        if (!isString(valueType)) {
                            throw new RestClientDefinitionException("Method " + headerFillingMethod.declaringClass().toString()
                                    + "#" + headerFillingMethod.name()
                                    + " has an unsupported return type for ClientHeaderParam. " +
                                    "Only String is supported when using complex expressions");
                        }
                    }

                    return new HeaderFillerInfo(valueType, n, supplier);

                } else {
                    throw new IllegalStateException("Unknown node type " + n.getClass().getName());
                }
            }).collect(Collectors.toList());

            AssignableResultHandle headerList = fillHeader.createVariable(List.class);
            fillHeader.assign(headerList, fillHeader.loadNull());
            if (headerFillerInfos.size() == 1) {
                HeaderFillerInfo headerFillerInfo = headerFillerInfos.get(0);
                ResultHandle headerFillerResult = headerFillerInfo.getResultHandleSupplier().get();
                BytecodeCreator notNullBranchTrue = fillHeader.ifNotNull(headerFillerResult).trueBranch();
                Type headerFillerMethodReturnType = headerFillerInfo.getValueType();
                if (isStringArray(headerFillerMethodReturnType)) {
                    // repack array to list
                    ResultHandle asList = notNullBranchTrue.invokeStaticMethod(ARRAYS_AS_LIST, headerFillerResult);
                    notNullBranchTrue.assign(headerList, asList);
                } else if (isString(headerFillerMethodReturnType)) {
                    notNullBranchTrue.assign(headerList, Gizmo.newArrayList(notNullBranchTrue, 1));
                    notNullBranchTrue.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, headerFillerResult);
                } else {
                    throw new IllegalStateException("Unhandled type: " + headerFillerMethodReturnType);
                }
            } else {
                ResultHandle nonNullValuesList = Gizmo.newArrayList(fillHeader, headerFillerInfos.size());
                for (HeaderFillerInfo headerFillerInfo : headerFillerInfos) {
                    if (!isString(headerFillerInfo.getValueType())) {
                        throw new IllegalStateException("Unhandled type: " + headerFillerInfo.getValueType());
                    }
                    ResultHandle value = headerFillerInfo.getResultHandleSupplier().get();
                    BytecodeCreator notNullBranch = fillHeader.ifNotNull(value).trueBranch();
                    notNullBranch.invokeInterfaceMethod(LIST_ADD_METHOD, nonNullValuesList, value);
                }
                ResultHandle sb = fillHeader.newInstance(MethodDescriptor.ofConstructor(StringBuilder.class));
                ForEachLoop loop = fillHeader.forEach(nonNullValuesList);
                BytecodeCreator block = loop.block();
                block.invokeVirtualMethod(STRING_BUILDER_APPEND, sb, loop.element());
                ResultHandle stringValue = Gizmo.toString(fillHeader, sb);

                ResultHandle stringValueLength = fillHeader.invokeVirtualMethod(STRING_LENGTH, stringValue);
                BytecodeCreator notEmptyStringBranchTrue = fillHeader.ifNonZero(stringValueLength).trueBranch();
                notEmptyStringBranchTrue.assign(headerList, Gizmo.newArrayList(notEmptyStringBranchTrue, 1));
                notEmptyStringBranchTrue.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, stringValue);
            }

            BytecodeCreator headerListNotNull = fillHeader.ifNotNull(headerList).trueBranch();
            headerListNotNull.invokeInterfaceMethod(MAP_PUT_METHOD, headerMap, headerListNotNull.load(headerName), headerList);

            if (!required) {
                CatchBlockCreator catchBlock = tryBlock.addCatch(Exception.class);
                ResultHandle log = catchBlock.readStaticField(FieldDescriptor.of(fillerClassName, "log", Logger.class));
                String errorMessage = String.format("Invoking header for header '%s' failed", headerName);
                catchBlock.invokeVirtualMethod(
                        MethodDescriptor.ofMethod(Logger.class, "warn", void.class, Object.class, Throwable.class),
                        log,
                        catchBlock.load(errorMessage), catchBlock.getCaughtException());
            }
        }
    }

    enum AccessibleType {
        INTERFACE_METHOD,
        STATIC_METHOD,
        METHOD_PARAMETER
    }

    private static class HeaderFillerInfo {
        private final Type valueType;
        private final Supplier resultHandleSupplier;

        private final Node source;

        HeaderFillerInfo(Type valueType, Node source, Supplier resultHandleSupplier) {
            this.valueType = valueType;
            this.source = source;
            this.resultHandleSupplier = resultHandleSupplier;
        }

        Type getValueType() {
            return valueType;
        }

        Supplier getResultHandleSupplier() {
            return resultHandleSupplier;
        }

        HeaderFillerInfo mapResultHandle(Function, Supplier> mapper) {
            return new HeaderFillerInfo(this.valueType, this.source, mapper.apply(this.resultHandleSupplier));
        }
    }

    private MethodInfo findMethod(ClassInfo declaringClass, ClassInfo restInterface, String methodName,
            String sourceAnnotationName) {
        MethodInfo result = null;
        for (MethodInfo method : declaringClass.methods()) {
            if (method.name().equals(methodName)) {
                if (result != null) {
                    throw new RestClientDefinitionException(String.format(
                            "Ambiguous %s definition, more than one method of name %s found on %s. Problematic interface: %s",
                            sourceAnnotationName, methodName, declaringClass, restInterface));
                } else {
                    result = method;
                }
            }
        }
        return result;
    }

    private static boolean isString(Type type) {
        return type.kind() == Type.Kind.CLASS && type.name().toString().equals(String.class.getName());
    }

    private static boolean isStringArray(Type returnType) {
        return returnType.kind() == Type.Kind.ARRAY && returnType.asArrayType().constituent().name().equals(STRING);
    }

    private static boolean isComputedParamContext(Type type) {
        return type.kind() == Type.Kind.CLASS && type.name().toString().equals(ComputedParamContext.class.getName());
    }

    private String mockInterface(ClassInfo declaringClass, BuildProducer generatedClass,
            IndexView index) {
        // we have an interface, we have to call a default method on it, we generate a (very simplistic) implementation:

        return interfaceMocks.computeIfAbsent(declaringClass, classInfo -> {
            String mockName = declaringClass.toString() + HashUtil.sha1(declaringClass.toString());
            ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClass, true);
            List interfaceNames = declaringClass.interfaceNames();
            Set methods = new HashSet<>();
            for (DotName interfaceName : interfaceNames) {
                ClassInfo interfaceClass = index.getClassByName(interfaceName);
                methods.addAll(interfaceClass.methods());
            }
            methods.addAll(declaringClass.methods());

            try (ClassCreator classCreator = ClassCreator.builder().className(mockName).interfaces(declaringClass.toString())
                    .classOutput(classOutput)
                    .build()) {

                for (MethodInfo method : methods) {
                    if (Modifier.isAbstract(method.flags())) {
                        MethodCreator methodCreator = classCreator.getMethodCreator(MethodDescriptor.of(method));
                        methodCreator.throwException(IllegalStateException.class, "This should never be called");
                    }
                }
            }
            return mockName;
        });
    }

    private AnnotationInstance[] extractAnnotations(AnnotationInstance groupAnnotation) {
        if (groupAnnotation != null) {
            AnnotationValue annotationValue = groupAnnotation.value();
            if (annotationValue != null) {
                return annotationValue.asNestedArray();
            }
        }
        return EMPTY_ANNOTATION_INSTANCES;
    }

    /**
     * ClientxxxParam annotations can be defined on a JAX-RS interface or a sub-client (sub-resource).
     * If we're filling parameters for a sub-client, we need to know the defining class of the ClientxxxParam
     * to properly resolve default methods of the "root" client
     */
    private static class ParamData {
        private final AnnotationInstance annotation;
        private final ClassInfo definingClass;

        public ParamData(AnnotationInstance annotation, ClassInfo definingClass) {
            this.annotation = annotation;
            this.definingClass = definingClass;
        }
    }

    /**
     * This class is meant to parse the values in {@link org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam}
     * into a list of supported types
     */
    static class RestClientAnnotationExpressionParser {

        private final String input;
        private final String sourceMethod;

        RestClientAnnotationExpressionParser(String input, String sourceMethod) {
            this.input = Objects.requireNonNull(input);
            this.sourceMethod = sourceMethod;
        }

        // this is a pretty naive implementation, but it suffices for what we are trying to do
        List parse() {
            int i = 0;
            int configStart = -1;
            int accessibleStart = -1;
            int verbatimStart = -1;
            List nodes = new ArrayList<>();
            while (i < input.length()) {
                char c = input.charAt(i);
                if (c == '$') {
                    if ((configStart != -1) || (accessibleStart != -1)) {
                        throw new IllegalArgumentException(createEffectiveErrorMessage("Cannot mix expressions"));
                    }
                    if (i == input.length() - 1) {
                        throw new IllegalArgumentException(createEffectiveErrorMessage("Illegal end of expression"));
                    }
                    if (input.charAt(i + 1) != '{') {
                        throw new IllegalArgumentException(createEffectiveErrorMessage("'$' must always be followed by '{'"));
                    }
                    if (verbatimStart != -1) {
                        nodes.add(new Verbatim(input.substring(verbatimStart, i)));
                    }
                    i += 2;
                    configStart = i;
                    verbatimStart = -1;
                } else if (c == '{') {
                    if ((configStart != -1) || (accessibleStart != -1)) {
                        throw new IllegalArgumentException(createEffectiveErrorMessage("Cannot mix expressions"));
                    }
                    if (i == input.length() - 1) {
                        throw new IllegalArgumentException(createEffectiveErrorMessage("Illegal end of expression"));
                    }
                    if (verbatimStart != -1) {
                        nodes.add(new Verbatim(input.substring(verbatimStart, i)));
                    }
                    i++;
                    accessibleStart = i;
                    verbatimStart = -1;
                } else if (c == '}') {
                    if ((configStart == -1) && (accessibleStart == -1)) {
                        throw new IllegalArgumentException(createEffectiveErrorMessage("Illegal end of expression"));
                    }
                    if (configStart != -1) {
                        nodes.add(new ConfigName(input.substring(configStart, i)));
                    } else {
                        nodes.add(new Accessible(input.substring(accessibleStart, i)));
                    }
                    configStart = -1;
                    accessibleStart = -1;
                    i++;
                } else {
                    if ((verbatimStart == -1) && (configStart == -1) && (accessibleStart == -1)) {
                        verbatimStart = i;
                    }
                    i++;
                }
            }
            if (verbatimStart != -1) {
                nodes.add(new Verbatim(input.substring(verbatimStart)));
            }

            return nodes;
        }

        private String createEffectiveErrorMessage(String errorMessage) {
            return "Invalid REST Client annotation value expression '" + input + "'"
                    + (sourceMethod != null ? ("found on method '" + sourceMethod + "'") : "") + ". Error is : '" + errorMessage
                    + "'";
        }

        static abstract class Node {

            protected final String value;

            Node(String value) {
                this.value = Objects.requireNonNull(value);
            }

            String getValue() {
                return value;
            }

            @Override
            public boolean equals(Object o) {
                if (this == o) {
                    return true;
                }
                if (!(o instanceof Node)) {
                    return false;
                }
                Node node = (Node) o;
                return Objects.equals(value, node.value);
            }

            @Override
            public int hashCode() {
                return Objects.hash(value);
            }
        }

        static class Verbatim extends Node {

            Verbatim(String value) {
                super(value);
            }

            @Override
            public String toString() {
                return "Verbatim{" +
                        "value='" + value + '\'' +
                        '}';
            }
        }

        static class ConfigName extends Node {

            ConfigName(String value) {
                super(value);
            }

            @Override
            public String toString() {
                return "ConfigName{" +
                        "value='" + value + '\'' +
                        '}';
            }
        }

        static class Accessible extends Node {

            Accessible(String value) {
                super(value);
            }

            @Override
            public String toString() {
                return "Accessible{" +
                        "value='" + value + '\'' +
                        '}';
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy