View Javadoc
1   /**
2    * This file Copyright (c) 2014-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.dam.templating.functions;
35  
36  import info.magnolia.cms.core.AggregationState;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.dam.api.Asset;
39  import info.magnolia.dam.api.AssetProvider;
40  import info.magnolia.dam.api.AssetProviderRegistry;
41  import info.magnolia.dam.api.AssetProviderRegistry.NoSuchAssetRendererException;
42  import info.magnolia.dam.api.AssetQuery;
43  import info.magnolia.dam.api.AssetRenderer;
44  import info.magnolia.dam.api.AssetRendition;
45  import info.magnolia.dam.api.Folder;
46  import info.magnolia.dam.api.Item;
47  import info.magnolia.dam.api.ItemKey;
48  import info.magnolia.dam.api.PathAwareAssetProvider;
49  import info.magnolia.dam.api.metadata.AssetMetadata;
50  import info.magnolia.dam.api.metadata.AssetMetadataDefinition;
51  import info.magnolia.dam.api.metadata.AssetMetadataRegistry;
52  import info.magnolia.dam.jcr.JcrAsset;
53  import info.magnolia.dam.jcr.JcrAssetProvider;
54  import info.magnolia.dam.jcr.JcrFolder;
55  import info.magnolia.jcr.wrapper.HTMLEscapingNodeWrapper;
56  import info.magnolia.objectfactory.Components;
57  
58  import java.beans.BeanInfo;
59  import java.beans.Introspector;
60  import java.beans.PropertyDescriptor;
61  import java.lang.reflect.Method;
62  import java.util.ArrayList;
63  import java.util.Arrays;
64  import java.util.Collections;
65  import java.util.HashMap;
66  import java.util.Iterator;
67  import java.util.List;
68  import java.util.Map;
69  import java.util.Map.Entry;
70  
71  import javax.inject.Inject;
72  import javax.inject.Singleton;
73  
74  import org.apache.commons.lang3.StringUtils;
75  import org.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  import com.google.common.base.Predicate;
79  import com.google.common.collect.Iterators;
80  import com.google.common.collect.Lists;
81  import com.google.common.collect.UnmodifiableIterator;
82  import com.google.common.net.MediaType;
83  
84  /**
85   * DAM templating functions exposed in FTL's as "damfn".
86   */
87  @Singleton
88  public class DamTemplatingFunctions {
89  
90      private static final Logger log = LoggerFactory.getLogger(DamTemplatingFunctions.class);
91  
92      public static final String METADATA_KEY_ACCESS = "metadata";
93      public static final String DAM_VERSION_1_PROVIDER_ID = "jcr";
94  
95      private final AssetProviderRegistry providerRegistry;
96      private final AssetMetadataRegistry metadataRegistry;
97  
98      /**
99       * @deprecated since 2.1 - use {@link #DamTemplatingFunctions(AssetProviderRegistry, AssetMetadataRegistry)} instead.
100      */
101     @Deprecated
102     public DamTemplatingFunctions(AssetProviderRegistry providerRegistry) {
103         this(providerRegistry, Components.getComponent(AssetMetadataRegistry.class));
104     }
105 
106     @Inject
107     public DamTemplatingFunctions(AssetProviderRegistry providerRegistry, AssetMetadataRegistry metadataRegistry) {
108         this.providerRegistry = providerRegistry;
109         this.metadataRegistry = metadataRegistry;
110     }
111 
112     /**
113      * @see ItemKey#asString()
114      * @return null if asset was not found.
115      */
116     public Asset getAsset(String itemKey) {
117         Item item = getItem(itemKey, true);
118         if (item != null) {
119             return (Asset) item;
120         }
121         return null;
122     }
123 
124     public Asset getAsset(String providerId, String assetPath) {
125         Item item = getItem(providerId, assetPath, true);
126         if (item != null) {
127             return (Asset) item;
128         }
129         return null;
130     }
131 
132     public Asset getAssetForAbsolutePath(String providerId, String absoluteAssetPath) {
133         AssetProvider provider = getProviderForId(providerId);
134         if (provider != null) {
135             String providerRootPath = provider.getRootFolder().getPath();
136             if (!StringUtils.endsWith(providerRootPath, "/")) {
137                 providerRootPath += "/";
138             }
139             String relativeAssetPath = StringUtils.removeStart(absoluteAssetPath, providerRootPath);
140             log.debug("Convert absolute '{}' to relative '{}' path", absoluteAssetPath, relativeAssetPath);
141             return getAsset(providerId, relativeAssetPath);
142         }
143         return null;
144     }
145 
146     /**
147      * @param itemKey {@link ItemKey#asString()}.
148      * @return null if folder was not found.
149      */
150     public Folder getFolder(String itemKey) {
151         Item item = getItem(itemKey, false);
152         if (item != null) {
153             return (Folder) item;
154         }
155         return null;
156     }
157 
158     /**
159      * @param folderPath relative path to the Folder.
160      * @return null if folder was not found.
161      * @throws IllegalArgumentException if the requested provider is not an implementation of {@link PathAwareAssetProvider}.
162      */
163     public Folder getFolder(String providerId, String folderPath) {
164         Item item = getItem(providerId, folderPath, false);
165         if (item != null) {
166             return (Folder) item;
167         }
168         return null;
169     }
170 
171     /**
172      * Set {@link AssetQuery#includesFolders()} or {@link AssetQuery#includesAssets()} in order to restrict the returned Items.
173      */
174     public Iterator<Item> getItems(String providerId, AssetQuery assetQuery) {
175         AssetProvider provider = getProviderForId(providerId);
176         if (provider != null) {
177             try {
178                 return provider.list(assetQuery);
179             } catch (Exception e) {
180                 log.warn("Exception occurred for the following query '{}' and asset provider '{}' : {}", assetQuery.toString(), providerId, e.getMessage());
181             }
182         }
183         return null;
184     }
185 
186     /**
187      * Based on the passed {@link MediaType} and {@link Asset}, get an appropriate {@link AssetRenderer}.
188      * From the {@link AssetRenderer} get the {@link AssetRendition} for the given renditionName.
189      */
190     public AssetRendition getRendition(Asset asset, MediaType mediaType, String renditionName) {
191         try {
192             final AssetRenderer renderer = providerRegistry.getRendererFor(asset, mediaType);
193             if (renderer.canRender(asset, mediaType)) {
194                 return renderer.render(asset, mediaType, renditionName);
195             }
196         } catch (NoSuchAssetRendererException nsare) {
197             log.warn("No rendition found for the following assetId '{}', mediaType '{}' and renditionName '{}'. Exception: {} ", asset.getItemKey().asString(), mediaType.toString(), renditionName, nsare);
198         }
199         return null;
200     }
201 
202     /**
203      * @return {@link AssetRendition} based on the {@link MediaType} built from the {@link Asset#getMimeType()}.
204      */
205     public AssetRendition getRendition(Asset asset, String renditionName) {
206         return getRendition(asset, MediaType.parse(asset.getMimeType()), renditionName);
207     }
208 
209     public AssetRendition getRendition(String itemKey, String renditionName) {
210         Asset asset = getAsset(itemKey);
211         if (asset == null) {
212             log.warn("Trying to get asset with item key {} returned null.", itemKey);
213             return null;
214         }
215         return getRendition(asset, MediaType.parse(asset.getMimeType()), renditionName);
216     }
217 
218     public AssetRendition getRendition(String itemKey, MediaType mediaType, String renditionName) {
219         return getRendition(getAsset(itemKey), mediaType, renditionName);
220     }
221 
222     /**
223      * @return {@link AssetProvider#provides(MediaType)} or false in case of any exceptions.
224      */
225     public boolean provides(String providerId, MediaType mediaType) {
226         try {
227             return providerRegistry.getProviderById(providerId).provides(mediaType);
228         } catch (Exception e) {
229             log.warn("Exception occurred for the following reason {}. False will be retruned. ", e.getMessage());
230             return false;
231         }
232     }
233 
234     /**
235      * Create a read only map containing: <br>
236      * - All asset property names and values <br>
237      * - a metadata child map containing all supported metadata property names and values.
238      */
239     public Map<String, Object> getAssetMap(Asset asset) {
240         Map<String, Class<? extends AssetMetadata>> supportedMetadata = new HashMap<>();
241         for (AssetMetadataDefinition metadataDefinition : metadataRegistry.getAll()) {
242             if (asset.supports(metadataDefinition.getMetadataClass())) {
243                 supportedMetadata.put(metadataDefinition.getNamespace(), metadataDefinition.getMetadataClass());
244             }
245         }
246         // TODO find a better way to filer the non desired property
247         List<String> rejectedGetter = Arrays.asList("getAssetProvider", "getMetadata", "getContentStream", "getParent", "getClass", "supports");
248 
249         // Set Asset Property
250         Map<String, Object> rootMap = convertBeanToMap(asset, rejectedGetter);
251         // Create the first level of metadata
252         Map<String, Object> metaDatasMap = new HashMap<>();
253         // Add all supported metadata's
254         for (Entry<String, Class<? extends AssetMetadata>> entry : supportedMetadata.entrySet()) {
255             AssetMetadata metaData = asset.getMetadata(entry.getValue());
256             Map<String, Object> metaDataMap = convertBeanToMap(metaData, rejectedGetter);
257             metaDatasMap.put(entry.getKey(), Collections.unmodifiableMap(metaDataMap));
258         }
259         rootMap.put(METADATA_KEY_ACCESS, Collections.unmodifiableMap(metaDatasMap));
260 
261         return Collections.unmodifiableMap(rootMap);
262     }
263 
264     public Map<String, Object> getAssetMap(String itemKey) {
265         return getAssetMap(getAsset(itemKey));
266     }
267 
268     /**
269      * @return The {@link Asset#getLink()} or null in case of exception or if Asset is not found.
270      */
271     public String getAssetLink(String assetKey) {
272         Asset asset = getAsset(assetKey);
273         if (asset != null) {
274             return asset.getLink();
275         }
276         return null;
277     }
278 
279     /**
280      * @return The {@link AssetRendition#getLink()} or null in case of exception or if {@link Asset} or {@link AssetRendition} are not found.
281      */
282     public String getAssetLink(String itemKey, String renditionName) {
283         AssetRendition rendition = getRendition(itemKey, renditionName);
284         if (rendition != null) {
285             return rendition.getLink();
286         }
287         return null;
288     }
289 
290     /**
291      * @return The {@link AssetRendition#getLink()} or null in case of exception or if {@link Asset} or {@link AssetRendition} are not found.
292      */
293     public String getAssetLink(Asset asset, String renditionName) {
294         return getAssetLink(asset, MediaType.parse(asset.getMimeType()), renditionName);
295     }
296 
297     /**
298      * @return The {@link AssetRendition#getLink()} or null in case of exception or if {@link Asset} or {@link AssetRendition} are not found.
299      */
300     public String getAssetLink(Asset asset, MediaType mediaType, String renditionName) {
301         AssetRendition rendition = getRendition(asset, mediaType, renditionName);
302         if (rendition != null) {
303             return rendition.getLink();
304         }
305         return null;
306     }
307 
308     /**
309      * @return {@link #getAssetMap(Asset)} for the {@link Asset} corresponding to the assetKey. Null in case of exception or if the {@link Asset} was not found.
310      */
311     public Map<String, Object> getAssetMapForAssetId(String assetKey) {
312         Asset asset = getAsset(assetKey);
313         if (asset != null) {
314             return getAssetMap(asset);
315         }
316         return null;
317     }
318 
319     // ==== Methods of the previous DamTemplatingFunctions API which are implemented for compatibility reason
320 
321     /**
322      * Retrieve an Asset List based on a folder identifier ({@link ItemKey#asString()}).
323      *
324      * @return The Assets List found for this folder identifier, or an Empty
325      * List if nothing found or in case of Exception.
326      * @deprecated use {@link #getFolder(String)} and the {@link Folder#getChildren()}.
327      */
328     @Deprecated
329     public List<Asset> getAssetsFromFolderId(String folderKey) {
330         Folder folder = getFolder(folderKey);
331         if (folder != null) {
332             return Lists.newArrayList(onlyAssets(folder.getChildren()));
333         }
334         return new ArrayList<>();
335     }
336 
337     /**
338      * @return {@link Asset} found under this path for the {@link info.magnolia.dam.jcr.DamConstants#DEFAULT_JCR_PROVIDER_ID} or null otherwise.
339      * @deprecated use {@link #getAssetForAbsolutePath(String, String)}.
340      */
341     @Deprecated
342     public Asset getAssetForPath(String assetPath) {
343         return getAssetForAbsolutePath(DAM_VERSION_1_PROVIDER_ID, assetPath);
344     }
345 
346     /**
347      * @deprecated use {@link #getAsset(String)}.
348      */
349     @Deprecated
350     public Asset getAssetForId(String assetIdentifier) {
351         return getAsset(assetIdentifier);
352     }
353 
354     /**
355      * @return The specified {@link Asset} based on the rendition Name. <br>
356      * In case of no rendition found, return the same Asset. <br>
357      * Return null in case of exception.
358      * @deprecated use {@link #getRendition(Asset, MediaType, String)}.
359      */
360     @Deprecated
361     public Asset getAssetRendition(Asset asset, String renditionName) {
362         AssetRendition assetRendition = getRendition(asset, renditionName);
363         if (assetRendition != null) {
364             return getAssetRendition(assetRendition);
365         }
366         return asset;
367     }
368 
369     /**
370      * @return The {@link Asset} based on the rendition Name and itemKey. <br>
371      * In case of no rendition found, return the same Asset. <br>
372      * Return null in case of exception.
373      * @deprecated use {@link #getRendition(String, MediaType, String)}.
374      */
375     @Deprecated
376     public Asset getAssetRenditionForAssetId(String itemKey, String renditionName) {
377         AssetRendition assetRendition = getRendition(itemKey, renditionName);
378         if (assetRendition != null) {
379             return getAssetRendition(assetRendition);
380         }
381         return getAsset(itemKey);
382     }
383 
384     /**
385      * @return if the rendition is already an {@link Asset} instance, return the rendition as an {@link Asset}. Else call {@link AssetRendition#getAsset()}.
386      */
387     private Asset getAssetRendition(AssetRendition assetRendition) {
388         if (assetRendition != null) {
389             if (assetRendition instanceof Asset) {
390                 return (Asset) assetRendition;
391             }
392             return assetRendition.getAsset();
393         }
394         return null;
395     }
396 
397     /**
398      * @return The {@link Asset#getLink()} or null in case of exception or if Asset is not found.
399      * @deprecated use {@link #getAssetLink(String)}.
400      */
401     @Deprecated
402     public String getAssetLinkForId(String assetKey) {
403         return getAssetLink(assetKey);
404     }
405 
406     /**
407      * @return The {@link AssetRendition#getLink()} or null in case of exception or if Asset is not found.
408      * @deprecated use {@link #getAssetLink(String, String)}.
409      */
410     @Deprecated
411     public String getAssetLinkForId(String itemKey, String renditionName) {
412         return getAssetLink(itemKey, renditionName);
413     }
414 
415     /**
416      * @return an Asset List found for this asset assetQuery and {@link info.magnolia.dam.jcr.DamConstants#DEFAULT_JCR_PROVIDER_ID} assetProvider, or an empty
417      * list if nothing found or in case of Exception.
418      * @deprecated use {@link #getItems(String, AssetQuery)}.
419      */
420     @Deprecated
421     public List<Asset> getAssetsForFilter(AssetQuery assetQuery) {
422         Iterator<Item> items = getItems(DAM_VERSION_1_PROVIDER_ID, assetQuery);
423         if (items != null) {
424             return Lists.newArrayList(onlyAssets(items));
425         }
426         return new ArrayList<>();
427     }
428 
429     @SuppressWarnings("unchecked")
430     // can cast to <Asset> because non-Assets are removed
431     private static UnmodifiableIterator<Asset> onlyAssets(Iterator<? extends Item> unfiltered) {
432         return (UnmodifiableIterator<Asset>) Iterators.filter(unfiltered, (Predicate<Item>) Item::isAsset);
433     }
434 
435     /**
436      * Convert all bean properties with getter's to a {@link Map}.
437      *
438      * @param rejectedGetter do not create a {@link Map} entry for getter's that are part of this list.
439      */
440     private Map<String, Object> convertBeanToMap(Object source, List<String> rejectedGetter) {
441         Map<String, Object> map = new HashMap<>();
442         try {
443             BeanInfo info = Introspector.getBeanInfo(source.getClass());
444             for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
445                 Method reader = pd.getReadMethod();
446                 if (reader != null && !rejectedGetter.contains(reader.getName()))
447                     map.put(pd.getName(), reader.invoke(source));
448             }
449         } catch (Exception e) {
450             log.warn("Could not populate the map with the bean property", e);
451         }
452         return map;
453     }
454 
455     /**
456      * @return the Item linked to the itemKey or null in any case of exception.
457      */
458     private Item getItem(String itemKey, boolean assetItem) {
459         try {
460             validateItemKey(itemKey);
461             ItemKey key = ItemKey.from(itemKey);
462             Item item = this.providerRegistry.getProviderFor(key).getItem(key);
463 
464             return determineItemToReturn(item, assetItem);
465         } catch (Exception e) {
466             if (MgnlContext.isWebContext()) {
467                 AggregationState aggregationState = MgnlContext.getWebContext().getAggregationState();
468                 log.warn("The following ItemKey '{}' generated exceptions when trying to retrieve the associated Item:[{}] at "
469                         + "Node:[{}] Workspace:[{}]", itemKey, e.getMessage(), aggregationState.getCurrentContentNode(), aggregationState.getRepository());
470             } else {
471                 log.warn("The following ItemKey '{}' generated exceptions when trying to retrieve the associated Item : {} ", itemKey, e.getMessage());
472             }
473         }
474         return null;
475     }
476 
477     /**
478      * @return the item to be returned if it matches the requested type (folder or asset), else null. If the item is a JcrAsset or a JcrFolder it is wrapped in HTMLEscapingNodeWrapper in order to prevent XSS
479      * (the elements in the folder being automatically wrapped upon calling its getNodes(..)).
480      */
481     private Item determineItemToReturn(Item item, boolean assetItem) {
482         boolean isAsset = assetItem && item.isAsset();
483         boolean isFolder = !assetItem && item.isFolder();
484         boolean isJcrAsset = isAsset && (item instanceof JcrAsset && item.getAssetProvider() instanceof JcrAssetProvider);
485         boolean isJcrFolder = isFolder && (item instanceof JcrFolder && item.getAssetProvider() instanceof JcrAssetProvider);
486 
487         if (isJcrAsset) {
488             return new JcrAsset((JcrAssetProvider) item.getAssetProvider(), new HTMLEscapingNodeWrapper(((JcrAsset) item).getNode(), true));
489         } else if (isJcrFolder) {
490             return new JcrFolder((JcrAssetProvider) item.getAssetProvider(), new HTMLEscapingNodeWrapper(((JcrFolder) item).getNode(), true));
491         } else if (isAsset || isFolder) {
492             return item;
493         }
494 
495         log.warn("An '{}' was requested, but a '{}' was found", (assetItem ? "Asset" : "Folder"), (!assetItem ? "Asset" : "Folder"));
496         return null;
497     }
498 
499     /**
500      * Validate ItemKey.
501      *
502      * @throws IllegalArgumentException if the itemKey is blank or not valid.
503      */
504     private void validateItemKey(String itemKey) {
505         if (StringUtils.isBlank(itemKey)) {
506             throw new IllegalArgumentException("ItemKey is null or blank.");
507         }
508         if (!ItemKey.isValid(itemKey)) {
509             throw new IllegalArgumentException("ItemKey is not valid.");
510         }
511     }
512 
513     /**
514      * @return the Item defined by the itemPath and providerId or null in any case of exception.
515      * @throws IllegalArgumentException if the requested provider is not an implementation of {@link PathAwareAssetProvider}.
516      */
517     private Item getItem(String providerId, String itemPath, boolean assetItem) {
518         AssetProvider provider = getProviderForId(providerId);
519         if (provider != null) {
520             // Check if the provider support path
521             if (!(provider instanceof PathAwareAssetProvider)) {
522                 throw new IllegalArgumentException("The provider '" + providerId + "' does not provide implementation for '" + PathAwareAssetProvider.class.getName() + "'. Retrieving an Item by path is not a supported operation ");
523             }
524             try {
525                 Item item = ((PathAwareAssetProvider) provider).getItem(itemPath);
526                 return determineItemToReturn(item, assetItem);
527             } catch (Exception e) {
528                 log.warn("The following itemPath '{}' for the provider '{}' generated exceptions when trying to retrieve the associated Asset : {}", itemPath, providerId, e.getMessage());
529             }
530         }
531         return null;
532     }
533 
534     /**
535      * @return the registered provider or null in case of exception.
536      */
537     private AssetProvider getProviderForId(String providerId) {
538         try {
539             return this.providerRegistry.getProviderById(providerId);
540         } catch (Exception e) {
541             log.warn("Exception occurred during the retrieval of the desired Asset Provider", e);
542             return null;
543         }
544     }
545 
546 }