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 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 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 getLazyBindingsType() { + return this.lazyBindingsType; + } + + /** + * Return the resolved supplierType for testing. + * + * @return the supplierType + */ + protected Class 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) Stream.of(lazyBindingsType.getDeclaredClasses()) + .filter(clazz -> Supplier.class.getSimpleName().equals(clazz.getSimpleName()) + && Supplier.class.isAssignableFrom(clazz)).findFirst().orElse(null); + this.lazyBindingsType = (Class) 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 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)); + } + } }