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

io.quarkus.resteasy.reactive.common.deployment.ResteasyReactiveCommonProcessor Maven / Gradle / Ivy

There is a newer version: 3.15.2
Show newest version
package io.quarkus.resteasy.reactive.common.deployment;

import static org.jboss.resteasy.reactive.common.model.ResourceInterceptor.FILTER_SOURCE_METHOD_METADATA_KEY;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.ext.Providers;
import jakarta.ws.rs.ext.RuntimeDelegate;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.CompositeIndex;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.jaxrs.RuntimeDelegateImpl;
import org.jboss.resteasy.reactive.common.model.InterceptorContainer;
import org.jboss.resteasy.reactive.common.model.PreMatchInterceptorContainer;
import org.jboss.resteasy.reactive.common.model.ResourceInterceptor;
import org.jboss.resteasy.reactive.common.model.ResourceInterceptors;
import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult;
import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult;
import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveInterceptorScanner;
import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveParameterContainerScanner;
import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner;
import org.jboss.resteasy.reactive.common.processor.scanning.SerializerScanningResult;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.BuildTimeConditionBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.maven.dependency.ResolvedDependency;
import io.quarkus.resteasy.reactive.common.runtime.JaxRsSecurityConfig;
import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig;
import io.quarkus.resteasy.reactive.spi.AbstractInterceptorBuildItem;
import io.quarkus.resteasy.reactive.spi.AdditionalResourceClassBuildItem;
import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem;
import io.quarkus.resteasy.reactive.spi.ContainerResponseFilterBuildItem;
import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem;
import io.quarkus.resteasy.reactive.spi.IgnoreStackMixingBuildItem;
import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem;
import io.quarkus.resteasy.reactive.spi.MessageBodyReaderOverrideBuildItem;
import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem;
import io.quarkus.resteasy.reactive.spi.MessageBodyWriterOverrideBuildItem;
import io.quarkus.resteasy.reactive.spi.ReaderInterceptorBuildItem;
import io.quarkus.resteasy.reactive.spi.WriterInterceptorBuildItem;
import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;

public class ResteasyReactiveCommonProcessor {

    private static final Logger LOG = Logger.getLogger(ResteasyReactiveCommonProcessor.class);

    private static final int LEGACY_READER_PRIORITY = Priorities.USER * 2; // readers are compared by decreased priority
    private static final int LEGACY_WRITER_PRIORITY = Priorities.USER / 2; // writers are compared by increased priority

    private static final String PROVIDERS_SERVICE_FILE = "META-INF/services/" + Providers.class.getName();
    private static final Predicate IS_RESTEASY_CLASSIC_CLIENT_DEP = d -> d.getArtifactId()
            .contains("resteasy-client");
    private static final Predicate IS_RESTEASY_CLASSIC_CORE_DEP = d -> d.getArtifactId()
            .equals("resteasy-core");
    private static final Predicate IS_NOT_TEST_SCOPED = d -> !"test".equals(d.getScope());

    @Produce(ServiceStartBuildItem.class)
    @BuildStep
    void checkMixingStacks(Capabilities capabilities, CurateOutcomeBuildItem curateOutcomeBuildItem,
            List ignoreStackMixingItems) {
        if (!ignoreStackMixingItems.isEmpty()) {
            return;
        }
        List resteasyClassicDeps = curateOutcomeBuildItem.getApplicationModel().getDependencies().stream()
                .filter(d -> d.getGroupId().equals("org.jboss.resteasy")).collect(Collectors.toList());
        boolean hasResteasyCoreDep = resteasyClassicDeps.stream()
                .anyMatch(IS_NOT_TEST_SCOPED.and(IS_RESTEASY_CLASSIC_CORE_DEP));
        if (!hasResteasyCoreDep) {
            return;
        }
        boolean hasResteasyClassicClient = resteasyClassicDeps.stream()
                .anyMatch(IS_NOT_TEST_SCOPED.and(IS_RESTEASY_CLASSIC_CLIENT_DEP));
        if (!hasResteasyClassicClient) { // there is no bulletproof way of knowing whether a server specific dependency has been included, so we deduce it by the absence of client dependency
            throw new DeploymentException("Mixing RESTEasy Reactive and RESTEasy Classic server parts is not supported");
        }
        if (capabilities.isPresent(Capability.REST_CLIENT_REACTIVE)) {
            throw new DeploymentException(
                    "Mixing RESTEasy Reactive and RESTEasy Classic client parts is not supported");
        } else {
            LOG.warn(
                    "Mixing RESTEasy Reactive server and RESTEasy Classic client parts might lead to unexpected results. Consider using 'quarkus-rest-client-reactive' instead.");
        }
    }

    @BuildStep
    void searchForProviders(Capabilities capabilities,
            BuildProducer producer) {
        if (capabilities.isPresent(Capability.RESTEASY) || capabilities.isPresent(Capability.RESTEASY_CLIENT)
                || QuarkusClassLoader.isClassPresentAtRuntime(
                        "org.jboss.resteasy.plugins.providers.JaxrsServerFormUrlEncodedProvider")) { // RESTEasy Classic could be imported via non-Quarkus dependencies
            // in this weird case we don't want the providers to be registered automatically as this would lead to multiple bean definitions
            return;
        }
        // TODO: should we also be looking for the specific provider files?
        producer.produce(new AdditionalApplicationArchiveMarkerBuildItem(PROVIDERS_SERVICE_FILE));
    }

    @BuildStep
    void setUpDenyAllJaxRs(JaxRsSecurityConfig securityConfig,
            BuildProducer defaultSecurityCheckProducer) {
        if (securityConfig.denyJaxRs()) {
            defaultSecurityCheckProducer.produce(DefaultSecurityCheckBuildItem.denyAll());
        } else if (securityConfig.defaultRolesAllowed().isPresent()) {
            defaultSecurityCheckProducer
                    .produce(DefaultSecurityCheckBuildItem.rolesAllowed(securityConfig.defaultRolesAllowed().get()));
        }
    }

    @BuildStep
    ApplicationResultBuildItem handleApplication(CombinedIndexBuildItem combinedIndexBuildItem,
            BuildProducer reflectiveClass,
            List buildTimeConditions,
            ResteasyReactiveConfig config) {
        ApplicationScanningResult result = ResteasyReactiveScanner
                .scanForApplicationClass(combinedIndexBuildItem.getComputingIndex(),
                        config.buildTimeConditionAware() ? getExcludedClasses(buildTimeConditions) : Collections.emptySet());
        if (result.getSelectedAppClass() != null) {
            reflectiveClass.produce(ReflectiveClassBuildItem.builder(result.getSelectedAppClass().name().toString())
                    .build());
        }
        return new ApplicationResultBuildItem(result);
    }

    @BuildStep
    public ResourceInterceptorsContributorBuildItem scanForIOInterceptors(CombinedIndexBuildItem combinedIndexBuildItem,
            ApplicationResultBuildItem applicationResultBuildItem) {
        return new ResourceInterceptorsContributorBuildItem(new Consumer() {
            @Override
            public void accept(ResourceInterceptors interceptors) {
                ResteasyReactiveInterceptorScanner.scanForIOInterceptors(interceptors,
                        combinedIndexBuildItem.getComputingIndex(),
                        applicationResultBuildItem.getResult());
            }
        });
    }

    @BuildStep
    public ResourceInterceptorsBuildItem buildResourceInterceptors(List scanningTasks,
            ApplicationResultBuildItem applicationResultBuildItem,
            BuildProducer additionalBeanBuildItemBuildProducer,
            List writerInterceptors,
            List readerInterceptors,
            List requestFilters,
            List responseFilters) {
        ResourceInterceptors resourceInterceptors = new ResourceInterceptors();
        for (ResourceInterceptorsContributorBuildItem i : scanningTasks) {
            i.getBuildTask().accept(resourceInterceptors);
        }
        AdditionalBeanBuildItem.Builder beanBuilder = AdditionalBeanBuildItem.builder();
        registerContainerBeans(beanBuilder, resourceInterceptors.getContainerResponseFilters());
        registerContainerBeans(beanBuilder, resourceInterceptors.getContainerRequestFilters());
        registerContainerBeans(beanBuilder, resourceInterceptors.getReaderInterceptors());
        registerContainerBeans(beanBuilder, resourceInterceptors.getWriterInterceptors());
        Set globalNameBindings = applicationResultBuildItem.getResult().getGlobalNameBindings();
        for (WriterInterceptorBuildItem i : writerInterceptors) {
            registerInterceptors(globalNameBindings, resourceInterceptors.getWriterInterceptors(), i, beanBuilder);
        }
        for (ReaderInterceptorBuildItem i : readerInterceptors) {
            registerInterceptors(globalNameBindings, resourceInterceptors.getReaderInterceptors(), i, beanBuilder);
        }
        for (ContainerRequestFilterBuildItem i : requestFilters) {
            registerInterceptors(globalNameBindings, resourceInterceptors.getContainerRequestFilters(), i, beanBuilder);
        }
        for (ContainerResponseFilterBuildItem i : responseFilters) {
            registerInterceptors(globalNameBindings, resourceInterceptors.getContainerResponseFilters(), i, beanBuilder);
        }
        additionalBeanBuildItemBuildProducer.produce(beanBuilder.setUnremovable().build());
        return new ResourceInterceptorsBuildItem(resourceInterceptors);
    }

    protected  void registerInterceptors(Set globalNameBindings,
            InterceptorContainer interceptors, B filterItem, AdditionalBeanBuildItem.Builder beanBuilder) {
        if (filterItem.isRegisterAsBean()) {
            beanBuilder.addBeanClass(filterItem.getClassName());
        }
        ResourceInterceptor interceptor = interceptors.create();
        interceptor.setClassName(filterItem.getClassName());
        Integer priority = filterItem.getPriority();
        if (priority != null) {
            interceptor.setPriority(priority);
        }
        if (filterItem instanceof ContainerRequestFilterBuildItem) {
            ContainerRequestFilterBuildItem crfbi = (ContainerRequestFilterBuildItem) filterItem;
            interceptor.setNonBlockingRequired(crfbi.isNonBlockingRequired());
            interceptor.setWithFormRead(crfbi.isWithFormRead());
            MethodInfo filterSourceMethod = crfbi.getFilterSourceMethod();
            if (filterSourceMethod != null) {
                interceptor.metadata = Map.of(FILTER_SOURCE_METHOD_METADATA_KEY, filterSourceMethod);
            }
        } else if (filterItem instanceof ContainerResponseFilterBuildItem) {
            MethodInfo filterSourceMethod = ((ContainerResponseFilterBuildItem) filterItem).getFilterSourceMethod();
            if (filterSourceMethod != null) {
                interceptor.metadata = Map.of(FILTER_SOURCE_METHOD_METADATA_KEY, filterSourceMethod);
            }
        }
        if (interceptors instanceof PreMatchInterceptorContainer
                && ((ContainerRequestFilterBuildItem) filterItem).isPreMatching()) {
            ((PreMatchInterceptorContainer) interceptors).addPreMatchInterceptor(interceptor);

        } else {
            Set nameBindingNames = filterItem.getNameBindingNames();
            if (nameBindingNames.isEmpty() || namePresent(nameBindingNames, globalNameBindings)) {
                interceptors.addGlobalRequestInterceptor(interceptor);
            } else {
                interceptor.setNameBindingNames(nameBindingNames);
                interceptors.addNameRequestInterceptor(interceptor);
            }
        }
    }

    private void registerContainerBeans(AdditionalBeanBuildItem.Builder additionalProviders,
            InterceptorContainer container) {
        for (ResourceInterceptor i : container.getGlobalResourceInterceptors()) {
            additionalProviders.addBeanClass(i.getClassName());
        }
        for (ResourceInterceptor i : container.getNameResourceInterceptors()) {
            additionalProviders.addBeanClass(i.getClassName());
        }
        if (container instanceof PreMatchInterceptorContainer) {
            for (ResourceInterceptor i : ((PreMatchInterceptorContainer) container).getPreMatchInterceptors()) {
                additionalProviders.addBeanClass(i.getClassName());
            }
        }
    }

    private boolean namePresent(Set nameBindingNames, Set globalNameBindings) {
        for (String i : globalNameBindings) {
            if (nameBindingNames.contains(i)) {
                return true;
            }
        }
        return false;
    }

    @BuildStep
    JaxRsResourceIndexBuildItem resourceIndex(CombinedIndexBuildItem combinedIndex,
            List generatedJaxRsResources,
            BuildProducer generatedBeansProducer) throws IOException {
        if (generatedJaxRsResources.isEmpty()) {
            return new JaxRsResourceIndexBuildItem(combinedIndex.getComputingIndex());
        }

        Indexer indexer = new Indexer();
        for (GeneratedJaxRsResourceBuildItem generatedJaxRsResource : generatedJaxRsResources) {
            indexer.index(new ByteArrayInputStream(generatedJaxRsResource.getData()));
            generatedBeansProducer
                    .produce(new GeneratedBeanBuildItem(generatedJaxRsResource.getName(), generatedJaxRsResource.getData()));
        }
        return new JaxRsResourceIndexBuildItem(CompositeIndex.create(combinedIndex.getComputingIndex(), indexer.complete()));
    }

    @BuildStep
    void scanResources(
            JaxRsResourceIndexBuildItem jaxRsResourceIndexBuildItem,
            List additionalResourceClassBuildItems,
            BuildProducer annotationsTransformerBuildItemBuildProducer,
            BuildProducer resourceScanningResultBuildItemBuildProducer) {

        Map additionalResources = new HashMap<>();
        Map additionalResourcePaths = new HashMap<>();
        for (AdditionalResourceClassBuildItem bi : additionalResourceClassBuildItems) {
            additionalResources.put(bi.getClassInfo().name(), bi.getClassInfo());
            additionalResourcePaths.put(bi.getClassInfo().name(), bi.getPath());
        }
        ResourceScanningResult res = ResteasyReactiveScanner.scanResources(jaxRsResourceIndexBuildItem.getIndexView(),
                additionalResources, additionalResourcePaths);
        if (res == null) {
            return;
        }
        if (!res.getResourcesThatNeedCustomProducer().isEmpty()) {
            annotationsTransformerBuildItemBuildProducer
                    .produce(new AnnotationsTransformerBuildItem(
                            new VetoingAnnotationTransformer(res.getResourcesThatNeedCustomProducer().keySet())));
        }
        resourceScanningResultBuildItemBuildProducer.produce(new ResourceScanningResultBuildItem(res));
    }

    @BuildStep
    public void setupEndpoints(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
            ApplicationResultBuildItem applicationResultBuildItem,
            BeanContainerBuildItem beanContainerBuildItem,
            Optional resourceScanningResultBuildItem,
            BuildProducer reflectiveClass,
            BuildProducer messageBodyWriterBuildItemBuildProducer,
            BuildProducer messageBodyReaderBuildItemBuildProducer) throws NoSuchMethodException {

        if (!resourceScanningResultBuildItem.isPresent()) {
            // no detected @Path, bail out
            return;
        }

        IndexView index = beanArchiveIndexBuildItem.getIndex();
        SerializerScanningResult serializers = ResteasyReactiveScanner.scanForSerializers(index,
                applicationResultBuildItem.getResult());
        for (var i : serializers.getReaders()) {
            messageBodyReaderBuildItemBuildProducer.produce(new MessageBodyReaderBuildItem(i.getClassName(),
                    i.getHandledClassName(), i.getMediaTypeStrings(), i.getRuntimeType(), i.isBuiltin(), i.getPriority()));
            reflectiveClass.produce(
                    ReflectiveClassBuildItem.builder(i.getClassName()).build());
        }
        for (var i : serializers.getWriters()) {
            messageBodyWriterBuildItemBuildProducer.produce(new MessageBodyWriterBuildItem(i.getClassName(),
                    i.getHandledClassName(), i.getMediaTypeStrings(), i.getRuntimeType(), i.isBuiltin(), i.getPriority()));
        }
    }

    @BuildStep
    void registerRuntimeDelegateImpl(BuildProducer serviceProviders) {
        serviceProviders.produce(new ServiceProviderBuildItem(RuntimeDelegate.class.getName(),
                RuntimeDelegateImpl.class.getName()));
    }

    /*
     * There are some MessageBodyReaders and MessageBodyWriters that are brought in transitively
     * by the inclusion of the extension like the 'quarkus-keycloak-admin-client'.
     * We need to make sure that these providers are not selected over the ones that our Quarkus extensions provide.
     * To do that, we first need to make them built-in (as the spec mandates that non-build-in providers are chosen
     * over built-in ones) and then we also need to change their priority
     */
    @BuildStep
    void deprioritizeLegacyProviders(BuildProducer readers,
            BuildProducer writers) {
        readers.produce(new MessageBodyReaderOverrideBuildItem(
                "org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider", LEGACY_READER_PRIORITY, true));
        readers.produce(new MessageBodyReaderOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider",
                LEGACY_READER_PRIORITY, true));
        readers.produce(new MessageBodyReaderOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.JacksonJaxbJsonProvider",
                LEGACY_READER_PRIORITY, true));
        readers.produce(new MessageBodyReaderOverrideBuildItem("org.keycloak.admin.client.JacksonProvider",
                LEGACY_READER_PRIORITY, true));

        writers.produce(new MessageBodyWriterOverrideBuildItem(
                "org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider", LEGACY_WRITER_PRIORITY, true));
        writers.produce(new MessageBodyWriterOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider",
                LEGACY_WRITER_PRIORITY, true));
        writers.produce(new MessageBodyWriterOverrideBuildItem("com.fasterxml.jackson.jakarta.rs.json.JacksonJaxbJsonProvider",
                LEGACY_WRITER_PRIORITY, true));
        writers.produce(new MessageBodyWriterOverrideBuildItem("org.keycloak.admin.client.JacksonProvider",
                LEGACY_WRITER_PRIORITY, true));
    }

    /**
     * @param buildTimeConditions the build time conditions from which the excluded classes are extracted.
     * @return the set of classes that have been annotated with unsuccessful build time conditions.
     */
    public static Set getExcludedClasses(List buildTimeConditions) {
        return buildTimeConditions.stream()
                .filter(item -> !item.isEnabled())
                .map(BuildTimeConditionBuildItem::getTarget)
                .filter(target -> target.kind() == AnnotationTarget.Kind.CLASS)
                .map(target -> target.asClass().toString())
                .collect(Collectors.toSet());
    }

    @BuildStep
    public void scanForParameterContainers(CombinedIndexBuildItem combinedIndexBuildItem,
            ApplicationResultBuildItem applicationResultBuildItem,
            BuildProducer parameterContainersBuildItemBuildProducer) {
        IndexView index = combinedIndexBuildItem.getComputingIndex();
        Set res = ResteasyReactiveParameterContainerScanner.scanParameterContainers(index,
                applicationResultBuildItem.getResult());
        parameterContainersBuildItemBuildProducer.produce(new ParameterContainersBuildItem(res));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy