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

graphql.kickstart.autoconfigure.web.servlet.GraphQLWebAutoConfiguration Maven / Gradle / Ivy

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 oEmbedler Inc. and Contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 *  documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 *  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
 *  persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package graphql.kickstart.autoconfigure.web.servlet;

import static graphql.kickstart.execution.GraphQLObjectMapper.newBuilder;

import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.execution.AsyncExecutionStrategy;
import graphql.execution.ExecutionStrategy;
import graphql.execution.SubscriptionExecutionStrategy;
import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.kickstart.autoconfigure.tools.GraphQLJavaToolsAutoConfiguration;
import graphql.kickstart.autoconfigure.web.GraphQLInvokerAutoConfiguration;
import graphql.kickstart.autoconfigure.web.OnSchemaOrSchemaProviderBean;
import graphql.kickstart.autoconfigure.web.servlet.metrics.MetricsInstrumentation;
import graphql.kickstart.execution.GraphQLInvoker;
import graphql.kickstart.execution.GraphQLObjectMapper;
import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider;
import graphql.kickstart.execution.config.ExecutionStrategyProvider;
import graphql.kickstart.execution.config.GraphQLBuilder;
import graphql.kickstart.execution.config.GraphQLBuilderConfigurer;
import graphql.kickstart.execution.config.GraphQLServletObjectMapperConfigurer;
import graphql.kickstart.execution.config.ObjectMapperProvider;
import graphql.kickstart.execution.error.GraphQLErrorHandler;
import graphql.kickstart.servlet.AbstractGraphQLHttpServlet;
import graphql.kickstart.servlet.AsyncTaskDecorator;
import graphql.kickstart.servlet.GraphQLConfiguration;
import graphql.kickstart.servlet.GraphQLHttpServlet;
import graphql.kickstart.servlet.cache.GraphQLResponseCacheManager;
import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider;
import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider;
import graphql.kickstart.servlet.context.GraphQLServletContextBuilder;
import graphql.kickstart.servlet.core.GraphQLServletListener;
import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder;
import graphql.kickstart.servlet.input.BatchInputPreProcessor;
import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory;
import graphql.kickstart.spring.error.ErrorHandlerSupplier;
import graphql.kickstart.spring.error.GraphQLErrorStartupListener;
import graphql.schema.GraphQLSchema;
import jakarta.servlet.MultipartConfigElement;
import java.time.Duration;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.util.UrlPathHelper;

/** @author oEmbedler Inc. */
@Slf4j
@AutoConfiguration
@RequiredArgsConstructor
@Import(GraphQLInvokerAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@Conditional(OnSchemaOrSchemaProviderBean.class)
@ConditionalOnProperty(
    value = "graphql.servlet.enabled",
    havingValue = "true",
    matchIfMissing = true)
@AutoConfigureAfter({GraphQLJavaToolsAutoConfiguration.class, JacksonAutoConfiguration.class})
@EnableConfigurationProperties({GraphQLServletProperties.class, AsyncServletProperties.class})
public class GraphQLWebAutoConfiguration {

  public static final String QUERY_EXECUTION_STRATEGY = "queryExecutionStrategy";
  public static final String MUTATION_EXECUTION_STRATEGY = "mutationExecutionStrategy";
  public static final String SUBSCRIPTION_EXECUTION_STRATEGY = "subscriptionExecutionStrategy";

  private final GraphQLServletProperties graphQLServletProperties;
  private final AsyncServletProperties asyncServletProperties;
  private final ErrorHandlerSupplier errorHandlerSupplier = new ErrorHandlerSupplier(null);

  @Bean
  public GraphQLErrorStartupListener graphQLErrorStartupListener(
      @Autowired(required = false) GraphQLErrorHandler errorHandler) {
    errorHandlerSupplier.setErrorHandler(errorHandler);
    return new GraphQLErrorStartupListener(
        errorHandlerSupplier, graphQLServletProperties.isExceptionHandlersEnabled());
  }

  @Bean
  @ConditionalOnClass(CorsFilter.class)
  @ConditionalOnProperty(
      prefix = "graphql.servlet",
      name = "cors-enabled",
      havingValue = "true",
      matchIfMissing = true)
  @ConfigurationProperties("graphql.servlet.cors")
  public CorsConfiguration corsConfiguration() {
    return new CorsConfiguration();
  }

  @Bean
  @ConditionalOnClass(CorsFilter.class)
  @ConditionalOnProperty(
      prefix = "graphql.servlet",
      name = "cors-enabled",
      havingValue = "true",
      matchIfMissing = true)
  public CorsFilter corsConfigurer(CorsConfiguration corsConfiguration) {
    log.info("Enabling cors filter");
    Map corsConfigurations = new LinkedHashMap<>(1);
    if (corsConfiguration.getAllowedMethods() == null) {
      corsConfiguration.setAllowedMethods(
          Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
    }
    corsConfigurations.put(graphQLServletProperties.getCorsMapping(), corsConfiguration);

    UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
    configurationSource.setCorsConfigurations(corsConfigurations);
    UrlPathHelper urlPathHelper = new UrlPathHelper();
    urlPathHelper.setAlwaysUseFullPath(true);
    configurationSource.setUrlPathHelper(urlPathHelper);

    return new CorsFilter(configurationSource);
  }

  @Bean
  @ConditionalOnMissingBean
  public GraphQLSchemaServletProvider graphQLSchemaProvider(GraphQLSchema schema) {
    return new DefaultGraphQLSchemaServletProvider(schema);
  }

  @Bean
  @ConditionalOnMissingBean
  public ExecutionStrategyProvider executionStrategyProvider(
      @Autowired(required = false) Map executionStrategies) {
    if (executionStrategies == null || executionStrategies.isEmpty()) {
      return new DefaultExecutionStrategyProvider(
          new AsyncExecutionStrategy(), null, new SubscriptionExecutionStrategy());
    } else if (executionStrategies.entrySet().size() == 1) {
      return new DefaultExecutionStrategyProvider(
          executionStrategies.entrySet().stream()
              .findFirst()
              .map(Entry::getValue)
              .orElseThrow(IllegalStateException::new));
    } else {

      if (!executionStrategies.containsKey(QUERY_EXECUTION_STRATEGY)) {
        throwIncorrectExecutionStrategyNameException();
      }

      if (executionStrategies.size() == 2
          && !(executionStrategies.containsKey(MUTATION_EXECUTION_STRATEGY)
              || executionStrategies.containsKey(SUBSCRIPTION_EXECUTION_STRATEGY))) {
        throwIncorrectExecutionStrategyNameException();
      }

      if (executionStrategies.size() >= 3
          && !(executionStrategies.containsKey(MUTATION_EXECUTION_STRATEGY)
              && executionStrategies.containsKey(SUBSCRIPTION_EXECUTION_STRATEGY))) {
        throwIncorrectExecutionStrategyNameException();
      }

      return new DefaultExecutionStrategyProvider(
          executionStrategies.get(QUERY_EXECUTION_STRATEGY),
          executionStrategies.get(MUTATION_EXECUTION_STRATEGY),
          executionStrategies.get(SUBSCRIPTION_EXECUTION_STRATEGY));
    }
  }

  private void throwIncorrectExecutionStrategyNameException() {
    throw new IllegalStateException(
        String.format(
            "When defining more than one execution strategy, they must be named %s, %s, or %s",
            QUERY_EXECUTION_STRATEGY,
            MUTATION_EXECUTION_STRATEGY,
            SUBSCRIPTION_EXECUTION_STRATEGY));
  }

  @Bean
  @ConditionalOnMissingBean
  public GraphQLInvocationInputFactory invocationInputFactory(
      GraphQLSchemaServletProvider schemaProvider,
      @Autowired(required = false) GraphQLServletContextBuilder contextBuilder,
      @Autowired(required = false) GraphQLServletRootObjectBuilder graphQLRootObjectBuilder) {
    GraphQLInvocationInputFactory.Builder builder =
        GraphQLInvocationInputFactory.newBuilder(schemaProvider);

    if (graphQLRootObjectBuilder != null) {
      builder.withGraphQLRootObjectBuilder(graphQLRootObjectBuilder);
    }

    if (contextBuilder != null) {
      builder.withGraphQLContextBuilder(contextBuilder);
    }

    return builder.build();
  }

  @Bean
  @ConditionalOnMissingBean
  public GraphQLBuilder graphQLBuilder(
      ExecutionStrategyProvider executionStrategyProvider,
      @Autowired(required = false) List instrumentations,
      @Autowired(required = false) PreparsedDocumentProvider preparsedDocumentProvider,
      @Autowired(required = false) GraphQLBuilderConfigurer graphQLBuilderConfigurer) {
    GraphQLBuilder graphQLBuilder = new GraphQLBuilder();
    graphQLBuilder.executionStrategyProvider(() -> executionStrategyProvider);

    if (instrumentations != null && !instrumentations.isEmpty()) {
      if (instrumentations.size() == 1) {
        graphQLBuilder.instrumentation(() -> instrumentations.get(0));
      } else {
        // Metrics instrumentation should be the last to run (we need that from
        // TracingInstrumentation)
        instrumentations.sort((a, b) -> a instanceof MetricsInstrumentation ? 1 : 0);
        graphQLBuilder.instrumentation(() -> new ChainedInstrumentation(instrumentations));
      }
    }

    if (preparsedDocumentProvider != null) {
      graphQLBuilder.preparsedDocumentProvider(() -> preparsedDocumentProvider);
    }

    if (graphQLBuilderConfigurer != null) {
      graphQLBuilder.graphQLBuilderConfigurer(() -> graphQLBuilderConfigurer);
    }

    return graphQLBuilder;
  }

  @Bean
  @ConditionalOnMissingBean
  public GraphQLObjectMapper graphQLObjectMapper(
      ObjectProvider objectMapperProviderObjectProvider,
      @Autowired(required = false) GraphQLServletObjectMapperConfigurer objectMapperConfigurer) {
    GraphQLObjectMapper.Builder builder = newBuilder();
    builder.withGraphQLErrorHandler(errorHandlerSupplier);

    ObjectMapperProvider objectMapperProvider = objectMapperProviderObjectProvider.getIfAvailable();

    if (objectMapperProvider != null) {
      builder.withObjectMapperProvider(objectMapperProvider);
    } else if (objectMapperConfigurer != null) {
      builder.withObjectMapperConfigurer(objectMapperConfigurer);
    }
    return builder.build();
  }

  @Bean
  @ConditionalOnMissingBean
  @ConditionalOnProperty(
      prefix = "graphql.servlet",
      name = "use-default-objectmapper",
      havingValue = "true",
      matchIfMissing = true)
  public ObjectMapperProvider objectMapperProvider(ObjectMapper objectMapper) {
    InjectableValues.Std injectableValues = new InjectableValues.Std();
    injectableValues.addValue(ObjectMapper.class, objectMapper);
    objectMapper.setInjectableValues(injectableValues);
    return () -> objectMapper;
  }

  @Bean
  @ConditionalOnMissingBean
  public GraphQLConfiguration graphQLServletConfiguration(
      GraphQLInvocationInputFactory invocationInputFactory,
      GraphQLInvoker graphQLInvoker,
      GraphQLObjectMapper graphQLObjectMapper,
      @Autowired(required = false) List listeners,
      @Autowired(required = false) BatchInputPreProcessor batchInputPreProcessor,
      @Autowired(required = false) GraphQLResponseCacheManager responseCacheManager,
      @Autowired(required = false) AsyncTaskDecorator asyncTaskDecorator,
      @Autowired(required = false) @Qualifier("graphqlAsyncTaskExecutor") Executor asyncExecutor) {
    Duration asyncTimeout =
        Optional.ofNullable(asyncServletProperties.getTimeout()) //
            .orElse(AsyncServletProperties.DEFAULT_TIMEOUT);
    long subscriptionTimeoutMilliseconds =
        Optional.ofNullable(graphQLServletProperties.getSubscriptionTimeout()) //
            .orElse(GraphQLServletProperties.DEFAULT_SUBSCRIPTION_TIMEOUT)
            .toMillis();
    return GraphQLConfiguration.with(invocationInputFactory)
        .with(graphQLInvoker)
        .with(graphQLObjectMapper)
        .with(listeners)
        .with(subscriptionTimeoutMilliseconds)
        .with(batchInputPreProcessor)
        .with(graphQLServletProperties.getContextSetting())
        .with(responseCacheManager)
        .asyncTimeout(asyncTimeout.toMillis())
        .with(asyncTaskDecorator)
        .asyncCorePoolSize(asyncServletProperties.getThreads().getMin())
        .asyncMaxPoolSize(asyncServletProperties.getThreads().getMax())
        .with(asyncExecutor)
        .build();
  }

  @Bean("graphqlAsyncTaskExecutor")
  @ConditionalOnMissingBean(name = "graphqlAsyncTaskExecutor")
  @ConditionalOnProperty(prefix = "graphql.servlet.async", name = "enabled", havingValue = "true")
  public Executor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(asyncServletProperties.getThreads().getMin());
    executor.setMaxPoolSize(asyncServletProperties.getThreads().getMax());
    executor.setThreadNamePrefix(asyncServletProperties.getThreads().getNamePrefix());
    executor.initialize();
    return executor;
  }

  @Bean
  @ConditionalOnMissingBean
  public GraphQLHttpServlet graphQLHttpServlet(GraphQLConfiguration graphQLConfiguration) {
    return GraphQLHttpServlet.with(graphQLConfiguration);
  }

  @Bean
  public ServletRegistrationBean graphQLServletRegistrationBean(
      AbstractGraphQLHttpServlet servlet,
      @Autowired(required = false) MultipartConfigElement multipartConfigElement) {
    ServletRegistrationBean registration =
        new ServletRegistrationBean<>(servlet, graphQLServletProperties.getServletMapping());
    registration.setMultipartConfig(
        Objects.requireNonNullElseGet(
            multipartConfigElement, () -> new MultipartConfigElement("")));
    registration.setAsyncSupported(asyncServletProperties.isEnabled());
    return registration;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy