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

io.quarkiverse.cxf.deployment.CxfClientProcessor Maven / Gradle / Ivy

The newest version!
package io.quarkiverse.cxf.deployment;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.inject.Inject;
import jakarta.xml.ws.BindingProvider;
import jakarta.xml.ws.soap.SOAPBinding;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.transport.http.HTTPTransportFactory;
import org.apache.cxf.wsdl11.CatalogWSDLLocator;
import org.apache.cxf.wsdl11.WSDLManagerImpl;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
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 io.quarkiverse.cxf.CXFClientData;
import io.quarkiverse.cxf.CXFClientInfo;
import io.quarkiverse.cxf.CXFRecorder;
import io.quarkiverse.cxf.ClientInjectionPoint;
import io.quarkiverse.cxf.CxfClientProducer;
import io.quarkiverse.cxf.CxfFixedConfig;
import io.quarkiverse.cxf.CxfFixedConfig.ClientFixedConfig;
import io.quarkiverse.cxf.HTTPConduitImpl;
import io.quarkiverse.cxf.HttpClientHTTPConduitFactory;
import io.quarkiverse.cxf.annotation.CXFClient;
import io.quarkiverse.cxf.graal.QuarkusCxfFeature;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.util.IoUtil;
import io.quarkus.gizmo.AnnotatedElement;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;

/**
 * Find WebService implementations and deploy them.
 */
public class CxfClientProcessor {

    private static final Logger LOGGER = Logger.getLogger(CxfClientProcessor.class);

    @BuildStep
    void collectClients(
            CxfFixedConfig config,
            CombinedIndexBuildItem combinedIndexBuildItem,
            BuildProducer nativeImageFeatures,
            BuildProducer proxies,
            BuildProducer clients,
            BuildProducer clientSeis) {
        IndexView index = combinedIndexBuildItem.getIndex();

        final AtomicBoolean hasRuntimeInitializedProxy = new AtomicBoolean(false);
        final Map clientSEIsInUse = findClientSEIsInUse(index, config);
        CxfDeploymentUtils.webServiceAnnotations(index)
                .forEach(annotation -> {
                    final ClassInfo wsClassInfo = annotation.target().asClass();
                    ClientFixedConfig clientConfig = clientSEIsInUse.get(wsClassInfo.name().toString());
                    if (clientConfig != null) {
                        final String sei = wsClassInfo.name().toString();
                        AnnotationInstance webserviceClient = findWebServiceClientAnnotation(index, wsClassInfo.name());
                        final String wsName;
                        final String wsNamespace;
                        if (webserviceClient != null) {
                            wsName = webserviceClient.value("name").asString();
                            wsNamespace = webserviceClient.value("targetNamespace").asString();
                        } else {
                            wsName = Optional.ofNullable(annotation.value("serviceName"))
                                    .map(AnnotationValue::asString)
                                    .orElse("");
                            wsNamespace = Optional.ofNullable(annotation.value("targetNamespace"))
                                    .map(AnnotationValue::asString)
                                    .orElseGet(() -> CxfDeploymentUtils.getNameSpaceFromClassInfo(wsClassInfo));
                        }
                        final String soapBinding = Optional
                                .ofNullable(wsClassInfo.declaredAnnotation(CxfDotNames.BINDING_TYPE_ANNOTATION))
                                .map(bindingType -> bindingType.value().asString())
                                .orElse(SOAPBinding.SOAP11HTTP_BINDING);

                        final ProxyInfo proxyInfo = ProxyInfo.of(
                                Optional.ofNullable(clientConfig.native_()).map(native_ -> native_.runtimeInitialized())
                                        .orElse(false),
                                wsClassInfo,
                                index);
                        proxies.produce(new NativeImageProxyDefinitionBuildItem(proxyInfo.interfaces));

                        clients.produce(
                                new CxfClientBuildItem(sei, soapBinding, wsNamespace, wsName, proxyInfo.isRuntimeInitialized));
                        clientSeis.produce(new ClientSeiBuildItem(sei));

                        hasRuntimeInitializedProxy.set(hasRuntimeInitializedProxy.get() || proxyInfo.isRuntimeInitialized);

                    }
                });

        if (hasRuntimeInitializedProxy.get()) {
            nativeImageFeatures.produce(new NativeImageFeatureBuildItem(QuarkusCxfFeature.class));
        }

    }

    @BuildStep
    @Record(ExecutionTime.RUNTIME_INIT)
    void startClient(
            CXFRecorder recorder,
            List clients,
            BuildProducer synthetics) {

        //
        // Create injectable CXFClientInfo bean for each SEI-only interface, i.e. for each
        // class annotated as @WebService and without implementation. This bean fuells a
        // producer bean producing CXF proxy clients.
        clients
                .stream()
                .map(client -> new CXFClientData(
                        client.getSoapBinding(),
                        client.getSei(),
                        client.getWsName(),
                        client.getWsNamespace(),
                        client.isProxyClassRuntimeInitialized()))
                .map(cxf -> {
                    LOGGER.debugf("producing dedicated CXFClientInfo bean named '%s' for SEI %s", cxf.getSei(), cxf.getSei());
                    return SyntheticBeanBuildItem
                            .configure(CXFClientData.class)
                            .named(cxf.getSei())
                            .runtimeValue(recorder.cxfClientData(cxf))
                            .unremovable()
                            .setRuntimeInit()
                            .done();
                }).forEach(synthetics::produce);
    }

    private static AnnotationInstance findWebServiceClientAnnotation(IndexView index, DotName seiName) {
        Collection annotations = index.getAnnotations(CxfDotNames.WEBSERVICE_CLIENT);
        for (AnnotationInstance annotation : annotations) {
            ClassInfo targetClass = annotation.target().asClass();

            for (MethodInfo method : targetClass.methods()) {
                if (method.returnType().name().equals(seiName)) {
                    return annotation;
                }
            }
        }

        return null;
    }

    public static Stream findClientInjectionPoints(IndexView index) {
        return index.getAnnotations(CxfDotNames.CXFCLIENT_ANNOTATION).stream()
                .map(annotationInstance -> {
                    final AnnotationTarget target = annotationInstance.target();
                    Type type;
                    switch (target.kind()) {
                        case FIELD:
                            type = target.asField().type();
                            break;
                        case METHOD_PARAMETER:
                            MethodParameterInfo paramInfo = target.asMethodParameter();
                            MethodInfo method = paramInfo.method();
                            type = method.parameterTypes().get(paramInfo.position());
                            break;
                        default:
                            type = null;
                            break;
                    }
                    if (type != null) {
                        type = type.name().equals(CxfDotNames.INJECT_INSTANCE) ? type.asParameterizedType().arguments().get(0)
                                : type;
                        final String typeName = type.name().toString();
                        try {
                            ClassLoader cl = Thread.currentThread().getContextClassLoader();
                            final Class sei = Class.forName(typeName, true, cl);
                            final AnnotationValue value = annotationInstance.value();
                            return new ClientInjectionPoint(value != null ? value.asString() : "", sei);
                        } catch (ClassNotFoundException e) {
                            throw new RuntimeException("Could not load Service Endpoint Interface " + typeName);
                        }
                    } else {
                        return null;
                    }
                })
                .filter(ip -> ip != null)
                .distinct();
    }

    private static Map findClientSEIsInUse(IndexView index, CxfFixedConfig config) {
        final Map seiToClientConfig = new TreeMap<>();
        findClientInjectionPoints(index).forEach(clientInjectionPoint -> {
            String sei = clientInjectionPoint.getSei().getName();
            final ClientFixedConfig clientConfig = findClientConfig(
                    config,
                    clientInjectionPoint.getConfigKey(),
                    sei);
            seiToClientConfig.put(sei, clientConfig);
        });
        return seiToClientConfig;
    }

    /**
     * Find a {@link ClientFixedConfig} by the given client configuration {@code key} or by the given
     * {@code serviceInterfaceName}.
     * Note that there is a similar algorithm implemented in
     * {@code io.quarkiverse.cxf.CxfClientProducer.selectorCXFClientInfo(CxfConfig, CxfFixedConfig, InjectionPoint, CXFClientInfo)}
     *
     * @param config the {@link CxfFixedConfig} to search in
     * @param key the key to lookup in the {@link CxfFixedConfig#clients} map
     * @param serviceInterfaceName {@link ClientFixedConfig#serviceInterface} to look for
     * @return a matching {@link ClientFixedConfig}, possibly a default one produced by
     *         {@link ClientFixedConfig#createDefault()}
     *
     * @throws IllegalStateException if there are too many {@link ClientFixedConfig}s available for the given
     *         {@code serviceInterfaceName}
     */
    static ClientFixedConfig findClientConfig(CxfFixedConfig config, String key, String serviceInterfaceName) {
        if (key != null && !key.isEmpty()) {
            ClientFixedConfig result = config.clients().get(key);
            if (result == null) {
                /*
                 * We cannot tell at build time, whether this is illegal, because there can be some runtime config
                 * for the given key that we do not see here. So we just return a default ClientFixedConfig
                 */
                return ClientFixedConfig.createDefault();
            }
            return result;
        }

        final List> configsBySei = config.clients().entrySet().stream()
                .filter(cl -> serviceInterfaceName.equals(cl.getValue().serviceInterface().orElse(null)))
                .filter(cl -> !cl.getValue().alternative())
                .collect(Collectors.toList());

        switch (configsBySei.size()) {
            case 0:
                /*
                 * We cannot tell at build time, whether this is illegal, because there can be some runtime config
                 * for the given key that we do not see here. So we just return a default ClientFixedConfig
                 */
                return ClientFixedConfig.createDefault();
            case 1:
                return configsBySei.get(0).getValue();
            default:
                throw new IllegalStateException("quarkus.cxf.*.service-interface = " + serviceInterfaceName
                        + " with alternative = false expected once, but found " + configsBySei.size() + " times in "
                        + configsBySei.stream().map(k -> "quarkus.cxf.\"" + k.getKey() + "\".service-interface")
                                .collect(Collectors.joining(", ")));
        }

    }

    /**
     * Build step to generate Producer beans suitable for injecting @CFXClient
     */
    @BuildStep
    void generateClientProducers(
            List clients,
            BuildProducer generatedBeans,
            BuildProducer unremovableBeans,
            BuildProducer reflectiveClasses) {
        clients
                .stream()
                .map(CxfClientBuildItem::getSei)
                .forEach(sei -> {
                    generateCxfClientProducer(sei, generatedBeans, unremovableBeans);
                });

        if (clients.stream().anyMatch(CxfClientBuildItem::isProxyClassRuntimeInitialized)) {
            reflectiveClasses
                    .produce(ReflectiveClassBuildItem.builder(CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME)
                            .build());
            copyMarkerInterfaceToApplication(generatedBeans);
        }
    }

    /**
     * Copies the {@value CxfClientProducer#RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME} from the current
     * classloader
     * to the user application. Why we have do that: First, the interface is package-visible so that adding it to
     * the client proxy definition forces GraalVM to generate the proxy class in
     * {@value CxfClientProducer#RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_PACKAGE} package rather than under a random
     * package/class name. Thanks to that we can request the postponed initialization of the generated proxy class by
     * package
     * name.
     * More details in #580.
     *
     * @param generatedBeans
     */
    private void copyMarkerInterfaceToApplication(BuildProducer generatedBeans) {
        byte[] bytes;
        try {
            bytes = IoUtil.readClassAsBytes(getClass().getClassLoader(),
                    CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME);
        } catch (IOException e) {
            throw new RuntimeException("Could not read " + CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME
                    + ".class from quarkus-cxf-deployment jar");
        }
        String className = CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME.replace('.', '/');
        generatedBeans.produce(new GeneratedBeanBuildItem(className, bytes));
    }

    private void generateCxfClientProducer(
            String sei,
            BuildProducer generatedBeans,
            BuildProducer unremovableBeans) {
        // For a given SEI we create a dedicated client producer class, i.e.
        //
        // >> @ApplicationScoped
        // >> [public] {SEI}CxfClientProducer implements CxfClientProducer {
        // >> @Inject
        // >> @Named(value="{SEI}")
        // >> public CXFClientInfo info;
        // >>
        // >> @Produces
        // >> @CXFClient
        // >> {SEI} createService(InjectionPoint ip) {
        // >> return ({SEI}) super().loadCxfClient(ip, this.info);
        // >> }
        // >>
        // >> @Produces
        // >> @CXFClient
        // >> CXFClientInfo createInfo(InjectionPoint ip) {
        // >> return ({SEI}) super().loadCxfClientInfo(ip, this.info);
        // >> }
        // >> }
        String cxfClientProducerClassName = sei + "CxfClientProducer";

        ClassOutput classoutput = new GeneratedBeanGizmoAdaptor(generatedBeans);

        try (ClassCreator classCreator = ClassCreator.builder()
                .classOutput(classoutput)
                .className(cxfClientProducerClassName)
                .superClass(CxfClientProducer.class)
                .build()) {

            classCreator.addAnnotation(ApplicationScoped.class);

            // generates:
            // >> public CXFClientData info;
            final FieldCreator info = classCreator
                    .getFieldCreator("info", CXFClientData.class.getName())
                    .setModifiers(Modifier.PUBLIC);

            // add @Named to info, i.e.
            // >> @Named(value="{SEI}")
            // >> public CXFClientData info;

            info.addAnnotation(
                    AnnotationInstance.create(DotNames.NAMED, null, new AnnotationValue[] {
                            AnnotationValue.createStringValue("value", sei)
                    }));

            // add @Inject annotation to info, i.e.
            // >> @Inject
            // >> @Named(value="{SEI}")
            // >> public CXFClientData info;
            info.addAnnotation(
                    AnnotationInstance
                            .create(DotName.createSimple(Inject.class.getName()), null, new AnnotationValue[] {}));

            // create method
            // >> @Produces
            // >> @CXFClient
            // >> {SEI} createService(InjectionPoint ip) { .. }

            try (MethodCreator createService = classCreator.getMethodCreator(
                    "createService",
                    sei,
                    InjectionPoint.class)) {
                createService.addAnnotation(Produces.class);
                createService.addAnnotation(CXFClient.class);

                final ResultHandle thisHandle = createService.getThis();
                final ResultHandle injectionPointHandle = createService.getMethodParam(0);
                final ResultHandle cxfClientInfoHandle = createService.readInstanceField(info.getFieldDescriptor(), thisHandle);

                MethodDescriptor loadCxfClient = MethodDescriptor.ofMethod(
                        CxfClientProducer.class,
                        "loadCxfClient",
                        "java.lang.Object",
                        InjectionPoint.class,
                        CXFClientData.class);
                // >> .. {
                // >> Object cxfClient = this.loadCxfClient(ip, this.info);
                // >> return ({SEI})cxfClient;
                // >> }

                final ResultHandle cxfClient = createService.invokeVirtualMethod(
                        loadCxfClient,
                        thisHandle,
                        injectionPointHandle,
                        cxfClientInfoHandle);
                createService.returnValue(createService.checkCast(cxfClient, sei));

            }

            /*
             * The client proxy implements Closeable so we need to generate a disposer method
             * that calls proxy.close(). This is important because e.g. java.net.http.HttpClient based CXF clients
             * have some associated threads that is better to stop immediately.
             */
            // create method
            // >> @Produces
            // >> @CXFClient
            // >> void closeClient(@Disposes @CXFClient {SEI} client) { .. }
            try (MethodCreator createService = classCreator.getMethodCreator(
                    "closeClient",
                    void.class,
                    sei)) {
                AnnotatedElement clientParamAnnotations = createService.getParameterAnnotations(0);
                clientParamAnnotations.addAnnotation(Disposes.class);
                clientParamAnnotations.addAnnotation(CXFClient.class);

                final ResultHandle thisHandle = createService.getThis();
                final ResultHandle clientHandle = createService.getMethodParam(0);

                MethodDescriptor closeClient = MethodDescriptor.ofMethod(
                        CxfClientProducer.class,
                        "closeCxfClient",
                        void.class,
                        Object.class);
                // >> .. {
                // >> this.closeCxfClient(client);
                // >> }

                createService.invokeVirtualMethod(
                        closeClient,
                        thisHandle,
                        clientHandle);
                createService.returnVoid();

            }
        }

        // Eventually let's produce
        produceUnremovableBean(unremovableBeans, cxfClientProducerClassName);
    }

    private void produceUnremovableBean(
            BuildProducer unremovables,
            String... args) {
        Arrays.stream(args)
                .map(UnremovableBeanBuildItem.BeanClassNameExclusion::new)
                .map(UnremovableBeanBuildItem::new)
                .forEach(unremovables::produce);
    }

    @BuildStep
    @Record(ExecutionTime.STATIC_INIT)
    void customizers(
            CXFRecorder recorder,
            CxfFixedConfig config,
            List features,
            BuildProducer customizers) {
        final HTTPConduitImpl factory = config.httpConduitFactory()
                .orElse(
                        io.quarkiverse.cxf.deployment.QuarkusCxfFeature.hc5Present(features)
                                ? HTTPConduitImpl.CXFDefault
                                : HTTPConduitImpl.QuarkusCXFDefault);
        switch (factory) {
            case CXFDefault:
                // nothing to do
                break;
            case QuarkusCXFDefault:
            case VertxHttpClientHTTPConduitFactory:
            case URLConnectionHTTPConduitFactory:
            case HttpClientHTTPConduitFactory: {
                customizers.produce(new RuntimeBusCustomizerBuildItem(recorder.setBusHTTPConduitFactory(factory)));
                break;
            }
            default:
                throw new IllegalStateException("Unexpected " + HTTPConduitImpl.class.getSimpleName() + " value: "
                        + config.httpConduitFactory());
        }
    }

    /**
     * Temporary workaround for https://github.com/quarkiverse/quarkus-cxf/issues/1608
     */
    @BuildStep
    @Record(ExecutionTime.STATIC_INIT)
    void workaroundAsyncWsdlInit(
            CXFRecorder recorder,
            BuildProducer customizers) {
        customizers.produce(new RuntimeBusCustomizerBuildItem(recorder.setQuarkusWSDLManager()));
    }

    /**
     * Temporary workaround for https://github.com/quarkiverse/quarkus-cxf/issues/1608
     */
    @BuildStep
    ReflectiveClassBuildItem workaroundAsyncWsdlInit() {
        return ReflectiveClassBuildItem.builder(WSDLManagerImpl.class, CatalogWSDLLocator.class).fields().build();
    }

    @BuildStep
    @Record(ExecutionTime.RUNTIME_INIT)
    void workaroundBadForceURLConnectionInit(CXFRecorder recorder) {
        recorder.workaroundBadForceURLConnectionInit();
    }

    /**
     * Allow some reflexive trickery in {@link CXFRecorder#workaroundBadForceURLConnectionInit()}.
     *
     * @return a new {@link ReflectiveClassBuildItem}
     */
    @BuildStep
    ReflectiveClassBuildItem reflectiveClass() {
        return ReflectiveClassBuildItem.builder(HTTPTransportFactory.class).fields().build();
    }

    private static class ProxyInfo {

        public static ProxyInfo of(
                boolean refersToRuntimeInitializedClasses,
                ClassInfo wsClassInfo,
                IndexView index) {
            final List result = new ArrayList<>();
            result.add(wsClassInfo.name().toString());
            result.add(BindingProvider.class.getName());
            result.add("java.io.Closeable");
            result.add(Client.class.getName());

            if (refersToRuntimeInitializedClasses) {
                result.add(io.quarkiverse.cxf.CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME);
            }
            return new ProxyInfo(result, refersToRuntimeInitializedClasses);
        }

        private ProxyInfo(List interfaces, boolean isRuntimeInitialized) {
            super();
            this.interfaces = interfaces;
            this.isRuntimeInitialized = isRuntimeInitialized;
        }

        private final List interfaces;
        private final boolean isRuntimeInitialized;

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy