View Javadoc
1   /**
2    * This file Copyright (c) 2014 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.DublinCore;
49  import info.magnolia.dam.api.metadata.MagnoliaAssetMetadata;
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  
55  import java.beans.BeanInfo;
56  import java.beans.Introspector;
57  import java.beans.PropertyDescriptor;
58  import java.lang.reflect.Method;
59  import java.util.ArrayList;
60  import java.util.Arrays;
61  import java.util.Collections;
62  import java.util.HashMap;
63  import java.util.Iterator;
64  import java.util.List;
65  import java.util.Map;
66  import java.util.Map.Entry;
67  
68  import javax.inject.Inject;
69  import javax.inject.Singleton;
70  
71  import org.apache.commons.lang3.StringUtils;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  import com.google.common.base.Predicate;
76  import com.google.common.collect.Iterators;
77  import com.google.common.collect.Lists;
78  import com.google.common.collect.UnmodifiableIterator;
79  import com.google.common.net.MediaType;
80  
81  /**
82   * Asset templating function exposed in FTL's as "damfn". This class exposed
83   * useful methods for FTL's and Model Class.
84   */
85  @Singleton
86  public class DamTemplatingFunctions {
87  
88      public static final String METADATA_KEY_ACCESS = "metadata";
89      public static final String DAM_VERSION_1_PROVIDER_ID = "jcr";
90  
91      private static final Logger log = LoggerFactory.getLogger(DamTemplatingFunctions.class);
92  
93      private final AssetProviderRegistry providerRegistry;
94  
95      @Inject
96      public DamTemplatingFunctions(AssetProviderRegistry providerRegistry) {
97          this.providerRegistry = providerRegistry;
98      }
99  
100     /**
101      * @param itemKey {@link ItemKey#asString()}.
102      * @return null if asset was not found.
103      */
104     public Asset getAsset(String itemKey) {
105         Item item = getItem(itemKey, true);
106         if (item != null) {
107             return (Asset) item;
108         }
109         return null;
110     }
111 
112     /**
113      * @param providerId
114      * @param assetPath relative path to the Asset.
115      * @return null if asset was not found.
116      * @throws IllegalArgumentException if the requested provider is not an implementation of {@link PathAwareAssetProvider}.
117      */
118     public Asset getAsset(String providerId, String assetPath) {
119         Item item = getItem(providerId, assetPath, true);
120         if (item != null) {
121             return (Asset) item;
122         }
123         return null;
124     }
125 
126     /**
127      * @param providerId
128      * @param assetPath absolute path to the Asset.
129      * @return null if asset was not found.
130      * @throws IllegalArgumentException if the requested provider is not an implementation of {@link PathAwareAssetProvider}.
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 providerId
160      * @param folderPath relative path to the Folder.
161      * @return null if folder was not found.
162      * @throws IllegalArgumentException if the requested provider is not an implementation of {@link PathAwareAssetProvider}.
163      */
164     public Folder getFolder(String providerId, String folderPath) {
165         Item item = getItem(providerId, folderPath, false);
166         if (item != null) {
167             return (Folder) item;
168         }
169         return null;
170     }
171 
172     /**
173      * Set {@link AssetQuery#includesFolders()} or {@link AssetQuery#includesAssets()} in order to restrict the returned Items.
174      */
175     public Iterator<Item> getItems(String providerId, AssetQuery assetQuery) {
176         AssetProvider provider = getProviderForId(providerId);
177         if (provider != null) {
178             try {
179                 return provider.list(assetQuery);
180             } catch (Exception e) {
181                 log.warn("Exception occurred for the following query '{}' and asset provider '{}' : {}", assetQuery.toString(), providerId, e.getMessage());
182             }
183         }
184         return null;
185     }
186 
187     /**
188      * Based on the passed {@link MediaType} and {@link Asset}, get an appropriate {@link AssetRenderer}.
189      * From the {@link AssetRenderer} get the {@link AssetRendition} for the given renditionName.
190      */
191     public AssetRendition getRendition(Asset asset, MediaType mediaType, String renditionName) {
192         AssetRenderer renderer = null;
193         try {
194             renderer = providerRegistry.getRendererFor(asset, mediaType);
195             if (renderer.canRender(asset, mediaType)) {
196                 return renderer.render(asset, mediaType, renditionName);
197             }
198         } catch (NoSuchAssetRendererException nsare) {
199             log.warn("No rendition found for the following assetId '{}', mediaType '{}' and renditionName '{}'. Exception: {} ", asset.getItemKey().asString(), mediaType.toString(), renditionName, nsare);
200         }
201         return null;
202     }
203 
204     /**
205      * @return {@link AssetRendition} based on the {@link MediaType} builded from the {@link Asset#getMimeType()}.
206      */
207     public AssetRendition getRendition(Asset asset, String renditionName) {
208         return getRendition(asset, MediaType.parse(asset.getMimeType()), renditionName);
209     }
210 
211     public AssetRendition getRendition(String itemKey, String renditionName) {
212         Asset asset = getAsset(itemKey);
213         if (asset == null) {
214             log.warn("Trying to get asset with item key {} returned null.", itemKey);
215             return null;
216         }
217         return getRendition(asset, MediaType.parse(asset.getMimeType()), renditionName);
218     }
219 
220     public AssetRendition getRendition(String itemKey, MediaType mediaType, String renditionName) {
221         return getRendition(getAsset(itemKey), mediaType, renditionName);
222     }
223 
224     /**
225      * @return {@link AssetProvider#provides(MediaType)} or false in case of any exceptions.
226      */
227     public boolean provides(String providerId, MediaType mediaType) {
228         try {
229             return providerRegistry.getProviderById(providerId).provides(mediaType);
230         } catch (Exception e) {
231             log.warn("Exception occurred for the following reason {}. False will be retruned. ", e.getMessage());
232             return false;
233         }
234     }
235 
236     /**
237      * Create a read only map containing: <br>
238      * - All asset property names and values <br>
239      * - a metadata child map containing all supported metadata property names and values.
240      */
241     public Map<String, Object> getAssetMap(Asset asset) {
242         // TODO To Remove once AssetProvider support this art of call or when MGNLDAM-418 is resolved.
243         Map<String, Class<? extends AssetMetadata>> supportedMetadata = new HashMap<String, Class<? extends AssetMetadata>>();
244         supportedMetadata.put("mgnl", MagnoliaAssetMetadata.class);
245         supportedMetadata.put("dc", DublinCore.class);
246         // TODO find a better way to filer the non desired property
247         List<String> rejectedGetter = Arrays.asList("getAssetProvider", "getMetadata", "getContentStream", "getParent", "getClass");
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<String, Object>();
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, MediaType mediaType, String renditionName) {
294         AssetRendition rendition = getRendition(asset, mediaType, renditionName);
295         if (rendition != null) {
296             return rendition.getLink();
297         }
298         return null;
299     }
300 
301     /**
302      * @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.
303      */
304     public Map<String, Object> getAssetMapForAssetId(String assetKey) {
305         Asset asset = getAsset(assetKey);
306         if (asset != null) {
307             return getAssetMap(asset);
308         }
309         return null;
310     }
311 
312     // ==== Methods of the previous DamTemplatingFunctions API which are implemented for compatibility reason
313 
314     /**
315      * Retrieve an Asset List based on a folder identifier ({@link ItemKey#asString()}).
316      *
317      * @return The Assets List found for this folder identifier, or an Empty
318      * List if nothing found or in case of Exception.
319      * @deprecated use {@link #getFolder(String)} and the {@link Folder#getChildren()}.
320      */
321     @Deprecated
322     public List<Asset> getAssetsFromFolderId(String folderKey) {
323         Folder folder = getFolder(folderKey);
324         if (folder != null) {
325             return Lists.newArrayList(onlyAssets(folder.getChildren()));
326         }
327         return new ArrayList<Asset>();
328     }
329 
330     /**
331      * @return {@link Asset} found under this path for the {@link info.magnolia.dam.jcr.DamConstants#DEFAULT_JCR_PROVIDER_ID} or null otherwise.
332      * @deprecated use {@link #getAssetForAbsolutePath(String, String)}.
333      */
334     @Deprecated
335     public Asset getAssetForPath(String assetPath) {
336         return getAssetForAbsolutePath(DAM_VERSION_1_PROVIDER_ID, assetPath);
337     }
338 
339     /**
340      * @deprecated use {@link #getAsset(String)}.
341      */
342     @Deprecated
343     public Asset getAssetForId(String assetIdentifier) {
344         return getAsset(assetIdentifier);
345     }
346 
347     /**
348      * @return The specified {@link Asset} based on the rendition Name. <br>
349      * In case of no rendition found, return the same Asset. <br>
350      * Return null in case of exception.
351      * @deprecated use {@link #getRendition(Asset, MediaType, String)}.
352      */
353     @Deprecated
354     public Asset getAssetRendition(Asset asset, String renditionName) {
355         AssetRendition assetRendition = getRendition(asset, renditionName);
356         if (assetRendition != null) {
357             return getAssetRendition(assetRendition);
358         }
359         return asset;
360     }
361 
362     /**
363      * @return The {@link Asset} based on the rendition Name and itemKey. <br>
364      * In case of no rendition found, return the same Asset. <br>
365      * Return null in case of exception.
366      * @deprecated use {@link #getRendition(String, MediaType, String)}.
367      */
368     @Deprecated
369     public Asset getAssetRenditionForAssetId(String itemKey, String renditionName) {
370         AssetRendition assetRendition = getRendition(itemKey, renditionName);
371         if (assetRendition != null) {
372             return getAssetRendition(assetRendition);
373         }
374         return getAsset(itemKey);
375     }
376 
377     /**
378      * @return if the rendition is already an {@link Asset} instance, return the rendition as an {@link Asset}. Else call {@link AssetRendition#getAsset()}.
379      */
380     private Asset getAssetRendition(AssetRendition assetRendition) {
381         if (assetRendition != null) {
382             if (assetRendition instanceof Asset) {
383                 return (Asset) assetRendition;
384             }
385             return assetRendition.getAsset();
386         }
387         return null;
388     }
389 
390     /**
391      * @return The {@link Asset#getLink()} or null in case of exception or if Asset is not found.
392      * @deprecated use {@link #getAssetLink(String)}.
393      */
394     @Deprecated
395     public String getAssetLinkForId(String assetKey) {
396         return getAssetLink(assetKey);
397     }
398 
399     /**
400      * @return The {@link AssetRendition#getLink()} or null in case of exception or if Asset is not found.
401      * @deprecated use {@link #getAssetLink(String, String)}.
402      */
403     @Deprecated
404     public String getAssetLinkForId(String itemKey, String renditionName) {
405         return getAssetLink(itemKey, renditionName);
406     }
407 
408     /**
409      * @return an Asset List found for this asset assetQuery and {@link info.magnolia.dam.jcr.DamConstants#DEFAULT_JCR_PROVIDER_ID} assetProvider, or an empty
410      * list if nothing found or in case of Exception.
411      * @deprecated use {@link #getItems(String, AssetQuery)}.
412      */
413     @Deprecated
414     public List<Asset> getAssetsForFilter(AssetQuery assetQuery) {
415         Iterator<Item> items = getItems(DAM_VERSION_1_PROVIDER_ID, assetQuery);
416         if (items != null) {
417             return Lists.newArrayList(onlyAssets(items));
418         }
419         return new ArrayList<Asset>();
420     }
421 
422     @SuppressWarnings("unchecked")
423     // can cast to <Asset> because non-Assets are removed
424     private static UnmodifiableIterator<Asset> onlyAssets(Iterator<? extends Item> unfiltered) {
425         return (UnmodifiableIterator<Asset>) Iterators.filter(unfiltered, new Predicate<Item>() {
426             @Override
427             public boolean apply(Item input) {
428                 return input.isAsset();
429             }
430         });
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<String, Object>();
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 '{}' generate exeptions when trying to retrieve the associated Item : {}", itemKey, e.getMessage());
465         }
466         return null;
467     }
468     /**
469      * @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
470      * (the elements in the folder being automatically wrapped upon calling its getNodes(..)).
471      */
472     private Item determineItemToReturn(Item item, boolean assetItem) {
473         boolean isAsset = assetItem && item.isAsset();
474         boolean isFolder = !assetItem && item.isFolder();
475         boolean isJcrAsset = isAsset && (item instanceof JcrAsset && item.getAssetProvider() instanceof JcrAssetProvider);
476         boolean isJcrFolder = isFolder && (item instanceof JcrFolder && item.getAssetProvider() instanceof JcrAssetProvider);
477 
478         if(isJcrAsset) {
479             return new JcrAsset((JcrAssetProvider) item.getAssetProvider(), new HTMLEscapingNodeWrapper(((JcrAsset)item).getNode(), true));
480 
481         } else if(isJcrFolder) {
482             return new JcrFolder((JcrAssetProvider) item.getAssetProvider(), new HTMLEscapingNodeWrapper(((JcrFolder)item).getNode(), true));
483 
484         } else if(isAsset || isFolder){
485             return item;
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 valide.");
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 exeptions 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 }