2020package com .adobe .acs .commons .wcm .properties .shared .impl ;
2121
2222import com .adobe .acs .commons .wcm .properties .shared .SharedComponentProperties ;
23+ import org .apache .felix .scr .annotations .Activate ;
2324import org .apache .felix .scr .annotations .Component ;
2425import org .apache .felix .scr .annotations .Reference ;
2526import org .apache .felix .scr .annotations .ReferenceCardinality ;
3435import org .slf4j .LoggerFactory ;
3536
3637import 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,
5564public 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 this is a SlingBindings instance (and therefore an instance of LazyBindings) or
236+ * 2. immediately, for all other kinds of Bindings.
237+ * This method should not need to be changed when pre-6.5.7 support is dropped.
238+ *
239+ * @param bindings the bindings
240+ * @param supplier a global SCP resource supplier
241+ */
242+ protected void injectGlobalProps (Bindings bindings , Supplier <Optional <Resource >> supplier ) {
243+ if (isLazy (bindings )) {
244+ bindings .put (SharedComponentProperties .GLOBAL_PROPERTIES_RESOURCE ,
245+ wrapSupplier (() -> supplier .get ().orElse (null )));
246+ bindings .put (SharedComponentProperties .GLOBAL_PROPERTIES ,
247+ wrapSupplier (() -> supplier .get ().map (Resource ::getValueMap ).orElse (null )));
248+ } else {
249+ supplier .get ().ifPresent (value -> {
250+ bindings .put (SharedComponentProperties .GLOBAL_PROPERTIES_RESOURCE , value );
251+ bindings .put (SharedComponentProperties .GLOBAL_PROPERTIES , value .getValueMap ());
252+ });
253+ }
254+ }
255+
256+ /**
257+ * Injects Shared SCP keys into the provided bindings in one of two ways:
258+ * 1. lazily if this is a SlingBindings instance (and therefore an instance of LazyBindings) or
259+ * 2. immediately, for all other kinds of Bindings.
260+ * This method should not need to be changed when pre-6.5.7 support is dropped.
261+ *
262+ * @param bindings the bindings
263+ * @param supplier a shared SCP resource supplier
264+ */
265+ protected void injectSharedProps (Bindings bindings , Supplier <Optional <Resource >> supplier ) {
266+ if (isLazy (bindings )) {
267+ bindings .put (SharedComponentProperties .SHARED_PROPERTIES_RESOURCE ,
268+ wrapSupplier (() -> supplier .get ().orElse (null )));
269+ bindings .put (SharedComponentProperties .SHARED_PROPERTIES ,
270+ wrapSupplier (() -> supplier .get ().map (Resource ::getValueMap ).orElse (null )));
271+ } else {
272+ supplier .get ().ifPresent (value -> {
273+ bindings .put (SharedComponentProperties .SHARED_PROPERTIES_RESOURCE , value );
274+ bindings .put (SharedComponentProperties .SHARED_PROPERTIES , value .getValueMap ());
275+ });
276+ }
277+ }
278+
279+ /**
280+ * Injects the Merged SCP Properties key into the provided bindings in one of two ways:
281+ * 1. lazily, if this is a SlingBindings instance (and therefore an instance of LazyBindings) or
282+ * 2. immediately, for all other kinds of Bindings.
283+ * This method should not need to be changed when pre-6.5.7 support is dropped, except maybe to change
284+ * {@code if (bindings instanceof LazyBindings)} to {@code if (bindings instanceof LazyBindings)} to
285+ * support lazy injection more generally.
286+ *
287+ * @param bindings the bindings
288+ * @param supplier a merged SCP ValueMap supplier
289+ */
290+ protected void injectMergedProps (Bindings bindings , Supplier <ValueMap > supplier ) {
291+ if (isLazy (bindings )) {
292+ bindings .put (SharedComponentProperties .MERGED_PROPERTIES , wrapSupplier (supplier ));
293+ } else {
294+ bindings .put (SharedComponentProperties .MERGED_PROPERTIES , supplier .get ());
295+ }
296+ }
297+
64298 @ Override
65299 public void addBindings (final Bindings bindings ) {
66300 final SlingHttpServletRequest request = (SlingHttpServletRequest ) bindings .get (SlingBindings .REQUEST );
@@ -85,34 +319,32 @@ private void setSharedProperties(final Bindings bindings,
85319 bindings .put (SharedComponentProperties .SHARED_PROPERTIES_PAGE_PATH , rootPagePath );
86320 String globalPropsPath = sharedComponentProperties .getGlobalPropertiesPath (resource );
87321 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- }));
322+ final Function <String , Resource > computer = resource .getResourceResolver ()::getResource ;
323+ final Supplier <Optional <Resource >> supplier = () -> cache .getResource (globalPropsPath , computer );
324+ injectGlobalProps (bindings , supplier );
95325 }
96326
97327 final String sharedPropsPath = sharedComponentProperties .getSharedPropertiesPath (resource );
98328 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- }));
329+ final Function <String , Resource > computer = resource .getResourceResolver ()::getResource ;
330+ final Supplier <Optional <Resource >> supplier = () -> cache .getResource (sharedPropsPath , computer );
331+ injectSharedProps (bindings , supplier );
106332 bindings .put (SharedComponentProperties .SHARED_PROPERTIES_PATH , sharedPropsPath );
107333 }
108334
109335 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- }));
336+ // don't directly capture the bindings passed to this method. Capture a weak reference to it instead.
337+ final WeakReference <Bindings > bindingsRef = new WeakReference <>(bindings );
338+ final Function <String , ValueMap > computer = (path ) -> {
339+ return Optional .ofNullable (bindingsRef .get ()).map (weakBindings -> {
340+ // if the bindings is a LazyBindings, this will trigger evaluation of Global and Shared properties
341+ ValueMap globalPropertyMap = (ValueMap ) weakBindings .get (SharedComponentProperties .GLOBAL_PROPERTIES );
342+ ValueMap sharedPropertyMap = (ValueMap ) weakBindings .get (SharedComponentProperties .SHARED_PROPERTIES );
343+ return sharedComponentProperties .mergeProperties (globalPropertyMap , sharedPropertyMap , resource );
344+ }).orElse (ValueMap .EMPTY );
345+ };
346+ final Supplier <ValueMap > supplier = () -> cache .getMergedProperties (mergedPropertiesPath , computer );
347+ injectMergedProps (bindings , supplier );
116348 // set this value to indicate cache validity downstream
117349 bindings .put (SharedComponentProperties .MERGED_PROPERTIES_PATH , resource .getPath ());
118350 }
0 commit comments