SLING-5010 Sling Models: Stale StaticInjectionAnnotationProcesssorFactory list depend...
authorStefan Seifert <sseifert@apache.org>
Wed, 9 Sep 2015 20:04:55 +0000 (20:04 +0000)
committerStefan Seifert <sseifert@apache.org>
Wed, 9 Sep 2015 20:04:55 +0000 (20:04 +0000)
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/models/impl@1702098 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/model/ModelClass.java
src/test/java/org/apache/sling/models/impl/StaticInjectionAPFLoadOrderTest.java [new file with mode: 0644]
src/test/java/org/apache/sling/models/testutil/ModelAdapterFactoryUtil.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index f3ca39f..11d43f8 100644 (file)
--- a/pom.xml
+++ b/pom.xml
             <scope>provided</scope>\r
         </dependency>\r
         <dependency>\r
+            <groupId>org.apache.sling</groupId>\r
+            <artifactId>org.apache.sling.testing.osgi-mock</artifactId>\r
+            <version>1.5.0</version>\r
+            <scope>test</scope>\r
+        </dependency>\r
+        <dependency>\r
             <groupId>org.mockito</groupId>\r
             <artifactId>mockito-all</artifactId>\r
             <version>1.9.5</version>\r
index 1ca8c5a..0a27d04 100644 (file)
@@ -19,6 +19,7 @@
 package org.apache.sling.models.impl;
 
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentNavigableMap;
@@ -61,8 +62,28 @@ final class AdapterImplementations {
     public void setStaticInjectAnnotationProcessorFactories(
             Collection<StaticInjectAnnotationProcessorFactory> factories) {
         this.sortedStaticInjectAnnotationProcessorFactories = factories.toArray(new StaticInjectAnnotationProcessorFactory[factories.size()]);
+        updateProcessorFactoriesInModelClasses();
     }
-
+    
+    /**
+     * Updates all {@link ModelClass} instances with updates list of static inject annotation processor factories.
+     */
+    private void updateProcessorFactoriesInModelClasses() {
+        Iterator<ModelClass<?>> items = modelClasses.values().iterator();
+        updateProcessorFactoriesInModelClasses(items);        
+        Iterator<ConcurrentNavigableMap<String,ModelClass<?>>> mapItems = adapterImplementations.values().iterator();
+        while (mapItems.hasNext()) {
+            ConcurrentNavigableMap<String,ModelClass<?>> mapItem = mapItems.next();
+            updateProcessorFactoriesInModelClasses(mapItem.values().iterator());
+        }
+    }
+    private void updateProcessorFactoriesInModelClasses(Iterator<ModelClass<?>> items) {
+        while (items.hasNext()) {
+            ModelClass<?> item = items.next();
+            item.updateProcessorFactories(sortedStaticInjectAnnotationProcessorFactories);
+        }
+    }
+    
     /**
      * Add implementation mapping for the given adapter type.
      * @param adapterType Adapter type
index b7e898c..ece0af8 100644 (file)
@@ -33,19 +33,27 @@ public class ModelClass<ModelType> {
 
     private final Class<ModelType> type;
     private final Model modelAnnotation;
-    private final ModelClassConstructor[] constructors;
-    private final InjectableField[] injectableFields;
-    private final InjectableMethod[] injectableMethods;
+    final DefaultInjectionStrategy defaultInjectionStrategy;
+    private volatile ModelClassConstructor[] constructors;
+    private volatile InjectableField[] injectableFields;
+    private volatile InjectableMethod[] injectableMethods;
 
     public ModelClass(Class<ModelType> type, StaticInjectAnnotationProcessorFactory[] processorFactories) {
         this.type = type;
         this.modelAnnotation = type.getAnnotation(Model.class);
-        final DefaultInjectionStrategy defaultInjectionStrategy;
         if (modelAnnotation == null) {
             defaultInjectionStrategy = DefaultInjectionStrategy.REQUIRED;
         } else {
             defaultInjectionStrategy = modelAnnotation.defaultInjectionStrategy();
         }
+        updateProcessorFactories(processorFactories);
+    }
+    
+    /**
+     * Updates processor factories after the model class was instantiated.
+     * @param processorFactories Static injector annotation processor factories
+     */
+    public void updateProcessorFactories(StaticInjectAnnotationProcessorFactory[] processorFactories) {
         this.constructors = getConstructors(type, processorFactories, defaultInjectionStrategy);
         this.injectableFields = getInjectableFields(type, processorFactories, defaultInjectionStrategy);
         this.injectableMethods = getInjectableMethods(type, processorFactories, defaultInjectionStrategy);
diff --git a/src/test/java/org/apache/sling/models/impl/StaticInjectionAPFLoadOrderTest.java b/src/test/java/org/apache/sling/models/impl/StaticInjectionAPFLoadOrderTest.java
new file mode 100644 (file)
index 0000000..70c7c27
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * 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.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
+import org.apache.sling.models.annotations.injectorspecific.SlingObject;
+import org.apache.sling.models.impl.injectors.SlingObjectInjector;
+import org.apache.sling.models.testutil.ModelAdapterFactoryUtil;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/**
+ * Test load order behavior of StaticInjectionAnnotationProcesssorFactory instances (SLING-5010).
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class StaticInjectionAPFLoadOrderTest {
+    
+    @Rule
+    public OsgiContext context = new OsgiContext();
+    
+    @Mock
+    private SlingHttpServletRequest request;
+    @Mock
+    private ResourceResolver resourceResolver;
+    
+    private ModelAdapterFactory factory;
+    
+    @Before
+    public void setUp() {
+        registerModelAdapterFactory();
+    }
+    
+    /**
+     * Registration order: 1. ModelFactory, 2. custom injector, 3. model
+     */
+    @Test
+    public void testFactory_Injector_Model() {
+        when(request.getResourceResolver()).thenReturn(null);
+
+        registerCustomInjector();
+        registerModel();
+        
+        // this should not throw an exception because resourceResovler is marked as optional
+        assertFalse(createModel().hasResourceResolver());
+    }
+    
+    /**
+     * Registration order: 1. ModelFactory, 2. custom injector, 3. model
+     */
+    @Test
+    public void testFactory_Injector_Model_WithResourceResolver() {
+        when(request.getResourceResolver()).thenReturn(resourceResolver);
+        
+        registerCustomInjector();
+        registerModel();
+        
+        assertTrue(createModel().hasResourceResolver());
+    }
+    
+    /**
+     * Registration order: 1. ModelFactory, 2. model, 3. custom injector
+     */
+    @Test
+    public void testFactory_Model_Injector() {
+        when(request.getResourceResolver()).thenReturn(null);
+
+        registerModel();
+        registerCustomInjector();
+        
+        // this should not throw an exception because resourceResovler is marked as optional
+        assertFalse(createModel().hasResourceResolver());
+    }
+    
+    /**
+     * Registration order: 1. ModelFactory, 2. model, 3. custom injector
+     */
+    @Test
+    public void testFactory_Model_Injector_WithResourceResolver() {
+        when(request.getResourceResolver()).thenReturn(resourceResolver);
+        
+        registerModel();
+        registerCustomInjector();
+        
+        assertTrue(createModel().hasResourceResolver());
+    }
+    
+    private void registerModelAdapterFactory() {
+        factory = context.registerInjectActivateService(new ModelAdapterFactory());
+    }
+
+    private void registerCustomInjector() {
+        context.registerInjectActivateService(new SlingObjectInjector());
+    }
+
+    private void registerModel() {
+        ModelAdapterFactoryUtil.addModelsForPackage(context.bundleContext(), TestModel.class);
+    }
+
+    private TestModel createModel() {
+        return factory.createModel(request, TestModel.class);
+    }
+    
+    
+    @Model(adaptables = SlingHttpServletRequest.class)
+    public static class TestModel {
+        
+        @SlingObject(injectionStrategy = InjectionStrategy.OPTIONAL)
+        private ResourceResolver resourceResolver;
+        
+        public boolean hasResourceResolver() {
+            return resourceResolver != null;
+        }
+        
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/models/testutil/ModelAdapterFactoryUtil.java b/src/test/java/org/apache/sling/models/testutil/ModelAdapterFactoryUtil.java
new file mode 100644 (file)
index 0000000..cca5baa
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * 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.testutil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Vector;
+
+import org.apache.sling.testing.mock.osgi.MockOsgi;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Helper methods for simulating sling models bundle events
+ * for registering sling model classes in ModelAdapterFactory.
+ */
+public final class ModelAdapterFactoryUtil {
+    
+    private ModelAdapterFactoryUtil() {
+        // static methods only
+    }
+
+    /**
+     * Scan classpaths for given package name (and sub packages) to scan for and
+     * register all classes with @Model annotation.
+     * @param packageName Java package name
+     */
+    public static void addModelsForPackage(BundleContext bundleContext, Class... classes) {
+        Bundle bundle = new ModelsPackageBundle(classes, Bundle.ACTIVE, bundleContext);
+        BundleEvent event = new BundleEvent(BundleEvent.STARTED, bundle);
+        MockOsgi.sendBundleEvent(bundleContext, event);
+    }
+
+    private static class ModelsPackageBundle implements Bundle {
+
+        private final Class[] classes;
+        private final int state;
+        private final BundleContext bundleContext;
+
+        public ModelsPackageBundle(Class[] classes, int state, BundleContext bundleContext) {
+            this.classes = classes;
+            this.state = state;
+            this.bundleContext = bundleContext;
+        }
+
+        @Override
+        public int getState() {
+            return this.state;
+        }
+
+        @Override
+        public Dictionary getHeaders() {
+            Dictionary<String, Object> headers = new Hashtable<String, Object>();
+            headers.put("Sling-Model-Packages", "dummy.package");
+            return headers;
+        }
+
+        @Override
+        public Enumeration findEntries(String path, String filePattern, boolean recurse) {
+            Vector<URL> urls = new Vector<URL>(); // NOPMD
+            for (int i = 0; i < classes.length; i++) {
+                try {
+                    urls.add(new URL("file:/" + classes[i].getName().replace('.', '/') + ".class"));
+                }
+                catch (MalformedURLException ex) {
+                    throw new RuntimeException("Malformed URL.", ex);
+                }
+            }
+            return urls.elements();
+        }
+
+        @Override
+        public Class loadClass(String name) throws ClassNotFoundException {
+            return getClass().getClassLoader().loadClass(name);
+        }
+
+        @Override
+        public BundleContext getBundleContext() {
+            return bundleContext;
+        }
+
+        @Override
+        public void start(int options) throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public void start() throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public void stop(int options) throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public void stop() throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public void update(InputStream input) throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public void update() throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public void uninstall() throws BundleException {
+            // do nothing
+        }
+
+        @Override
+        public long getBundleId() {
+            return 0;
+        }
+
+        @Override
+        public String getLocation() {
+            return null;
+        }
+
+        @Override
+        public ServiceReference[] getRegisteredServices() { // NOPMD
+            return null;
+        }
+
+        @Override
+        public ServiceReference[] getServicesInUse() { // NOPMD
+            return null;
+        }
+
+        @Override
+        public boolean hasPermission(Object permission) {
+            return false;
+        }
+
+        @Override
+        public URL getResource(String name) {
+            return null;
+        }
+
+        @Override
+        public Dictionary getHeaders(String locale) {
+            return null;
+        }
+
+        @Override
+        public String getSymbolicName() {
+            return null;
+        }
+
+        @Override
+        public Enumeration getResources(String name) throws IOException {
+            return null;
+        }
+
+        @Override
+        public Enumeration getEntryPaths(String path) {
+            return null;
+        }
+
+        @Override
+        public URL getEntry(String path) {
+            return null;
+        }
+
+        @Override
+        public long getLastModified() {
+            return 0;
+        }
+
+    }
+
+}