diff --git a/CHANGELOG.md b/CHANGELOG.md
index 482c46124b..480fad6b22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,8 +9,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com)
[unreleased changes details]: https://github.com/Adobe-Consulting-Services/acs-aem-commons/compare/acs-aem-commons-5.0.14...HEAD
+### Added
+
- #2941 - Add Query Builder support in Report Builder
+### Fixed
+
+- #2960 - SharedComponentPropertiesBindingsValuesProvider should support LazyBindings
+
## 5.3.4 - 2022-08-22
### Added
diff --git a/bundle/pom.xml b/bundle/pom.xml
index b1589c95b4..b98930f136 100644
--- a/bundle/pom.xml
+++ b/bundle/pom.xml
@@ -41,6 +41,37 @@
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ download-sling-api-2-22-0-jar
+ process-test-resources
+
+ copy
+
+
+
+ -
+ org.apache.sling
+ org.apache.sling.api
+ 2.22.0
+ jar
+
+
+
+
+ ${project.build.testOutputDirectory}
+
+
+
+
biz.aQute.bnd
bnd-baseline-maven-plugin
diff --git a/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProvider.java b/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProvider.java
index 5bcf3cec95..32781804d4 100644
--- a/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProvider.java
+++ b/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProvider.java
@@ -20,6 +20,7 @@
package com.adobe.acs.commons.wcm.properties.shared.impl;
import com.adobe.acs.commons.wcm.properties.shared.SharedComponentProperties;
+import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
@@ -34,6 +35,12 @@
import org.slf4j.LoggerFactory;
import javax.script.Bindings;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
/**
* Bindings Values Provider that adds bindings for globalProperties,
@@ -55,12 +62,232 @@
public class SharedComponentPropertiesBindingsValuesProvider implements BindingsValuesProvider {
private static final Logger log = LoggerFactory.getLogger(SharedComponentPropertiesBindingsValuesProvider.class);
+ /**
+ * The LazyBindings class, and its Supplier child interface, are introduced in org.apache.sling.api version 2.22.0,
+ * which is first included in AEM 6.5 SP7.
+ */
+ protected static final String FQDN_LAZY_BINDINGS = "org.apache.sling.api.scripting.LazyBindings";
+ protected static final String SUPPLIER_PROXY_LABEL = "ACS AEM Commons SCP BVP reflective Proxy for LazyBindings.Supplier";
+
/**
* Bind if available, check for null when reading.
*/
@Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL_UNARY)
SharedComponentProperties sharedComponentProperties;
+ /**
+ * Added for pre-6.5.7 support for LazyBindings. This holds the LazyBindings interface
+ * if it is discovered on activation, and is used to check if the {@link #addBindings(Bindings)} param
+ * is an instance of LazyBindings. This hack is necessary until this bundle can drop support for
+ * AEM versions prior to 6.5.7, at which point this variable can be removed, and the {@link #isLazy(Bindings)}
+ * method can be simplified to return {@code bindings instanceof LazyBindings}.
+ */
+ private Class extends Bindings> lazyBindingsType;
+
+ /**
+ * Added for pre-6.5.7 support for LazyBindings. This holds the LazyBindings.Supplier interface
+ * if it is discovered on activation, and is used to create reflection Proxy instances as a hack
+ * until this bundle can drop support for AEM versions prior to 6.5.7, at which point this variable
+ * can be removed, and the {@link #wrapSupplier(Supplier)} method can be simplified to accept a
+ * LazyBindings.Supplier instead of a java.util.function.Supplier and return it (for matching a
+ * lambda expression passed at the call site), or to simply return a lambda that calls the get()
+ * method on the java.util.function.Supplier argument.
+ */
+ private Class extends Supplier> supplierType;
+
+ /**
+ * This variable only exists to facilitate testing for pre-6.5.7 LazyBindings support, so that a non-classpath
+ * class loader can be injected, to provide the LazyBindings class.
+ */
+ private ClassLoader lazyBindingsClassLoader = SlingBindings.class.getClassLoader();
+
+ /**
+ * Called by the unit test to inject a URL class loader that provides a LazyBindings instance
+ * at {@link #FQDN_LAZY_BINDINGS}.
+ *
+ * @param classLoader a new class loader
+ * @return the old class loader
+ */
+ protected ClassLoader swapLazyBindingsClassLoaderForTesting(ClassLoader classLoader) {
+ if (classLoader != null) {
+ ClassLoader oldClassLoader = this.lazyBindingsClassLoader;
+ this.lazyBindingsClassLoader = classLoader;
+ return oldClassLoader;
+ }
+ return null;
+ }
+
+ /**
+ * Return the resolved lazyBindingsType for testing.
+ *
+ * @return the lazyBindingsType
+ */
+ protected Class extends Bindings> getLazyBindingsType() {
+ return this.lazyBindingsType;
+ }
+
+ /**
+ * Return the resolved supplierType for testing.
+ *
+ * @return the supplierType
+ */
+ protected Class extends Supplier> getSupplierType() {
+ return this.supplierType;
+ }
+
+ /**
+ * This method ensures that the provided supplier is appropriately typed for insertion into a SlingBindings
+ * object. It primarily facilitates lambda type inference (i.e., {@code wrapSupplier(() -> something)} forces
+ * inference to the functional interface type of the method parameter). And so long as pre-6.5.7 AEMs are supported,
+ * this method is also responsible for constructing the {@link Proxy} instance when LazyBindings is present at
+ * runtime, and for immediately returning {@code Supplier.get()} when it is not present.
+ * After support for pre-6.5.7 AEMs is dropped, the method return type can be changed from {@code Object} to
+ * {@code LazyBindings.Supplier} to fully support lazy injection.
+ *
+ * @param supplier the provided supplier
+ * @return the Supplier as a LazyBindings.Supplier if supported, or the value of the provided supplier if not
+ */
+ protected Object wrapSupplier(final Supplier> supplier) {
+ if (this.supplierType != null) {
+ return Proxy.newProxyInstance(lazyBindingsClassLoader, new Class[]{this.supplierType},
+ new SupplierWrapper(supplier));
+ }
+ return supplier.get();
+ }
+
+ /**
+ * The only purpose of this class is to drive the pre-6.5.7 reflection-based Proxy instance returned
+ * by {@link #wrapSupplier(Supplier)}.
+ */
+ protected static class SupplierWrapper implements InvocationHandler {
+ private final Supplier> wrapped;
+
+ public SupplierWrapper(final Supplier> supplier) {
+ this.wrapped = supplier;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ // we are implementing a @FunctionalInterface, so don't get carried away with implementing
+ // Object methods.
+ if ("get".equals(method.getName())) {
+ return wrapped.get();
+ } else if ("toString".equals(method.getName())) {
+ // return this marker string for visibility in debugging tools. Otherwise,
+ // the default toString is "\"null\"", which is confusing
+ return SUPPLIER_PROXY_LABEL;
+ }
+ return method.getDefaultValue();
+ }
+ }
+
+ /**
+ * The purpose of this activate method is to determine if we are running in a 6.5.7+ AEM environment
+ * without having to explicitly require {@code org.apache.sling.api.scripting} package version 2.5.0.
+ */
+ @Activate
+ protected void activate() {
+ // use SlingBindings class loader to check for LazyBindings class,
+ // to minimize risk involved with using reflection.
+ try {
+ this.checkAndSetLazyBindingsType(lazyBindingsClassLoader.loadClass(FQDN_LAZY_BINDINGS));
+ } catch (ReflectiveOperationException cnfe) {
+ log.info("LazyBindings not found, will resort to injecting immediate Bindings values", cnfe);
+ }
+ }
+
+ /**
+ * Check that the provided {@code lazyBindingsType} implements {@link Bindings} and defines an enclosed marker
+ * interface named {@code Supplier} that extends {@link Supplier}, and if so, set {@code this.lazyBindingsType} and
+ * {@code this.supplierType}. Otherwise, set both to {@code null}.
+ */
+ @SuppressWarnings({"squid:S1872", "unchecked"})
+ protected void checkAndSetLazyBindingsType(final Class> lazyBindingsType) {
+ if (lazyBindingsType != null && Bindings.class.isAssignableFrom(lazyBindingsType)) {
+ this.supplierType = (Class extends Supplier>) Stream.of(lazyBindingsType.getDeclaredClasses())
+ .filter(clazz -> Supplier.class.getSimpleName().equals(clazz.getSimpleName())
+ && Supplier.class.isAssignableFrom(clazz)).findFirst().orElse(null);
+ this.lazyBindingsType = (Class extends Bindings>) lazyBindingsType;
+ } else {
+ log.info("Supplier interface not declared by lazyBindingsType: {}, will resort to immediate Bindings values",
+ lazyBindingsType);
+ this.supplierType = null;
+ this.lazyBindingsType = null;
+ }
+ }
+
+ /**
+ * Check if provided {@code bindings} implements LazyBindings.
+ *
+ * @param bindings the parameter from {@link #addBindings(Bindings)}
+ * @return true if bindings implements LazyBindings
+ */
+ private boolean isLazy(Bindings bindings) {
+ return Optional.ofNullable(this.lazyBindingsType)
+ .map(clazz -> clazz.isInstance(bindings))
+ .orElse(false);
+ }
+
+ /**
+ * Injects Global SCP keys into the provided bindings in one of two ways:
+ * 1. lazily, if {@code bindings} is an instance of {@code LazyBindings}
+ * 2. immediately, for all other kinds of {@code Bindings}
+ *
+ * @param bindings the bindings
+ * @param supplier a global SCP resource supplier
+ */
+ protected void injectGlobalProps(Bindings bindings, Supplier> supplier) {
+ if (isLazy(bindings)) {
+ bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE,
+ wrapSupplier(() -> supplier.get().orElse(null)));
+ bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES,
+ wrapSupplier(() -> supplier.get().map(Resource::getValueMap).orElse(null)));
+ } else {
+ supplier.get().ifPresent(value -> {
+ bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE, value);
+ bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES, value.getValueMap());
+ });
+ }
+ }
+
+ /**
+ * Injects Shared SCP keys into the provided bindings in one of two ways:
+ * 1. lazily, if {@code bindings} is an instance of {@code LazyBindings}
+ * 2. immediately, for all other kinds of {@code Bindings}
+ *
+ * @param bindings the bindings
+ * @param supplier a shared SCP resource supplier
+ */
+ protected void injectSharedProps(Bindings bindings, Supplier> supplier) {
+ if (isLazy(bindings)) {
+ bindings.put(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE,
+ wrapSupplier(() -> supplier.get().orElse(null)));
+ bindings.put(SharedComponentProperties.SHARED_PROPERTIES,
+ wrapSupplier(() -> supplier.get().map(Resource::getValueMap).orElse(null)));
+ } else {
+ supplier.get().ifPresent(value -> {
+ bindings.put(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE, value);
+ bindings.put(SharedComponentProperties.SHARED_PROPERTIES, value.getValueMap());
+ });
+ }
+ }
+
+ /**
+ * Injects the Merged SCP Properties key into the provided bindings in one of two ways:
+ * 1. lazily, if {@code bindings} is an instance of {@code LazyBindings}
+ * 2. immediately, for all other kinds of {@code Bindings}
+ *
+ * @param bindings the bindings
+ * @param supplier a merged SCP ValueMap supplier
+ */
+ protected void injectMergedProps(Bindings bindings, Supplier supplier) {
+ if (isLazy(bindings)) {
+ bindings.put(SharedComponentProperties.MERGED_PROPERTIES, wrapSupplier(supplier));
+ } else {
+ bindings.put(SharedComponentProperties.MERGED_PROPERTIES, supplier.get());
+ }
+ }
+
@Override
public void addBindings(final Bindings bindings) {
final SlingHttpServletRequest request = (SlingHttpServletRequest) bindings.get(SlingBindings.REQUEST);
@@ -83,38 +310,35 @@ private void setSharedProperties(final Bindings bindings,
if (rootPagePath != null) {
// set this value even when global or shared resources are not found to indicate cache validity downstream
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PAGE_PATH, rootPagePath);
+
String globalPropsPath = sharedComponentProperties.getGlobalPropertiesPath(resource);
- if (globalPropsPath != null) {
- bindings.putAll(cache.getBindings(globalPropsPath, (newBindings) -> {
- final Resource globalPropsResource = resource.getResourceResolver().getResource(globalPropsPath);
- if (globalPropsResource != null) {
- newBindings.put(SharedComponentProperties.GLOBAL_PROPERTIES, globalPropsResource.getValueMap());
- newBindings.put(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE, globalPropsResource);
- }
- }));
- }
+ // perform null path check within the supplier
+ final Supplier> supplyGlobalResource = () ->
+ globalPropsPath != null
+ ? cache.getResource(globalPropsPath, resource.getResourceResolver()::getResource)
+ : Optional.empty();
+ injectGlobalProps(bindings, supplyGlobalResource);
final String sharedPropsPath = sharedComponentProperties.getSharedPropertiesPath(resource);
- if (sharedPropsPath != null) {
- bindings.putAll(cache.getBindings(sharedPropsPath, (newBindings) -> {
- Resource sharedPropsResource = resource.getResourceResolver().getResource(sharedPropsPath);
- if (sharedPropsResource != null) {
- newBindings.put(SharedComponentProperties.SHARED_PROPERTIES, sharedPropsResource.getValueMap());
- newBindings.put(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE, sharedPropsResource);
- }
- }));
- bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PATH, sharedPropsPath);
- }
+ // perform null path check within the supplier
+ final Supplier> supplySharedResource = () ->
+ sharedPropsPath != null
+ ? cache.getResource(sharedPropsPath, resource.getResourceResolver()::getResource)
+ : Optional.empty();
+ injectSharedProps(bindings, supplySharedResource);
+ bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PATH, sharedPropsPath);
final String mergedPropertiesPath = resource.getPath();
- bindings.putAll(cache.getBindings(mergedPropertiesPath, (newBindings) -> {
- ValueMap globalPropertyMap = (ValueMap) bindings.get(SharedComponentProperties.GLOBAL_PROPERTIES);
- ValueMap sharedPropertyMap = (ValueMap) bindings.get(SharedComponentProperties.SHARED_PROPERTIES);
- newBindings.put(SharedComponentProperties.MERGED_PROPERTIES,
- sharedComponentProperties.mergeProperties(globalPropertyMap, sharedPropertyMap, resource));
- }));
+ final Supplier supplyMergedProperties = () ->
+ cache.getMergedProperties(mergedPropertiesPath, (path) -> {
+ ValueMap globalPropertyMap = supplyGlobalResource.get().map(Resource::getValueMap).orElse(ValueMap.EMPTY);
+ ValueMap sharedPropertyMap = supplySharedResource.get().map(Resource::getValueMap).orElse(ValueMap.EMPTY);
+ return sharedComponentProperties.mergeProperties(globalPropertyMap, sharedPropertyMap, resource);
+ });
+ injectMergedProps(bindings, supplyMergedProperties);
+
// set this value to indicate cache validity downstream
- bindings.put(SharedComponentProperties.MERGED_PROPERTIES_PATH, resource.getPath());
+ bindings.put(SharedComponentProperties.MERGED_PROPERTIES_PATH, mergedPropertiesPath);
}
}
diff --git a/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedPropertiesRequestCache.java b/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedPropertiesRequestCache.java
index b8557f3a26..63b59af036 100644
--- a/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedPropertiesRequestCache.java
+++ b/bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedPropertiesRequestCache.java
@@ -20,12 +20,14 @@
package com.adobe.acs.commons.wcm.properties.shared.impl;
-import javax.script.Bindings;
-import javax.script.SimpleBindings;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+
import javax.servlet.ServletRequest;
import java.util.HashMap;
import java.util.Map;
-import java.util.function.Consumer;
+import java.util.Optional;
+import java.util.function.Function;
/**
* Simple cache for global and shared properties bindings keyed by path and persisted in a request attribute.
@@ -33,7 +35,8 @@
public final class SharedPropertiesRequestCache {
private static final String REQUEST_ATTRIBUTE_NAME = SharedPropertiesRequestCache.class.getName();
- private final Map cache = new HashMap<>();
+ private final Map> resourceCache = new HashMap<>();
+ private final Map mergedCache = new HashMap<>();
/**
* Constructor.
@@ -42,13 +45,14 @@ private SharedPropertiesRequestCache() {
/* only me */
}
- public Bindings getBindings(final String propertiesPath,
- final Consumer computeIfNotFound) {
- return cache.computeIfAbsent(propertiesPath, key -> {
- final Bindings bindings = new SimpleBindings();
- computeIfNotFound.accept(bindings);
- return bindings;
- });
+ public Optional getResource(final String path, final Function computeIfNotFound) {
+ return resourceCache.computeIfAbsent(path,
+ key -> Optional.ofNullable(computeIfNotFound.apply(key)));
+ }
+
+ public ValueMap getMergedProperties(final String path, final Function computeIfNotFound) {
+ return mergedCache.computeIfAbsent(path,
+ key -> Optional.ofNullable(computeIfNotFound.apply(key)).orElse(ValueMap.EMPTY));
}
public static SharedPropertiesRequestCache fromRequest(ServletRequest req) {
diff --git a/bundle/src/test/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProviderTest.java b/bundle/src/test/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProviderTest.java
index e12e950c56..ae949eafd1 100644
--- a/bundle/src/test/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProviderTest.java
+++ b/bundle/src/test/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProviderTest.java
@@ -21,16 +21,21 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
+import java.util.function.Supplier;
import javax.script.Bindings;
import javax.script.SimpleBindings;
@@ -48,109 +53,325 @@
import com.adobe.acs.commons.wcm.PageRootProvider;
import com.adobe.acs.commons.wcm.properties.shared.SharedComponentProperties;
+import org.osgi.annotation.versioning.ConsumerType;
@RunWith(MockitoJUnitRunner.class)
public class SharedComponentPropertiesBindingsValuesProviderTest {
- public static final String SITE_ROOT = "/content/acs-commons";
- public static final String RESOURCE_TYPE = "acs-commons/components/content/generic-text";
-
- private PageRootProvider pageRootProvider;
- private Resource resource;
- private Resource sharedPropsResource;
- private Resource globalPropsResource;
- private SlingHttpServletRequest request;
- private Bindings bindings;
- private ResourceResolver resourceResolver;
- private ValueMap sharedProps;
- private ValueMap globalProps;
-
- @Before
- public void setUp() throws Exception {
- resource = mock(Resource.class);
- pageRootProvider = mock(PageRootProvider.class);
- bindings = new SimpleBindings();
- sharedPropsResource = mock(Resource.class);
- globalPropsResource = mock(Resource.class);
- resourceResolver = mock(ResourceResolver.class);
- request = mock(SlingHttpServletRequest.class);
-
- final String globalPropsPath = SITE_ROOT + "/jcr:content/" + SharedComponentProperties.NN_GLOBAL_COMPONENT_PROPERTIES;
- final String sharedPropsPath = SITE_ROOT + "/jcr:content/" + SharedComponentProperties.NN_SHARED_COMPONENT_PROPERTIES + "/"
- + RESOURCE_TYPE;
-
- bindings.put(SlingBindings.REQUEST, request);
- bindings.put(SlingBindings.RESOURCE, resource);
-
- when(resource.getResourceResolver()).thenReturn(resourceResolver);
- when(resource.getResourceType()).thenReturn(RESOURCE_TYPE);
- when(resourceResolver.getSearchPath()).thenReturn(new String[]{"/apps/", "/libs/"});
- when(resourceResolver.getResource(sharedPropsPath)).thenReturn(sharedPropsResource);
- when(resourceResolver.getResource(globalPropsPath)).thenReturn(globalPropsResource);
-
- when(resource.getPath()).thenReturn(SITE_ROOT);
- when(pageRootProvider.getRootPagePath(anyString())).thenReturn(SITE_ROOT);
-
-
- sharedProps = new ValueMapDecorator(new HashMap());
- globalProps = new ValueMapDecorator(new HashMap());
- sharedProps.put("shared", "value");
- globalProps.put("global", "value");
-
- when(globalPropsResource.getValueMap()).thenReturn(globalProps);
- when(sharedPropsResource.getValueMap()).thenReturn(sharedProps);
- when(resource.getValueMap()).thenReturn(ValueMap.EMPTY);
- }
-
- @Test
- public void testGetCanonicalResourceTypeRelativePath() {
- // make this test readable by wrapping the long method name with a function
- final BiFunction, String> asFunction =
- (resourceType, searchPaths) -> SharedComponentPropertiesImpl
- .getCanonicalResourceTypeRelativePath(resourceType,
- Optional.ofNullable(searchPaths)
- .map(list -> list.toArray(new String[0])).orElse(null));
-
- final List emptySearchPaths = Collections.emptyList();
- final List realSearchPaths = Arrays.asList("/apps/", "/libs/");
- assertNull("expect null for null rt", asFunction.apply(null, emptySearchPaths));
- assertNull("expect null for empty rt", asFunction.apply("", emptySearchPaths));
- assertNull("expect null for absolute rt and null search paths",
- asFunction.apply("/fail/" + RESOURCE_TYPE, null));
- assertNull("expect null for cq:Page",
- asFunction.apply("cq:Page", realSearchPaths));
- assertNull("expect null for nt:unstructured",
- asFunction.apply("nt:unstructured", realSearchPaths));
- assertNull("expect null for absolute rt and empty search paths",
- asFunction.apply("/fail/" + RESOURCE_TYPE, emptySearchPaths));
- assertNull("expect null for sling nonexisting rt",
- asFunction.apply(Resource.RESOURCE_TYPE_NON_EXISTING, emptySearchPaths));
- assertEquals("expect same for relative rt", RESOURCE_TYPE,
- asFunction.apply(RESOURCE_TYPE, emptySearchPaths));
- assertEquals("expect same for relative rt and real search paths", RESOURCE_TYPE,
- asFunction.apply(RESOURCE_TYPE, realSearchPaths));
- assertEquals("expect relative for /apps/ + relative and real search paths", RESOURCE_TYPE,
- asFunction.apply("/apps/" + RESOURCE_TYPE, realSearchPaths));
- assertEquals("expect relative for /libs/ + relative and real search paths", RESOURCE_TYPE,
- asFunction.apply("/libs/" + RESOURCE_TYPE, realSearchPaths));
- assertNull("expect null for /fail/ + relative and real search paths",
- asFunction.apply("/fail/" + RESOURCE_TYPE, realSearchPaths));
- }
-
- @Test
- public void addBindings() {
- final SharedComponentPropertiesImpl sharedComponentProperties = new SharedComponentPropertiesImpl();
- sharedComponentProperties.pageRootProvider = pageRootProvider;
-
- final SharedComponentPropertiesBindingsValuesProvider sharedComponentPropertiesBindingsValuesProvider
- = new SharedComponentPropertiesBindingsValuesProvider();
-
- sharedComponentPropertiesBindingsValuesProvider.sharedComponentProperties = sharedComponentProperties;
- sharedComponentPropertiesBindingsValuesProvider.addBindings(bindings);
-
- assertEquals(sharedPropsResource, bindings.get(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE));
- assertEquals(globalPropsResource, bindings.get(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE));
- assertEquals(sharedProps, bindings.get(SharedComponentProperties.SHARED_PROPERTIES));
- assertEquals(globalProps, bindings.get(SharedComponentProperties.GLOBAL_PROPERTIES));
- }
+ public static final String SITE_ROOT = "/content/acs-commons";
+ public static final String RESOURCE_TYPE = "acs-commons/components/content/generic-text";
+
+ /**
+ * Pre-6.5.7 LazyBindings support.
+ */
+ private static final String REL_PATH_SLING_API_2_22_0 = "org.apache.sling.api-2.22.0.jar";
+
+ private PageRootProvider pageRootProvider;
+ private Resource resource;
+ private Resource sharedPropsResource;
+ private Resource globalPropsResource;
+ private SlingHttpServletRequest request;
+ private Bindings bindings;
+
+ private ResourceResolver resourceResolver;
+ private ValueMap sharedProps;
+ private ValueMap globalProps;
+
+ private ValueMap localProps;
+
+ /**
+ * Pre-6.5.7 LazyBindings support. This class simulates the LazyBindings and LazyBindings.Supplier class hierarchy
+ * until this project upgrades to a dependency list that includes org.apache.sling.api version 2.22.0+.
+ *
+ * @see LazyBindings
+ */
+ @ConsumerType
+ private static class LazyLikeBindings extends SimpleBindings {
+ @ConsumerType
+ @FunctionalInterface
+ interface Supplier extends java.util.function.Supplier {
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ resource = mock(Resource.class);
+ pageRootProvider = mock(PageRootProvider.class);
+ bindings = new SimpleBindings();
+ sharedPropsResource = mock(Resource.class);
+ globalPropsResource = mock(Resource.class);
+ resourceResolver = mock(ResourceResolver.class);
+ request = mock(SlingHttpServletRequest.class);
+
+ final String globalPropsPath = SITE_ROOT + "/jcr:content/" + SharedComponentProperties.NN_GLOBAL_COMPONENT_PROPERTIES;
+ final String sharedPropsPath = SITE_ROOT + "/jcr:content/" + SharedComponentProperties.NN_SHARED_COMPONENT_PROPERTIES + "/"
+ + RESOURCE_TYPE;
+
+ bindings.put(SlingBindings.REQUEST, request);
+ bindings.put(SlingBindings.RESOURCE, resource);
+
+ when(resource.getResourceResolver()).thenReturn(resourceResolver);
+ when(resource.getResourceType()).thenReturn(RESOURCE_TYPE);
+ when(resourceResolver.getSearchPath()).thenReturn(new String[]{"/apps/", "/libs/"});
+ when(resourceResolver.getResource(sharedPropsPath)).thenReturn(sharedPropsResource);
+ when(resourceResolver.getResource(globalPropsPath)).thenReturn(globalPropsResource);
+
+ when(resource.getPath()).thenReturn(SITE_ROOT);
+ when(pageRootProvider.getRootPagePath(anyString())).thenReturn(SITE_ROOT);
+
+
+ sharedProps = new ValueMapDecorator(new HashMap());
+ globalProps = new ValueMapDecorator(new HashMap());
+ localProps = new ValueMapDecorator(new HashMap());
+
+ sharedProps.put("shared", "value");
+ globalProps.put("global", "value");
+ localProps.put("local", "value");
+
+ when(globalPropsResource.getValueMap()).thenReturn(globalProps);
+ when(sharedPropsResource.getValueMap()).thenReturn(sharedProps);
+ when(resource.getValueMap()).thenReturn(localProps);
+ }
+
+ @Test
+ public void testGetCanonicalResourceTypeRelativePath() {
+ // make this test readable by wrapping the long method name with a function
+ final BiFunction, String> asFunction =
+ (resourceType, searchPaths) -> SharedComponentPropertiesImpl
+ .getCanonicalResourceTypeRelativePath(resourceType,
+ Optional.ofNullable(searchPaths)
+ .map(list -> list.toArray(new String[0])).orElse(null));
+
+ final List emptySearchPaths = Collections.emptyList();
+ final List realSearchPaths = Arrays.asList("/apps/", "/libs/");
+ assertNull("expect null for null rt", asFunction.apply(null, emptySearchPaths));
+ assertNull("expect null for empty rt", asFunction.apply("", emptySearchPaths));
+ assertNull("expect null for absolute rt and null search paths",
+ asFunction.apply("/fail/" + RESOURCE_TYPE, null));
+ assertNull("expect null for cq:Page",
+ asFunction.apply("cq:Page", realSearchPaths));
+ assertNull("expect null for nt:unstructured",
+ asFunction.apply("nt:unstructured", realSearchPaths));
+ assertNull("expect null for absolute rt and empty search paths",
+ asFunction.apply("/fail/" + RESOURCE_TYPE, emptySearchPaths));
+ assertNull("expect null for sling nonexisting rt",
+ asFunction.apply(Resource.RESOURCE_TYPE_NON_EXISTING, emptySearchPaths));
+ assertEquals("expect same for relative rt", RESOURCE_TYPE,
+ asFunction.apply(RESOURCE_TYPE, emptySearchPaths));
+ assertEquals("expect same for relative rt and real search paths", RESOURCE_TYPE,
+ asFunction.apply(RESOURCE_TYPE, realSearchPaths));
+ assertEquals("expect relative for /apps/ + relative and real search paths", RESOURCE_TYPE,
+ asFunction.apply("/apps/" + RESOURCE_TYPE, realSearchPaths));
+ assertEquals("expect relative for /libs/ + relative and real search paths", RESOURCE_TYPE,
+ asFunction.apply("/libs/" + RESOURCE_TYPE, realSearchPaths));
+ assertNull("expect null for /fail/ + relative and real search paths",
+ asFunction.apply("/fail/" + RESOURCE_TYPE, realSearchPaths));
+ }
+
+ @Test
+ public void addBindings() {
+ final SharedComponentPropertiesImpl sharedComponentProperties = new SharedComponentPropertiesImpl();
+ sharedComponentProperties.pageRootProvider = pageRootProvider;
+
+ final SharedComponentPropertiesBindingsValuesProvider sharedComponentPropertiesBindingsValuesProvider
+ = new SharedComponentPropertiesBindingsValuesProvider();
+
+ sharedComponentPropertiesBindingsValuesProvider.sharedComponentProperties = sharedComponentProperties;
+ sharedComponentPropertiesBindingsValuesProvider.activate();
+ sharedComponentPropertiesBindingsValuesProvider.addBindings(bindings);
+
+ assertEquals(sharedPropsResource, bindings.get(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE));
+ assertEquals(globalPropsResource, bindings.get(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE));
+ assertEquals(sharedProps, bindings.get(SharedComponentProperties.SHARED_PROPERTIES));
+ assertEquals(globalProps, bindings.get(SharedComponentProperties.GLOBAL_PROPERTIES));
+
+ ValueMap mergedProps = (ValueMap) bindings.get(SharedComponentProperties.MERGED_PROPERTIES);
+
+ assertEquals("value", mergedProps.get("global", String.class));
+ assertEquals("value", mergedProps.get("shared", String.class));
+ assertEquals("value", mergedProps.get("local", String.class));
+ }
+
+ @Test
+ public void addToLazyBindings() {
+ final SharedComponentPropertiesImpl sharedComponentProperties = new SharedComponentPropertiesImpl();
+ sharedComponentProperties.pageRootProvider = pageRootProvider;
+
+ final SharedComponentPropertiesBindingsValuesProvider sharedComponentPropertiesBindingsValuesProvider
+ = new SharedComponentPropertiesBindingsValuesProvider();
+
+ sharedComponentPropertiesBindingsValuesProvider.sharedComponentProperties = sharedComponentProperties;
+ sharedComponentPropertiesBindingsValuesProvider.activate();
+ sharedComponentPropertiesBindingsValuesProvider.checkAndSetLazyBindingsType(LazyLikeBindings.class);
+
+ LazyLikeBindings lazyBindings = new LazyLikeBindings();
+ lazyBindings.putAll(bindings);
+ sharedComponentPropertiesBindingsValuesProvider.addBindings(lazyBindings);
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource
+ Object sharedPropsObject = lazyBindings.get(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE);
+ assertTrue(sharedPropsObject instanceof LazyLikeBindings.Supplier);
+ assertEquals(SharedComponentPropertiesBindingsValuesProvider.SUPPLIER_PROXY_LABEL, sharedPropsObject.toString());
+ // compare that the value returned by the supplier with the expected resource
+ assertEquals(sharedPropsResource, ((LazyLikeBindings.Supplier) sharedPropsObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource
+ Object globalPropsObject = lazyBindings.get(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE);
+ assertTrue(globalPropsObject instanceof LazyLikeBindings.Supplier);
+ // compare that the value returned by the supplier with the expected resource
+ assertEquals(globalPropsResource, ((LazyLikeBindings.Supplier) globalPropsObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a ValueMap
+ Object sharedPropsVmObject = lazyBindings.get(SharedComponentProperties.SHARED_PROPERTIES);
+ assertTrue(sharedPropsVmObject instanceof LazyLikeBindings.Supplier);
+ // compare that the value returned by the supplier with the expected ValueMap
+ assertEquals(sharedProps, ((LazyLikeBindings.Supplier) sharedPropsVmObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a ValueMap
+ Object globalPropsVmObject = lazyBindings.get(SharedComponentProperties.GLOBAL_PROPERTIES);
+ assertTrue(globalPropsVmObject instanceof LazyLikeBindings.Supplier);
+ // compare that the value returned by the supplier with the expected ValueMap
+ assertEquals(globalProps, ((LazyLikeBindings.Supplier) globalPropsVmObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource. Acquire this Supplier BEFORE
+ // resetting the Global and Shared properties bindings to demonstrate that the same bindings instance
+ // is also accessed lazily by the Merged props supplier.
+ Object mergedPropsVmObject = lazyBindings.get(SharedComponentProperties.MERGED_PROPERTIES);
+ assertTrue(mergedPropsVmObject instanceof LazyLikeBindings.Supplier);
+
+ // reset the Global and Shared properties bindings to contain the supplied values that will be consumed by
+ // the Merged properties supplier binding.
+ lazyBindings.put(SharedComponentProperties.GLOBAL_PROPERTIES, globalProps);
+ lazyBindings.put(SharedComponentProperties.SHARED_PROPERTIES, sharedProps);
+ // NOW call the merged properties supplier function.
+ ValueMap mergedProps = (ValueMap) ((LazyLikeBindings.Supplier) mergedPropsVmObject).get();
+
+ // compare the contents of the ValueMap returned by the supplier with the expected key/values from the separate maps
+ assertEquals("value", mergedProps.get("global", String.class));
+ assertEquals("value", mergedProps.get("shared", String.class));
+ assertEquals("value", mergedProps.get("local", String.class));
+ }
+
+ @Test
+ public void addToLazyBindings_NonConformant() {
+ final SharedComponentPropertiesImpl sharedComponentProperties = new SharedComponentPropertiesImpl();
+ sharedComponentProperties.pageRootProvider = pageRootProvider;
+
+ final SharedComponentPropertiesBindingsValuesProvider sharedComponentPropertiesBindingsValuesProvider
+ = new SharedComponentPropertiesBindingsValuesProvider();
+
+
+ sharedComponentPropertiesBindingsValuesProvider.sharedComponentProperties = sharedComponentProperties;
+ sharedComponentPropertiesBindingsValuesProvider.activate();
+ sharedComponentPropertiesBindingsValuesProvider.checkAndSetLazyBindingsType(SimpleBindings.class);
+
+ // test that the wrapSupplier() method returns the value from the supplier, rather than a supplier itself
+ assertEquals("immediate", sharedComponentPropertiesBindingsValuesProvider
+ .wrapSupplier(() -> "immediate").toString());
+
+ SimpleBindings lazyBindings = new SimpleBindings();
+ lazyBindings.putAll(bindings);
+
+ sharedComponentPropertiesBindingsValuesProvider.addBindings(lazyBindings);
+
+ // confirm that the non-conformant bindings is storing the resource
+ Object sharedPropsObject = lazyBindings.get(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE);
+ // compare that the value returned by the supplier with the expected resource
+ assertEquals(sharedPropsResource, sharedPropsObject);
+
+ // confirm that the non-conformant bindings is storing the resource
+ Object globalPropsObject = lazyBindings.get(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE);
+ // compare that the value returned by the supplier with the expected resource
+ assertEquals(globalPropsResource, globalPropsObject);
+
+ // confirm that the non-conformant bindings is storing the ValueMap
+ Object sharedPropsVmObject = lazyBindings.get(SharedComponentProperties.SHARED_PROPERTIES);
+ // compare that the value returned by the supplier with the expected ValueMap
+ assertEquals(sharedProps, sharedPropsVmObject);
+
+ // confirm that the non-conformant bindings is storing the ValueMap
+ Object globalPropsVmObject = lazyBindings.get(SharedComponentProperties.GLOBAL_PROPERTIES);
+ // compare that the value returned by the supplier with the expected ValueMap
+ assertEquals(globalProps, globalPropsVmObject);
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource. Acquire this Supplier BEFORE
+ // resetting the Global and Shared properties bindings to demonstrate that the same bindings instance
+ // is also accessed lazily by the Merged props supplier.
+ Object mergedPropsVmObject = lazyBindings.get(SharedComponentProperties.MERGED_PROPERTIES);
+ ValueMap mergedProps = (ValueMap) mergedPropsVmObject;
+
+ // compare the contents of the ValueMap returned by the supplier with the expected key/values from the separate maps
+ assertEquals("value", mergedProps.get("global", String.class));
+ assertEquals("value", mergedProps.get("shared", String.class));
+ assertEquals("value", mergedProps.get("local", String.class));
+ }
+
+ @Test
+ public void addToLazyBindings_SlingApiJar() throws Exception {
+ try (final URLClassLoader slingApiClassLoader = new URLClassLoader(
+ new URL[]{getClass().getClassLoader().getResource(REL_PATH_SLING_API_2_22_0)},
+ getClass().getClassLoader())) {
+
+ final SharedComponentPropertiesImpl sharedComponentProperties = new SharedComponentPropertiesImpl();
+ sharedComponentProperties.pageRootProvider = pageRootProvider;
+
+ final SharedComponentPropertiesBindingsValuesProvider sharedComponentPropertiesBindingsValuesProvider
+ = new SharedComponentPropertiesBindingsValuesProvider();
+ // swap classloader
+ sharedComponentPropertiesBindingsValuesProvider.swapLazyBindingsClassLoaderForTesting(slingApiClassLoader);
+ sharedComponentPropertiesBindingsValuesProvider.sharedComponentProperties = sharedComponentProperties;
+ // activate service to load classes
+ sharedComponentPropertiesBindingsValuesProvider.activate();
+
+ // test that the wrapSupplier() method returns the proxy supplier, rather than the supplied value
+ assertEquals(SharedComponentPropertiesBindingsValuesProvider.SUPPLIER_PROXY_LABEL,
+ sharedComponentPropertiesBindingsValuesProvider.wrapSupplier(() -> "immediate").toString());
+
+ // inject our own suppliers map for a side-channel to the suppliers
+ final Map suppliers = new HashMap<>();
+ Bindings lazyBindings = sharedComponentPropertiesBindingsValuesProvider
+ .getLazyBindingsType().getConstructor(Map.class).newInstance(suppliers);
+ lazyBindings.putAll(bindings);
+ final Class extends Supplier> supplierType = sharedComponentPropertiesBindingsValuesProvider.getSupplierType();
+
+ sharedComponentPropertiesBindingsValuesProvider.addBindings(lazyBindings);
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource
+ Object sharedPropsObject = suppliers.get(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE);
+ assertTrue(supplierType.isInstance(sharedPropsObject));
+ // compare that the value returned by the supplier with the expected resource
+ assertEquals(sharedPropsResource, ((Supplier>) sharedPropsObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource
+ Object globalPropsObject = suppliers.get(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE);
+ assertTrue(supplierType.isInstance(globalPropsObject));
+ // compare that the value returned by the supplier with the expected resource
+ assertEquals(globalPropsResource, ((Supplier>) globalPropsObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a ValueMap
+ Object sharedPropsVmObject = suppliers.get(SharedComponentProperties.SHARED_PROPERTIES);
+ assertTrue(supplierType.isInstance(sharedPropsVmObject));
+ // compare that the value returned by the supplier with the expected ValueMap
+ assertEquals(sharedProps, ((Supplier>) sharedPropsVmObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a ValueMap
+ Object globalPropsVmObject = suppliers.get(SharedComponentProperties.GLOBAL_PROPERTIES);
+ assertTrue(supplierType.isInstance(globalPropsVmObject));
+ // compare that the value returned by the supplier with the expected ValueMap
+ assertEquals(globalProps, ((Supplier>) globalPropsVmObject).get());
+
+ // confirm that the bindings is storing a marked Supplier, rather than a resource. Acquire this Supplier BEFORE
+ // resetting the Global and Shared properties bindings to demonstrate that the same bindings instance
+ // is also accessed lazily by the Merged props supplier.
+ Object mergedPropsVmObject = suppliers.get(SharedComponentProperties.MERGED_PROPERTIES);
+ assertTrue(supplierType.isInstance(mergedPropsVmObject));
+ // compare that the value returned by the supplier with the expected ValueMap
+ ValueMap mergedProps = (ValueMap) ((Supplier>) mergedPropsVmObject).get();
+
+ // compare the contents of the ValueMap returned by the supplier with the expected key/values from the separate maps
+ assertEquals("value", mergedProps.get("global", String.class));
+ assertEquals("value", mergedProps.get("shared", String.class));
+ assertEquals("value", mergedProps.get("local", String.class));
+ }
+ }
}