SLING-7529 - implemented layout inheritance in log encoder (#3)


diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoder.java b/src/main/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoder.java
index 702acf8..996a5b0 100644
--- a/src/main/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoder.java
+++ b/src/main/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoder.java
@@ -45,12 +45,24 @@
     }
 
     private Layout<ILoggingEvent> getLayout(String loggerName) {
-        // TODO Handle layout inheritance wrt logger names
-        Layout<ILoggingEvent> layout = layoutByCategory.get(loggerName);
-        if (layout == null) {
-            layout = defaultLayout;
+        String bestMatchLayoutKey = getBestMatchLayoutKey(loggerName);
+        return layoutByCategory.getOrDefault(bestMatchLayoutKey, defaultLayout);
+    }
+
+    private String getBestMatchLayoutKey(String loggerName) {
+        if (layoutByCategory.containsKey(loggerName)) {
+            // fastpath for exact name match
+            return loggerName;
         }
-        return layout;
+        String bestMatch = loggerName;
+        int bestMatchLength = 0;
+        for (String layoutKey : layoutByCategory.keySet()) {
+            if (loggerName.startsWith(layoutKey) && loggerName.charAt(layoutKey.length()) == '.' && layoutKey.length() > bestMatchLength) {
+                bestMatch = layoutKey;
+                bestMatchLength = layoutKey.length();
+            }
+        }
+        return bestMatch;
     }
 
     private byte[] convertToBytes(String s) {
diff --git a/src/test/java/org/apache/sling/commons/log/logback/internal/util/AbstractPatternLayoutWrapper.java b/src/test/java/org/apache/sling/commons/log/logback/internal/util/AbstractPatternLayoutWrapper.java
new file mode 100644
index 0000000..93ed24f
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/log/logback/internal/util/AbstractPatternLayoutWrapper.java
@@ -0,0 +1,177 @@
+/*
+ * 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.sling.commons.log.logback.internal.util;
+
+import ch.qos.logback.classic.PatternLayout;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Context;
+import ch.qos.logback.core.pattern.PostCompileProcessor;
+import ch.qos.logback.core.status.Status;
+
+import java.util.Map;
+
+/**
+ * Abstract wrapper for {@link PatternLayout} class. Can be extended to implement 'Decorator' design pattern.
+ *
+ * @apiNote This has probably no use outside of testing
+ */
+public abstract class AbstractPatternLayoutWrapper extends PatternLayout {
+
+    protected final PatternLayout wrapped;
+
+    protected AbstractPatternLayoutWrapper(final PatternLayout wrapped) {
+        this.wrapped = wrapped;
+    }
+
+    @Override
+    public String doLayout(final ILoggingEvent event) {
+        return wrapped.doLayout(event);
+    }
+
+    @Override
+    public String getFileHeader() {
+        return wrapped.getFileHeader();
+    }
+
+    @Override
+    public String getPresentationHeader() {
+        return wrapped.getPresentationHeader();
+    }
+
+    @Override
+    public String getPresentationFooter() {
+        return wrapped.getPresentationFooter();
+    }
+
+    @Override
+    public String getFileFooter() {
+        return wrapped.getFileFooter();
+    }
+
+    @Override
+    public String getContentType() {
+        return wrapped.getContentType();
+    }
+
+    @Override
+    public void setContext(final Context context) {
+        wrapped.setContext(context);
+    }
+
+    @Override
+    public Context getContext() {
+        return wrapped.getContext();
+    }
+
+    @Override
+    public void addStatus(final Status status) {
+        wrapped.addStatus(status);
+    }
+
+    @Override
+    public void addInfo(final String s) {
+        wrapped.addInfo(s);
+    }
+
+    @Override
+    public void addInfo(final String msg, final Throwable ex) {
+        wrapped.addInfo(msg, ex);
+    }
+
+    @Override
+    public void addWarn(final String s) {
+        wrapped.addWarn(s);
+    }
+
+    @Override
+    public void addWarn(final String msg, final Throwable ex) {
+        wrapped.addWarn(msg, ex);
+    }
+
+    @Override
+    public void addError(final String s) {
+        wrapped.addError(s);
+    }
+
+    @Override
+    public void addError(final String msg, final Throwable ex) {
+        wrapped.addError(msg, ex);
+    }
+
+    @Override
+    public void start() {
+        wrapped.start();
+    }
+
+    @Override
+    public void stop() {
+        wrapped.stop();
+    }
+
+    @Override
+    public boolean isStarted() {
+        return wrapped.isStarted();
+    }
+
+    @Override
+    public Map<String, String> getDefaultConverterMap() {
+        return wrapped.getDefaultConverterMap();
+    }
+
+    @Override
+    public Map<String, String> getEffectiveConverterMap() {
+        return wrapped.getEffectiveConverterMap();
+    }
+
+    @Override
+    public void setPostCompileProcessor(PostCompileProcessor<ILoggingEvent> postCompileProcessor) {
+        wrapped.setPostCompileProcessor(postCompileProcessor);
+    }
+
+    @Override
+    public String getPattern() {
+        return wrapped.getPattern();
+    }
+
+    @Override
+    public void setPattern(String pattern) {
+        wrapped.setPattern(pattern);
+    }
+
+    @Override
+    public String toString() {
+        return wrapped.toString();
+    }
+
+    @Override
+    public Map<String, String> getInstanceConverterMap() {
+        return wrapped.getInstanceConverterMap();
+    }
+
+    @Override
+    public boolean isOutputPatternAsHeader() {
+        return wrapped.isOutputPatternAsHeader();
+    }
+
+    @Override
+    public void setOutputPatternAsHeader(boolean outputPatternAsHeader) {
+        wrapped.isOutputPatternAsHeader();
+    }
+}
diff --git a/src/test/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoderTest.java b/src/test/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoderTest.java
new file mode 100644
index 0000000..a32e406
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/log/logback/internal/util/LoggerSpecificEncoderTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.sling.commons.log.logback.internal.util;
+
+import ch.qos.logback.classic.PatternLayout;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import org.apache.felix.framework.util.ImmutableList;
+import org.apache.sling.commons.log.logback.internal.LogConfig;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.HashSet;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class LoggerSpecificEncoderTest {
+
+    private LoggerSpecificEncoder tested;
+
+    @Mock
+    private ILoggingEvent mockTestEvent;
+
+    @Before
+    public void setUp() {
+        tested = new LoggerSpecificEncoder(new PrefixTestLayout("DEFAULT:"));
+        when(mockTestEvent.getMessage()).thenReturn("test message");
+        when(mockTestEvent.getLoggerName()).thenReturn("org.apache.sling.testing.FooBar");
+    }
+
+    @Test
+    public void testShouldReturnWithDefaultLayoutForNoConfigs() {
+        assertThat(tested.encode(mockTestEvent), is(equalTo("DEFAULT:test message".getBytes())));
+    }
+
+    @Test
+    public void testShouldIgnoreNonmatchingLayoutCategories() {
+        LogConfig logConfigMock = mock(LogConfig.class);
+        when(logConfigMock.getCategories()).thenReturn(new HashSet<>(ImmutableList.newInstance("org.apache.commons", "com.initech.sling")));
+        when(logConfigMock.createLayout()).thenReturn(new PrefixTestLayout("INITECH:"));
+        tested.addLogConfig(logConfigMock);
+
+        assertThat(tested.encode(mockTestEvent), is(equalTo("DEFAULT:test message".getBytes())));
+    }
+
+    @Test
+    public void testShouldIgnorePartialMatchingPackageName() {
+        LogConfig logConfigMock = mock(LogConfig.class);
+        when(logConfigMock.getCategories()).thenReturn(new HashSet<>(ImmutableList.newInstance("org.apache.sling.test")));
+        when(logConfigMock.createLayout()).thenReturn(new PrefixTestLayout("INITECH:"));
+        tested.addLogConfig(logConfigMock);
+
+        assertThat(tested.encode(mockTestEvent), is(equalTo("DEFAULT:test message".getBytes())));
+    }
+
+    @Test
+    public void testShouldUseExactMatchingCategoryLayout() {
+        LogConfig logConfigMock = mock(LogConfig.class);
+        when(logConfigMock.getCategories()).thenReturn(new HashSet<>(ImmutableList.newInstance("org.apache.sling.testing.FooBar")));
+        when(logConfigMock.createLayout()).thenReturn(new PrefixTestLayout("INITECH:"));
+        tested.addLogConfig(logConfigMock);
+
+        assertThat(tested.encode(mockTestEvent), is(equalTo("INITECH:test message".getBytes())));
+    }
+
+    @Test
+    public void testShouldUseInheritedCategoryLayout() {
+        LogConfig logConfigMock = mock(LogConfig.class);
+        when(logConfigMock.getCategories()).thenReturn(new HashSet<>(ImmutableList.newInstance("org.apache")));
+        when(logConfigMock.createLayout()).thenReturn(new PrefixTestLayout("INITECH:"));
+        tested.addLogConfig(logConfigMock);
+
+        assertThat(tested.encode(mockTestEvent), is(equalTo("INITECH:test message".getBytes())));
+    }
+
+    /**
+     * Simple partial implementation of {@link PatternLayout} that redirects all method calls that are not explicitly extended to an
+     * underlying mock (available as a protected {@link PrefixTestLayout#wrapped} field).
+     */
+    private static class PrefixTestLayout extends AbstractPatternLayoutWrapper {
+
+        private final String prefix;
+
+        private PrefixTestLayout(String prefix) {
+            super(mock(PatternLayout.class));
+            this.prefix = prefix;
+        }
+
+        @Override
+        public String doLayout(ILoggingEvent event) {
+            return prefix + event.getMessage();
+        }
+    }
+}
\ No newline at end of file