Skip to content

spring server updates #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.springframework.stereotype.Component
@Component
class MyGraphQLContextFactory: GraphQLContextFactory<MyGraphQLContext> {

override fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): MyGraphQLContext = MyGraphQLContext(
override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): MyGraphQLContext = MyGraphQLContext(
myCustomValue = request.headers.getFirst("MyHeader") ?: "defaultContext",
request = request,
response = response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.expediagroup.graphql.spring

import com.expediagroup.graphql.spring.execution.SimpleSubscriptionHandler
import com.expediagroup.graphql.spring.execution.SubscriptionHandler
import com.expediagroup.graphql.spring.execution.SubscriptionWebSocketHandler
import com.expediagroup.graphql.spring.operations.Subscription
import com.fasterxml.jackson.databind.ObjectMapper
import graphql.GraphQL
Expand All @@ -39,13 +40,16 @@ class SubscriptionAutoConfiguration {

@Bean
@ConditionalOnMissingBean
fun subscriptionHandler(graphQL: GraphQL, objectMapper: ObjectMapper): SubscriptionHandler = SimpleSubscriptionHandler(graphQL, objectMapper)
fun subscriptionHandler(graphQL: GraphQL): SubscriptionHandler = SimpleSubscriptionHandler(graphQL)

@Bean
@ConditionalOnMissingBean
fun websocketHandlerAdapter(): WebSocketHandlerAdapter = WebSocketHandlerAdapter()

@Bean
fun subscriptionHandlerMapping(config: GraphQLConfigurationProperties, subscriptionHandler: SubscriptionHandler): HandlerMapping =
SimpleUrlHandlerMapping(mapOf(config.subscriptions.endpoint to subscriptionHandler), Ordered.HIGHEST_PRECEDENCE)
fun subscriptionWebSocketHandler(handler: SubscriptionHandler, objectMapper: ObjectMapper) = SubscriptionWebSocketHandler(handler, objectMapper)

@Bean
fun subscriptionHandlerMapping(config: GraphQLConfigurationProperties, subscriptionWebSocketHandler: SubscriptionWebSocketHandler): HandlerMapping =
SimpleUrlHandlerMapping(mapOf(config.subscriptions.endpoint to subscriptionWebSocketHandler), Ordered.HIGHEST_PRECEDENCE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,29 @@

package com.expediagroup.graphql.spring.execution

import kotlinx.coroutines.reactor.mono
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono

/**
* [org.springframework.core.Ordered] value used for the [ContextWebFilter] order in which it will be applied to the incoming requests.
* Smaller value take higher precedence.
*/
const val GRAPHQL_CONTEXT_FILTER_ODER = 0

/**
* Default web filter that populates GraphQL context in the reactor subscriber context.
*/
@Order(GRAPHQL_CONTEXT_FILTER_ODER)
class ContextWebFilter(private val contextFactory: GraphQLContextFactory<Any>) : WebFilter, Ordered {

@Suppress("ForbiddenVoid")
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val context = contextFactory.generateContext(exchange.request, exchange.response)
return chain.filter(exchange).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, context) }
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> = mono {
contextFactory.generateContext(exchange.request, exchange.response)
}.flatMap { graphQLContext ->
chain.filter(exchange).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, graphQLContext) }
}

override fun getOrder(): Int = GRAPHQL_CONTEXT_FILTER_ODER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ interface GraphQLContextFactory<out T : Any> {
/**
* Generate GraphQL context based on the incoming request and the corresponding response.
*/
fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): T
suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): T
}

/**
* Default context factory that generates empty GraphQL context.
*/
internal object EmptyContextFactory : GraphQLContextFactory<GraphQLContext> {

override fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): GraphQLContext = GraphQLContext.newContext().build()
override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): GraphQLContext = GraphQLContext.newContext().build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,65 +16,41 @@

package com.expediagroup.graphql.spring.execution

import com.expediagroup.graphql.spring.exception.SimpleKotlinGraphQLError
import com.expediagroup.graphql.spring.model.GraphQLRequest
import com.expediagroup.graphql.spring.model.GraphQLResponse
import com.expediagroup.graphql.spring.model.toExecutionInput
import com.expediagroup.graphql.spring.model.toGraphQLResponse
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import graphql.ExecutionResult
import graphql.GraphQL
import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux

/**
* WebSocket handler for handling GraphQL subscriptions.
* GraphQL subscription handler.
*/
interface SubscriptionHandler : WebSocketHandler {
interface SubscriptionHandler {

/**
* Execute GraphQL subscription request and return a Publisher that emits 0 to N [GraphQLResponse]s.
* Execute GraphQL subscription request and return a Reactor Flux Publisher that emits 0 to N [GraphQLResponse]s.
*/
fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse>
}

/**
* Default WebSocket handler for handling GraphQL subscriptions.
* Default implementation of GraphQL subscription handler.
*/
open class SimpleSubscriptionHandler(
private val graphQL: GraphQL,
private val objectMapper: ObjectMapper
) : SubscriptionHandler {

private val logger = LoggerFactory.getLogger(SubscriptionHandler::class.java)

@Suppress("ForbiddenVoid")
override fun handle(session: WebSocketSession): Mono<Void> {
val response = session.receive()
.concatMap {
val graphQLRequest = objectMapper.readValue<GraphQLRequest>(it.payloadAsText)
executeSubscription(graphQLRequest)
.doOnSubscribe { logger.trace("WebSocket GraphQL subscription subscribe, ID=${session.id}") }
.doOnCancel { logger.trace("WebSocket GraphQL subscription cancel, ID=${session.id}") }
.doOnComplete { logger.trace("WebSocket GraphQL subscription complete, ID=${session.id}") }
.doFinally { session.close() }
}
.map { objectMapper.writeValueAsString(it) }
.map { session.textMessage(it) }

return session.send(response)
}

override fun getSubProtocols(): List<String> = listOf("graphql-ws")

override fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse> =
graphQL.execute(graphQLRequest.toExecutionInput())
.getData<Publisher<ExecutionResult>>()
.toFlux()
.map { it.toGraphQLResponse() }
open class SimpleSubscriptionHandler(private val graphQL: GraphQL) : SubscriptionHandler {

override fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse> = Mono.subscriberContext()
.flatMapMany { reactorContext ->
val graphQLContext = reactorContext.getOrDefault<Any>(GRAPHQL_CONTEXT_KEY, null)
graphQL.execute(graphQLRequest.toExecutionInput(graphQLContext))
.getData<Publisher<ExecutionResult>>()
.toFlux()
.map { result -> result.toGraphQLResponse() }
.onErrorResume { error -> Flux.just(GraphQLResponse(errors = listOf(SimpleKotlinGraphQLError(error)))) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.spring.execution

import com.expediagroup.graphql.spring.model.GraphQLRequest
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.slf4j.LoggerFactory
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
import reactor.core.publisher.Mono

/**
* Default WebSocket handler for handling GraphQL subscriptions.
*/
class SubscriptionWebSocketHandler(
private val subscriptionHandler: SubscriptionHandler,
private val objectMapper: ObjectMapper
) : WebSocketHandler {

private val logger = LoggerFactory.getLogger(SubscriptionWebSocketHandler::class.java)

@Suppress("ForbiddenVoid")
override fun handle(session: WebSocketSession): Mono<Void> {
val response = session.receive()
.concatMap {
val graphQLRequest = objectMapper.readValue<GraphQLRequest>(it.payloadAsText)
subscriptionHandler.executeSubscription(graphQLRequest)
.doOnSubscribe { logger.trace("WebSocket GraphQL subscription subscribe, ID=${session.id}") }
.doOnCancel { logger.trace("WebSocket GraphQL subscription cancel, ID=${session.id}") }
.doOnComplete { logger.trace("WebSocket GraphQL subscription complete, ID=${session.id}") }
.doFinally { session.close() }
}
.map { objectMapper.writeValueAsString(it) }
.map { session.textMessage(it) }

return session.send(response)
}

override fun getSubProtocols(): List<String> = listOf("graphql-ws")
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.spring

import com.expediagroup.graphql.SchemaGeneratorConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.spring

import com.expediagroup.graphql.SchemaGeneratorConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.spring

import com.expediagroup.graphql.SchemaGeneratorConfig
Expand All @@ -8,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import graphql.GraphQL
import graphql.schema.GraphQLSchema
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -99,7 +116,9 @@ class SubscriptionConfigurationTest {
fun subscription(): Subscription = SimpleSubscription()

@Bean
fun subscriptionHandler(): SubscriptionHandler = mockk()
fun subscriptionHandler(): SubscriptionHandler = mockk {
every { executeSubscription(any()) } returns Flux.empty()
}

@Bean
fun webSocketHandlerAdapter(): WebSocketHandlerAdapter = mockk()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
/*
* Copyright 2019 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.spring.execution

import graphql.GraphQLContext
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import reactor.util.context.Context
import kotlin.test.assertNotNull

class ContextWebFilterTest {

@Test
@Suppress("ForbiddenVoid")
fun `verify web filter populates context in the subscriber context`() {
val mockRequest = mockk<ServerHttpRequest>()
val exchange = mockk<ServerWebExchange> {
every { request } returns mockRequest
var generatedContext: Context? = null
val exchange: ServerWebExchange = mockk {
every { request } returns mockk()
every { response } returns mockk()
}
val chain = mockk<WebFilterChain> {
every { filter(exchange) } returns Mono.empty()
val chain: WebFilterChain = mockk {
every { filter(any()) } returns Mono.subscriberContext().flatMap {
generatedContext = it
Mono.empty<Void>()
}
}

val contextFilter = ContextWebFilter(EmptyContextFactory)
StepVerifier.create(contextFilter.filter(exchange, chain))
.expectAccessibleContext()
.hasSize(1)
.hasKey(GRAPHQL_CONTEXT_KEY)
.assertThat {
val graphQLContext = it.getOrDefault<GraphQLContext>(GRAPHQL_CONTEXT_KEY, null)
assertNotNull(graphQLContext)
}
.then()
.verifyComplete()

assertNotNull(generatedContext)
val graphQLContext = generatedContext?.getOrDefault<GraphQLContext>(GRAPHQL_CONTEXT_KEY, null)
assertNotNull(graphQLContext)
}
}
Loading