Skip to content

Commit b560d2a

Browse files
committed
issue #2960 shared-component-properties add support for lazy bindings
1 parent b881c90 commit b560d2a

File tree

5 files changed

+624
-138
lines changed

5 files changed

+624
-138
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com)
99
<!-- Keep this up to date! After a release, change the tag name to the latest release -->
1010
[unreleased changes details]: https://github.com/Adobe-Consulting-Services/acs-aem-commons/compare/acs-aem-commons-5.0.14...HEAD
1111

12+
### Added
13+
1214
- #2941 - Add Query Builder support in Report Builder
1315

16+
### Fixed
17+
18+
- #2960 - SharedComponentPropertiesBindingsValuesProvider should support LazyBindings
19+
1420
## 5.3.4 - 2022-08-22
1521

1622
### Added

bundle/pom.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@
4141
</archive>
4242
</configuration>
4343
</plugin>
44+
<plugin>
45+
<groupId>org.apache.maven.plugins</groupId>
46+
<artifactId>maven-dependency-plugin</artifactId>
47+
<executions>
48+
<execution>
49+
<!--
50+
this execution downloads org.apache.sling.api-2.22.0.jar for
51+
*TESTING* pre-6.5.7 LazyBindings support in our BindingsValuesProvider services,
52+
and specifically, SharedComponentPropertiesBindingsValuesProvider.
53+
-->
54+
<id>download-sling-api-2-22-0-jar</id>
55+
<phase>process-test-resources</phase>
56+
<goals>
57+
<goal>copy</goal>
58+
</goals>
59+
<configuration>
60+
<artifactItems>
61+
<item>
62+
<groupId>org.apache.sling</groupId>
63+
<artifactId>org.apache.sling.api</artifactId>
64+
<version>2.22.0</version>
65+
<type>jar</type>
66+
</item>
67+
</artifactItems>
68+
<!-- drop it into the target/test-classes directory -->
69+
<!-- required by SharedComponentPropertiesBindingsValuesProviderTest -->
70+
<outputDirectory>${project.build.testOutputDirectory}</outputDirectory>
71+
</configuration>
72+
</execution>
73+
</executions>
74+
</plugin>
4475
<plugin>
4576
<groupId>biz.aQute.bnd</groupId>
4677
<artifactId>bnd-baseline-maven-plugin</artifactId>

bundle/src/main/java/com/adobe/acs/commons/wcm/properties/shared/impl/SharedComponentPropertiesBindingsValuesProvider.java

Lines changed: 250 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package com.adobe.acs.commons.wcm.properties.shared.impl;
2121

2222
import com.adobe.acs.commons.wcm.properties.shared.SharedComponentProperties;
23+
import org.apache.felix.scr.annotations.Activate;
2324
import org.apache.felix.scr.annotations.Component;
2425
import org.apache.felix.scr.annotations.Reference;
2526
import org.apache.felix.scr.annotations.ReferenceCardinality;
@@ -34,6 +35,12 @@
3435
import org.slf4j.LoggerFactory;
3536

3637
import javax.script.Bindings;
38+
import java.lang.reflect.InvocationHandler;
39+
import java.lang.reflect.Method;
40+
import java.lang.reflect.Proxy;
41+
import java.util.Optional;
42+
import java.util.function.Supplier;
43+
import java.util.stream.Stream;
3744

3845
/**
3946
* Bindings Values Provider that adds bindings for globalProperties,
@@ -55,12 +62,232 @@
5562
public class SharedComponentPropertiesBindingsValuesProvider implements BindingsValuesProvider {
5663
private static final Logger log = LoggerFactory.getLogger(SharedComponentPropertiesBindingsValuesProvider.class);
5764

65+
/**
66+
* The LazyBindings class, and its Supplier child interface, are introduced in org.apache.sling.api version 2.22.0,
67+
* which is first included in AEM 6.5 SP7.
68+
*/
69+
protected static final String FQDN_LAZY_BINDINGS = "org.apache.sling.api.scripting.LazyBindings";
70+
protected static final String SUPPLIER_PROXY_LABEL = "ACS AEM Commons SCP BVP reflective Proxy for LazyBindings.Supplier";
71+
5872
/**
5973
* Bind if available, check for null when reading.
6074
*/
6175
@Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL_UNARY)
6276
SharedComponentProperties sharedComponentProperties;
6377

78+
/**
79+
* Added for pre-6.5.7 support for LazyBindings. This holds the LazyBindings interface
80+
* if it is discovered on activation, and is used to check if the {@link #addBindings(Bindings)} param
81+
* is an instance of LazyBindings. This hack is necessary until this bundle can drop support for
82+
* AEM versions prior to 6.5.7, at which point this variable can be removed, and the {@link #isLazy(Bindings)}
83+
* method can be simplified to return {@code bindings instanceof LazyBindings}.
84+
*/
85+
private Class<? extends Bindings> lazyBindingsType;
86+
87+
/**
88+
* Added for pre-6.5.7 support for LazyBindings. This holds the LazyBindings.Supplier interface
89+
* if it is discovered on activation, and is used to create reflection Proxy instances as a hack
90+
* until this bundle can drop support for AEM versions prior to 6.5.7, at which point this variable
91+
* can be removed, and the {@link #wrapSupplier(Supplier)} method can be simplified to accept a
92+
* LazyBindings.Supplier instead of a java.util.function.Supplier and return it (for matching a
93+
* lambda expression passed at the call site), or to simply return a lambda that calls the get()
94+
* method on the java.util.function.Supplier argument.
95+
*/
96+
private Class<? extends Supplier> supplierType;
97+
98+
/**
99+
* This variable only exists to facilitate testing for pre-6.5.7 LazyBindings support, so that a non-classpath
100+
* class loader can be injected, to provide the LazyBindings class.
101+
*/
102+
private ClassLoader lazyBindingsClassLoader = SlingBindings.class.getClassLoader();
103+
104+
/**
105+
* Called by the unit test to inject a URL class loader that provides a LazyBindings instance
106+
* at {@link #FQDN_LAZY_BINDINGS}.
107+
*
108+
* @param classLoader a new class loader
109+
* @return the old class loader
110+
*/
111+
protected ClassLoader swapLazyBindingsClassLoaderForTesting(ClassLoader classLoader) {
112+
if (classLoader != null) {
113+
ClassLoader oldClassLoader = this.lazyBindingsClassLoader;
114+
this.lazyBindingsClassLoader = classLoader;
115+
return oldClassLoader;
116+
}
117+
return null;
118+
}
119+
120+
/**
121+
* Return the resolved lazyBindingsType for testing.
122+
*
123+
* @return the lazyBindingsType
124+
*/
125+
protected Class<? extends Bindings> getLazyBindingsType() {
126+
return this.lazyBindingsType;
127+
}
128+
129+
/**
130+
* Return the resolved supplierType for testing.
131+
*
132+
* @return the supplierType
133+
*/
134+
protected Class<? extends Supplier> getSupplierType() {
135+
return this.supplierType;
136+
}
137+
138+
/**
139+
* This method ensures that the provided supplier is appropriately typed for insertion into a SlingBindings
140+
* object. It primarily facilitates lambda type inference (i.e., {@code wrapSupplier(() -> something)} forces
141+
* inference to the functional interface type of the method parameter). And so long as pre-6.5.7 AEMs are supported,
142+
* this method is also responsible for constructing the {@link Proxy} instance when LazyBindings is present at
143+
* runtime, and for immediately returning {@code Supplier.get()} when it is not present.
144+
* After support for pre-6.5.7 AEMs is dropped, the method return type can be changed from {@code Object} to
145+
* {@code <T> LazyBindings.Supplier<T>} to fully support lazy injection.
146+
*
147+
* @param supplier the provided supplier
148+
* @return the Supplier as a LazyBindings.Supplier if supported, or the value of the provided supplier if not
149+
*/
150+
protected Object wrapSupplier(final Supplier<?> supplier) {
151+
if (this.supplierType != null) {
152+
return Proxy.newProxyInstance(lazyBindingsClassLoader, new Class[]{this.supplierType},
153+
new SupplierWrapper(supplier));
154+
}
155+
return supplier.get();
156+
}
157+
158+
/**
159+
* The only purpose of this class is to drive the pre-6.5.7 reflection-based Proxy instance returned
160+
* by {@link #wrapSupplier(Supplier)}.
161+
*/
162+
protected static class SupplierWrapper implements InvocationHandler {
163+
private final Supplier<?> wrapped;
164+
165+
public SupplierWrapper(final Supplier<?> supplier) {
166+
this.wrapped = supplier;
167+
}
168+
169+
@Override
170+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
171+
// we are implementing a @FunctionalInterface, so don't get carried away with implementing
172+
// Object methods.
173+
if ("get".equals(method.getName())) {
174+
return wrapped.get();
175+
} else if ("toString".equals(method.getName())) {
176+
// return this marker string for visibility in debugging tools. Otherwise,
177+
// the default toString is "\"null\"", which is confusing
178+
return SUPPLIER_PROXY_LABEL;
179+
}
180+
return method.getDefaultValue();
181+
}
182+
}
183+
184+
/**
185+
* The purpose of this activate method is to determine if we are running in a 6.5.7+ AEM environment
186+
* without having to explicitly require {@code org.apache.sling.api.scripting} package version 2.5.0.
187+
*/
188+
@Activate
189+
protected void activate() {
190+
// use SlingBindings class loader to check for LazyBindings class,
191+
// to minimize risk involved with using reflection.
192+
try {
193+
this.checkAndSetLazyBindingsType(lazyBindingsClassLoader.loadClass(FQDN_LAZY_BINDINGS));
194+
} catch (ReflectiveOperationException cnfe) {
195+
log.info("LazyBindings not found, will resort to injecting immediate Bindings values", cnfe);
196+
}
197+
}
198+
199+
/**
200+
* Check that the provided {@code lazyBindingsType} implements {@link Bindings} and defines an enclosed marker
201+
* interface named {@code Supplier} that extends {@link Supplier}, and if so, set {@code this.lazyBindingsType} and
202+
* {@code this.supplierType}. Otherwise, set both to {@code null}.
203+
*/
204+
@SuppressWarnings({"squid:S1872", "unchecked"})
205+
protected void checkAndSetLazyBindingsType(final Class<?> lazyBindingsType) {
206+
if (lazyBindingsType != null && Bindings.class.isAssignableFrom(lazyBindingsType)) {
207+
this.supplierType = (Class<? extends Supplier>) Stream.of(lazyBindingsType.getDeclaredClasses())
208+
.filter(clazz -> Supplier.class.getSimpleName().equals(clazz.getSimpleName())
209+
&& Supplier.class.isAssignableFrom(clazz)).findFirst().orElse(null);
210+
this.lazyBindingsType = (Class<? extends Bindings>) lazyBindingsType;
211+
} else {
212+
log.info("Supplier interface not declared by lazyBindingsType: {}, will resort to immediate Bindings values",
213+
lazyBindingsType);
214+
this.supplierType = null;
215+
this.lazyBindingsType = null;
216+
}
217+
}
218+
219+
/**
220+
* Check if provided {@code bindings} implements LazyBindings.
221+
*
222+
* @param bindings the parameter from {@link #addBindings(Bindings)}
223+
* @return true if bindings implements LazyBindings
224+
*/
225+
private boolean isLazy(Bindings bindings) {
226+
return Optional.ofNullable(this.lazyBindingsType)
227+
.map(clazz -> clazz.isInstance(bindings))
228+
.orElse(false);
229+
}
230+
231+
/**
232+
* Injects Global SCP keys into the provided bindings in one of two ways:
233+
* 1. lazily, if {@code bindings} is an instance of {@code LazyBindings}
234+
* 2. immediately, for all other kinds of {@code Bindings}
235+
*
236+
* @param bindings the bindings
237+
* @param supplier a global SCP resource supplier
238+
*/
239+
protected void injectGlobalProps(Bindings bindings, Supplier<Optional<Resource>> supplier) {
240+
if (isLazy(bindings)) {
241+
bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE,
242+
wrapSupplier(() -> supplier.get().orElse(null)));
243+
bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES,
244+
wrapSupplier(() -> supplier.get().map(Resource::getValueMap).orElse(null)));
245+
} else {
246+
supplier.get().ifPresent(value -> {
247+
bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE, value);
248+
bindings.put(SharedComponentProperties.GLOBAL_PROPERTIES, value.getValueMap());
249+
});
250+
}
251+
}
252+
253+
/**
254+
* Injects Shared SCP keys into the provided bindings in one of two ways:
255+
* 1. lazily, if {@code bindings} is an instance of {@code LazyBindings}
256+
* 2. immediately, for all other kinds of {@code Bindings}
257+
*
258+
* @param bindings the bindings
259+
* @param supplier a shared SCP resource supplier
260+
*/
261+
protected void injectSharedProps(Bindings bindings, Supplier<Optional<Resource>> supplier) {
262+
if (isLazy(bindings)) {
263+
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE,
264+
wrapSupplier(() -> supplier.get().orElse(null)));
265+
bindings.put(SharedComponentProperties.SHARED_PROPERTIES,
266+
wrapSupplier(() -> supplier.get().map(Resource::getValueMap).orElse(null)));
267+
} else {
268+
supplier.get().ifPresent(value -> {
269+
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE, value);
270+
bindings.put(SharedComponentProperties.SHARED_PROPERTIES, value.getValueMap());
271+
});
272+
}
273+
}
274+
275+
/**
276+
* Injects the Merged SCP Properties key into the provided bindings in one of two ways:
277+
* 1. lazily, if {@code bindings} is an instance of {@code LazyBindings}
278+
* 2. immediately, for all other kinds of {@code Bindings}
279+
*
280+
* @param bindings the bindings
281+
* @param supplier a merged SCP ValueMap supplier
282+
*/
283+
protected void injectMergedProps(Bindings bindings, Supplier<ValueMap> supplier) {
284+
if (isLazy(bindings)) {
285+
bindings.put(SharedComponentProperties.MERGED_PROPERTIES, wrapSupplier(supplier));
286+
} else {
287+
bindings.put(SharedComponentProperties.MERGED_PROPERTIES, supplier.get());
288+
}
289+
}
290+
64291
@Override
65292
public void addBindings(final Bindings bindings) {
66293
final SlingHttpServletRequest request = (SlingHttpServletRequest) bindings.get(SlingBindings.REQUEST);
@@ -83,38 +310,35 @@ private void setSharedProperties(final Bindings bindings,
83310
if (rootPagePath != null) {
84311
// set this value even when global or shared resources are not found to indicate cache validity downstream
85312
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PAGE_PATH, rootPagePath);
313+
86314
String globalPropsPath = sharedComponentProperties.getGlobalPropertiesPath(resource);
87-
if (globalPropsPath != null) {
88-
bindings.putAll(cache.getBindings(globalPropsPath, (newBindings) -> {
89-
final Resource globalPropsResource = resource.getResourceResolver().getResource(globalPropsPath);
90-
if (globalPropsResource != null) {
91-
newBindings.put(SharedComponentProperties.GLOBAL_PROPERTIES, globalPropsResource.getValueMap());
92-
newBindings.put(SharedComponentProperties.GLOBAL_PROPERTIES_RESOURCE, globalPropsResource);
93-
}
94-
}));
95-
}
315+
// perform null path check within the supplier
316+
final Supplier<Optional<Resource>> supplyGlobalResource = () ->
317+
globalPropsPath != null
318+
? cache.getResource(globalPropsPath, resource.getResourceResolver()::getResource)
319+
: Optional.empty();
320+
injectGlobalProps(bindings, supplyGlobalResource);
96321

97322
final String sharedPropsPath = sharedComponentProperties.getSharedPropertiesPath(resource);
98-
if (sharedPropsPath != null) {
99-
bindings.putAll(cache.getBindings(sharedPropsPath, (newBindings) -> {
100-
Resource sharedPropsResource = resource.getResourceResolver().getResource(sharedPropsPath);
101-
if (sharedPropsResource != null) {
102-
newBindings.put(SharedComponentProperties.SHARED_PROPERTIES, sharedPropsResource.getValueMap());
103-
newBindings.put(SharedComponentProperties.SHARED_PROPERTIES_RESOURCE, sharedPropsResource);
104-
}
105-
}));
106-
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PATH, sharedPropsPath);
107-
}
323+
// perform null path check within the supplier
324+
final Supplier<Optional<Resource>> supplySharedResource = () ->
325+
sharedPropsPath != null
326+
? cache.getResource(sharedPropsPath, resource.getResourceResolver()::getResource)
327+
: Optional.empty();
328+
injectSharedProps(bindings, supplySharedResource);
329+
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PATH, sharedPropsPath);
108330

109331
final String mergedPropertiesPath = resource.getPath();
110-
bindings.putAll(cache.getBindings(mergedPropertiesPath, (newBindings) -> {
111-
ValueMap globalPropertyMap = (ValueMap) bindings.get(SharedComponentProperties.GLOBAL_PROPERTIES);
112-
ValueMap sharedPropertyMap = (ValueMap) bindings.get(SharedComponentProperties.SHARED_PROPERTIES);
113-
newBindings.put(SharedComponentProperties.MERGED_PROPERTIES,
114-
sharedComponentProperties.mergeProperties(globalPropertyMap, sharedPropertyMap, resource));
115-
}));
332+
final Supplier<ValueMap> supplyMergedProperties = () ->
333+
cache.getMergedProperties(mergedPropertiesPath, (path) -> {
334+
ValueMap globalPropertyMap = supplyGlobalResource.get().map(Resource::getValueMap).orElse(ValueMap.EMPTY);
335+
ValueMap sharedPropertyMap = supplySharedResource.get().map(Resource::getValueMap).orElse(ValueMap.EMPTY);
336+
return sharedComponentProperties.mergeProperties(globalPropertyMap, sharedPropertyMap, resource);
337+
});
338+
injectMergedProps(bindings, supplyMergedProperties);
339+
116340
// set this value to indicate cache validity downstream
117-
bindings.put(SharedComponentProperties.MERGED_PROPERTIES_PATH, resource.getPath());
341+
bindings.put(SharedComponentProperties.MERGED_PROPERTIES_PATH, mergedPropertiesPath);
118342
}
119343
}
120344

0 commit comments

Comments
 (0)