SLING-6785 - support caching for adaptations
authorJustin Edelson <justin@apache.org>
Thu, 27 Apr 2017 13:37:01 +0000 (13:37 +0000)
committerJustin Edelson <justin@apache.org>
Thu, 27 Apr 2017 13:37:01 +0000 (13:37 +0000)
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1792884 13f79535-47bb-0310-9956-ffa450edef68

src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
src/test/java/org/apache/sling/models/impl/CachingTest.java [new file with mode: 0644]
src/test/java/org/apache/sling/models/testmodels/classes/CachedModel.java [new file with mode: 0644]
src/test/java/org/apache/sling/models/testmodels/classes/UncachedModel.java [new file with mode: 0644]
src/test/java/org/apache/sling/models/testmodels/interfaces/CachedModel.java [new file with mode: 0644]
src/test/java/org/apache/sling/models/testmodels/interfaces/UncachedModel.java [new file with mode: 0644]

index 241e14e..a07dcd9 100644 (file)
@@ -35,6 +35,7 @@ import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.WeakHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
@@ -216,6 +217,13 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
     // Use threadlocal to count recursive invocations and break recursing if a max. limit is reached (to avoid cyclic dependencies)
     private ThreadLocal<ThreadInvocationCounter> invocationCountThreadLocal;
 
+    private Map<Object, Map<Class, Object>> adapterCache;
+
+    // use a smaller initial capacity than the default as we expect a relatively small number of
+    // adapters per adaptable
+    private final int INNER_CACHE_INITIAL_CAPACITY = 4;
+
+
     public <AdapterType> AdapterType getAdapter(Object adaptable, Class<AdapterType> type) {
         Result<AdapterType> result = internalCreateModel(adaptable, type);
         if (!result.wasSuccessful()) {
@@ -297,7 +305,7 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
     }
 
     @SuppressWarnings("unchecked")
-    private <ModelType> Result<ModelType> internalCreateModel(Object adaptable, Class<ModelType> requestedType) {
+    private <ModelType> Result<ModelType> internalCreateModel(final Object adaptable, final Class<ModelType> requestedType) {
         Result<ModelType> result;
         ThreadInvocationCounter threadInvocationCounter = invocationCountThreadLocal.get();
         if (threadInvocationCounter.isMaximumReached()) {
@@ -317,6 +325,17 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
             boolean isAdaptable = false;
 
             Model modelAnnotation = modelClass.getModelAnnotation();
+
+            if (modelAnnotation.cache()) {
+                Map<Class, Object> adaptableCache = adapterCache.get(adaptable);
+                if (adaptableCache != null) {
+                    ModelType cachedObject = (ModelType) adaptableCache.get(requestedType);
+                    if (cachedObject != null) {
+                        return new Result<ModelType>(cachedObject);
+                    }
+                }
+            }
+
             Class<?>[] declaredAdaptable = modelAnnotation.adaptables();
             for (Class<?> clazz : declaredAdaptable) {
                 if (clazz.isInstance(adaptable)) {
@@ -335,6 +354,16 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
                     Result<InvocationHandler> handlerResult = createInvocationHandler(adaptable, modelClass);
                     if (handlerResult.wasSuccessful()) {
                         ModelType model = (ModelType) Proxy.newProxyInstance(modelClass.getType().getClassLoader(), new Class<?>[] { modelClass.getType() }, handlerResult.getValue());
+
+                        if (modelAnnotation.cache()) {
+                            Map<Class, Object> adaptableCache = adapterCache.get(adaptable);
+                            if (adaptableCache == null) {
+                                adaptableCache = new ConcurrentHashMap<Class, Object>(INNER_CACHE_INITIAL_CAPACITY);
+                                adapterCache.put(adaptable, adaptableCache);
+                            }
+                            adaptableCache.put(requestedType, model);
+                        }
+
                         result = new Result<ModelType>(model);
                     } else {
                         return new Result<ModelType>(handlerResult.getThrowable());
@@ -342,6 +371,15 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
                 } else {
                     try {
                         result = createObject(adaptable, modelClass);
+
+                        if (result.wasSuccessful() && modelAnnotation.cache()) {
+                            Map<Class, Object> adaptableCache = adapterCache.get(adaptable);
+                            if (adaptableCache == null) {
+                                adaptableCache = new ConcurrentHashMap<Class, Object>(INNER_CACHE_INITIAL_CAPACITY);
+                                adapterCache.put(adaptable, adaptableCache);
+                            }
+                            adaptableCache.put(requestedType, result.getValue());
+                        }
                     } catch (Exception e) {
                         String msg = String.format("Unable to create model %s", modelClass.getType());
                         return new Result<ModelType>(new ModelClassException(msg, e));
@@ -972,6 +1010,8 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
             }
         };
 
+        this.adapterCache = Collections.synchronizedMap(new WeakHashMap<Object, Map<Class, Object>>());
+
         BundleContext bundleContext = ctx.getBundleContext();
         this.queue = new ReferenceQueue<Object>();
         this.disposalCallbacks = new ConcurrentHashMap<java.lang.ref.Reference<Object>, DisposalCallbackRegistryImpl>();
@@ -999,6 +1039,7 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
 
     @Deactivate
     protected void deactivate() {
+        this.adapterCache = null;
         this.clearDisposalCallbackRegistryQueue();
         this.listener.unregisterAll();
         this.adapterImplementations.removeAll();
diff --git a/src/test/java/org/apache/sling/models/impl/CachingTest.java b/src/test/java/org/apache/sling/models/impl/CachingTest.java
new file mode 100644 (file)
index 0000000..d9e4782
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * 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.models.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.impl.injectors.RequestAttributeInjector;
+import org.apache.sling.models.testmodels.classes.CachedModel;
+import org.apache.sling.models.testmodels.classes.UncachedModel;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+
+import java.util.Hashtable;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CachingTest {
+
+    @Mock
+    private SlingHttpServletRequest request;
+
+    @Mock
+    private ComponentContext componentCtx;
+
+    @Mock
+    private BundleContext bundleContext;
+
+    private ModelAdapterFactory factory;
+
+    @Before
+    public void setup() {
+        when(componentCtx.getBundleContext()).thenReturn(bundleContext);
+        when(componentCtx.getProperties()).thenReturn(new Hashtable<String, Object>());
+
+        factory = new ModelAdapterFactory();
+        factory.activate(componentCtx);
+        factory.bindInjector(new RequestAttributeInjector(), new ServicePropertiesMap(0, 0));
+        factory.adapterImplementations.addClassesAsAdapterAndImplementation(CachedModel.class, UncachedModel.class,
+                org.apache.sling.models.testmodels.interfaces.CachedModel.class, org.apache.sling.models.testmodels.interfaces.UncachedModel.class);
+
+        when(request.getAttribute("testValue")).thenReturn("test");
+    }
+
+    @Test
+    public void testCachedClass() {
+        CachedModel cached1 = factory.getAdapter(request, CachedModel.class);
+        CachedModel cached2 = factory.getAdapter(request, CachedModel.class);
+
+        assertTrue(cached1 == cached2);
+        assertEquals("test", cached1.getTestValue());
+        assertEquals("test", cached2.getTestValue());
+
+        verify(request, times(1)).getAttribute("testValue");
+    }
+
+    @Test
+    public void testNoCachedClass() {
+        UncachedModel uncached1 = factory.getAdapter(request, UncachedModel.class);
+        UncachedModel uncached2 = factory.getAdapter(request, UncachedModel.class);
+
+        assertTrue(uncached1 != uncached2);
+        assertEquals("test", uncached1.getTestValue());
+        assertEquals("test", uncached2.getTestValue());
+
+        verify(request, times(2)).getAttribute("testValue");
+    }
+
+    @Test
+    public void testCachedInterface() {
+        org.apache.sling.models.testmodels.interfaces.CachedModel cached1 = factory.getAdapter(request, org.apache.sling.models.testmodels.interfaces.CachedModel.class);
+        org.apache.sling.models.testmodels.interfaces.CachedModel cached2 = factory.getAdapter(request, org.apache.sling.models.testmodels.interfaces.CachedModel.class);
+
+        assertTrue(cached1 == cached2);
+        assertEquals("test", cached1.getTestValue());
+        assertEquals("test", cached2.getTestValue());
+
+        verify(request, times(1)).getAttribute("testValue");
+    }
+
+    @Test
+    public void testNoCachedInterface() {
+        org.apache.sling.models.testmodels.interfaces.UncachedModel uncached1 = factory.getAdapter(request, org.apache.sling.models.testmodels.interfaces.UncachedModel.class);
+        org.apache.sling.models.testmodels.interfaces.UncachedModel uncached2 = factory.getAdapter(request, org.apache.sling.models.testmodels.interfaces.UncachedModel.class);
+
+        assertTrue(uncached1 != uncached2);
+        assertEquals("test", uncached1.getTestValue());
+        assertEquals("test", uncached2.getTestValue());
+
+        verify(request, times(2)).getAttribute("testValue");
+    }
+}
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/CachedModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/CachedModel.java
new file mode 100644 (file)
index 0000000..2e02526
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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.models.testmodels.classes;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+import javax.inject.Inject;
+
+@Model(adaptables = SlingHttpServletRequest.class, cache = true)
+public class CachedModel {
+
+    @Inject
+    private String testValue;
+
+    public String getTestValue() {
+        return testValue;
+    }
+}
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/UncachedModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/UncachedModel.java
new file mode 100644 (file)
index 0000000..bd39352
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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.models.testmodels.classes;
+
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+import javax.inject.Inject;
+
+@Model(adaptables = SlingHttpServletRequest.class)
+public class UncachedModel {
+
+    @Inject
+    private String testValue;
+
+    public String getTestValue() {
+        return testValue;
+    }
+}
diff --git a/src/test/java/org/apache/sling/models/testmodels/interfaces/CachedModel.java b/src/test/java/org/apache/sling/models/testmodels/interfaces/CachedModel.java
new file mode 100644 (file)
index 0000000..491593d
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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.models.testmodels.interfaces;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+import javax.inject.Inject;
+
+@Model(adaptables = SlingHttpServletRequest.class, cache = true)
+public interface CachedModel {
+
+    @Inject
+    String getTestValue();
+}
diff --git a/src/test/java/org/apache/sling/models/testmodels/interfaces/UncachedModel.java b/src/test/java/org/apache/sling/models/testmodels/interfaces/UncachedModel.java
new file mode 100644 (file)
index 0000000..79614e8
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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.models.testmodels.interfaces;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+
+import javax.inject.Inject;
+
+@Model(adaptables = SlingHttpServletRequest.class)
+public interface UncachedModel {
+
+    @Inject
+    String getTestValue();
+}