diff --git a/spring-geode-starter-logging/src/main/java/org/springframework/geode/logging/slf4j/logback/CompositeAppender.java b/spring-geode-starter-logging/src/main/java/org/springframework/geode/logging/slf4j/logback/CompositeAppender.java new file mode 100644 index 00000000..33b41ea1 --- /dev/null +++ b/spring-geode-starter-logging/src/main/java/org/springframework/geode/logging/slf4j/logback/CompositeAppender.java @@ -0,0 +1,105 @@ +/* + * 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.logging.slf4j.logback; + +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Context; + +/** + * {@link CompositeAppender} is an {@link Appender} implementation implementing + * the Composite Software Design Pattern + * + * The {@literal Composite Software Design Pattern} enables two or more {@link Appender} objects to be composed + * and treated as a single instance of {@link Appender}. + * + * @author John Blum + * @see ch.qos.logback.core.Appender + * @see ch.qos.logback.core.AppenderBase + * @since 1.3.0 + */ +public class CompositeAppender extends AppenderBase { + + protected static final String DEFAULT_NAME = "composite"; + + private final Appender one; + private final Appender two; + + /** + * Factory method used to compose two {@link Appender} objects into a {@literal Composite} {@link Appender}. + * + * @param {@link Class type} of {@link Appender} to compose. + * @param one first {@link Appender} to compose. + * @param two second {@link Appender} to compose. + * @return {@link Appender} one if {@link Appender} two is {@literal null}; + * Return {@link Appender} two if {@link Appender} one is {@literal null}. + * Otherwise, return a {@literal Composite} {@link Appender} composed of {@link Appender} one + * and {@link Appender} two. + * @see ch.qos.logback.core.Appender + */ + public static Appender compose(Appender one, Appender two) { + return one == null ? two : two == null ? one : new CompositeAppender<>(one, two); + } + + /** + * Constructs a new instance of {@link CompositeAppender} composed of {@link Appender} one and {@link Appender} two. + * + * @param one first {@link Appender} in the composite. + * @param two second {@link Appender} in the composite. + * @see ch.qos.logback.core.Appender + */ + private CompositeAppender(Appender one, Appender two) { + + this.one = one; + this.two = two; + this.name = DEFAULT_NAME; + this.started = true; + } + + protected Appender getAppenderOne() { + return this.one; + } + + protected Appender getAppenderTwo() { + return this.two; + } + + @Override + public void setContext(Context context) { + + super.setContext(context); + + getAppenderOne().setContext(context); + getAppenderTwo().setContext(context); + } + + @Override + public Context getContext() { + + Context context = super.getContext(); + + context = context != null ? context : getAppenderOne().getContext(); + context = context != null ? context : getAppenderTwo().getContext(); + + return context; + } + + @Override + protected void append(T eventObject) { + getAppenderOne().doAppend(eventObject); + getAppenderTwo().doAppend(eventObject); + } +} diff --git a/spring-geode-starter-logging/src/test/java/org/springframework/geode/logging/slf4j/logback/CompositeAppenderUnitTests.java b/spring-geode-starter-logging/src/test/java/org/springframework/geode/logging/slf4j/logback/CompositeAppenderUnitTests.java new file mode 100644 index 00000000..355e9f13 --- /dev/null +++ b/spring-geode-starter-logging/src/test/java/org/springframework/geode/logging/slf4j/logback/CompositeAppenderUnitTests.java @@ -0,0 +1,158 @@ +/* + * 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.logging.slf4j.logback; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +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 org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.Context; + +/** + * Unit Tests for {@link CompositeAppender}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.geode.logging.slf4j.logback.CompositeAppender + * @see ch.qos.logback.core.Appender + * @since 1.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class CompositeAppenderUnitTests { + + @Mock + private Appender mockAppenderOne; + + @Mock + private Appender mockAppenderTwo; + + @Test + public void composeIsNullSafe() { + assertThat(CompositeAppender.compose(null, null)).isNull(); + } + + @Test + public void composeSingleAppenderReturnsTheAppender() { + + assertThat(CompositeAppender.compose(this.mockAppenderOne, null)).isEqualTo(this.mockAppenderOne); + assertThat(CompositeAppender.compose(null, this.mockAppenderTwo)).isEqualTo(this.mockAppenderTwo); + } + + @Test + public void composeTwoAppendersReturnsCompositeAppender() { + + Appender composite = CompositeAppender.compose(this.mockAppenderOne, this.mockAppenderTwo); + + assertThat(composite).isInstanceOf(CompositeAppender.class); + assertThat((((CompositeAppender) composite).getAppenderOne())).isEqualTo(this.mockAppenderOne); + assertThat((((CompositeAppender) composite).getAppenderTwo())).isEqualTo(this.mockAppenderTwo); + assertThat(composite.getName()).isEqualTo(CompositeAppender.DEFAULT_NAME); + assertThat(composite.isStarted()).isTrue(); + } + + @Test + public void setContextConfiguresContextOnCompositeAppenderAndComposedAppenders() { + + Context mockContext = mock(Context.class); + + Appender compositeAppender = CompositeAppender.compose(this.mockAppenderOne, this.mockAppenderTwo); + + assertThat(compositeAppender).isInstanceOf(CompositeAppender.class); + + compositeAppender.setContext(mockContext); + + assertThat(compositeAppender.getContext()).isEqualTo(mockContext); + + verify(this.mockAppenderOne, times(1)).setContext(eq(mockContext)); + verify(this.mockAppenderTwo, times(1)).setContext(eq(mockContext)); + } + + @Test + public void getContextReturnsConfiguredContext() { + + Context mockContext = mock(Context.class); + + Appender compositeAppender = CompositeAppender.compose(this.mockAppenderOne, this.mockAppenderTwo); + + assertThat(compositeAppender).isInstanceOf(CompositeAppender.class); + + compositeAppender.setContext(mockContext); + + assertThat(compositeAppender.getContext()).isEqualTo(mockContext); + + verify(this.mockAppenderOne, never()).getContext(); + verify(this.mockAppenderTwo, never()).getContext(); + } + + @Test + public void getContextReturnsAppenderOneContext() { + + Context mockContext = mock(Context.class); + + doReturn(mockContext).when(this.mockAppenderOne).getContext(); + + Appender compositeAppender = CompositeAppender.compose(this.mockAppenderOne, this.mockAppenderTwo); + + assertThat(compositeAppender).isInstanceOf(CompositeAppender.class); + assertThat(compositeAppender.getContext()).isEqualTo(mockContext); + + verify(this.mockAppenderOne, times(1)).getContext(); + verify(this.mockAppenderTwo, never()).getContext(); + } + + @Test + public void getContextReturnsAppenderTwoContext() { + + Context mockContext = mock(Context.class); + + doReturn(mockContext).when(this.mockAppenderTwo).getContext(); + + Appender compositeAppender = CompositeAppender.compose(this.mockAppenderOne, this.mockAppenderTwo); + + assertThat(compositeAppender).isInstanceOf(CompositeAppender.class); + assertThat(compositeAppender.getContext()).isEqualTo(mockContext); + + verify(this.mockAppenderOne, times(1)).getContext(); + verify(this.mockAppenderTwo, times(1)).getContext(); + } + + @Test + public void appendCallsAppenderOneAppendAndAppenderTwoAppend() { + + Appender compositeAppender = CompositeAppender.compose(this.mockAppenderOne, this.mockAppenderTwo); + + assertThat(compositeAppender).isInstanceOf(CompositeAppender.class); + + ((CompositeAppender) compositeAppender).append("TEST"); + + verify(this.mockAppenderOne, times(1)).doAppend(eq("TEST")); + verify(this.mockAppenderTwo, times(1)).doAppend(eq("TEST")); + } +}