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 .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,
5562public 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