diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WACasConfiguration.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WACasConfiguration.java new file mode 100644 index 00000000000..924d7000f5b --- /dev/null +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WACasConfiguration.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 org.apache.syncope.wa.starter.config; + +import org.apache.syncope.wa.starter.events.WACasConfigurationEventListener; +import org.apereo.cas.configuration.CasConfigurationPropertiesEnvironmentManager; +import org.apereo.cas.support.events.listener.CasConfigurationEventListener; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ScopedProxyMode; + +@Configuration(proxyBeanMethods = false) +public class WACasConfiguration { + + @ConditionalOnMissingBean(name = "casConfigurationEventListener") + @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) + @Bean + public CasConfigurationEventListener casConfigurationEventListener( + @Qualifier("configurationPropertiesEnvironmentManager") + final CasConfigurationPropertiesEnvironmentManager manager, + final ConfigurationPropertiesBindingPostProcessor binder, + final ContextRefresher contextRefresher, + final ConfigurableApplicationContext applicationContext) { + + return new WACasConfigurationEventListener(manager, binder, contextRefresher, applicationContext); + } +} diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/events/WACasConfigurationEventListener.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/events/WACasConfigurationEventListener.java new file mode 100644 index 00000000000..10358067fea --- /dev/null +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/events/WACasConfigurationEventListener.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 org.apache.syncope.wa.starter.events; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; +import org.apache.commons.lang3.ObjectUtils; +import org.apereo.cas.configuration.CasConfigurationPropertiesEnvironmentManager; +import org.apereo.cas.support.events.config.CasConfigurationModifiedEvent; +import org.apereo.cas.support.events.listener.CasConfigurationEventListener; +import org.apereo.cas.util.function.FunctionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; +import org.springframework.context.ApplicationContext; + +/** + * CAS configuration listener variant that avoids manually re-initializing web servlets without ServletConfig. + */ +public class WACasConfigurationEventListener implements CasConfigurationEventListener { + + protected static final Logger LOG = LoggerFactory.getLogger(WACasConfigurationEventListener.class); + + private final CasConfigurationPropertiesEnvironmentManager configurationPropertiesEnvironmentManager; + + private final ConfigurationPropertiesBindingPostProcessor binder; + + private final ContextRefresher contextRefresher; + + private final ApplicationContext applicationContext; + + public WACasConfigurationEventListener( + final CasConfigurationPropertiesEnvironmentManager configurationPropertiesEnvironmentManager, + final ConfigurationPropertiesBindingPostProcessor binder, + final ContextRefresher contextRefresher, + final ApplicationContext applicationContext) { + + this.configurationPropertiesEnvironmentManager = configurationPropertiesEnvironmentManager; + this.binder = binder; + this.contextRefresher = contextRefresher; + this.applicationContext = applicationContext; + } + + @Override + public void onRefreshScopeRefreshed(final RefreshScopeRefreshedEvent event) { + LOG.info("Refreshing application context beans eagerly..."); + initializeBeansEagerly(); + } + + @Override + public void onEnvironmentChangedEvent(final EnvironmentChangeEvent event) { + LOG.trace("Received event [{}]", event); + rebind(); + } + + @Override + public void handleConfigurationModifiedEvent(final CasConfigurationModifiedEvent event) { + if (event.isEligibleForContextRefresh()) { + LOG.info("Received event [{}]. Refreshing CAS configuration...", event); + final Set keys = contextRefresher.refresh(); + LOG.info("Refreshed the following settings: [{}].", keys); + rebind(); + LOG.info("CAS finished rebinding configuration with new settings [{}]", + ObjectUtils.defaultIfNull(keys, new ArrayList<>(0))); + } + } + + private void initializeBeansEagerly() { + FunctionUtils.doAndHandle(unused -> { + for (final String beanName : applicationContext.getBeanDefinitionNames()) { + Objects.requireNonNull(applicationContext.getBean(beanName).getClass()); + } + }); + } + + private void rebind() { + LOG.info("Refreshing CAS configuration. Stand by..."); + final Object ctx = FunctionUtils.doIfNotNull(configurationPropertiesEnvironmentManager, + () -> configurationPropertiesEnvironmentManager.rebindCasConfigurationProperties(applicationContext), + () -> CasConfigurationPropertiesEnvironmentManager.rebindCasConfigurationProperties( + binder, applicationContext)). + get(); + Objects.requireNonNull(ctx); + initializeBeansEagerly(); + } +} diff --git a/wa/starter/src/test/java/org/apache/syncope/wa/starter/events/WACasConfigurationEventListenerTest.java b/wa/starter/src/test/java/org/apache/syncope/wa/starter/events/WACasConfigurationEventListenerTest.java new file mode 100644 index 00000000000..cc2d70dbf89 --- /dev/null +++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/events/WACasConfigurationEventListenerTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 org.apache.syncope.wa.starter.events; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +public class WACasConfigurationEventListenerTest { + + @Test + public void refreshDoesNotInitializeDispatcherServlet() { + final ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); + final DispatcherServlet dispatcherServlet = new DispatcherServlet(); + when(applicationContext.getBeanDefinitionNames()).thenReturn(new String[] { "dispatcherServlet" }); + when(applicationContext.getBean("dispatcherServlet")).thenReturn(dispatcherServlet); + when(applicationContext.containsBean("dispatcherServlet")).thenReturn(true); + when(applicationContext.getBean(DispatcherServlet.class)).thenReturn(dispatcherServlet); + + final WACasConfigurationEventListener listener = + new WACasConfigurationEventListener(null, null, null, applicationContext); + assertDoesNotThrow(() -> listener.onRefreshScopeRefreshed(new RefreshScopeRefreshedEvent())); + + verify(applicationContext).getBean("dispatcherServlet"); + verify(applicationContext, never()).containsBean("dispatcherServlet"); + verify(applicationContext, never()).getBean(DispatcherServlet.class); + } +}