You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:55:51 UTC

[sling-org-apache-sling-models-impl] 21/24: SLING-3886 - adding support for adapter indirection where the adapting target is a superclass or implemented interface of the implementation class. Thanks to Stefan for the patch!

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.models.impl-1.1.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-models-impl.git

commit d3879008bbd614fb565821121cb7ae196dc36b2a
Author: Justin Edelson <ju...@apache.org>
AuthorDate: Fri Aug 29 18:53:41 2014 +0000

    SLING-3886 - adding support for adapter indirection where the adapting target is a superclass or implemented interface of the implementation class. Thanks to Stefan for the patch!
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/models/impl@1621361 13f79535-47bb-0310-9956-ffa450edef68
---
 .../sling/models/impl/AdapterImplementations.java  | 120 ++++++++++
 .../models/impl/FirstImplementationPicker.java     |  45 ++++
 .../sling/models/impl/ModelAdapterFactory.java     |  38 +++-
 .../models/impl/ModelConfigurationPrinter.java     |  22 +-
 .../models/impl/ModelPackageBundleListener.java    |  97 ++++++--
 .../models/impl/AdapterImplementationsTest.java    | 125 ++++++++++
 .../sling/models/impl/ImplementsExtendsTest.java   | 253 +++++++++++++++++++++
 .../implpicker/FirstImplementationPickerTest.java  |  52 +++++
 .../implextend/EvenSimplerPropertyModel.java       |  14 ++
 .../implextend/ExtendsClassPropertyModel.java      |  34 +++
 .../ImplementsInterfacePropertyModel.java          |  55 +++++
 .../ImplementsInterfacePropertyModel2.java         |  54 +++++
 .../InvalidImplementsInterfacePropertyModel.java   |  54 +++++
 .../implextend/InvalidSampleServiceInterface.java  |  31 +++
 .../classes/implextend/SampleServiceInterface.java |  31 +++
 .../classes/implextend/SimplePropertyModel.java    |  47 ++++
 16 files changed, 1048 insertions(+), 24 deletions(-)

diff --git a/src/main/java/org/apache/sling/models/impl/AdapterImplementations.java b/src/main/java/org/apache/sling/models/impl/AdapterImplementations.java
new file mode 100644
index 0000000..2ee3a91
--- /dev/null
+++ b/src/main/java/org/apache/sling/models/impl/AdapterImplementations.java
@@ -0,0 +1,120 @@
+/*
+ * 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 java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import org.apache.sling.models.spi.ImplementationPicker;
+
+/**
+ * Collects alternative adapter implementations that may be defined in a @Model.adapters attribute.
+ * If multiple models implement the same adapter they are all collected and can be chose via a ImplementationPicker.
+ * The implementation is thread-safe.
+ */
+final class AdapterImplementations {
+    
+    private final ConcurrentMap<String,ConcurrentNavigableMap<String,Class<?>>> adapterImplementations
+            = new ConcurrentHashMap<String,ConcurrentNavigableMap<String,Class<?>>>();
+
+    private volatile ImplementationPicker[] sortedImplementationPickers = new ImplementationPicker[0];
+
+    public void setImplementationPickers(Collection<ImplementationPicker> implementationPickers) {
+        this.sortedImplementationPickers = implementationPickers.toArray(new ImplementationPicker[implementationPickers.size()]);
+    }
+
+    public ImplementationPicker[] getImplementationPickers() {
+        return this.sortedImplementationPickers;
+    }
+
+    /**
+     * Add implementation mapping for the given adapter type.
+     * @param adapterType Adapter type
+     * @param implType Implementation type
+     */
+    public void add(Class<?> adapterType, Class<?> implType) {
+        // although we already use a ConcurrentMap synchronize explicitly because we apply non-atomic operations on it
+        synchronized (adapterImplementations) {
+            String key = adapterType.getName();
+            ConcurrentNavigableMap<String,Class<?>> implementations = adapterImplementations.get(key);
+            if (implementations == null) {
+                // to have a consistent ordering independent of bundle loading use a ConcurrentSkipListMap that sorts by class name
+                implementations = new ConcurrentSkipListMap<String,Class<?>>();
+                adapterImplementations.put(key, implementations);
+            }
+            implementations.put(implType.getName(), implType);
+        }
+    }
+    
+    /**
+     * Remove implementation mapping for the given adapter type.
+     * @param adapterTypeName Adapter type name
+     * @param implTypeName Implementation type name
+     */
+    public void remove(String adapterTypeName, String implTypeName) {
+        // although we already use a ConcurrentMap synchronize explicitly because we apply non-atomic operations on it
+        synchronized (adapterImplementations) {
+            String key = adapterTypeName;
+            ConcurrentNavigableMap<String,Class<?>> implementations = adapterImplementations.get(key);
+            if (implementations!=null) {
+                implementations.remove(implTypeName);
+                if (implementations.isEmpty()) {
+                    adapterImplementations.remove(key);
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove all implementation mappings.
+     */
+    public void removeAll() {
+        adapterImplementations.clear();
+    }
+
+    /**
+     * Lookup the best-matching implementation for the given adapter type by enquiring the {@link ImplementationPicker} services.
+     * @param adapterType Adapter type
+     * @param adaptable Adaptable for reference
+     * @return Implementation type or null if none detected
+     */
+    public Class<?> lookup(Class<?> adapterType, Object adaptable) {
+        String key = adapterType.getName();
+
+        ConcurrentNavigableMap<String,Class<?>> implementations = adapterImplementations.get(key);
+        if (implementations==null || implementations.isEmpty()) {
+            return null;
+        }
+        Collection<Class<?>> implementationsCollection = implementations.values();
+        Class<?>[] implementationsArray = implementationsCollection.toArray(new Class<?>[implementationsCollection.size()]);
+
+        for (ImplementationPicker picker : this.sortedImplementationPickers) {
+            Class<?> implementation = picker.pick(adapterType, implementationsArray, adaptable);
+            if (implementation != null) {
+                return implementation;
+            }
+        }
+
+        return null;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/models/impl/FirstImplementationPicker.java b/src/main/java/org/apache/sling/models/impl/FirstImplementationPicker.java
new file mode 100644
index 0000000..230ba2c
--- /dev/null
+++ b/src/main/java/org/apache/sling/models/impl/FirstImplementationPicker.java
@@ -0,0 +1,45 @@
+/*
+ * 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.models.spi.ImplementationPicker;
+import org.osgi.framework.Constants;
+
+/**
+ * Picks first implementation.
+ * This is the default implementation which is not very intelligent because it just picks
+ * the first implementation from the list that is alphabetically ordered by class name.
+ * But at least it gives a consistent behavior.
+ * It's service ranking is set to the highest value to allow more intelligent implementations to step in.
+ */
+@Component
+@Service
+@Property(name = Constants.SERVICE_RANKING, intValue = Integer.MAX_VALUE)
+public class FirstImplementationPicker implements ImplementationPicker {
+
+    @Override
+    public Class<?> pick(Class<?> adapterType, Class<?>[] implementationsTypes, Object adaptable) {
+        // implementations is never null or empty
+        return implementationsTypes[0];
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java b/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
index a749875..41b2c87 100644
--- a/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
+++ b/src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
@@ -69,9 +69,10 @@ import org.apache.sling.models.annotations.Optional;
 import org.apache.sling.models.annotations.Required;
 import org.apache.sling.models.annotations.Source;
 import org.apache.sling.models.annotations.Via;
+import org.apache.sling.models.spi.AcceptsNullName;
 import org.apache.sling.models.spi.DisposalCallback;
 import org.apache.sling.models.spi.DisposalCallbackRegistry;
-import org.apache.sling.models.spi.AcceptsNullName;
+import org.apache.sling.models.spi.ImplementationPicker;
 import org.apache.sling.models.spi.Injector;
 import org.apache.sling.models.spi.injectorspecific.InjectAnnotation;
 import org.apache.sling.models.spi.injectorspecific.InjectAnnotationProcessor;
@@ -141,7 +142,13 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable {
 
     private volatile InjectAnnotationProcessorFactory[] sortedInjectAnnotationProcessorFactories = new InjectAnnotationProcessorFactory[0];
 
-    private ModelPackageBundleListener listener;
+    @Reference(name = "implementationPicker", referenceInterface = ImplementationPicker.class,
+            cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+    private final Map<Object, ImplementationPicker> implementationPickers = new TreeMap<Object, ImplementationPicker>();
+
+    ModelPackageBundleListener listener;
+    
+    final AdapterImplementations adapterImplementations = new AdapterImplementations();
 
     private ServiceRegistration jobRegistration;
 
@@ -160,6 +167,12 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable {
         };
         threadInvocationCounter.increase();
         try {
+            // check if a different implementation class was registered for this adapter type
+            Class<?> implementationType = this.adapterImplementations.lookup(type, adaptable);
+            if (implementationType != null) {
+                type = (Class<AdapterType>) implementationType;
+            }
+
             Model modelAnnotation = type.getAnnotation(Model.class);
             if (modelAnnotation == null) {
                 return null;
@@ -908,7 +921,7 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable {
 
         this.jobRegistration = bundleContext.registerService(Runnable.class.getName(), this, properties);
 
-        this.listener = new ModelPackageBundleListener(ctx.getBundleContext(), this);
+        this.listener = new ModelPackageBundleListener(ctx.getBundleContext(), this, this.adapterImplementations);
 
         Hashtable<Object, Object> printerProps = new Hashtable<Object, Object>();
         printerProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
@@ -924,6 +937,7 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable {
     @Deactivate
     protected void deactivate() {
         this.listener.unregisterAll();
+        this.adapterImplementations.removeAll();
         if (jobRegistration != null) {
             jobRegistration.unregister();
             jobRegistration = null;
@@ -962,6 +976,20 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable {
         }
     }
 
+    protected void bindImplementationPicker(final ImplementationPicker implementationPicker, final Map<String, Object> props) {
+        synchronized (implementationPickers) {
+            implementationPickers.put(ServiceUtil.getComparableForServiceRanking(props), implementationPicker);
+            this.adapterImplementations.setImplementationPickers(implementationPickers.values());
+        }
+    }
+
+    protected void unbindImplementationPicker(final ImplementationPicker implementationPicker, final Map<String, Object> props) {
+        synchronized (implementationPickers) {
+            implementationPickers.remove(ServiceUtil.getComparableForServiceRanking(props));
+            this.adapterImplementations.setImplementationPickers(implementationPickers.values());
+        }
+    }
+
     Injector[] getInjectors() {
         return sortedInjectors;
     }
@@ -970,4 +998,8 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable {
         return sortedInjectAnnotationProcessorFactories;
     }
 
+    ImplementationPicker[] getImplementationPickers() {
+        return adapterImplementations.getImplementationPickers();
+    }
+
 }
diff --git a/src/main/java/org/apache/sling/models/impl/ModelConfigurationPrinter.java b/src/main/java/org/apache/sling/models/impl/ModelConfigurationPrinter.java
index 67e754b..97ef951 100644
--- a/src/main/java/org/apache/sling/models/impl/ModelConfigurationPrinter.java
+++ b/src/main/java/org/apache/sling/models/impl/ModelConfigurationPrinter.java
@@ -18,6 +18,7 @@ package org.apache.sling.models.impl;
 
 import java.io.PrintWriter;
 
+import org.apache.sling.models.spi.ImplementationPicker;
 import org.apache.sling.models.spi.Injector;
 import org.apache.sling.models.spi.injectorspecific.InjectAnnotationProcessorFactory;
 
@@ -30,9 +31,11 @@ public class ModelConfigurationPrinter {
     }
 
     public void printConfiguration(PrintWriter printWriter) {
+        
+        // injectors
         printWriter.println("Sling Models Injectors:");
         Injector[] injectors = modelAdapterFactory.getInjectors();
-        if (injectors == null) {
+        if (injectors == null || injectors.length == 0) {
             printWriter.println("none");
         } else {
             for (Injector injector : injectors) {
@@ -41,9 +44,11 @@ public class ModelConfigurationPrinter {
             }
         }
         printWriter.println();
+        
+        // inject annotations processor factories
         printWriter.println("Sling Models Inject Annotation Processor Factories:");
         InjectAnnotationProcessorFactory[] factories = modelAdapterFactory.getInjectAnnotationProcessorFactories();
-        if (factories == null) {
+        if (factories == null || factories.length == 0) {
             printWriter.println("none");
         } else {
             for (InjectAnnotationProcessorFactory factory : factories) {
@@ -51,6 +56,19 @@ public class ModelConfigurationPrinter {
                 printWriter.println();
             }
         }
+        printWriter.println();
+        
+        // implementation pickers
+        printWriter.println("Sling Models Implementation Pickers:");
+        ImplementationPicker[] pickers = modelAdapterFactory.getImplementationPickers();
+        if (pickers == null || pickers.length == 0) {
+            printWriter.println("none");
+        } else {
+            for (ImplementationPicker picker : pickers) {
+                printWriter.printf("%s", picker.getClass().getName());
+                printWriter.println();
+            }
+        }
     }
 
 }
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/models/impl/ModelPackageBundleListener.java b/src/main/java/org/apache/sling/models/impl/ModelPackageBundleListener.java
index 9e9c908..f249fb8 100644
--- a/src/main/java/org/apache/sling/models/impl/ModelPackageBundleListener.java
+++ b/src/main/java/org/apache/sling/models/impl/ModelPackageBundleListener.java
@@ -30,6 +30,7 @@ import org.apache.sling.models.annotations.Model;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleEvent;
+import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.util.tracker.BundleTracker;
 import org.osgi.util.tracker.BundleTrackerCustomizer;
@@ -38,24 +39,34 @@ import org.slf4j.LoggerFactory;
 
 public class ModelPackageBundleListener implements BundleTrackerCustomizer {
 
-    private static final String HEADER = "Sling-Model-Packages";
-
+    static final String HEADER = "Sling-Model-Packages";
+    
     /**
      * Service registration property for the adapter condition.
      */
     private static final String PROP_ADAPTER_CONDITION = "adapter.condition";
 
+    /**
+     * The model implementation class that initiated the service registration.
+     */
+    private static final String PROP_IMPLEMENTATION_CLASS = "models.adapter.implementationClass";
+
     private static final Logger log = LoggerFactory.getLogger(ModelPackageBundleListener.class);
-    
+
     private final BundleContext bundleContext;
 
     private final BundleTracker bundleTracker;
 
     private final AdapterFactory factory;
     
-    public ModelPackageBundleListener(BundleContext bundleContext, AdapterFactory factory) {
+    private final AdapterImplementations adapterImplementations;
+    
+    public ModelPackageBundleListener(BundleContext bundleContext,
+            AdapterFactory factory,
+            AdapterImplementations adapterImplementations) {
         this.bundleContext = bundleContext;
         this.factory = factory;
+        this.adapterImplementations = adapterImplementations;
         this.bundleTracker = new BundleTracker(bundleContext, Bundle.ACTIVE, this);
         this.bundleTracker.open();
     }
@@ -84,22 +95,25 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
                     URL url = classUrls.nextElement();
                     String className = toClassName(url);
                     try {
-                        Class<?> clazz = bundle.loadClass(className);
-                        Model annotation = clazz.getAnnotation(Model.class);
+                        Class<?> implType = bundle.loadClass(className);
+                        Model annotation = implType.getAnnotation(Model.class);
                         if (annotation != null) {
-                            Class<?>[] adaptables = annotation.adaptables();
-                            String[] classNames = toStringArray(adaptables);
-                            Dictionary<String, Object> registrationProps = new Hashtable<String, Object>();
-                            registrationProps.put(AdapterFactory.ADAPTER_CLASSES, className);
-                            registrationProps.put(AdapterFactory.ADAPTABLE_CLASSES, classNames);
-
-                            String condition = annotation.condition();
-                            if (StringUtils.isNotBlank(condition)) {
-                                registrationProps.put(PROP_ADAPTER_CONDITION, condition);
+                            
+                            // get list of adapters from annotation - if not given use annotated class itself
+                            Class<?>[] adapterTypes = annotation.adapters();
+                            if (adapterTypes.length == 0) {
+                                adapterTypes = new Class<?>[] { implType };
+                            }
+                            // register adapter only if given adapters are valid
+                            if (validateAdapterClasses(implType, adapterTypes)) {
+                                for (Class<?> adapterType : adapterTypes) {
+                                    if (adapterType != implType) {
+                                        adapterImplementations.add(adapterType, implType);
+                                    }
+                                }
+                                ServiceRegistration reg = registerAdapterFactory(adapterTypes, annotation.adaptables(), implType, annotation.condition());
+                                regs.add(reg);
                             }
-                            ServiceRegistration reg = bundleContext.registerService(AdapterFactory.SERVICE_NAME,
-                                    factory, registrationProps);
-                            regs.add(reg);
                         }
                     } catch (ClassNotFoundException e) {
                         log.warn("Unable to load class", e);
@@ -119,6 +133,12 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
     public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
         if (object instanceof ServiceRegistration[]) {
             for (ServiceRegistration reg : (ServiceRegistration[]) object) {
+                ServiceReference ref = reg.getReference();
+                String[] adapterTypeNames = PropertiesUtil.toStringArray(ref.getProperty(AdapterFactory.ADAPTER_CLASSES));
+                String implTypeName = PropertiesUtil.toString(ref.getProperty(PROP_IMPLEMENTATION_CLASS), null);
+                for (String adapterTypeName : adapterTypeNames) {
+                    adapterImplementations.remove(adapterTypeName, implTypeName);
+                }
                 reg.unregister();
             }
         }
@@ -142,5 +162,44 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
         }
         return arr;
     }
-
+    
+    /**
+     * Validate list of adapter classes. Make sure all given are either the annotated class itself,
+     * or an interface or superclass of it.
+     * A warning is written if this it not the case, and false is returned.
+     * @param clazz Annotated class
+     * @param adapterClasses Adapter classes
+     * @return true if validation was successful
+     */
+    private boolean validateAdapterClasses(Class<?> clazz, Class<?>[] adapterClasses) {
+        for (Class<?> adapterClass : adapterClasses) {
+            if (!adapterClass.isAssignableFrom(clazz)) {
+                log.warn("Unable to register model class {} because adapter class {} is not valid.",
+                        clazz.getName(), adapterClass.getName());
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * Registers an adapter factory for a annotated sling models class.
+     * @param adapterTypes Adapter (either the class itself, or interface or superclass of it)
+     * @param adaptableTypes Classes to adapt from
+     * @param implType Type of the implementation class
+     * @param condition Condition (optional)
+     * @return Service registration
+     */
+    private ServiceRegistration registerAdapterFactory(Class<?>[] adapterTypes, Class<?>[] adaptableTypes, Class<?> implType, String condition) {
+        Dictionary<String, Object> registrationProps = new Hashtable<String, Object>();
+        registrationProps.put(AdapterFactory.ADAPTER_CLASSES, toStringArray(adapterTypes));
+        registrationProps.put(AdapterFactory.ADAPTABLE_CLASSES, toStringArray(adaptableTypes));
+        registrationProps.put(PROP_IMPLEMENTATION_CLASS, implType.getName());
+
+        if (StringUtils.isNotBlank(condition)) {
+            registrationProps.put(PROP_ADAPTER_CONDITION, condition);
+        }
+        return bundleContext.registerService(AdapterFactory.SERVICE_NAME, factory, registrationProps);
+    }
+    
 }
diff --git a/src/test/java/org/apache/sling/models/impl/AdapterImplementationsTest.java b/src/test/java/org/apache/sling/models/impl/AdapterImplementationsTest.java
new file mode 100644
index 0000000..3dfebfa
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/impl/AdapterImplementationsTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.assertNull;
+
+import java.util.Arrays;
+
+import org.apache.sling.models.spi.ImplementationPicker;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AdapterImplementationsTest {
+
+    private static final Class<?> SAMPLE_ADAPTER = Comparable.class;
+    private static final Object SAMPLE_ADAPTABLE = new Object();    
+
+    private AdapterImplementations underTest;
+    
+    @Before
+    public void setUp() {
+        underTest = new AdapterImplementations();
+        underTest.setImplementationPickers(Arrays.asList(new ImplementationPicker[] {
+            new FirstImplementationPicker()
+        }));
+    }
+    
+    @Test
+    public void testNoMapping() {
+        assertNull(underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+        
+        // make sure this raises no exception
+        underTest.remove(SAMPLE_ADAPTER.getName(), String.class.getName());
+    }
+    
+    @Test
+    public void testSingleMapping() {
+        underTest.add(SAMPLE_ADAPTER, String.class);
+        
+        assertEquals(String.class, underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+        
+        underTest.remove(SAMPLE_ADAPTER.getName(), String.class.getName());
+
+        assertNull(underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+    }
+
+    @Test
+    public void testMultipleMappings() {
+        underTest.add(SAMPLE_ADAPTER, String.class);
+        underTest.add(SAMPLE_ADAPTER, Integer.class);
+        underTest.add(SAMPLE_ADAPTER, Long.class);
+        
+        assertEquals(Integer.class, underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+        
+        underTest.remove(SAMPLE_ADAPTER.getName(), Integer.class.getName());
+
+        assertEquals(Long.class, underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+
+        underTest.remove(SAMPLE_ADAPTER.getName(), Long.class.getName());
+        underTest.remove(SAMPLE_ADAPTER.getName(), String.class.getName());
+        
+        assertNull(underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+    }
+    
+    @Test
+    public void testRemoveAll() {
+        underTest.add(SAMPLE_ADAPTER, String.class);
+        underTest.add(SAMPLE_ADAPTER, Integer.class);
+        underTest.add(SAMPLE_ADAPTER, Long.class);
+        
+        underTest.removeAll();
+        
+        assertNull(underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+    }
+    
+    @Test
+    public void testMultipleImplementationPickers() {
+        underTest.setImplementationPickers(Arrays.asList(
+            new NoneImplementationPicker(),
+            new LastImplementationPicker(),
+            new FirstImplementationPicker()
+        ));
+
+        underTest.add(SAMPLE_ADAPTER, String.class);
+        underTest.add(SAMPLE_ADAPTER, Integer.class);
+        underTest.add(SAMPLE_ADAPTER, Long.class);
+        
+        assertEquals(String.class, underTest.lookup(SAMPLE_ADAPTER, SAMPLE_ADAPTABLE));
+    }
+    
+    static final class NoneImplementationPicker implements ImplementationPicker {
+        @Override
+        public Class<?> pick(Class<?> adapterType, Class<?>[] implementationsTypes, Object adaptable) {
+            return null;
+        }        
+    }
+    
+    static final class LastImplementationPicker implements ImplementationPicker {
+        @Override
+        public Class<?> pick(Class<?> adapterType, Class<?>[] implementationsTypes, Object adaptable) {
+            return implementationsTypes[implementationsTypes.length - 1];
+        }        
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/models/impl/ImplementsExtendsTest.java b/src/test/java/org/apache/sling/models/impl/ImplementsExtendsTest.java
new file mode 100644
index 0000000..afea6b0
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/impl/ImplementsExtendsTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Vector;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.models.impl.injectors.ValueMapInjector;
+import org.apache.sling.models.spi.ImplementationPicker;
+import org.apache.sling.models.testmodels.classes.implextend.EvenSimplerPropertyModel;
+import org.apache.sling.models.testmodels.classes.implextend.ExtendsClassPropertyModel;
+import org.apache.sling.models.testmodels.classes.implextend.ImplementsInterfacePropertyModel;
+import org.apache.sling.models.testmodels.classes.implextend.ImplementsInterfacePropertyModel2;
+import org.apache.sling.models.testmodels.classes.implextend.InvalidImplementsInterfacePropertyModel;
+import org.apache.sling.models.testmodels.classes.implextend.InvalidSampleServiceInterface;
+import org.apache.sling.models.testmodels.classes.implextend.SampleServiceInterface;
+import org.apache.sling.models.testmodels.classes.implextend.SimplePropertyModel;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ImplementsExtendsTest {
+
+    @Mock
+    private ComponentContext componentCtx;
+
+    @Mock
+    private BundleContext bundleContext;
+
+    @Mock
+    private Bundle bundle;
+
+    @Mock
+    private BundleEvent bundleEvent;
+
+    private ModelAdapterFactory factory;
+
+    private ServiceRegistration[] registeredAdapterFactories;
+
+    private ImplementationPicker firstImplementationPicker = new FirstImplementationPicker();
+
+    private ServicePropertiesMap firstImplementationPickerProps = new ServicePropertiesMap(3, Integer.MAX_VALUE);
+
+    @SuppressWarnings("unchecked")
+    @Before
+    public void setup() throws ClassNotFoundException, MalformedURLException {
+        when(componentCtx.getBundleContext()).thenReturn(bundleContext);
+        when(componentCtx.getProperties()).thenReturn(new Hashtable<String, Object>());
+        when(bundleContext.registerService(anyString(), anyObject(), any(Dictionary.class))).then(new Answer<ServiceRegistration>() {
+            @Override
+            public ServiceRegistration answer(InvocationOnMock invocation) throws Throwable {
+                final Dictionary<String, Object> props = (Dictionary<String, Object>)invocation.getArguments()[2];
+                ServiceRegistration reg = mock(ServiceRegistration.class);
+                ServiceReference ref = mock(ServiceReference.class);
+                when(reg.getReference()).thenReturn(ref);
+                when(ref.getProperty(anyString())).thenAnswer(new Answer<Object>() {
+                    @Override
+                    public Object answer(InvocationOnMock invocation) throws Throwable {
+                        String key = (String)invocation.getArguments()[0];
+                        return props.get(key);
+                    }
+                });
+                return reg;
+            }
+        });
+
+        factory = new ModelAdapterFactory();
+        factory.activate(componentCtx);
+        factory.bindInjector(new ValueMapInjector(), new ServicePropertiesMap(2, 2));
+        factory.bindImplementationPicker(firstImplementationPicker, firstImplementationPickerProps);
+
+        // simulate bundle add for ModelPackageBundleListener
+        Dictionary<String, String> headers = new Hashtable<String,String>();
+        headers.put(ModelPackageBundleListener.HEADER, "org.apache.sling.models.testmodels.classes.implextend");
+        when(bundle.getHeaders()).thenReturn(headers);
+
+        Vector<URL> classUrls = new Vector<URL>();
+        classUrls.add(getClassUrl(ExtendsClassPropertyModel.class));
+        classUrls.add(getClassUrl(ImplementsInterfacePropertyModel.class));
+        classUrls.add(getClassUrl(ImplementsInterfacePropertyModel2.class));
+        classUrls.add(getClassUrl(InvalidImplementsInterfacePropertyModel.class));
+        classUrls.add(getClassUrl(InvalidSampleServiceInterface.class));
+        classUrls.add(getClassUrl(SampleServiceInterface.class));
+        classUrls.add(getClassUrl(SimplePropertyModel.class));
+        when(bundle.findEntries(anyString(), anyString(), anyBoolean())).thenReturn(classUrls.elements());
+
+        when(bundle.loadClass(anyString())).then(new Answer<Class<?>>() {
+            @Override
+            public Class<?> answer(InvocationOnMock invocation) throws ClassNotFoundException {
+                String className = (String)invocation.getArguments()[0];
+                return ImplementsExtendsTest.this.getClass().getClassLoader().loadClass(className);
+            }
+        });
+
+        registeredAdapterFactories = (ServiceRegistration[])factory.listener.addingBundle(bundle, bundleEvent);
+    }
+
+    private URL getClassUrl(Class<?> clazz) throws MalformedURLException {
+        String path = "file:/" + clazz.getName().replace('.', '/') + ".class";
+        return new URL(path);
+    }
+
+    @After
+    public void tearDown() {
+        // simulate bundle remove for ModelPackageBundleListener
+        factory.listener.removedBundle(bundle, bundleEvent, registeredAdapterFactories);
+        
+        // make sure adaption is not longer possible: implementation class mapping is removed
+        Resource res = getMockResourceWithProps();
+        SampleServiceInterface model = factory.getAdapter(res, SampleServiceInterface.class);
+        assertNull(model);
+    }
+
+    /**
+     * Try to adapt to interface, with an different implementation class that has the @Model annotation
+     */
+    @Test
+    public void testImplementsInterfaceModel() {
+        Resource res = getMockResourceWithProps();
+        SampleServiceInterface model = factory.getAdapter(res, SampleServiceInterface.class);
+        assertNotNull(model);
+        assertEquals(ImplementsInterfacePropertyModel.class, model.getClass());
+        assertEquals("first-value|null|third-value", model.getAllProperties());
+    }
+
+    /**
+     * Try to adapt in a case where there is no picker available.
+     * This causes the extend adaptation to fail, but the case where the
+     * class is the adapter still works.
+     */
+    @Test
+    public void testImplementsNoPicker() {
+        factory.unbindImplementationPicker(firstImplementationPicker, firstImplementationPickerProps);
+
+        Resource res = getMockResourceWithProps();
+        SampleServiceInterface model = factory.getAdapter(res, SampleServiceInterface.class);
+        assertNull(model);
+
+        model = factory.getAdapter(res, ImplementsInterfacePropertyModel.class);
+        assertNotNull(model);
+        assertEquals("first-value|null|third-value", model.getAllProperties());
+    }
+
+    /**
+     * Ensure that the implementation class itself cannot be adapted to if it is not part of the "adapter" property in the annotation.
+     */
+    /*
+    -- disabled because this cannot work in unit test where the adapterFactory is called directly
+    -- it is enabled in integration tests
+    @Test
+    public void testImplementsInterfaceModel_ImplClassNotMapped() {
+        Resource res = getMockResourceWithProps();
+        ImplementsInterfacePropertyModel model = factory.getAdapter(res, ImplementsInterfacePropertyModel.class);
+        assertNull(model);
+    }
+    */
+
+    /**
+     * Test implementation class with a mapping that is not valid (an interface that is not implemented).
+     */
+    @Test
+    public void testInvalidImplementsInterfaceModel() {
+        Resource res = getMockResourceWithProps();
+        InvalidSampleServiceInterface model = factory.getAdapter(res, InvalidSampleServiceInterface.class);
+        assertNull(model);
+    }
+
+    /**
+     * Test to adapt to a superclass of the implementation class with the appropriate mapping in the @Model annotation.
+     */
+    @Test
+    public void testExtendsClassModel() {
+        Resource res = getMockResourceWithProps();
+
+        SimplePropertyModel model = factory.getAdapter(res, SimplePropertyModel.class);
+        assertNotNull(model);
+        assertEquals("!first-value|null|third-value!", model.getAllProperties());
+
+        EvenSimplerPropertyModel simplerModel = factory.getAdapter(res, EvenSimplerPropertyModel.class);
+        assertNotNull(simplerModel);
+        assertEquals("first-value", model.getFirst());
+    }
+
+    /**
+     * Try to adapt to interface, with an different implementation class that has the @Model annotation
+     */
+    @Test
+    public void testImplementsInterfaceModelWithPickLastImplementationPicker() {
+        factory.bindImplementationPicker(new AdapterImplementationsTest.LastImplementationPicker(), new ServicePropertiesMap(3, 1));
+
+        Resource res = getMockResourceWithProps();
+        SampleServiceInterface model = factory.getAdapter(res, SampleServiceInterface.class);
+        assertNotNull(model);
+        assertEquals(ImplementsInterfacePropertyModel2.class, model.getClass());
+        assertEquals("first-value|null|third-value", model.getAllProperties());
+    }
+
+    private Resource getMockResourceWithProps() {
+        Map<String, Object> map = new HashMap<String, Object>();
+        map.put("first", "first-value");
+        map.put("third", "third-value");
+        ValueMap vm = new ValueMapDecorator(map);
+
+        Resource res = mock(Resource.class);
+        when(res.adaptTo(ValueMap.class)).thenReturn(vm);
+        return res;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/models/impl/implpicker/FirstImplementationPickerTest.java b/src/test/java/org/apache/sling/models/impl/implpicker/FirstImplementationPickerTest.java
new file mode 100644
index 0000000..1bc8cfb
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/impl/implpicker/FirstImplementationPickerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.implpicker;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.sling.models.impl.FirstImplementationPicker;
+import org.apache.sling.models.spi.ImplementationPicker;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FirstImplementationPickerTest {
+
+    private static final Class<?> SAMPLE_ADAPTER = Comparable.class;
+    private static final Object SAMPLE_ADAPTABLE = new Object();
+
+    private ImplementationPicker underTest;
+
+    @Before
+    public void setUp() {
+        underTest = new FirstImplementationPicker();
+    }
+
+    @Test
+    public void testPickOneImplementation() {
+        Class<?>[] implementations = new Class<?>[] { String.class };
+        assertEquals(String.class, underTest.pick(SAMPLE_ADAPTER, implementations, SAMPLE_ADAPTABLE));
+    }
+
+    @Test
+    public void testPickMultipleImplementations() {
+        Class<?>[] implementations = new Class<?>[] { Integer.class, Long.class, String.class };
+        assertEquals(Integer.class, underTest.pick(SAMPLE_ADAPTER, implementations, SAMPLE_ADAPTABLE));
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/EvenSimplerPropertyModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/EvenSimplerPropertyModel.java
new file mode 100644
index 0000000..d1ea125
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/EvenSimplerPropertyModel.java
@@ -0,0 +1,14 @@
+package org.apache.sling.models.testmodels.classes.implextend;
+
+import javax.inject.Inject;
+
+public class EvenSimplerPropertyModel {
+
+    @Inject
+    private String first;
+
+    public String getFirst() {
+        return first;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ExtendsClassPropertyModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ExtendsClassPropertyModel.java
new file mode 100644
index 0000000..6ab6d26
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ExtendsClassPropertyModel.java
@@ -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.implextend;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+
+/**
+ * This is an example for a model that can not be adapted itself, but only 
+ * to a superclass it extends. This superclass is defined as "adapters".
+ */
+@Model(adaptables = Resource.class, adapters = { SimplePropertyModel.class, EvenSimplerPropertyModel.class })
+public class ExtendsClassPropertyModel extends SimplePropertyModel {
+
+    @Override
+    public String getAllProperties() {
+        return "!" + super.getAllProperties() + "!";
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ImplementsInterfacePropertyModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ImplementsInterfacePropertyModel.java
new file mode 100644
index 0000000..a4d5d5f
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ImplementsInterfacePropertyModel.java
@@ -0,0 +1,55 @@
+/*
+ * 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.implextend;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.Optional;
+
+/**
+ * This is an example for a model that can not be adapted itself, but only 
+ * to an interface it implements. This interfaces is defined as "adapters".
+ */
+@Model(adaptables = Resource.class, adapters = { SampleServiceInterface.class, ImplementsInterfacePropertyModel.class })
+public class ImplementsInterfacePropertyModel implements SampleServiceInterface {
+
+    @Inject
+    private String first;
+
+    @Inject
+    @Optional
+    private String second;
+
+    @Inject
+    @Named("third")
+    private String thirdProperty;
+
+    @Override
+    public String getAllProperties() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(first)
+            .append("|")
+            .append(second)
+            .append("|")
+            .append(thirdProperty);
+        return sb.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ImplementsInterfacePropertyModel2.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ImplementsInterfacePropertyModel2.java
new file mode 100644
index 0000000..a4df61c
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/ImplementsInterfacePropertyModel2.java
@@ -0,0 +1,54 @@
+/*
+ * 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.implextend;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.Optional;
+
+/**
+ * Additional model class that implements the same interface as {@link ImplementsInterfacePropertyModel}.
+ */
+@Model(adaptables = Resource.class, adapters = SampleServiceInterface.class)
+public class ImplementsInterfacePropertyModel2 implements SampleServiceInterface {
+
+    @Inject
+    private String first;
+
+    @Inject
+    @Optional
+    private String second;
+
+    @Inject
+    @Named("third")
+    private String thirdProperty;
+
+    @Override
+    public String getAllProperties() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(first)
+            .append("|")
+            .append(second)
+            .append("|")
+            .append(thirdProperty);
+        return sb.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/InvalidImplementsInterfacePropertyModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/InvalidImplementsInterfacePropertyModel.java
new file mode 100644
index 0000000..980b9b4
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/InvalidImplementsInterfacePropertyModel.java
@@ -0,0 +1,54 @@
+/*
+ * 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.implextend;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.Optional;
+
+/**
+ * This model defines an invalid adapters property containing an interface it does not implement.
+ */
+@Model(adaptables = Resource.class, adapters = InvalidSampleServiceInterface.class)
+public class InvalidImplementsInterfacePropertyModel implements SampleServiceInterface {
+
+    @Inject
+    private String first;
+
+    @Inject
+    @Optional
+    private String second;
+
+    @Inject
+    @Named("third")
+    private String thirdProperty;
+
+    @Override
+    public String getAllProperties() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(first)
+            .append("|")
+            .append(second)
+            .append("|")
+            .append(thirdProperty);
+        return sb.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/InvalidSampleServiceInterface.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/InvalidSampleServiceInterface.java
new file mode 100644
index 0000000..38ab451
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/InvalidSampleServiceInterface.java
@@ -0,0 +1,31 @@
+/*
+ * 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.implextend;
+
+/**
+ * Example "service" interface to which sling models can adapt.
+ */
+public interface InvalidSampleServiceInterface {
+
+    /**
+     * @return concanated string with all properties
+     */
+    String getAllProperties();
+    
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/SampleServiceInterface.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/SampleServiceInterface.java
new file mode 100644
index 0000000..753c802
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/SampleServiceInterface.java
@@ -0,0 +1,31 @@
+/*
+ * 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.implextend;
+
+/**
+ * Example "service" interface to which sling models can adapt.
+ */
+public interface SampleServiceInterface {
+
+    /**
+     * @return concatenated string with all properties
+     */
+    String getAllProperties();
+    
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/models/testmodels/classes/implextend/SimplePropertyModel.java b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/SimplePropertyModel.java
new file mode 100644
index 0000000..7ab8c5f
--- /dev/null
+++ b/src/test/java/org/apache/sling/models/testmodels/classes/implextend/SimplePropertyModel.java
@@ -0,0 +1,47 @@
+/*
+ * 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.implextend;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.sling.models.annotations.Optional;
+
+/**
+ * Base class without @Model annotation.
+ */
+public class SimplePropertyModel extends EvenSimplerPropertyModel {
+
+    @Inject
+    @Optional
+    private String second;
+
+    @Inject
+    @Named("third")
+    private String thirdProperty;
+
+    public String getAllProperties() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getFirst())
+            .append("|")
+            .append(second)
+            .append("|")
+            .append(thirdProperty);
+        return sb.toString();
+    }
+
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.