spring reactive get locale in repository #Query - spring-boot

I have localized entities in a reactive spring boot application.
The current implementation works fine by depending on the Accept-Language header:
Controller
#RestController
class TaskController(val taskService: TaskService) {
#GetMapping("/tasks")
suspend fun getTasks(locale: Locale): Map<Long?,Task> {
return taskService.getTasks(locale).toList().associateBy(Task::id)
}
}
Service
#Service
class TaskServiceImpl(val taskRepository : TaskRepository) : TaskService {
override fun getTasks(locale: Locale): Flow<Task> {
return taskRepository.findWithLocale(locale.language)
}
Repository
interface TaskRepository : CoroutineCrudRepository<Task, Long> {
#Query("SELECT * FROM task t LEFT JOIN task_l10n tl10n ON tl10n.task_id=t.id WHERE tl10n.locale = :locale")
fun findWithLocale(locale: String): Flow<Task>
}
Question
As you can see, I need to pass down the locale from the controller to the service and then to the repository. Because a lot of our API will suffer from this noise/boilerplate I wonder if I somehow can use/inject the locale.language in a #Query("... where locale = :locale") without passing it as a parameter?
In MVC we could use LocaleContextHolder.getLocale() at least on the service level and get rid of some of the noise but it is not available in a reactive stack because it is bound to the thread and not to the coroutine.
APPROACH
From this spring.io post I hope to be able to setup the SpEL evaluation context in a request-aware way - i.e. to access the locale of the current request.
Something along the lines of this
#Query("... WHERE tl10n.locale = ?#{locale().language}"
But I cannot figure out these things:
how to put the reactive context on the SpEL context on a per request basis?
How to populate the reactive context with the request's locale?
Step 1
First I found that the exchange would provide access to the locale by debugging how Spring resolves the controller method's argument: it's done in ServerWebExchangeMethodArgumentResolver
exchange.getLocaleContext().getLocale()
So if I could access the exchange from a SpEL context, I would be happy.
?#{exchange.getLocaleContext().getLocale()}
which is not possible, because the SpEL context does not know about the reactive context:
Property or field 'exchange' cannot be found on object of type 'java.lang.Object[]' - maybe not public or not valid?
Step 2
Next I figured out that I can come up with a SpEL extension holding the locale and add the extension to the reactive repositories (actually I am using kotlin coroutines based CoroutineCrudRepository) by using a BeanPostProcessor on my repositories to make my custom SpEL extension available in the #Query("... ?#{locale()}") methods:
/**
* Adds extensions to the SpEL evaluation context.
*/
#Configuration
class RepositorySpELExtensionConfiguration {
companion object {
// list of provided extensions
val contextProviderWithExtensions =
ReactiveExtensionAwareQueryMethodEvaluationContextProvider(listOf(ReactiveLocaleAwareSpELExtension.INSTANCE))
}
/**
* Registers the customizer to the context to make spring aware of the bean post processor.
*/
#Bean
fun spELContextInRepositoriesCustomizer(): AddExtensionsToRepositoryBeanPostProcessor {
return AddExtensionsToRepositoryBeanPostProcessor()
}
/**
* Sets the [contextProviderWithExtensions] for SpEL in the [R2dbcRepositoryFactoryBean]s which makes the extensions
* usable in `#Query(...)` methods.
*/
class AddExtensionsToRepositoryBeanPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any {
if (bean is R2dbcRepositoryFactoryBean<*, *, *>) {
bean.addRepositoryFactoryCustomizer { it.setEvaluationContextProvider(contextProviderWithExtensions) }
}
return bean
}
}
/**
* Makes the [LocaleAwareSpELExtension] available in a reactive context.
*/
enum class ReactiveLocaleAwareSpELExtension : ReactiveEvaluationContextExtension {
INSTANCE;
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.just(LocaleAwareSpELExtension("en"))
}
override fun getExtensionId(): String {
ReactiveQueryMethodEvaluationContextProvider.DEFAULT
return "localeAwareSpELExtension"
}
}
/**
* Provides the requests locale as SpEL extension.
*
* Use it like this:
* ```
* #Query("... WHERE locale = :#{locale()}")
* ```
*/
class LocaleAwareSpELExtension(private val locale: String) : EvaluationContextExtension {
override fun getRootObject(): LocaleAwareSpELExtension {
return this
}
override fun getExtensionId(): String {
return "localeAwareSpELExtension"
}
#Suppress("unused") // Potentially used by `#Query(...) methods.
fun locale(): String {
return locale
}
}
}
This works fine hurray - but as you can see, I hardcoded the locale to "en" when creating the extension.
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.just(LocaleAwareSpELExtension("en"))
}
When debugging this line, I know that it is executed on a per request basis, which makes me hope that I should be able to somehow use the request context to populate the locale.
Step 3
I am stuck on how to get the request context to populate the locale in the SpEL extension as shown in step 2. This is what I try:
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.deferContextual { it.get<Mono<ServerWebExchange>>(ServerWebExchangeContextFilter.EXCHANGE_CONTEXT_ATTRIBUTE) }
.map { it.localeContext.locale?.language ?: "en" }
.map { LocaleAwareSpELExtension(it) }
}
but the exchange is not available in the context:
Context does not contain key: org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.EXCHANGE_CONTEXT
From what I understand the context is not populated, because the Mono is not returned from the spring controller but it is used in the Query SpEL and I guess this is not the same context chain?
I think it must be possible, because it is similar to how the principal and security context would be made available, but I not fully understand how ReactiveSecurityContextHolder works - especially not how exactly it is populated for the SpEL context. In fact, in my default configuration it is not even populated - again I think because it is not the same context chain. Another difference here is that spring explicitly sets the security context to the reactive context in the ReactorContextWebFilter.
I think I now could create my own reactive request filter to populate the locale context, but I don't know how to make the reactive context available to the SpEL context.

Turns out adding the filter works (s. below). I still don't fully understand how the context population of SpEL VS reactive context works in this scenario, but it works..
#Component
class ReactorContextLocaleWebFilter : WebFilter {
companion object {
val KEY = ReactorContextLocaleWebFilter::class.java
}
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange).contextWrite { context: Context ->
if (context.hasKey(KEY))
context
else
withLocaleContext(context, exchange)
}
}
private fun withLocaleContext(mainContext: Context, exchange: ServerWebExchange): Context {
return mainContext
.putAll(Mono.just(exchange.localeContext.locale?.language ?: "en").`as` { lang: Mono<String> ->
Context.of(KEY, lang).readOnly()
})
}
}
and then adjusting the extension creation to use the web filter:
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.deferContextual { it.get<Mono<String>>(ReactorContextLocaleWebFilter.KEY) }
.map { LocaleAwareSpELExtension(it) }
}

Related

failed to validate request params in a Spring Boot/Kotlin Coroutines controller

In a SpringBoot/Kotlin Coroutines project, I have a controller class like this.
#RestContollser
#Validated
class PostController(private val posts: PostRepository) {
suspend fun search(#RequestParam q:String, #RequestParam #Min(0) offset:Int, #RequestParam #Min(1) limit:Int): ResponseEntity<Any> {}
}
The validation on the #ResquestBody works as the general Spring WebFlux, but when testing
validating request params , it failed and throws an exception like:
java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:
It is not a ConstraintViolationException.
I think this is a bug in the framework when you are using coroutines (update , it is, I saw Happy Songs comment). In summary:
"#Validated is indeed not yet Coroutines compliant, we need to fix that by using Coroutines aware methods to discover method parameters."
The trouble is that the signature of the method on your controller is actually enhanced by Spring to have an extra parameter, like this, adding a continuation:
public java.lang.Object com.example.react.PostController.search(java.lang.String,int,int,kotlin.coroutines.Continuation)
so when the hibernate validator calls getParameter names to get the list of parameters on your method, it thinks there are 4 in total on the request, and then gets an index out of bounds exception trying to get the 4th (index 3).
If you put a breakpoint on the return of this:
#Override
public E get(int index) {
return a[index];
}
and put a breakpoint condition of index ==3 && a.length <4 you can see what is going on.
I'd report it as a bug on the Spring issue tracker.
You might be better off taking an alternative approach, as described here, using a RequestBody as a DTO and using the #Valid annotation
https://www.vinsguru.com/spring-webflux-validation/
Thanks for the happy songs' comments, I found the best solution by now to overcome this barrier from the Spring Github issues#23499.
As explained in comments of this issue and PaulNuk's answer, there is a Continuation will be appended to the method arguments at runtime, which will fail the index computation of the method parameter names in the Hibernate Validator.
The solution is changing the ParameterNameDiscoverer.getParameterNames(Method) method and adding a empty string as the additional parameter name when it is a suspend function.
class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() {
override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE
override fun postProcessConfiguration(configuration: javax.validation.Configuration<*>) {
super.postProcessConfiguration(configuration)
val discoverer = PrioritizedParameterNameDiscoverer()
discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer())
discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer())
discoverer.addDiscoverer(LocalVariableTableParameterNameDiscoverer())
val defaultProvider = configuration.defaultParameterNameProvider
configuration.parameterNameProvider(object : ParameterNameProvider {
override fun getParameterNames(constructor: Constructor<*>): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(constructor)
return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor)
}
override fun getParameterNames(method: Method): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(method)
return paramNames?.toList() ?: defaultProvider.getParameterNames(method)
}
})
}
}
class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer {
private val defaultProvider = KotlinReflectionParameterNameDiscoverer()
override fun getParameterNames(constructor: Constructor<*>): Array<String>? =
defaultProvider.getParameterNames(constructor)
override fun getParameterNames(method: Method): Array<String>? {
val defaultNames = defaultProvider.getParameterNames(method) ?: return null
val function = method.kotlinFunction
return if (function != null && function.isSuspend) {
defaultNames + ""
} else defaultNames
}
}
Then declare a new validator factory bean.
#Primary
#Bean
#Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun defaultValidator(): LocalValidatorFactoryBean {
val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean()
factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject()
return factoryBean
}
Get the complete sample codes from my Github.

Togglz - Username Activation Strategy Implementation

I am trying to implement UsernameActivation Startegy in Springboot using togglz, but due to insufficient example/documentation on this, I am unable to do so. It is a simple Poc in maven. Here are my classes:
public enum Features implements Feature{
#Label("just a description")
#EnabledByDefault
HELLO_WORLD,
#Label("Hello World Feature")
#DefaultActivationStrategy(id = UsernameActivationStrategy.ID, parameters =
{#ActivationParameter(name = UsernameActivationStrategy.PARAM_USERS, value = "suga")
})
HELLO,
#Label("another descrition")
#EnabledByDefault
REVERSE_GREETING;
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}
#Component
public class Togglz implements TogglzConfig {
public Class<? extends Feature> getFeatureClass() {
return Features.class;
}
public StateRepository getStateRepository() {
return new FileBasedStateRepository(new File("/tmp/features.properties"));
}
public UserProvider getUserProvider() {
return new SpringSecurityUserProvider("ADMIN_ROLE");
}
}
I want to use UsernameActivation strategy but i am not sure what more code changes I need to do in order for it to work. I do know that it is somehow related to UserProvider. Also, I am not sure how will it compare the username value and how will it capture the current user value. Any idea around this will be of great help!
I had to override the getUserProvider method. Since I am using Spring for auto-configuration, and not extending ToggleConfig, I added this as a bean to load at startup.
#Bean
public UserProvider getUserProvider() {
return new UserProvider() {
#Override
public FeatureUser getCurrentUser() {
String username = <MyAppSecurityProvider>.getUserName();
boolean isAdmin = "admin".equals(username);
return new SimpleFeatureUser(username, isAdmin);
}
};
}
Note: I had to do this as my app uses our inbuilt security mechanism. Reading the documentation, it looks like its easier if you are using standard security such as Spring or Servlet.
My config in application.yml(the same thing you have in the annotation)
togglz:
features:
FRIST_FEATURE:
enabled: true
strategy: username
param:
users: user1,user2
SECOND_FEATURE:
enabled: true
strategy: username
param:
users: user2,user3

Springdoc GroupedOpenApi not following global parameters set with OperationCustomizer

When using GroupedOpenApi to define an API group, the common set of parameters that are added to every endpoint is not present in the parameters list.
Below are the respective codes
#Bean
public GroupedOpenApi v1Apis() {
return GroupedOpenApi.builder().group("v1 APIs")
// hide all v2 APIs
.pathsToExclude("/api/v2/**", "/v2/**")
// show all v1 APIs
.pathsToMatch("/api/v1/**", "/v1/**")
.build();
}
And the class to add the Standard Headers to all the endpoints
#Component
public class GlobalHeaderAdder implements OperationCustomizer {
#Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
operation.addParametersItem(new Parameter().$ref("#/components/parameters/ClientID"));
operation.addSecurityItem(new SecurityRequirement().addList("Authorization"));
List<Parameter> parameterList = operation.getParameters();
if (parameterList!=null && !parameterList.isEmpty()) {
Collections.rotate(parameterList, 1);
}
return operation;
}
}
Actual Output
Expected Output
Workaround
Adding the paths to be included/excluded in the application properties file solves the error. But something at the code level will be much appreciated.
Attach the required OperationCustomizerobject while building the Api Group.
#Bean
public GroupedOpenApi v1Apis(GlobalHeaderAdder globalHeaderAdder) {
return GroupedOpenApi.builder().group("v1 APIs")
// hide all v2 APIs
.pathsToExclude("/api/v2/**", "/v2/**")
// show all v1 APIs
.pathsToMatch("/api/v1/**", "/v1/**")
.addOperationCustomizer(globalHeaderAdded)
.build();
}
Edit: Answer updated with reference to #Value not providing values from application properties Spring Boot
Alternative to add and load OperationCustomizer in the case you declare yours open api groups by properties springdoc.group-configs[0].group= instead definition by Java code in a Spring Configuration GroupedOpenApi.builder().
#Bean
public Map<String, GroupedOpenApi> configureGroupedsOpenApi(Map<String, GroupedOpenApi> groupedsOpenApi, OperationCustomizer operationCustomizer) {
groupedsOpenApi.forEach((id, groupedOpenApi) -> groupedOpenApi.getOperationCustomizers()
.add(operationCustomizer));
return groupedsOpenApi;
}

Spring Cloud - HystrixCommand - How to properly enable with shared libraries

Using Springboot 1.5.x, Spring Cloud, and JAX-RS:
I could use a second pair of eyes since it is not clear to me whether the Spring configured, Javanica HystrixCommand works for all use cases or whether I may have an error in my code. Below is an approximation of what I'm doing, the code below will not actually compile.
From below WebService lives in a library with separate package path to the main application(s). Meanwhile MyWebService lives in the application that is in the same context path as the Springboot application. Also MyWebService is functional, no issues there. This just has to do with the visibility of HystrixCommand annotation in regards to Springboot based configuration.
At runtime, what I notice is that when a code like the one below runs, I do see "commandKey=A" in my response. This one I did not quite expect since it's still running while the data is obtained. And since we log the HystrixRequestLog, I also see this command key in my logs.
But all the other Command keys are not visible at all, regardless of where I place them in the file. If I remove CommandKey-A then no commands are visible whatsoever.
Thoughts?
// Example WebService that we use as a shared component for performing a backend call that is the same across different resources
#RequiredArgsConstructor
#Accessors(fluent = true)
#Setter
public abstract class WebService {
private final #Nonnull Supplier<X> backendFactory;
#Setter(AccessLevel.PACKAGE)
private #Nonnull Supplier<BackendComponent> backendComponentSupplier = () -> new BackendComponent();
#GET
#Produces("application/json")
#HystrixCommand(commandKey="A")
public Response mainCall() {
Object obj = new Object();
try {
otherCommandMethod();
} catch (Exception commandException) {
// do nothing (for this example)
}
// get the hystrix request information so that we can determine what was executed
Optional<Collection<HystrixInvokableInfo<?>>> executedCommands = hystrixExecutedCommands();
// set the hystrix data, viewable in the response
obj.setData("hystrix", executedCommands.orElse(Collections.emptyList()));
if(hasError(obj)) {
return Response.serverError()
.entity(obj)
.build();
}
return Response.ok()
.entity(healthObject)
.build();
}
#HystrixCommand(commandKey="B")
private void otherCommandMethod() {
backendComponentSupplier
.get()
.observe()
.toBlocking()
.subscribe();
}
Optional<Collection<HystrixInvokableInfo<?>>> hystrixExecutedCommands() {
Optional<HystrixRequestLog> hystrixRequest = Optional
.ofNullable(HystrixRequestLog.getCurrentRequest());
// get the hystrix executed commands
Optional<Collection<HystrixInvokableInfo<?>>> executedCommands = Optional.empty();
if (hystrixRequest.isPresent()) {
executedCommands = Optional.of(hystrixRequest.get()
.getAllExecutedCommands());
}
return executedCommands;
}
#Setter
#RequiredArgsConstructor
public class BackendComponent implements ObservableCommand<Void> {
#Override
#HystrixCommand(commandKey="Y")
public Observable<Void> observe() {
// make some backend call
return backendFactory.get()
.observe();
}
}
}
// then later this component gets configured in the specific applications with sample configuraiton that looks like this:
#SuppressWarnings({ "unchecked", "rawtypes" })
#Path("resource/somepath")
#Component
public class MyWebService extends WebService {
#Inject
public MyWebService(Supplier<X> backendSupplier) {
super((Supplier)backendSupplier);
}
}
There is an issue with mainCall() calling otherCommandMethod(). Methods with #HystrixCommand can not be called from within the same class.
As discussed in the answers to this question this is a limitation of Spring's AOP.

Spring WS (DefaultWsdl11Definition) HTTP status code with void

We have a (working) SOAP web service based on Spring WS with DefaultWsdl11Definition.
This is basically what it looks like:
#Endpoint("name")
public class OurEndpoint {
#PayloadRoot(namespace = "somenamespace", localPart = "localpart")
public void onMessage(#RequestPayload SomePojo pojo) {
// do stuff
}
}
It is wired in Spring and it is correctly processing all of our SOAP requests. The only problem is that the method returns a 202 Accepted. This is not what the caller wants, he'd rather have us return 204 No Content (or if that is not possible an empty 200 OK).
Our other endpoints have a valid response object, and do return 200 OK. It seems void causes 202 when 204 might be more appropriate?
Is it possible to change the response code in Spring WS? We can't seem to find the correct way to do this.
Things we tried and didn't work:
Changing the return type to:
HttpStatus.NO_CONTENT
org.w3c.dom.Element <- not accepted
Adding #ResponseStatus <- this is for MVC, not WS
Any ideas?
Instead of what I wrote in the comments it is possibly the easiest to create a delegation kind of solution.
public class DelegatingMessageDispatcher extends MessageDispatcher {
private final WebServiceMessageReceiver delegate;
public DelegatingMessageDispatcher(WebServiceMessageReceiver delegate) {
this.delegate = delegate;
}
public void receive(MessageContext messageContext) throws Exception {
this.delegate.receive(messageContext);
if (!messageContext.hasResponse()) {
TransportContext tc = TransportContextHolder.getTransportContext();
if (tc != null && tc.getConnection() instanceof HttpServletConnection) {
((HttpServletConnection) tc.getConnection()).getHttpServletResponse().setStatus(200);
}
}
}
}
Then you need to configure a bean named messageDispatcher which would wrap the default SoapMessageDispatcher.
#Bean
public MessageDispatcher messageDispatcher() {
return new DelegatingMessageDispatcher(soapMessageDispatcher());
}
#Bean
public MessageDispatcher soapMessageDispatcher() {
return new SoapMessageDispatcher();
}
Something like that should do the trick. Now when response is created (In the case of a void return type), the status as you want is send back to the client.
When finding a proper solutions we've encountered some ugly problems:
Creating custom adapters/interceptors is problematic because the handleResponse method isn't called by Spring when you don't have a response (void)
Manually setting the status code doesn't work because HttpServletConnection keeps a boolean statusCodeSet which doesn't get updated
But luckily we managed to get it working with the following changes:
/**
* If a web service has no response, this handler returns: 204 No Content
*/
public class NoContentInterceptor extends EndpointInterceptorAdapter {
#Override
public void afterCompletion(MessageContext messageContext, Object o, Exception e) throws Exception {
if (!messageContext.hasResponse()) {
TransportContext tc = TransportContextHolder.getTransportContext();
if (tc != null && tc.getConnection() instanceof HttpServletConnection) {
HttpServletConnection connection = ((HttpServletConnection) tc.getConnection());
// First we force the 'statusCodeSet' boolean to true:
connection.setFaultCode(null);
// Next we can set our custom status code:
connection.getHttpServletResponse().setStatus(204);
}
}
}
}
Next we need to register this interceptor, this can be easily done using Spring's XML:
<sws:interceptors>
<bean class="com.something.NoContentInterceptor"/>
</sws:interceptors>
A big thanks to #m-deinum for pointing us in the right direction!
To override the afterCompletion method really helped me out in the exact same situation. And for those who use code based Spring configuration, hereĀ“s how one can add the interceptor for a specific endpoint.
Annotate the custom interceptor with #Component, next register the custom interceptor to a WsConfigurerAdapter like this:
#EnableWs
#Configuration
public class EndpointConfig extends WsConfigurerAdapter {
/**
* Add our own interceptor for the specified WS endpoint.
* #param interceptors
*/
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(new PayloadRootSmartSoapEndpointInterceptor(
new NoContentInterceptor(),
"NAMESPACE",
"LOCAL_PART"
));
}
}
NAMESPACE and LOCAL_PART should correspond to the endpoint.
If someone ever wanted to set custom HTTP status when returning non-void response, here is solution:
Spring Boot WS-Server - Custom Http Status

Resources