SLING-9680 support custom BindingsValuesProvider in a way that keeps the dynamic nature of the "OOTB" bindings depending on current resource
diff --git a/core/src/main/java/org/apache/sling/testing/mock/sling/context/MockSlingBindings.java b/core/src/main/java/org/apache/sling/testing/mock/sling/context/MockSlingBindings.java
index 0f6f464..8bcb9d8 100644
--- a/core/src/main/java/org/apache/sling/testing/mock/sling/context/MockSlingBindings.java
+++ b/core/src/main/java/org/apache/sling/testing/mock/sling/context/MockSlingBindings.java
@@ -20,28 +20,39 @@
 
 import javax.jcr.Node;
 import javax.jcr.Session;
+import javax.script.SimpleBindings;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.scripting.SlingBindings;
+import org.apache.sling.scripting.api.BindingsValuesProvider;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
 
 /**
  * Mock extension of {@link SlingBindings} that dynamically evaluates properties read from SlingBindings from the current mock context.
  * Normally the SlingBingings are set statically for each script execution, but in mocks where no script is really executed
  * it's easier to evaluate them from current context. 
  */
-class MockSlingBindings extends SlingBindings {
+class MockSlingBindings extends SlingBindings implements EventHandler {
     private static final long serialVersionUID = 1L;
 
     private static final String PROP_CURRENT_NODE = "currentNode";
     private static final String PROP_CURRENT_SESSION = "currentSession";
     
+    /**
+     * OSGi service property to set to "true" on BindingsValuesProvider implementations that should be ignored
+     * when populating the "non-dynamic" bindings properties.
+     */
+    static final String SERVICE_PROPERTY_MOCK_SLING_BINDINGS_IGNORE = "MockSlingBindings-ignore";
+    
     private final SlingContextImpl context;
 
     public MockSlingBindings(SlingContextImpl context) {
         this.context = context;
+        populateFromBindingsValuesProvider();
     }
 
     @Override
@@ -54,7 +65,27 @@
         }
         return super.get(key);
     }
-    
+
+    /**
+     * Removes all (non-dynamic) properties from bindings and populates them from all registered {@link BindingsValuesProvider} implementations.
+     */
+    private void populateFromBindingsValuesProvider() {
+        SimpleBindings bindings = new SimpleBindings();
+        for (BindingsValuesProvider provider : context.getServices(BindingsValuesProvider.class,
+                "(!(" + SERVICE_PROPERTY_MOCK_SLING_BINDINGS_IGNORE + "=true))")) {
+            provider.addBindings(bindings);
+        }
+        this.clear();
+        this.putAll(bindings);
+    }
+
+    @Override
+    public void handleEvent(Event event) {
+        // is triggered by OSGi events fired by {@link org.apache.sling.scripting.core.impl.BindingsValuesProvidersByContextImpl}
+        // whenever a new bindings value provider is added or removed
+        populateFromBindingsValuesProvider();
+    }
+
     static @Nullable Object resolveSlingBindingProperty(@NotNull SlingContextImpl context, @NotNull String property) {
 
         // -- Sling --
@@ -79,7 +110,7 @@
         if (StringUtils.equals(property, OUT)) {
             return context.response().getWriter();
         }
-        
+
         // -- JCR --
         // this emulates behavior of org.apache.sling.jcr.resource.internal.scripting.JcrObjectsBindingsValuesProvider
         if (StringUtils.equals(property, PROP_CURRENT_NODE)) {
@@ -91,7 +122,7 @@
         if (StringUtils.equals(property, PROP_CURRENT_SESSION)) {
             return context.resourceResolver().adaptTo(Session.class);
         }
-        
+
         return null;
     }
 
diff --git a/core/src/main/java/org/apache/sling/testing/mock/sling/context/SlingContextImpl.java b/core/src/main/java/org/apache/sling/testing/mock/sling/context/SlingContextImpl.java
index 7389b97..8211bda 100644
--- a/core/src/main/java/org/apache/sling/testing/mock/sling/context/SlingContextImpl.java
+++ b/core/src/main/java/org/apache/sling/testing/mock/sling/context/SlingContextImpl.java
@@ -18,6 +18,9 @@
  */
 package org.apache.sling.testing.mock.sling.context;
 
+import static org.apache.sling.testing.mock.sling.context.MockSlingBindings.SERVICE_PROPERTY_MOCK_SLING_BINDINGS_IGNORE;
+import static org.osgi.service.event.EventConstants.EVENT_TOPIC;
+
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -63,6 +66,7 @@
 import org.osgi.annotation.versioning.ConsumerType;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceReference;
+import org.osgi.service.event.EventHandler;
 
 /**
  * Defines Sling context objects with lazy initialization.
@@ -180,7 +184,8 @@
         registerService(SlingSettingsService.class, new MockSlingSettingService(DEFAULT_RUN_MODES));
         registerService(MimeTypeService.class, new MockMimeTypeService());
         registerInjectActivateService(new ResourceBuilderFactoryService());
-        registerInjectActivateService(new JcrObjectsBindingsValuesProvider());
+        registerInjectActivateService(new JcrObjectsBindingsValuesProvider(),
+                SERVICE_PROPERTY_MOCK_SLING_BINDINGS_IGNORE, true);
         registerInjectActivateService(new MockResourceBundleProvider());
         registerInjectActivateService(new MockXSSAPIImpl());
         
@@ -283,7 +288,12 @@
             this.request = new MockSlingHttpServletRequest(this.resourceResolver(), this.bundleContext());
 
             // initialize sling bindings
-            SlingBindings bindings = new MockSlingBindings(this);
+            MockSlingBindings bindings = new MockSlingBindings(this);
+            
+            // register as OSGi event handler to get notified on events fired by BindingsValuesProvidersByContextImpl
+            this.registerService(EventHandler.class, bindings,
+                    EVENT_TOPIC, "org/apache/sling/scripting/core/BindingsValuesProvider/*");
+            
             this.request.setAttribute(SlingBindings.class.getName(), bindings);
         }
         return this.request;
diff --git a/core/src/test/java/org/apache/sling/testing/mock/sling/SlingBindingsTest.java b/core/src/test/java/org/apache/sling/testing/mock/sling/SlingBindingsTest.java
deleted file mode 100644
index 9f1f823..0000000
--- a/core/src/test/java/org/apache/sling/testing/mock/sling/SlingBindingsTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.testing.mock.sling;
-
-import static org.junit.Assert.assertNotNull;
-
-import org.apache.sling.api.resource.Resource;
-import org.apache.sling.testing.mock.sling.context.models.SlingBindingsModel;
-import org.apache.sling.testing.mock.sling.junit.SlingContext;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-@SuppressWarnings("null")
-public class SlingBindingsTest {
-
-    @Rule
-    public SlingContext context = new SlingContext(ResourceResolverType.JCR_MOCK);
-
-    private Resource currentResource;
-
-    @Before
-    public void setUp() throws Exception {
-        context.addModelsForClasses(SlingBindingsModel.class);
-        currentResource = context.create().resource("/content/testPage/testResource");
-        context.currentResource(currentResource);
-    }
-
-    @Test
-    public void testBindings() {
-        SlingBindingsModel model = context.request().adaptTo(SlingBindingsModel.class);
-
-        assertNotNull(model);
-        assertNotNull(model.getResolver());
-        assertNotNull(model.getResource());
-        assertNotNull(model.getRequest());
-        assertNotNull(model.getResponse());
-        assertNotNull(model.getCurrentNode());
-        assertNotNull(model.getcurrentSession());
-    }
-
-}
diff --git a/core/src/test/java/org/apache/sling/testing/mock/sling/context/SlingBindingsTest.java b/core/src/test/java/org/apache/sling/testing/mock/sling/context/SlingBindingsTest.java
new file mode 100644
index 0000000..7a9495d
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/testing/mock/sling/context/SlingBindingsTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.testing.mock.sling.context;
+
+import static org.apache.sling.testing.mock.sling.context.MockSlingBindings.SERVICE_PROPERTY_MOCK_SLING_BINDINGS_IGNORE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import javax.script.Bindings;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.scripting.SlingBindings;
+import org.apache.sling.scripting.api.BindingsValuesProvider;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.context.models.SlingBindingsModel;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+@SuppressWarnings("null")
+public class SlingBindingsTest {
+
+    @Rule
+    public SlingContext context = new SlingContext(ResourceResolverType.JCR_MOCK);
+
+    private Resource currentResource;
+
+    @Before
+    public void setUp() throws Exception {
+        // setup a custom BindingsValuesProvider
+        context.registerService(BindingsValuesProvider.class, new BindingsValuesProvider() {
+            @Override
+            public void addBindings(Bindings bindings) {
+                bindings.put("custom-param-1", "value-1");
+            }
+        });
+
+        // setup another custom BindingsValuesProvider which should be ignored
+        context.registerService(BindingsValuesProvider.class, new BindingsValuesProvider() {
+            @Override
+            public void addBindings(Bindings bindings) {
+                bindings.put("custom-param-2", "value-2");
+            }
+        }, SERVICE_PROPERTY_MOCK_SLING_BINDINGS_IGNORE, true);
+
+        context.addModelsForClasses(SlingBindingsModel.class);
+        currentResource = context.create().resource("/content/testPage/testResource");
+        context.currentResource(currentResource);
+
+        // setup a custom BindingsValuesProvider after touching request first time/setting current resource
+        context.registerService(BindingsValuesProvider.class, new BindingsValuesProvider() {
+            @Override
+            public void addBindings(Bindings bindings) {
+                bindings.put("custom-param-3", "value-3");
+            }
+        });
+        // wait a short time to get sure OSGi events get distributed announcing the new BindingsValueProvider
+        Thread.sleep(25);
+    }
+
+    @Test
+    public void testModelBindings() {
+        SlingBindingsModel model = context.request().adaptTo(SlingBindingsModel.class);
+
+        assertNotNull(model);
+        assertNotNull(model.getResolver());
+        assertNotNull(model.getResource());
+        assertEquals(currentResource.getPath(), model.getResource().getPath());
+        assertNotNull(model.getRequest());
+        assertNotNull(model.getResponse());
+        assertNotNull(model.getCurrentNode());
+        assertNotNull(model.getCurrentSession());
+        assertEquals("value-1", model.getCustomParam1());
+        assertNull(model.getCustomParam2());
+        assertEquals("value-3", model.getCustomParam3());
+    }
+
+    @Test
+    public void testCustomBindingsValuesProvider() {
+        SlingBindings bindings = (SlingBindings)context.request().getAttribute(SlingBindings.class.getName());
+        assertNotNull(bindings);
+        assertEquals(currentResource.getPath(), bindings.getResource().getPath());
+        assertEquals("value-1", bindings.get("custom-param-1"));
+        assertNull(bindings.get("custom-param-2"));
+        assertEquals("value-3", bindings.get("custom-param-3"));
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/testing/mock/sling/context/models/SlingBindingsModel.java b/core/src/test/java/org/apache/sling/testing/mock/sling/context/models/SlingBindingsModel.java
index 0d26bea..6a15464 100644
--- a/core/src/test/java/org/apache/sling/testing/mock/sling/context/models/SlingBindingsModel.java
+++ b/core/src/test/java/org/apache/sling/testing/mock/sling/context/models/SlingBindingsModel.java
@@ -50,6 +50,16 @@
     Node getCurrentNode();
 
     @ScriptVariable(injectionStrategy = InjectionStrategy.OPTIONAL)
-    Session getcurrentSession();
+    Session getCurrentSession();
+
+    // -- Custom --
+    @ScriptVariable(name = "custom-param-1", injectionStrategy = InjectionStrategy.OPTIONAL)
+    String getCustomParam1();
+
+    @ScriptVariable(name = "custom-param-2", injectionStrategy = InjectionStrategy.OPTIONAL)
+    String getCustomParam2();
+
+    @ScriptVariable(name = "custom-param-3", injectionStrategy = InjectionStrategy.OPTIONAL)
+    String getCustomParam3();
 
 }