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         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         // TODO To Remove once AssetProvider support this art of call or when MGNLDAM-418 is resolved.
239         Map<String, Class<? extends AssetMetadata>> supportedMetadata = new HashMap<String, Class<? extends AssetMetadata>>();
240         supportedMetadata.put("mgnl", MagnoliaAssetMetadata.class);
241         supportedMetadata.put("dc", DublinCore.class);
242         // TODO find a better way to filer the non desired property
243         List<String> rejectedGetter = Arrays.asList("getAssetProvider", "getMetadata", "getContentStream", "getParent", "getClass");
244 
245         // Set Asset Property
246         Map<String, Object> rootMap = convertBeanToMap(asset, rejectedGetter);
247         // Create the first level of metadata
248         Map<String, Object> metaDatasMap = new HashMap<String, Object>();
249         // Add all supported metadata's
250         for (Entry<String, Class<? extends AssetMetadata>> entry : supportedMetadata.entrySet()) {
251             AssetMetadata metaData = asset.getMetadata(entry.getValue());
252             Map<String, Object> metaDataMap = convertBeanToMap(metaData, rejectedGetter);
253             metaDatasMap.put(entry.getKey(), Collections.unmodifiableMap(metaDataMap));
254         }
255         rootMap.put(METADATA_KEY_ACCESS, Collections.unmodifiableMap(metaDatasMap));
256 
257         return Collections.unmodifiableMap(rootMap);
258     }
259 
260     public Map<String, Object> getAssetMap(String itemKey) {
261         return getAssetMap(getAsset(itemKey));
262     }
263 
264     /**
265      * @return The {@link Asset#getLink()} or null in case of exception or if Asset is not found.
266      */
267     public String getAssetLink(String assetKey) {
268         Asset asset = getAsset(assetKey);
269         if (asset != null) {
270             return asset.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(String itemKey, String renditionName) {
279         AssetRendition rendition = getRendition(itemKey, renditionName);
280         if (rendition != null) {
281             return rendition.getLink();
282         }
283         return null;
284     }
285 
286     /**
287      * @return The {@link AssetRendition#getLink()} or null in case of exception or if {@link Asset} or {@link AssetRendition} are not found.
288      */
289     public String getAssetLink(Asset asset, MediaType mediaType, String renditionName) {
290         AssetRendition rendition = getRendition(asset, mediaType, renditionName);
291         if (rendition != null) {
292             return rendition.getLink();
293         }
294         return null;
295     }
296 
297     /**
298      * @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.
299      */
300     public Map<String, Object> getAssetMapForAssetId(String assetKey) {
301         Asset asset = getAsset(assetKey);
302         if (asset != null) {
303             return getAssetMap(asset);
304         }
305         return null;
306     }
307 
308     // ==== Methods of the previous DamTemplatingFunctions API which are implemented for compatibility reason
309 
310     /**
311      * Retrieve an Asset List based on a folder identifier ({@link ItemKey#asString()}).
312      *
313      * @return The Assets List found for this folder identifier, or an Empty
314      * List if nothing found or in case of Exception.
315      * @deprecated use {@link #getFolder(String)} and the {@link Folder#getChildren()}.
316      */
317     @Deprecated
318     public List<Asset> getAssetsFromFolderId(String folderKey) {
319         Folder folder = getFolder(folderKey);
320         if (folder != null) {
321             return Lists.newArrayList(onlyAssets(folder.getChildren()));
322         }
323         return new ArrayList<Asset>();
324     }
325 
326     /**
327      * @return {@link Asset} found under this path for the {@link DamConstants#DEFAULT_JCR_PROVIDER_ID} or null otherwise.
328      * @deprecated use {@link #getAssetForAbsolutePath(String, String)}.
329      */
330     @Deprecated
331     public Asset getAssetForPath(String assetPath) {
332         return getAssetForAbsolutePath(DAM_VERSION_1_PROVIDER_ID, assetPath);
333     }
334 
335     /**
336      * @deprecated use {@link #getAsset(String)}.
337      */
338     @Deprecated
339     public Asset getAssetForId(String assetIdentifier) {
340         return getAsset(assetIdentifier);
341     }
342 
343     /**
344      * @return The specified {@link Asset} based on the rendition Name. <br>
345      * In case of no rendition found, return the same Asset. <br>
346      * Return null in case of exception.
347      * @deprecated use {@link #getRendition(Asset, MediaType, String)}.
348      */
349     @Deprecated
350     public Asset getAssetRendition(Asset asset, String renditionName) {
351         AssetRendition assetRendition = getRendition(asset, renditionName);
352         if (assetRendition != null) {
353             return getAssetRendition(assetRendition);
354         }
355         return asset;
356     }
357 
358     /**
359      * @return The {@link Asset} based on the rendition Name and itemKey. <br>
360      * In case of no rendition found, return the same Asset. <br>
361      * Return null in case of exception.
362      * @deprecated use {@link #getRendition(String, MediaType, String)}.
363      */
364     @Deprecated
365     public Asset getAssetRenditionForAssetId(String itemKey, String renditionName) {
366         AssetRendition assetRendition = getRendition(itemKey, renditionName);
367         if (assetRendition != null) {
368             return getAssetRendition(assetRendition);
369         }
370         return getAsset(itemKey);
371     }
372 
373     /**
374      * @return if the rendition is already an {@link Asset} instance, return the rendition as an {@link Asset}. Else call {@link AssetRendition#getAsset()}.
375      */
376     private Asset getAssetRendition(AssetRendition assetRendition) {
377         if (assetRendition != null) {
378             if (assetRendition instanceof Asset) {
379                 return (Asset) assetRendition;
380             }
381             return assetRendition.getAsset();
382         }
383         return null;
384     }
385 
386     /**
387      * @return The {@link Asset#getLink()} or null in case of exception or if Asset is not found.
388      * @deprecated use {@link #getAssetLink(String)}.
389      */
390     @Deprecated
391     public String getAssetLinkForId(String assetKey) {
392         return getAssetLink(assetKey);
393     }
394 
395     /**
396      * @return The {@link AssetRendition#getLink()} or null in case of exception or if Asset is not found.
397      * @deprecated use {@link #getAssetLink(String, String)}.
398      */
399     @Deprecated
400     public String getAssetLinkForId(String itemKey, String renditionName) {
401         return getAssetLink(itemKey, renditionName);
402     }
403 
404     /**
405      * @return an Asset List found for this asset assetQuery and {@link DamConstants#DEFAULT_JCR_PROVIDER_ID} assetProvider, or an empty
406      * list if nothing found or in case of Exception.
407      * @deprecated use {@link #getItems(String, AssetQuery)}.
408      */
409     @Deprecated
410     public List<Asset> getAssetsForFilter(AssetQuery assetQuery) {
411         Iterator<Item> items = getItems(DAM_VERSION_1_PROVIDER_ID, assetQuery);
412         if (items != null) {
413             return Lists.newArrayList(onlyAssets(items));
414         }
415         return new ArrayList<Asset>();
416     }
417 
418     @SuppressWarnings("unchecked")
419     // can cast to <Asset> because non-Assets are removed
420     private static UnmodifiableIterator<Asset> onlyAssets(Iterator<? extends Item> unfiltered) {
421         return (UnmodifiableIterator<Asset>) Iterators.filter(unfiltered, new Predicate<Item>() {
422             @Override
423             public boolean apply(Item input) {
424                 return input.isAsset();
425             }
426         });
427     }
428 
429     /**
430      * Convert all bean properties with getter's to a {@link Map}.
431      *
432      * @param rejectedGetter do not create a {@link Map} entry for getter's that are part of this list.
433      */
434     private Map<String, Object> convertBeanToMap(Object source, List<String> rejectedGetter) {
435         Map<String, Object> map = new HashMap<String, Object>();
436         try {
437             BeanInfo info = Introspector.getBeanInfo(source.getClass());
438             for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
439                 Method reader = pd.getReadMethod();
440                 if (reader != null && !rejectedGetter.contains(reader.getName()))
441                     map.put(pd.getName(), reader.invoke(source));
442             }
443         } catch (Exception e) {
444             log.warn("Could not populate the map with the bean property", e);
445         }
446         return map;
447     }
448 
449     /**
450      * @return the Item linked to the itemKey or null in any case of exception.
451      */
452     private Item getItem(String itemKey, boolean assetItem) {
453         try {
454             validateItemKey(itemKey);
455             ItemKey key = ItemKey.from(itemKey);
456             Item item = this.providerRegistry.getProviderFor(key).getItem(key);
457 
458             return determineItemToReturn(item, assetItem);
459         } catch (Exception e) {
460             log.warn("The following ItemKey '{}' generate exeptions when trying to retrieve the associated Item : {}", itemKey, e.getMessage());
461         }
462         return null;
463     }
464     /**
465      * @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
466      * (the elements in the folder being automatically wrapped upon calling its getNodes(..)).
467      */
468     private Item determineItemToReturn(Item item, boolean assetItem) {
469         boolean isAsset = assetItem && item.isAsset();
470         boolean isFolder = !assetItem && item.isFolder();
471         boolean isJcrAsset = isAsset && (item instanceof JcrAsset && item.getAssetProvider() instanceof JcrAssetProvider);
472         boolean isJcrFolder = isFolder && (item instanceof JcrFolder && item.getAssetProvider() instanceof JcrAssetProvider);
473 
474         if(isJcrAsset) {
475             return new JcrAsset((JcrAssetProvider) item.getAssetProvider(), new HTMLEscapingNodeWrapper(((JcrAsset)item).getNode(), true));
476 
477         } else if(isJcrFolder) {
478             return new JcrFolder((JcrAssetProvider) item.getAssetProvider(), new HTMLEscapingNodeWrapper(((JcrFolder)item).getNode(), true));
479 
480         } else if(isAsset || isFolder){
481             return item;
482         }
483         log.warn("An '{}' was requested, but a '{}' was found", (assetItem ? "Asset" : "Folder"), (!assetItem ? "Asset" : "Folder"));
484         return null;
485     }
486 
487     /**
488      * Validate ItemKey.
489      *
490      * @throws IllegalArgumentException if the itemKey is blank or not valid.
491      */
492     private void validateItemKey(String itemKey) {
493         if (StringUtils.isBlank(itemKey)) {
494             throw new IllegalArgumentException("ItemKey is null or blank.");
495         }
496         if (!ItemKey.isValid(itemKey)) {
497             throw new IllegalArgumentException("ItemKey is not valide.");
498         }
499     }
500 
501     /**
502      * @return the Item defined by the itemPath and providerId or null in any case of exception.
503      * @throws IllegalArgumentException if the requested provider is not an implementation of {@link PathAwareAssetProvider}.
504      */
505     private Item getItem(String providerId, String itemPath, boolean assetItem) {
506         AssetProvider provider = getProviderForId(providerId);
507         if (provider != null) {
508             // Check if the provider support path
509             if (!(provider instanceof PathAwareAssetProvider)) {
510                 throw new IllegalArgumentException("The provider '" + providerId + "' does not provide implementation for '" + PathAwareAssetProvider.class.getName() + "'. Retrieving an Item by path is not a supported operation ");
511             }
512             try {
513                 Item item = ((PathAwareAssetProvider) provider).getItem(itemPath);
514                 return determineItemToReturn(item, assetItem);
515             } catch (Exception e) {
516                 log.warn("The following itemPath '{}' for the provider '{}' generated exeptions when trying to retrieve the associated Asset : {}", itemPath, providerId, e.getMessage());
517             }
518         }
519         return null;
520     }
521 
522     /**
523      * @return the registered provider or null in case of exception.
524      */
525     private AssetProvider getProviderForId(String providerId) {
526         try {
527             return this.providerRegistry.getProviderById(providerId);
528         } catch (Exception e) {
529             log.warn("Exception occurred during the retrieval of the desired Asset Provider", e);
530             return null;
531         }
532     }
533 
534 }