diff --git a/spring-geode/src/main/java/org/springframework/geode/context/logging/GeodeLoggingApplicationListener.java b/spring-geode/src/main/java/org/springframework/geode/context/logging/GeodeLoggingApplicationListener.java new file mode 100644 index 00000000..ca8e1562 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/context/logging/GeodeLoggingApplicationListener.java @@ -0,0 +1,129 @@ +/* + * Copyright 2019 the original author or authors. + * + * 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 org.springframework.geode.context.logging; + +import java.util.Arrays; +import java.util.Properties; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.logging.LoggingApplicationListener; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.GenericApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.Environment; +import org.springframework.data.gemfire.config.annotation.EnableLogging; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Spring {@link GenericApplicationListener} used to configure Apache Geode Logging from existing Spring Data + * for Apache Geode Logging configuration support, such as when using the {@link EnableLogging} annotation + * or alternatively using {@link Properties}. + * + * This listener must be ordered before the Spring Boot {@link LoggingApplicationListener}. + * + * @author John Blum + * @see java.util.Properties + * @see org.springframework.boot.SpringApplication + * @see org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent + * @see org.springframework.boot.context.logging.LoggingApplicationListener + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.event.GenericApplicationListener + * @see org.springframework.core.Ordered + * @see org.springframework.core.env.Environment + * @since 1.3.0 + */ +public class GeodeLoggingApplicationListener implements GenericApplicationListener { + + private static final Class[] EVENT_TYPES = { ApplicationEnvironmentPreparedEvent.class }; + private static final Class[] SOURCE_TYPES = { ApplicationContext.class, SpringApplication.class }; + + public static final String SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY = "spring.boot.data.gemfire.log.level"; + public static final String SPRING_DATA_GEMFIRE_CACHE_LOG_LEVEL = "spring.data.gemfire.cache.log-level"; + public static final String SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL = "spring.data.gemfire.logging.level"; + + @Override + public int getOrder() { + + return LoggingApplicationListener.DEFAULT_ORDER > Ordered.HIGHEST_PRECEDENCE + ? LoggingApplicationListener.DEFAULT_ORDER - 1 + : Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void onApplicationEvent(@Nullable ApplicationEvent event) { + + if (event instanceof ApplicationEnvironmentPreparedEvent) { + + ApplicationEnvironmentPreparedEvent environmentPreparedEvent = (ApplicationEnvironmentPreparedEvent) event; + + onApplicationEnvironmentPreparedEvent(environmentPreparedEvent); + } + } + + protected void onApplicationEnvironmentPreparedEvent( + @NonNull ApplicationEnvironmentPreparedEvent environmentPreparedEvent) { + + Assert.notNull(environmentPreparedEvent, "ApplicationEnvironmentPreparedEvent must not be null"); + + Environment environment = environmentPreparedEvent.getEnvironment(); + + if (isSystemPropertyNotSet(SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) { + + String logLevel = environment.getProperty(SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY, + environment.getProperty(SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL, + environment.getProperty(SPRING_DATA_GEMFIRE_CACHE_LOG_LEVEL))); + + setSystemProperty(SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY, logLevel); + } + } + + protected boolean isSystemPropertySet(@Nullable String propertyName) { + return StringUtils.hasText(propertyName) && StringUtils.hasText(System.getProperty(propertyName)); + } + + protected boolean isSystemPropertyNotSet(@Nullable String propertyName) { + return !isSystemPropertySet(propertyName); + } + + protected void setSystemProperty(@NonNull String propertyName, @Nullable String propertyValue) { + + Assert.hasText(propertyName, () -> String.format("PropertyName [%s] is required", propertyName)); + + if (StringUtils.hasText(propertyValue)) { + System.setProperty(propertyName, propertyValue); + } + } + + @Override + public boolean supportsEventType(@NonNull ResolvableType eventType) { + + Class rawType = eventType.getRawClass(); + + return rawType != null && Arrays.stream(EVENT_TYPES).anyMatch(it -> it.isAssignableFrom(rawType)); + } + + @Override + public boolean supportsSourceType(@Nullable Class sourceType) { + return sourceType != null && Arrays.stream(SOURCE_TYPES).anyMatch(it -> it.isAssignableFrom(sourceType)); + } +} diff --git a/spring-geode/src/main/resources/META-INF/spring.factories b/spring-geode/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..bd284d8a --- /dev/null +++ b/spring-geode/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Spring Boot for Apache Geode Application Listeners +org.springframework.context.ApplicationListener=\ +org.springframework.geode.context.logging.GeodeLoggingApplicationListener diff --git a/spring-geode/src/test/java/org/springframework/geode/context/logging/GeodeLoggingApplicationListenerUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/context/logging/GeodeLoggingApplicationListenerUnitTests.java new file mode 100644 index 00000000..a4d26f89 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/context/logging/GeodeLoggingApplicationListenerUnitTests.java @@ -0,0 +1,344 @@ +/* + * Copyright 2019 the original author or authors. + * + * 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 org.springframework.geode.context.logging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.logging.LoggingApplicationListener; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport; +import org.springframework.web.context.WebApplicationContext; + +/** + * Unit Tests for {@link GeodeLoggingApplicationListener}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport + * @see org.springframework.geode.context.logging.GeodeLoggingApplicationListener + * @since 1.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class GeodeLoggingApplicationListenerUnitTests extends IntegrationTestsSupport { + + @Mock + private ApplicationEnvironmentPreparedEvent mockEvent; + + @Mock + private ConfigurableEnvironment mockEnvironment; + + @Spy + private GeodeLoggingApplicationListener listener; + + @Before + public void setup() { + doReturn(this.mockEnvironment).when(this.mockEvent).getEnvironment(); + } + + @After + public void tearDown() { + + System.getProperties().stringPropertyNames().stream() + .filter(propertyName -> propertyName.startsWith("spring")) + .forEach(System::clearProperty); + } + + @Test + public void getOrderReturnsHigherPrecedenceThanSpringBootLoggingApplicationListenerOrderByDefault() { + assertThat(this.listener.getOrder()).isLessThan(new LoggingApplicationListener().getOrder()); + } + + @Test + public void onApplicationEventIsNullSafe() { + + this.listener.onApplicationEvent(null); + + verify(this.listener, never()).onApplicationEnvironmentPreparedEvent(any()); + } + + @Test + public void onApplicationEventWithApplicationEnvironmentPreparedEventProcessesEvent() { + + doNothing().when(this.listener) + .onApplicationEnvironmentPreparedEvent(any(ApplicationEnvironmentPreparedEvent.class)); + + this.listener.onApplicationEvent(this.mockEvent); + + verify(this.listener, times(1)) + .onApplicationEnvironmentPreparedEvent(eq(this.mockEvent)); + } + + @Test + public void onApplicationEventWithNonApplicationEnvironmentPreparedEventWillNotProcessEvent() { + + ApplicationEvent mockEvent = mock(ApplicationEvent.class); + + this.listener.onApplicationEvent(mockEvent); + + verify(this.listener, never()).onApplicationEnvironmentPreparedEvent(any()); + } + + @Test + public void onApplicationEnvironmentPreparedEventConfiguresApacheGeodeLoggingFromSpringBootDataGemFireLogLevelProperty() { + + assertThat(System.getProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) + .isNullOrEmpty(); + + doReturn("DEBUG").when(this.mockEnvironment) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY), + ArgumentMatchers.any()); + + this.listener.onApplicationEnvironmentPreparedEvent(this.mockEvent); + + assertThat(System.getProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) + .isEqualTo("DEBUG"); + + verify(this.mockEnvironment, times(1)) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL), + ArgumentMatchers.eq(null)); + } + + @Test + public void onApplicationEnvironmentPreparedEventConfiguresApacheGeodeLoggingFromSpringDataGemFireCacheLogLevelProperty() { + + assertThat(System.getProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) + .isNullOrEmpty(); + + doReturn("DEBUG").when(this.mockEnvironment) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_DATA_GEMFIRE_CACHE_LOG_LEVEL)); + + Arrays.asList(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY, + GeodeLoggingApplicationListener.SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL).forEach(propertyName -> + doAnswer(invocation -> invocation.getArgument(1, String.class)) + .when(this.mockEnvironment).getProperty(eq(propertyName), anyString())); + + this.listener.onApplicationEnvironmentPreparedEvent(this.mockEvent); + + assertThat(System.getProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) + .isEqualTo("DEBUG"); + + verify(this.mockEnvironment, times(1)) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY), + eq("DEBUG")); + } + + @Test + public void onApplicationEnvironmentPreparedEventConfiguresApacheGeodeLoggingFromSpringDataGemFireLoggingLevelProperty() { + + assertThat(System.getProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) + .isNullOrEmpty(); + + doReturn("DEBUG").when(this.mockEnvironment) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL), + ArgumentMatchers.any()); + + doAnswer(invocation -> invocation.getArgument(1, String.class)).when(this.mockEnvironment) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY), anyString()); + + this.listener.onApplicationEnvironmentPreparedEvent(this.mockEvent); + + assertThat(System.getProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) + .isEqualTo("DEBUG"); + + verify(this.mockEnvironment, times(1)) + .getProperty(eq(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY), + eq("DEBUG")); + } + + @Test + public void onApplicationEnvironmentPreparedEventWillNotConfigureApacheGeodeLoggingWhenLogLevelIsExplicitlyConfigured() { + + System.setProperty(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY, "OFF"); + + this.listener.onApplicationEnvironmentPreparedEvent(this.mockEvent); + + verify(this.listener, never()) + .setSystemProperty(eq(GeodeLoggingApplicationListener.SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY), any()); + } + + @Test(expected = IllegalArgumentException.class) + public void onApplicationEnvironmentPreparedEventWithNullEventThrowsIllegalArgumentException() { + + try { + this.listener.onApplicationEnvironmentPreparedEvent(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("ApplicationEnvironmentPreparedEvent must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void isSystemPropertySetIsNullSafe() { + assertThat(this.listener.isSystemPropertySet(null)).isFalse(); + } + + @Test + public void isSystemPropertySetReturnsTrue() { + + System.setProperty("spring.test.property", "TEST"); + + assertThat(this.listener.isSystemPropertySet("spring.test.property")).isTrue(); + } + + @Test + public void isSystemPropertyNotSetIsNullSafe() { + assertThat(this.listener.isSystemPropertyNotSet(null)).isTrue(); + } + + @Test + public void isSystemPropertySetReturnsFalse() { + + System.setProperty("spring.test.property.one", " "); + System.setProperty("spring.test.property.two", ""); + + assertThat(this.listener.isSystemPropertySet("spring.non-existing.property")).isFalse(); + assertThat(this.listener.isSystemPropertySet("spring.test.property.one")).isFalse(); + assertThat(this.listener.isSystemPropertySet("spring.test.property.two")).isFalse(); + } + + @Test + public void isSystemPropertyNotSetReturnsTrue() { + + System.setProperty("spring.test.property.one", " "); + System.setProperty("spring.test.property.two", ""); + + assertThat(this.listener.isSystemPropertyNotSet("spring.non-existing.property")).isTrue(); + assertThat(this.listener.isSystemPropertyNotSet("spring.test.property.one")).isTrue(); + assertThat(this.listener.isSystemPropertyNotSet("spring.test.property.two")).isTrue(); + } + + @Test + public void isSystemPropertyNotSetReturnFalse() { + + System.setProperty("spring.test.property", "TEST"); + + assertThat(this.listener.isSystemPropertyNotSet("spring.test.property")).isFalse(); + } + + @Test + public void setSystemPropertyWithValue() { + + assertThat(System.getProperty("spring.test.property")).isNull(); + + this.listener.setSystemProperty("spring.test.property", "TEST"); + + assertThat(System.getProperty("spring.test.property")).isEqualTo("TEST"); + } + + @Test + public void setSystemPropertyWithNoValue() { + + this.listener.setSystemProperty("spring.test.property.one", null); + this.listener.setSystemProperty("spring.test.property.two", ""); + this.listener.setSystemProperty("spring.test.property.three", " "); + + assertThat(System.getProperty("spring.test.property.one")).isNull(); + } + + public void testSetSystemPropertyWithNoPropertyNameThrowsIllegalArgumentException(String propertyName) { + + try { + this.listener.setSystemProperty(propertyName, "TEST"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("PropertyName [%s] is required", propertyName); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void setSystemPropertyWithBlankPropertyNameThrowsIllegalArgumentException() { + testSetSystemPropertyWithNoPropertyNameThrowsIllegalArgumentException(" "); + } + + @Test(expected = IllegalArgumentException.class) + public void setSystemPropertyWithEmptyPropertyNameThrowsIllegalArgumentException() { + testSetSystemPropertyWithNoPropertyNameThrowsIllegalArgumentException(""); + } + + @Test(expected = IllegalArgumentException.class) + public void setSystemPropertyWithNullPropertyNameThrowsIllegalArgumentException() { + testSetSystemPropertyWithNoPropertyNameThrowsIllegalArgumentException(null); + } + + @Test + public void supportsEventTypeReturnsTrue() { + assertThat(this.listener.supportsEventType(ResolvableType.forClass(ApplicationEnvironmentPreparedEvent.class))) + .isTrue(); + } + + @Test + public void supportsEventTypeReturnsFalse() { + + assertThat(this.listener.supportsEventType(ResolvableType.NONE)).isFalse(); + assertThat(this.listener.supportsEventType(ResolvableType.forClass(ApplicationEvent.class))).isFalse(); + assertThat(this.listener.supportsEventType(ResolvableType.forClass(Object.class))).isFalse(); + } + + @Test + public void supportsSourceTypeReturnsTrue() { + + assertThat(this.listener.supportsSourceType(ApplicationContext.class)).isTrue(); + assertThat(this.listener.supportsSourceType(SpringApplication.class)).isTrue(); + assertThat(this.listener.supportsSourceType(WebApplicationContext.class)).isTrue(); + } + + @Test + public void supportSourceTypeReturnsFalse() { + + assertThat(this.listener.supportsSourceType(null)).isFalse(); + assertThat(this.listener.supportsSourceType(Object.class)).isFalse(); + assertThat(this.listener.supportsSourceType(BeanFactory.class)).isFalse(); + assertThat(this.listener.supportsSourceType(SpringApplicationBuilder.class)).isFalse(); + } +}