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