SLING-5992 - Introduce resource type to model class binding
authorJustin Edelson <justin@apache.org>
Wed, 31 Aug 2016 13:24:44 +0000 (13:24 +0000)
committerJustin Edelson <justin@apache.org>
Wed, 31 Aug 2016 13:24:44 +0000 (13:24 +0000)
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1758602 13f79535-47bb-0310-9956-ffa450edef68

pom.xml
src/main/java/org/apache/sling/models/impl/AdapterImplementations.java
src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
src/main/java/org/apache/sling/models/impl/ModelPackageBundleListener.java
src/main/java/org/apache/sling/models/impl/ResourceTypeBasedResourcePicker.java [new file with mode: 0644]
src/test/java/org/apache/sling/models/impl/AdapterImplementationsTest.java

diff --git a/pom.xml b/pom.xml
index a7e4bd6..9988aaa 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
     <groupId>org.apache.sling</groupId>
     <artifactId>org.apache.sling.models.impl</artifactId>
     <packaging>bundle</packaging>
-    <version>1.2.9-SNAPSHOT</version>
+    <version>1.3.0-SNAPSHOT</version>
     <name>Apache Sling Models Implementation</name>
     <description>Apache Sling Models Implementation</description>
 
@@ -63,7 +63,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.models.api</artifactId>
-            <version>1.2.2</version>
+            <version>1.3.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -85,7 +85,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.api</artifactId>
-            <version>2.2.0</version>
+            <version>2.4.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
index 36ca013..43b4c74 100644 (file)
@@ -20,15 +20,24 @@ package org.apache.sling.models.impl;
 
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentNavigableMap;
 import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.models.impl.model.ModelClass;
 import org.apache.sling.models.spi.ImplementationPicker;
 import org.apache.sling.models.spi.injectorspecific.StaticInjectAnnotationProcessorFactory;
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Collects alternative adapter implementations that may be defined in a @Model.adapters attribute.
@@ -37,13 +46,20 @@ import org.apache.sling.models.spi.injectorspecific.StaticInjectAnnotationProces
  * The implementation is thread-safe.
  */
 final class AdapterImplementations {
-    
+
+    private static final Logger log = LoggerFactory.getLogger(AdapterImplementations.class);
+
     private final ConcurrentMap<String,ConcurrentNavigableMap<String,ModelClass<?>>> adapterImplementations
             = new ConcurrentHashMap<String,ConcurrentNavigableMap<String,ModelClass<?>>>();
 
     private final ConcurrentMap<String,ModelClass<?>> modelClasses
             = new ConcurrentHashMap<String,ModelClass<?>>();
     
+    private final ConcurrentMap<String, Class<?>> resourceTypeMappingsForResources = new ConcurrentHashMap<String, Class<?>>();
+    private final ConcurrentMap<String, Class<?>> resourceTypeMappingsForRequests = new ConcurrentHashMap<String, Class<?>>();
+    private final ConcurrentMap<Bundle, List<String>> resourceTypeRemovalListsForResources = new ConcurrentHashMap<Bundle, List<String>>();
+    private final ConcurrentMap<Bundle, List<String>> resourceTypeRemovalListsForRequests = new ConcurrentHashMap<Bundle, List<String>>();
+
     private volatile ImplementationPicker[] sortedImplementationPickers = new ImplementationPicker[0];
     private volatile StaticInjectAnnotationProcessorFactory[] sortedStaticInjectAnnotationProcessorFactories = new StaticInjectAnnotationProcessorFactory[0];
 
@@ -217,4 +233,100 @@ final class AdapterImplementations {
         return true;
     }
 
+     public void registerModelToResourceType(final Bundle bundle, final String resourceType, final Class<?> adaptableType, final Class<?> clazz) {
+         if (resourceType.startsWith("/")) {
+             log.warn("Registering model class {} for adaptable {} with absolute resourceType {}." ,
+                     new Object[] { clazz, adaptableType, resourceType });
+         }
+         ConcurrentMap<String, Class<?>> map;
+         ConcurrentMap<Bundle, List<String>> resourceTypeRemovalLists;
+         if (adaptableType == Resource.class) {
+             map = resourceTypeMappingsForResources;
+             resourceTypeRemovalLists = resourceTypeRemovalListsForResources;
+         } else if (adaptableType == SlingHttpServletRequest.class) {
+             map = resourceTypeMappingsForRequests;
+             resourceTypeRemovalLists = resourceTypeRemovalListsForRequests;
+         } else {
+             log.warn("Found model class {} with resource type {} for adaptable {}. Unsupported type for resourceType binding.",
+                     new Object[] { clazz, resourceType, adaptableType });
+             return;
+         }
+         Class<?> existingMapping = map.putIfAbsent(resourceType, clazz);
+         if (existingMapping == null) {
+             resourceTypeRemovalLists.putIfAbsent(bundle, new CopyOnWriteArrayList<String>());
+             resourceTypeRemovalLists.get(bundle).add(resourceType);
+         } else {
+             log.warn("Skipped registering {} for resourceType {} under adaptable {} because of existing mapping to {}",
+                     new Object[] { clazz, resourceType, adaptableType, existingMapping });
+         }
+     }
+
+     public void removeResourceTypeBindings(final Bundle bundle) {
+         List<String> registeredResourceTypes = resourceTypeRemovalListsForResources.remove(bundle);
+         if (registeredResourceTypes != null) {
+             for (String resourceType : registeredResourceTypes) {
+                 resourceTypeMappingsForResources.remove(resourceType);
+             }
+         }
+         registeredResourceTypes = resourceTypeRemovalListsForRequests.remove(bundle);
+         if (registeredResourceTypes != null) {
+             for (String resourceType : registeredResourceTypes) {
+                 resourceTypeMappingsForRequests.remove(resourceType);
+             }
+         }
+     }
+
+     public Class<?> getModelClassForRequest(final SlingHttpServletRequest request) {
+         return getModelClassForResource(request.getResource(), resourceTypeMappingsForRequests);
+     }
+
+    public Class<?> getModelClassForResource(final Resource resource) {
+        return getModelClassForResource(resource, resourceTypeMappingsForResources);
+    }
+
+    protected static Class<?> getModelClassForResource(final Resource resource, final Map<String, Class<?>> map) {
+        if (resource == null) {
+            return null;
+        }
+        ResourceResolver resolver = resource.getResourceResolver();
+        final String originalResourceType = resource.getResourceType();
+        Class<?> modelClass = getClassFromResourceTypeMap(originalResourceType, map, resolver);
+        if (modelClass != null) {
+            return modelClass;
+        } else {
+            String resourceType = resolver.getParentResourceType(resource);
+            while (resourceType != null) {
+                modelClass = getClassFromResourceTypeMap(resourceType, map, resolver);
+                if (modelClass != null) {
+                    return modelClass;
+                } else {
+                    resourceType = resolver.getParentResourceType(resourceType);
+                }
+            }
+            Resource resourceTypeResource = resolver.getResource(originalResourceType);
+            return getModelClassForResource(resourceTypeResource, map);
+        }
+    }
+
+    private static Class<?> getClassFromResourceTypeMap(final String resourceType, final Map<String, Class<?>> map, final ResourceResolver resolver) {
+        Class<?> modelClass = map.get(resourceType);
+        if (modelClass == null) {
+            for (String searchPath : resolver.getSearchPath()) {
+                if (resourceType.startsWith("/")) {
+                    if (resourceType.startsWith(searchPath)) {
+                        modelClass = map.get(resourceType.substring(searchPath.length()));
+                        if (modelClass != null) {
+                            break;
+                        }
+                    }
+                } else {
+                    modelClass = map.get(searchPath + resourceType);
+                    if (modelClass != null) {
+                        break;
+                    }
+                }
+            }
+        }
+        return modelClass;
+    }
 }
index 7a7a683..4003ceb 100644 (file)
@@ -54,8 +54,10 @@ import org.apache.felix.scr.annotations.ReferencePolicy;
 import org.apache.felix.scr.annotations.ReferencePolicyOption;
 import org.apache.felix.scr.annotations.References;
 import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.adapter.Adaptable;
 import org.apache.sling.api.adapter.AdapterFactory;
+import org.apache.sling.api.resource.Resource;
 import org.apache.sling.commons.osgi.PropertiesUtil;
 import org.apache.sling.commons.osgi.RankedServices;
 import org.apache.sling.models.annotations.Model;
@@ -1028,4 +1030,40 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
         return adapterImplementations.getImplementationPickers();
     }
 
+    @Override
+    public boolean isModelAvailableForRequest(@Nonnull SlingHttpServletRequest request) {
+        return adapterImplementations.getModelClassForRequest(request) != null;
+    }
+
+    @Override
+    public boolean isModelAvailableForResource(@Nonnull Resource resource) {
+        return adapterImplementations.getModelClassForResource(resource) != null;
+    }
+
+    @Override
+    public Object getModelFromResource(Resource resource) {
+        Class<?> clazz = this.adapterImplementations.getModelClassForResource(resource);
+        if (clazz == null) {
+            throw new ModelClassException("Could find model registered for resource type: " + resource.getResourceType());
+        }
+        return handleBoundModelResult(internalCreateModel(resource, clazz));
+    }
+
+    @Override
+    public Object getModelFromRequest(SlingHttpServletRequest request) {
+        Class<?> clazz = this.adapterImplementations.getModelClassForRequest(request);
+        if (clazz == null) {
+            throw new ModelClassException("Could find model registered for request path: " + request.getServletPath());
+        }
+        return handleBoundModelResult(internalCreateModel(request, clazz));
+    }
+
+    private Object handleBoundModelResult(Result<?> result) {
+        if (!result.wasSuccessful()) {
+            throw result.getThrowable();
+        } else {
+            return result.getValue();
+        }
+    }
+
 }
index 94963c4..3345f14 100644 (file)
@@ -57,12 +57,12 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
 
     private final BundleTracker bundleTracker;
 
-    private final AdapterFactory factory;
+    private final ModelAdapterFactory factory;
     
     private final AdapterImplementations adapterImplementations;
     
     public ModelPackageBundleListener(BundleContext bundleContext,
-            AdapterFactory factory,
+            ModelAdapterFactory factory,
             AdapterImplementations adapterImplementations) {
         this.bundleContext = bundleContext;
         this.factory = factory;
@@ -112,6 +112,15 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
                                 ServiceRegistration reg = registerAdapterFactory(adapterTypes, annotation.adaptables(), implType, annotation.condition());
                                 regs.add(reg);
                             }
+                            String[] resourceTypes = annotation.resourceType();
+                            for (String resourceType : resourceTypes) {
+                                if (StringUtils.isNotEmpty(resourceType)) {
+                                    for (Class<?> adaptable : annotation.adaptables()) {
+                                        adapterImplementations.registerModelToResourceType(bundle, resourceType, adaptable, implType);
+                                    }
+                                }
+                            }
+
                         }
                     } catch (ClassNotFoundException e) {
                         log.warn("Unable to load class", e);
@@ -140,6 +149,8 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
                 reg.unregister();
             }
         }
+        adapterImplementations.removeResourceTypeBindings(bundle);
+
     }
 
     public synchronized void unregisterAll() {
diff --git a/src/main/java/org/apache/sling/models/impl/ResourceTypeBasedResourcePicker.java b/src/main/java/org/apache/sling/models/impl/ResourceTypeBasedResourcePicker.java
new file mode 100644 (file)
index 0000000..7d8750b
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.spi.ImplementationPicker;
+import org.osgi.framework.Constants;
+
+import javax.annotation.Nonnull;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@Service
+@Property(name = Constants.SERVICE_RANKING, intValue = (Integer.MAX_VALUE - 1))
+public class ResourceTypeBasedResourcePicker implements ImplementationPicker {
+
+    @Override
+    public Class<?> pick(@Nonnull Class<?> adapterType, @Nonnull Class<?>[] implementationsTypes, @Nonnull Object adaptable) {
+        final Resource resource = findResource(adaptable);
+        if (resource == null) {
+            return null;
+        }
+        final ResourceResolver resolver = resource.getResourceResolver();
+
+        Map<String, Class<?>> implementationsByRT = mapByResourceType(implementationsTypes);
+        return AdapterImplementations.getModelClassForResource(resource, implementationsByRT);
+    }
+
+    private Resource findResource(Object adaptable) {
+        if (adaptable instanceof Resource) {
+            return (Resource) adaptable;
+        } else if (adaptable instanceof SlingHttpServletRequest) {
+            return ((SlingHttpServletRequest) adaptable).getResource();
+        } else {
+            return null;
+        }
+    }
+
+    private Map<String, Class<?>> mapByResourceType(Class<?>[] implementationTypes) {
+        Map<String, Class<?>> retval = new HashMap<String, Class<?>>(implementationTypes.length);
+
+        for (Class<?> clazz : implementationTypes) {
+            Model modelAnnotation = clazz.getAnnotation(Model.class);
+            // this really should always be non-null at this point, but just in case...
+            if (modelAnnotation != null) {
+                String[] resourceTypes = modelAnnotation.resourceType();
+                for (String resourceType : resourceTypes) {
+                    if (!retval.containsKey(resourceType)) {
+                        retval.put(resourceType, clazz);
+                    }
+                }
+            }
+        }
+        return retval;
+    }
+
+}
index 103a318..0dc9702 100644 (file)
@@ -20,14 +20,21 @@ package org.apache.sling.models.impl;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
 
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.models.spi.ImplementationPicker;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
 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;
 
 @RunWith(MockitoJUnitRunner.class)
 public class AdapterImplementationsTest {
@@ -36,6 +43,18 @@ public class AdapterImplementationsTest {
     private static final Object SAMPLE_ADAPTABLE = new Object();    
 
     private AdapterImplementations underTest;
+
+    @Mock
+    private Resource resource;
+
+    @Mock
+    private Resource childResource;
+
+    @Mock
+    private SlingHttpServletRequest request;
+
+    @Mock
+    private ResourceResolver resourceResolver;
     
     @Before
     public void setUp() {
@@ -114,6 +133,116 @@ public class AdapterImplementationsTest {
         
         assertEquals(SAMPLE_ADAPTER, underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE).getType());
     }
+
+    @Test
+    public void testResourceTypeRegistrationForResource() {
+        when(resource.getResourceType()).thenReturn("sling/rt/one");
+        when(resource.getResourceResolver()).thenReturn(resourceResolver);
+        when(childResource.getResourceType()).thenReturn("sling/rt/child");
+        when(childResource.getResourceResolver()).thenReturn(resourceResolver);
+        when(resourceResolver.getParentResourceType(resource)).thenReturn(null);
+        when(resourceResolver.getParentResourceType(childResource)).thenReturn("sling/rt/one");
+        when(resourceResolver.getSearchPath()).thenReturn(new String[] { "/apps/", "/libs/" });
+
+        // ensure we don't have any registrations for 'sling/rt/one'
+        assertNull(underTest.getModelClassForResource(resource));
+        assertNull(underTest.getModelClassForResource(childResource));
+
+        // now add a mapping for Resource -> String
+        BundleContext bundleContext = MockOsgi.newBundleContext();
+        underTest.registerModelToResourceType(bundleContext.getBundle(), "sling/rt/one", Resource.class, String.class);
+        assertEquals(String.class, underTest.getModelClassForResource(resource));
+        assertEquals(String.class, underTest.getModelClassForResource(childResource));
+
+        // ensure that trying to reregister the resource type is a no-op
+        BundleContext secondBundleContext = MockOsgi.newBundleContext();
+        underTest.registerModelToResourceType(secondBundleContext.getBundle(), "sling/rt/one", Resource.class, Integer.class);
+        assertEquals(String.class, underTest.getModelClassForResource(resource));
+        assertEquals(String.class, underTest.getModelClassForResource(childResource));
+
+        underTest.removeResourceTypeBindings(bundleContext.getBundle());
+        assertNull(underTest.getModelClassForResource(resource));
+        assertNull(underTest.getModelClassForResource(childResource));
+    }
+
+    @Test
+    public void testResourceTypeRegistrationForAbsolutePath() {
+        when(resource.getResourceType()).thenReturn("sling/rt/one");
+        when(resource.getResourceResolver()).thenReturn(resourceResolver);
+        when(childResource.getResourceType()).thenReturn("sling/rt/child");
+        when(childResource.getResourceResolver()).thenReturn(resourceResolver);
+        when(resourceResolver.getParentResourceType(resource)).thenReturn(null);
+        when(resourceResolver.getParentResourceType(childResource)).thenReturn("sling/rt/one");
+        when(resourceResolver.getSearchPath()).thenReturn(new String[] { "/apps/", "/libs/" });
+
+        // ensure we don't have any registrations for 'sling/rt/one'
+        assertNull(underTest.getModelClassForResource(resource));
+        assertNull(underTest.getModelClassForResource(childResource));
+
+        // now add a mapping for Resource -> String
+        BundleContext bundleContext = MockOsgi.newBundleContext();
+        underTest.registerModelToResourceType(bundleContext.getBundle(), "/apps/sling/rt/one", Resource.class, String.class);
+        assertEquals(String.class, underTest.getModelClassForResource(resource));
+        assertEquals(String.class, underTest.getModelClassForResource(childResource));
+
+        underTest.removeResourceTypeBindings(bundleContext.getBundle());
+        assertNull(underTest.getModelClassForResource(resource));
+        assertNull(underTest.getModelClassForResource(childResource));
+    }
+
+    @Test
+    public void testResourceTypeRegistrationForResourceHavingAbsolutePath() {
+        when(resource.getResourceType()).thenReturn("/apps/sling/rt/one");
+        when(resource.getResourceResolver()).thenReturn(resourceResolver);
+        when(childResource.getResourceType()).thenReturn("/apps/sling/rt/child");
+        when(childResource.getResourceResolver()).thenReturn(resourceResolver);
+        when(resourceResolver.getParentResourceType(resource)).thenReturn(null);
+        when(resourceResolver.getParentResourceType(childResource)).thenReturn("/apps/sling/rt/one");
+        when(resourceResolver.getSearchPath()).thenReturn(new String[] { "/apps/", "/libs/" });
+
+        // ensure we don't have any registrations for 'sling/rt/one'
+        assertNull(underTest.getModelClassForResource(resource));
+        assertNull(underTest.getModelClassForResource(childResource));
+
+        // now add a mapping for Resource -> String
+        BundleContext bundleContext = MockOsgi.newBundleContext();
+        underTest.registerModelToResourceType(bundleContext.getBundle(), "sling/rt/one", Resource.class, String.class);
+        assertEquals(String.class, underTest.getModelClassForResource(resource));
+        assertEquals(String.class, underTest.getModelClassForResource(childResource));
+
+        underTest.removeResourceTypeBindings(bundleContext.getBundle());
+        assertNull(underTest.getModelClassForResource(resource));
+        assertNull(underTest.getModelClassForResource(childResource));
+    }
+
+    @Test
+    public void testResourceTypeRegistrationForRequest() {
+        when(resource.getResourceType()).thenReturn("sling/rt/one");
+        when(resource.getResourceResolver()).thenReturn(resourceResolver);
+        when(resourceResolver.getParentResourceType(resource)).thenReturn(null);
+        when(resourceResolver.getSearchPath()).thenReturn(new String[] { "/apps/", "/libs/" });
+        when(request.getResource()).thenReturn(resource);
+
+        // ensure we don't have any registrations for 'sling/rt/one'
+        assertNull(underTest.getModelClassForRequest(request));
+        assertNull(underTest.getModelClassForResource(resource));
+
+        // now add a mapping for SlingHttpServletRequest -> String
+        BundleContext bundleContext = MockOsgi.newBundleContext();
+        underTest.registerModelToResourceType(bundleContext.getBundle(), "sling/rt/one", SlingHttpServletRequest.class, String.class);
+        underTest.registerModelToResourceType(bundleContext.getBundle(), "sling/rt/one", Resource.class, Integer.class);
+        assertEquals(String.class, underTest.getModelClassForRequest(request));
+        assertEquals(Integer.class, underTest.getModelClassForResource(resource));
+
+        // ensure that trying to reregister the resource type is a no-op
+        BundleContext secondBundleContext = MockOsgi.newBundleContext();
+        underTest.registerModelToResourceType(secondBundleContext.getBundle(), "sling/rt/one", SlingHttpServletRequest.class, Integer.class);
+        assertEquals(String.class, underTest.getModelClassForRequest(request));
+
+        underTest.removeResourceTypeBindings(bundleContext.getBundle());
+        assertNull(underTest.getModelClassForRequest(request));
+        assertNull(underTest.getModelClassForResource(resource));
+    }
     
     static final class NoneImplementationPicker implements ImplementationPicker {
         @Override