Skip to content

Commit 5f7fc40

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

File tree

5 files changed

+621
-132
lines changed

5 files changed

+621
-132
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: 247 additions & 20 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,14 @@
3435
import org.slf4j.LoggerFactory;
3536

3637
import javax.script.Bindings;
38+
import java.lang.ref.WeakReference;
39+
import java.lang.reflect.InvocationHandler;
40+
import java.lang.reflect.Method;
41+
import java.lang.reflect.Proxy;
42+
import java.util.Optional;
43+
import java.util.function.Function;
44+
import java.util.function.Supplier;
45+
import java.util.stream.Stream;
3746

3847
/**
3948
* Bindings Values Provider that adds bindings for globalProperties,
@@ -55,12 +64,232 @@
5564
public class SharedComponentPropertiesBindingsValuesProvider implements BindingsValuesProvider {
5665
private static final Logger log = LoggerFactory.getLogger(SharedComponentPropertiesBindingsValuesProvider.class);
5766

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

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

97322
final String sharedPropsPath = sharedComponentProperties.getSharedPropertiesPath(resource);
98323
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-
}));
324+
final Function<String, Resource> computer = resource.getResourceResolver()::getResource;
325+
final Supplier<Optional<Resource>> supplier = () -> cache.getResource(sharedPropsPath, computer);
326+
injectSharedProps(bindings, supplier);
106327
bindings.put(SharedComponentProperties.SHARED_PROPERTIES_PATH, sharedPropsPath);
107328
}
108329

109330
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-
}));
331+
// don't directly capture the bindings passed to this method. Capture a weak reference to it instead.
332+
final WeakReference<Bindings> bindingsRef = new WeakReference<>(bindings);
333+
final Function<String, ValueMap> computer = (path) -> {
334+
return Optional.ofNullable(bindingsRef.get()).map(weakBindings -> {
335+
// if the bindings is a LazyBindings, this will trigger evaluation of Global and Shared properties
336+
ValueMap globalPropertyMap = (ValueMap) weakBindings.get(SharedComponentProperties.GLOBAL_PROPERTIES);
337+
ValueMap sharedPropertyMap = (ValueMap) weakBindings.get(SharedComponentProperties.SHARED_PROPERTIES);
338+
return sharedComponentProperties.mergeProperties(globalPropertyMap, sharedPropertyMap, resource);
339+
}).orElse(ValueMap.EMPTY);
340+
};
341+
final Supplier<ValueMap> supplier = () -> cache.getMergedProperties(mergedPropertiesPath, computer);
342+
injectMergedProps(bindings, supplier);
116343
// set this value to indicate cache validity downstream
117344
bindings.put(SharedComponentProperties.MERGED_PROPERTIES_PATH, resource.getPath());
118345
}

0 commit comments

Comments
 (0)