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 .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 ;
37
44
38
45
/**
39
46
* Bindings Values Provider that adds bindings for globalProperties,
55
62
public class SharedComponentPropertiesBindingsValuesProvider implements BindingsValuesProvider {
56
63
private static final Logger log = LoggerFactory .getLogger (SharedComponentPropertiesBindingsValuesProvider .class );
57
64
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
+
58
72
/**
59
73
* Bind if available, check for null when reading.
60
74
*/
61
75
@ Reference (policyOption = ReferencePolicyOption .GREEDY , cardinality = ReferenceCardinality .OPTIONAL_UNARY )
62
76
SharedComponentProperties sharedComponentProperties ;
63
77
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
+
64
291
@ Override
65
292
public void addBindings (final Bindings bindings ) {
66
293
final SlingHttpServletRequest request = (SlingHttpServletRequest ) bindings .get (SlingBindings .REQUEST );
@@ -83,38 +310,35 @@ private void setSharedProperties(final Bindings bindings,
83
310
if (rootPagePath != null ) {
84
311
// set this value even when global or shared resources are not found to indicate cache validity downstream
85
312
bindings .put (SharedComponentProperties .SHARED_PROPERTIES_PAGE_PATH , rootPagePath );
313
+
86
314
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 );
96
321
97
322
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 );
108
330
109
331
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
+
116
340
// 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 );
118
342
}
119
343
}
120
344
0 commit comments