Setting base names based on request headers in ReloadableResourceBundleMessageSource - spring

I have a spring boot application which has the following messages outside the jar/war
/i18n/myApplication/messages/companyA/messages.properties
/i18n/myApplication/messages/companyA/messages_fr_FR.properties
/i18n/myApplication/messages/companyB/messages.properties
/i18n/myApplication/messages/companyB/messages_fr_FR.properties
/i18n/myApplication/messages/companyB/messages_zh_HK.properties
In request header I would get the following
X-Company=CompanyA
Accept-Language=fr-FR
How do I set baseNames dynamically based on the company and locale?
Also for CompanyA I do not want to look for messages in CompanyB for Eg:
if
X-Company=CompanyA
Accept-Language=zh-HK
I should be able to default to en-IN properties.
Any new company that gets added I don't want to make any code changes to support it
I was thinking of extending ReloadableResourceBundleMessageSource

In order for our application to be able to determine which locale is currently being used, we need to add a LocaleResolverbean:
`
#Bean
public LocaleResolver localeResolver() {
    SessionLocaleResolver slr = new SessionLocaleResolver();
    slr.setDefaultLocale(Locale.US);
    return slr;
}
`
The LocaleResolver interface has implementations that determine the current locale based on the session, cookies, the Accept-Language header, or a fixed value.
Next, we need to add an interceptor bean that will switch to a new locale based on the value of the lang parameter appended to a request:
`
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
    LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
    lci.setParamName("lang");
    return lci;
}
`
In order to take effect, this bean needs to be added to the application’s interceptor registry.
To achieve this, our #Configuration class has to implement the WebMvcConfigurer interface and override the addInterceptors() method:
`
#Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(localeChangeInterceptor());
}
`
It you read the documentation giù will find the simplest example Accept-language header which perfectly fits your needs

Related

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;
}

Howto inject Picocli parsed parameters into Spring Bean definitions?

I'm trying to use Picocli with Spring Boot 2.2 to pass command line parameters to a Spring Bean, but not sure how to structure this. For example, I have the following #Command to specify a connection username and password from the command line, however, want to use those params to define a Bean:
#Component
#CommandLine.Command
public class ClearJdoCommand extends HelpAwarePicocliCommand {
#CommandLine.Option(names={"-u", "--username"}, description = "Username to connect to MQ")
String username;
#CommandLine.Option(names={"-p", "--password"}, description = "Password to connect to MQ")
String password;
#Autowired
JMSMessagePublisherBean jmsMessagePublisher;
#Override
public void run() {
super.run();
jmsMessagePublisher.publishMessage( "Test Message");
}
}
#Configuration
public class Config {
#Bean
public InitialContext getJndiContext() throws NamingException {
// Set up the namingContext for the JNDI lookup
final Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
env.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
env.put(Context.SECURITY_PRINCIPAL, username);
env.put(Context.SECURITY_CREDENTIALS, password);
return new InitialContext(env);
}
#Bean
public JMSPublisherBean getJmsPublisher(InitialContext ctx){
return new JMSPublisherBean(ctx);
}
}
I'm stuck in a bit of a circular loop here. I need the command-line username/password to instantiate my JMSPublisherBean, but these are only available at runtime and not available at startup.
I have managed to get around the issue by using Lazy intialization, injecting the ClearJdoCommand bean into the Configuration bean and retrieving the JMSPublisherBean in my run() from the Spring context, but that seems like an ugly hack. Additionally, it forces all my beans to be Lazy, which is not my preference.
Is there another/better approach to accomplish this?
Second option might be to use pure PicoCli (not PicoCli spring boot starter) and let it run command; command will not be Spring bean and will only be used to validate parameters.
In its call method, Command would create SpringApplication, populate it with properties (via setDefaultProperties or using JVM System.setProperty - difference is that environment variables will overwrite default properties while system properties have higher priority).
#Override
public Integer call() {
var application = new SpringApplication(MySpringConfiguration.class);
application.setBannerMode(Mode.OFF);
System.setProperty("my.property.first", propertyFirst);
System.setProperty("my.property.second", propertySecond);
try (var context = application.run()) {
var myBean = context.getBean(MyBean.class);
myBean.run(propertyThird);
}
return 0;
}
This way, PicoCli will validate input, provide help etc. but you can control configuration of Spring Boot application. You can even use different Spring configurations for different commands. I believe this approach is more natural then passing all properties to CommandLineRunner in Spring container
One idea that may be useful is to parse the command line in 2 passes:
the first pass is just to pick up the information needed for configuration/initialization
in the second pass we pick up additional options and execute the application
To implement this, I would create a separate class that "duplicates" the options that are needed for configuration. This class would have an #Unmatched field for the remaining args, so they are ignored by picocli. For example:
class Security {
#Option(names={"-u", "--username"})
static String username;
#Option(names={"-p", "--password"}, interactive = true, arity = "0..1")
static String password;
#Unmatched List<String> ignored;
}
In the first pass, we just want to extract the username/password info, we don't want to execute the application just yet. We can use the CommandLine.parseArgs or CommandLine.populateCommand methods for that.
So, our main method can look something like this:
public static void main(String[] args) throws Exception {
// use either populateCommand or parseArgs
Security security = CommandLine.populateCommand(new Security(), args);
if (security.username == null || security.password == null) {
System.err.println("Missing required user name or password");
new CommandLine(new ClearJdoCommand()).usage(System.err);
System.exit(CommandLine.ExitCode.USAGE);
}
// remainder of your normal main method here, something like this?
System.exit(SpringApplication.exit(SpringApplication.run(MySpringApp.class, args)));
}
I would still keep (duplicate) the usage and password options in the ClearJdoCommand class, so the application can print a nice usage help message when needed.
Note that I made the fields in the Security class static.
This is a workaround (hack?) that allows us to pass information to the getJndiContext method.
#Bean
public InitialContext getJndiContext() throws NamingException {
// Set up the namingContext for the JNDI lookup
final Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
env.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
env.put(Context.SECURITY_PRINCIPAL, Security.username); // use info from 1st pass
env.put(Context.SECURITY_CREDENTIALS, Security.password);
return new InitialContext(env);
}
There is probably a better way to pass information to this method.
Any Spring experts willing to jump in and show us a nicer alternative?

spring internalization language change from controller

In spring internalization i'm using this configuration
#Bean
public LocaleResolver localeResolver() {
//for this demo, we'll use a SessionLocaleResolver object
//as the name implies, it stores locale info in the session
SessionLocaleResolver resolver = new SessionLocaleResolver();
//default to US locale
resolver.setDefaultLocale(Locale.US);
//get out
return resolver;
}
/**
* This interceptor allows visitors to change the locale on a per-request basis
* #return a LocaleChangeInterceptor object
*/
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
//instantiate the object with an empty constructor
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
//the request param that we'll use to determine the locale
interceptor.setParamName("lang");
//get out
return interceptor;
}
/**
* This is where we'll add the intercepter object
* that handles internationalization
*/
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
And I'm able to change languages from html i.e script and its working fine
window.location.replace('?lang=' + selectedOption);
But is there any way that I can change it from a controller because preferred language will be stored inside DB
so language will be fetched and to be set
example
u = ucr.findByNameContaining(name);
u.getLanguage
<< i have to set language returned from above line >>
#RequestMapping(value = {"/welcome"}, method = RequestMethod.POST)
public String notificationChannelSearchPost(ModelMap model,HttpSession session
,#RequestParam(value="name", defaultValue="") String name) {
u = ucr.findByNameContaining(name);
u.getLanguage
<< i have to set language returned from above line >>
return "welcom.html";
}
thank you
You can set the spring Locale manually in Java by doing something like this
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
localeResolver.setLocale(request, response,new Locale(u.getLanguage()));

Micrometer filter is ignored with CompositeMeterRegistry

I use Spring Boot 2.1.2.RELEASE, and I try to use Micrometer with CompositeMeterRegistry. My goal is to publish some selected meters to ElasticSearch. The code below shows my sample config. The problem is, that the filter is completely ignored (so all metrics are sent to ElasticSearch), although I can see in the logs that it was processed ("filter reply of meter ..." lines).
Strangely, if I define the MeterFilter as a Spring bean, then it's applied to ALL registries (however, I want it to be applied only on "elasticMeterRegistry").
Here is a sample configuration class:
#Configuration
public class AppConfiguration {
#Bean
public ElasticConfig elasticConfig() {
return new ElasticConfig() {
#Override
#Nullable
public String get(final String k) {
return null;
}
};
}
#Bean
public MeterRegistry meterRegistry(final ElasticConfig elasticConfig) {
final CompositeMeterRegistry registry = new CompositeMeterRegistry();
registry.add(new SimpleMeterRegistry());
registry.add(new JmxMeterRegistry(new JmxConfig() {
#Override
public Duration step() {
return Duration.ofSeconds(10);
}
#Override
#Nullable
public String get(String k) {
return null;
}
}, Clock.SYSTEM));
final ElasticMeterRegistry elasticMeterRegistry = new ElasticMeterRegistry(elasticConfig, Clock.SYSTEM);
elasticMeterRegistry.config().meterFilter(new MeterFilter() {
#Override
public MeterFilterReply accept(Meter.Id id) {
final MeterFilterReply reply =
id.getName().startsWith("logback")
? MeterFilterReply.NEUTRAL
: MeterFilterReply.DENY;
log.info("filter reply of meter {}: {}", id.getName(), reply);
return reply;
}
});
registry.add(elasticMeterRegistry);
return registry;
}
}
So, I expect ElasticSearch to receive only "logback" metrics, and JMX to receive all metrics.
UPDATE:
I have played with filters and found a "solution", but I don't really understand why the code above doesn't work.
This works:
elasticMeterRegistry.config().meterFilter(new MeterFilter() {
#Override
public MeterFilterReply accept(Meter.Id id) {
final MeterFilterReply reply =
id.getName().startsWith("logback")
? MeterFilterReply.ACCEPT
: MeterFilterReply.DENY;
log.info("filter reply of meter {}: {}", id.getName(), reply);
return reply;
}
});
The difference is: I return ACCEPT instead of NEUTRAL.
Strangely, the following code does not work (ES gets all metrics):
elasticMeterRegistry.config().meterFilter(
MeterFilter.accept(id -> id.getName().startsWith("logback")));
But this works:
elasticMeterRegistry.config().meterFilter(
MeterFilter.accept(id -> id.getName().startsWith("logback")));
elasticMeterRegistry.config().meterFilter(
MeterFilter.deny());
CONCLUSION:
So, it seems that instead of NEUTRAL, the filter should return ACCEPT. But for meters not starting with "logback", my original filter (with NEUTRAL) returns DENY. Then why are those metrics published to ElasticSearch registry?
Can someone explain this?
This is really a composite of questions. I'll just point out a few points.
For the MeterRegistry bean you defined, Spring Boot will auto-configure an ElasticMeterRegistry bean as there's no ElasticMeterRegistry bean. Instead of creating a CompositeMeterRegistry bean on your own, just define a custom ElasticMeterRegistry bean which is applied the MeterFilter you want and let Spring Boot create one (CompositeMeterRegistry bean) for you.
For MeterFilterReply, ACCEPT will accept the meter immediately, DENY will deny the meter immediately, and NEUTRAL will postpone the decision to next filter(s). Basically meters will be accepted unless there's any DENY.

How to define the execution order of interceptor in Spring Boot application?

I define an interceptor and register it in a class (annotated with Configuration) which extends WebMvcConfigurerAdapter; however, I also use some third-party libraries which also define some interceptors. I want my interceptor to be the last one in the interceptor execution chain. It seems there is no way to enforce this. How to define the execution order of interceptor in Spring Boot application?
If we've Multiple Interceptors, Instead of #Order Annotation we can do as below.
#EnableWebMvc
#Configuration
public class WebMVCConfig implements WebMvcConfigurer {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addWebRequestInterceptor(new WebRequestInterceptor() {
//Overrides
}).order(Ordered.HIGHEST_PRECEDENCE);
registry
.addWebRequestInterceptor(new WebRequestInterceptor() {
//Overrides
}).order(Ordered.LOWEST_PRECEDENCE);
}
}
All Feign clients become proxies, so there is only one way to change the order of the request interceptors. But you cannot use it, because if you change the proxy to SGLIB, it will not work.
if(bean instanceof YoursFeignClientBean) {
Class<Proxy> superclass = (Class<Proxy>) bean.getClass().getSuperclass();
Field h = superclass.getDeclaredField("h");
h.setAccessible(true);
// its FeignInvocationHandler
InvocationHandler ih = (InvocationHandler) ReflectionUtils.getField(h, bean);
Field dispatch = ih.getClass().getDeclaredField("dispatch");
dispatch.setAccessible(true);
Map<Method, InvocationHandlerFactory.MethodHandler> map =
(Map<Method, InvocationHandlerFactory.MethodHandler>) dispatch.get(ih);
for (Method method : map.keySet()) {
InvocationHandlerFactory.MethodHandler handler = map.get(method);
Field requestInterceptors = handler.getClass().getDeclaredField("requestInterceptors");
requestInterceptors.setAccessible(true);
List<RequestInterceptor> interceptorList = (List<RequestInterceptor>)
ReflectionUtils.getField(requestInterceptors, handler);
RequestInterceptor ri = interceptorList.stream().filter(t -> t.getClass().getName().startsWith(YoursFeignConfig.class.getName())).findFirst().get();
// reorder
interceptorList.remove(ri);
interceptorList.add(ri);
}
According to my experience, Interceptors are adding a stack. For this reason you should addRegister above which one you want before calling than other.

Resources