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