stormpath / stormpath-sdk-java

Official Java SDK for the Stormpath User Management REST API
222 stars 155 forks source link

i18n message translation fails in some Spring Boot web apps #703

Closed lhazlewood closed 8 years ago

lhazlewood commented 8 years ago

AbstractStormpathWebMvcConfiguration#stormpathSpringMessageSource() checks to see if the specified message source is null or a placeholder, where 'placeholder' is indicated if it is a DelegatingMessageSource that does not have a parent.

I encountered a scenario today where the message source was a DelegatingMessageSource and it did have a parent. However, the parent was itself a DelegatingMessageSource (which did not have a parent). I suspect this occurs when nesting application contexts or other 'frameworky' scenarios.

I believe the best/safest thing to do is to keep walking the parent hierarchy until we find one that is 1) not a DelegatingMessageSource and 2) not null.

If these two conditions are met, the CompositeMessageSource instance should be created using the Stormpath i18nPropertiesMessageSource and the discovered non-DelegatingMessageSource.

If these two conditions are not met, then just create/return the Stormpath i18nPropertiesMessageSource.

The quick workaround for this scenario until this bug is fixed is for the app developer to explicitly declare a MessageSource bean in their Spring configuration:

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource src = new ResourceBundleMessageSource();
    src.setBasename("com.stormpath.sdk.servlet.i18n");
    src.setDefaultEncoding("UTF-8");
    return src;
}

And the stormpathSpringMessageSource won't be created (preferring the user default).

lhazlewood commented 8 years ago

P.S. I believe this should be in the 1.0 release because it could impact anyone evaluating the Stormpath Default Spring Boot Starter for the first time and it was nasty to try to figure out (took a while debugging w/ break points and digging deep into Spring and Stormpath code internals).

lhazlewood commented 8 years ago

It turns out that this is a problem even when solving the DelegatingMessageSource/parent issue. If the user does not declare an actual bean named messageSource (or configure a spring.messages.basename property which automatically produces the equivalent bean), the MessageSource instance we create still won't be used.

jarias commented 8 years ago

@lhazlewood I haven't been able to repo this, I grab the spring-boot-webmvc example and inject the MessageSource without declaring one and did the following:

@Controller
public class HelloController {

    @Autowired
    private MessageSource messageSource;

    @RequestMapping("/")
    public String home(HttpServletRequest request, Model model) {

        String name = messageSource.getMessage("world", new Object[]{}, Locale.getDefault());

        Account account = AccountResolver.INSTANCE.getAccount(request);
        if (account != null) {
            name = account.getGivenName();
            model.addAttribute(account);
        }

        model.addAttribute("name", name);

        return "hello";
    }

}

And property world was resolved.

Could you provide the exact case you where working on so I can try and reproduce it.

Thanks.

lhazlewood commented 8 years ago

I'll see if I can provide a sample app that demonstrates this

lhazlewood commented 8 years ago

@jarias please see this example:

https://github.com/stormpath/stormpath-spring-boot-i18n-test

After checkout, uncomment the @Bean annotation in MessagesConfig and then run it.

lhazlewood commented 8 years ago

@jarias if this only occurs when running Spring Cloud Config Server, let's put it back on the 'ready' column and work on it last. We still need to solve it, but it might not be critical for 1.0.

jarias commented 8 years ago

ok @lhazlewood

mraible commented 8 years ago

According to this question on Stack Overflow, this is a bug in Spring Boot. However, it should be fixed in Spring Boot 1.3.0+.

mraible commented 8 years ago

I tried adding the following test to stormpath-spring-boot-i18n-test:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestApplication.class)
public class MessagesourceBugApplicationTests {

    @Autowired
    private MessageSource messageSource;

    @Test
    public void loadsMessage() {
        Assert.assertEquals("Login", messageSource.getMessage("stormpath.web.login.form.title", null, Locale.getDefault()));
    }

}

However, it blows up when I try to run it:

java.lang.IllegalStateException: Failed to load ApplicationContext

    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
    at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83)
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:117)
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83)
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:228)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:230)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:249)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:253)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityWebAppConfig': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.setObjectPostProcessor(org.springframework.security.config.annotation.ObjectPostProcessor); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.config.annotation.ObjectPostProcessor] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766)
    at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
    at org.springframework.boot.test.SpringApplicationContextLoader.loadContext(SpringApplicationContextLoader.java:98)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
    ... 29 more
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.setObjectPostProcessor(org.springframework.security.config.annotation.ObjectPostProcessor); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.config.annotation.ObjectPostProcessor] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:661)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331)
    ... 45 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.config.annotation.ObjectPostProcessor] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:1373)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1119)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1014)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:618)
    ... 47 more
mraible commented 8 years ago

@lhazlewood @jarias I created a PR so you can see how this test fails: https://github.com/stormpath/stormpath-spring-boot-i18n-test/pull/1

mraible commented 8 years ago

I was able to make the resource handler work for CSS and JS by doing the following.

  1. In TestApplication.java, I added @ComponentScan("com.stormpath.spring.config") to the class. This causes StormpathWebMvcStaticResourceConfigurer#addResourceHandlers to be called. Without it, this method is not called.
  2. In StormpathWebMvcStaticResourceConfigurer#addResourceHandlers, I added registry.setOrder(-300);.
lhazlewood commented 8 years ago

Component scanning declarations definitely shouldn't be necessary for Spring Boot apps since we declare necessary things in various spring.factories files.

Also, we currently use the Spring Boot 1.3.5 dependency, so it doesn't appear to be fixed in 1.3.x? (or we've done something wrong).

lhazlewood commented 8 years ago

@mraible StormpathWebMvcConfiguration (which contains StormpathWebMvcStaticResourceConfigurer) should only be used in non-SpringBoot Spring apps. Did you enable that component scan in a Spring Boot app?

mraible commented 8 years ago

@lhazlewood I noticed that StormpathWebMvcAutoConfiguration does get hit, but StormpathWebMvcConfiguration does not get hit. My brute-force debugging method was to put logging messages in StormpathWebMvcStaticResourceConfigurer#addResourceHandlers:

@SuppressWarnings("SpringFacetCodeInspection")
@Configuration
public static class StormpathWebMvcStaticResourceConfigurer extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        System.out.println("**********************************\n\n\n");
        System.out.println("adding resource handlers");
        System.out.println("**********************************\n\n\n");

I did this in stormpath-spring-boot-i18n-test, which is a Spring Boot app.

How are stormpath.js and stormpath.css mapped without this?

mraible commented 8 years ago

@lhazlewood This app no longer works with the latest Stormpath code. I'm guessing it's because it uses Spring Boot 1.3.5 and we use Spring Boot 1.4.0. Here's the error on startup:

java.lang.IllegalStateException: Error processing condition on com.stormpath.spring.boot.autoconfigure.StormpathWebMvcAutoConfiguration.stormpathApplicationResolver
    at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:64)
    at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:102)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:178)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:140)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:116)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:333)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:243)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:273)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:98)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:678)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:520)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766)
    at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1191)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1180)
    at com.stormpath.test.TestApplication.main(TestApplication.java:12)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:478)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.IllegalArgumentException: @ConditionalOnMissingBean annotations must specify at least one bean (type, name or annotation)
    at org.springframework.util.Assert.isTrue(Assert.java:68)
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition$BeanSearchSpec.validate(OnBeanCondition.java:279)
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition$BeanSearchSpec.<init>(OnBeanCondition.java:275)
    at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:111)
    at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
    ... 23 common frames omitted

If I change ${spring.boot.version} to be 1.4.0.RELEASE, I get a different error:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tomcatEmbeddedServletContainerFactory' defined in class path resource [org/springframework/boot/autoconfigure/web/EmbeddedServletContainerAutoConfiguration$EmbeddedTomcat.class]: Initialization of bean failed; nested exception is java.lang.NoSuchMethodError: org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer.addErrorPages([Lorg/springframework/boot/context/embedded/ErrorPage;)V
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.getEmbeddedServletContainerFactory(EmbeddedWebApplicationContext.java:199)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.createEmbeddedServletContainer(EmbeddedWebApplicationContext.java:162)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.onRefresh(EmbeddedWebApplicationContext.java:134)
    ... 14 more
Caused by: java.lang.NoSuchMethodError: org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer.addErrorPages([Lorg/springframework/boot/context/embedded/ErrorPage;)V
    at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$ErrorPageCustomizer.customize(ErrorMvcAutoConfiguration.java:269)
    at org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor.postProcessBeforeInitialization(EmbeddedServletContainerCustomizerBeanPostProcessor.java:68)
    at org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor.postProcessBeforeInitialization(EmbeddedServletContainerCustomizerBeanPostProcessor.java:54)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:408)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
    ... 22 more

I noticed that you're not using spring-boot-starter-parent as a parent, so I changed that and removed some versions that are overriding the defaults. Now it starts up and the CSS and JS are rendered correctly.

--- a/pom.xml
+++ b/pom.xml
@@ -5,9 +5,9 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
-        <groupId>org.sonatype.oss</groupId>
-        <artifactId>oss-parent</artifactId>
-        <version>7</version>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>1.4.0.RELEASE</version>
     </parent>

     <groupId>com.stormpath.test</groupId>
@@ -21,7 +21,7 @@
         <url>https://stormpath.com</url>
     </organization>

-    <!--
+    <!--
     <scm>
         <connection>scm:git:git@github.com:stormpath/chancellor.git</connection>
         <developerConnection>scm:git:git@github.com:stormpath/chancellor.git</developerConnection>
@@ -37,11 +37,9 @@
         <maven.enforcer.plugin.version>1.4</maven.enforcer.plugin.version>
         <samza.version>0.9.1</samza.version>
         <slf4j.version>1.7.21</slf4j.version>
-        <spring.version>4.2.6.RELEASE</spring.version>
-        <spring.boot.version>1.3.5.RELEASE</spring.boot.version>
+        <spring.boot.version>1.4.0.RELEASE</spring.boot.version>
         <spring.cloud.version>Brixton.RELEASE</spring.cloud.version>
         <spring.cloud.config.server.version>1.1.0.RELEASE</spring.cloud.config.server.version>
-        <spring.security.version>4.0.4.RELEASE</spring.security.version>
         <stormpath.sdk.version>1.0.0.RC-SNAPSHOT</stormpath.sdk.version>

         <!-- test-related properties: -->

Unfortunately, there's still an issue with i18n.

mraible commented 8 years ago

I'm able to get i18n to work if I remove spring-cloud-config-server as a dependency in pom.xml. Previously, I only had to remove the @EnableConfigServer annotation, so it seems that spring-cloud is overriding the MessageSource.

mraible commented 8 years ago

I was unable to fix this issue, but I did create a ticket in the Spring Cloud Config project. https://github.com/spring-cloud/spring-cloud-config/issues/462

lhazlewood commented 8 years ago

Closing - merged to the 1.1.x branch.