20
20
package com .adobe .acs .commons .wcm .properties .shared .impl ;
21
21
22
22
import com .adobe .acs .commons .wcm .properties .shared .SharedComponentProperties ;
23
+ import org .apache .felix .scr .annotations .Activate ;
23
24
import org .apache .felix .scr .annotations .Component ;
24
25
import org .apache .felix .scr .annotations .Reference ;
25
26
import org .apache .felix .scr .annotations .ReferenceCardinality ;
34
35
import org .slf4j .LoggerFactory ;
35
36
36
37
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 ;
37
46
38
47
/**
39
48
* Bindings Values Provider that adds bindings for globalProperties,
55
64
public class SharedComponentPropertiesBindingsValuesProvider implements BindingsValuesProvider {
56
65
private static final Logger log = LoggerFactory .getLogger (SharedComponentPropertiesBindingsValuesProvider .class );
57
66
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
+
58
74
/**
59
75
* Bind if available, check for null when reading.
60
76
*/
61
77
@ Reference (policyOption = ReferencePolicyOption .GREEDY , cardinality = ReferenceCardinality .OPTIONAL_UNARY )
62
78
SharedComponentProperties sharedComponentProperties ;
63
79
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
+
64
293
@ Override
65
294
public void addBindings (final Bindings bindings ) {
66
295
final SlingHttpServletRequest request = (SlingHttpServletRequest ) bindings .get (SlingBindings .REQUEST );
@@ -85,34 +314,32 @@ private void setSharedProperties(final Bindings bindings,
85
314
bindings .put (SharedComponentProperties .SHARED_PROPERTIES_PAGE_PATH , rootPagePath );
86
315
String globalPropsPath = sharedComponentProperties .getGlobalPropertiesPath (resource );
87
316
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 );
95
320
}
96
321
97
322
final String sharedPropsPath = sharedComponentProperties .getSharedPropertiesPath (resource );
98
323
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 );
106
327
bindings .put (SharedComponentProperties .SHARED_PROPERTIES_PATH , sharedPropsPath );
107
328
}
108
329
109
330
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 );
116
343
// set this value to indicate cache validity downstream
117
344
bindings .put (SharedComponentProperties .MERGED_PROPERTIES_PATH , resource .getPath ());
118
345
}
0 commit comments