View Javadoc
1   /**
2    * This file Copyright (c) 2011-2017 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.templating.functions;
35  
36  import info.magnolia.cms.beans.config.ServerConfiguration;
37  import info.magnolia.cms.beans.config.URI2RepositoryManager;
38  import info.magnolia.cms.core.AggregationState;
39  import info.magnolia.cms.core.NodeData;
40  import info.magnolia.cms.i18n.I18nContentSupport;
41  import info.magnolia.cms.util.ContentUtil;
42  import info.magnolia.cms.util.PathUtil;
43  import info.magnolia.cms.util.QueryUtil;
44  import info.magnolia.cms.util.SelectorUtil;
45  import info.magnolia.cms.util.SiblingsHelper;
46  import info.magnolia.context.MgnlContext;
47  import info.magnolia.context.WebContext;
48  import info.magnolia.context.HTMLEscapingWebContextWrapper;
49  import info.magnolia.jcr.inheritance.InheritanceNodeWrapper;
50  import info.magnolia.jcr.util.ContentMap;
51  import info.magnolia.jcr.util.MetaDataUtil;
52  import info.magnolia.jcr.util.NodeTypes;
53  import info.magnolia.jcr.util.NodeUtil;
54  import info.magnolia.jcr.util.PropertyUtil;
55  import info.magnolia.jcr.util.SessionUtil;
56  import info.magnolia.jcr.wrapper.HTMLEscapingContentDecorator;
57  import info.magnolia.jcr.wrapper.HTMLEscapingNodeWrapper;
58  import info.magnolia.jcr.wrapper.I18nNodeWrapper;
59  import info.magnolia.link.LinkUtil;
60  import info.magnolia.objectfactory.Components;
61  import info.magnolia.rendering.engine.RenderingEngine;
62  import info.magnolia.rendering.template.configured.ConfiguredInheritance;
63  import info.magnolia.rendering.template.type.DefaultTemplateTypes;
64  import info.magnolia.rendering.template.type.TemplateTypeHelper;
65  import info.magnolia.repository.RepositoryConstants;
66  import info.magnolia.templating.inheritance.DefaultInheritanceContentDecorator;
67  import info.magnolia.util.EscapeUtil;
68  
69  import java.net.URI;
70  import java.net.URISyntaxException;
71  import java.util.ArrayList;
72  import java.util.Arrays;
73  import java.util.Calendar;
74  import java.util.Collection;
75  import java.util.Collections;
76  import java.util.LinkedHashMap;
77  import java.util.List;
78  import java.util.Locale;
79  import java.util.Map;
80  import java.util.Set;
81  
82  import javax.inject.Inject;
83  import javax.inject.Provider;
84  import javax.jcr.Node;
85  import javax.jcr.PathNotFoundException;
86  import javax.jcr.Property;
87  import javax.jcr.PropertyType;
88  import javax.jcr.RepositoryException;
89  
90  import org.apache.commons.io.FileUtils;
91  import org.apache.commons.lang3.StringUtils;
92  import org.apache.jackrabbit.util.ISO8601;
93  import org.slf4j.Logger;
94  import org.slf4j.LoggerFactory;
95  
96  /**
97   * An object exposing several methods useful for templates. It is exposed in templates as <code>cmsfn</code>.
98   */
99  public class TemplatingFunctions {
100 
101     private static final Logger log = LoggerFactory.getLogger(TemplatingFunctions.class);
102 
103     private final Provider<WebContext> webContextProvider;
104     private final TemplateTypeHelper templateTypeHelper;
105     private final Provider<I18nContentSupport> i18nContentSupport;
106 
107     @Inject
108     public TemplatingFunctions(TemplateTypeHelper templateTypeFunctions, Provider<I18nContentSupport> i18nContentSupport, Provider<WebContext> webContextProvider) {
109         this.webContextProvider = webContextProvider;
110         this.templateTypeHelper = templateTypeFunctions;
111         this.i18nContentSupport = i18nContentSupport;
112     }
113 
114     /**
115      * @deprecated since 5.4.11/5.5.1. Use {@link #TemplatingFunctions(info.magnolia.rendering.template.type.TemplateTypeHelper, javax.inject.Provider, javax.inject.Provider)} instead.
116      */
117     @Deprecated
118     public TemplatingFunctions(Provider<AggregationState> aggregationStateProvider, TemplateTypeHelper templateTypeFunctions, Provider<I18nContentSupport> i18nContentSupport) {
119         this(templateTypeFunctions, i18nContentSupport, new Provider<WebContext>() {
120             @Override
121             public WebContext get() {
122                 return Components.getComponent(WebContext.class);
123             }
124         });
125     }
126 
127     /**
128      * @deprecated since 5.4.1 - use {@link TemplatingFunctions(Provider<AggregationState>, TemplateTypeHelper, Provider<I18nContentSupport>)} instead.
129      */
130     @Deprecated
131     public TemplatingFunctions(Provider<AggregationState> aggregationStateProvider, TemplateTypeHelper templateTypeFunctions) {
132         this(templateTypeFunctions, new Provider<I18nContentSupport>() {
133             @Override
134             public I18nContentSupport get() {
135                 return Components.getComponent(I18nContentSupport.class);
136             }
137         }, new Provider<WebContext>() {
138             @Override
139             public WebContext get() {
140                 return Components.getComponent(WebContext.class);
141             }
142         });
143     }
144 
145     /**
146      * @deprecated since 5.4 - use {@link TemplatingFunctions(Provider<AggregationState>, info.magnolia.rendering.template.type.TemplateTypeHelper)} instead.
147      */
148     @Deprecated
149     public TemplatingFunctions(Provider<AggregationState> aggregationStateProvider) {
150         this(Components.getComponent(TemplateTypeHelper.class), new Provider<I18nContentSupport>() {
151             @Override
152             public I18nContentSupport get() {
153                 return Components.getComponent(I18nContentSupport.class);
154             }
155         }, new Provider<WebContext>() {
156             @Override
157             public WebContext get() {
158                 return Components.getComponent(WebContext.class);
159             }
160         });
161     }
162 
163     /**
164      * Return the equivalent jcr node object for the provided contentMap.
165      * Useful for converting an object for use in methods that expect a node.
166      */
167     public Node asJCRNode(ContentMap contentMap) {
168         return contentMap == null ? null : contentMap.getJCRNode();
169     }
170 
171     /**
172      * Return the equivalent contentMap for the provided node object.
173      * Useful for converting a node for use in templates - or for methods that expect a contentMap.
174      */
175     public ContentMap asContentMap(Node content) {
176         return content == null ? null : new ContentMap(content);
177     }
178 
179     /**
180      * Returns the child nodes of the provided node.
181      */
182     public List<Node> children(Node content) throws RepositoryException {
183         return content == null ? null : asNodeList(NodeUtil.getNodes(content, NodeUtil.EXCLUDE_META_DATA_FILTER));
184     }
185 
186     /**
187      * Returns the child nodes of the provided node of a specific nodeType.
188      */
189     public List<Node> children(Node content, String nodeTypeName) throws RepositoryException {
190         return content == null ? null : asNodeList(NodeUtil.getNodes(content, nodeTypeName));
191     }
192 
193     /**
194      * Returns the child nodes of the provided contentMap - as a list of ContentMaps.
195      */
196     public List<ContentMap> children(ContentMap content) throws RepositoryException {
197         return content == null ? null : asContentMapList(NodeUtil.getNodes(asJCRNode(content), NodeUtil.EXCLUDE_META_DATA_FILTER));
198     }
199 
200     /**
201      * Returns the child nodes of the the provided contentMap of a specific nodeType - as a list of ContentMaps.
202      */
203     public List<ContentMap> children(ContentMap content, String nodeTypeName) throws RepositoryException {
204         return content == null ? null : asContentMapList(NodeUtil.getNodes(asJCRNode(content), nodeTypeName));
205     }
206 
207     /**
208      * See {@link #root(Node)} for details.
209      */
210     public ContentMap root(ContentMap contentMap) throws RepositoryException {
211         return contentMap == null ? null : asContentMap(this.root(contentMap.getJCRNode()));
212     }
213 
214     /**
215      * See {@link #root(Node, String)} for details.
216      */
217     public ContentMap root(ContentMap contentMap, String nodeTypeName) throws RepositoryException {
218         return contentMap == null ? null : asContentMap(this.root(contentMap.getJCRNode(), nodeTypeName));
219     }
220 
221     /**
222      * Returns the root node of the workspace to which the provided node belongs.
223      */
224     public Node root(Node content) throws RepositoryException {
225         return this.root(content, null);
226     }
227 
228     /**
229      * Returns the *oldest* ancestor node of the provided node which has the specified nodeType.
230      */
231     public Node root(Node content, String nodeTypeName) throws RepositoryException {
232         if (content == null) {
233             return null;
234         }
235         if (nodeTypeName == null) {
236             return (Node) content.getAncestor(0);
237         }
238         if (isRoot(content) && content.isNodeType(nodeTypeName)) {
239             return content;
240         }
241 
242         Node parentNode = this.parent(content, nodeTypeName);
243         while (parent(parentNode, nodeTypeName) != null) {
244             parentNode = this.parent(parentNode, nodeTypeName);
245         }
246         return parentNode;
247     }
248 
249     /**
250      * See {@link #parent(Node)} for details.
251      */
252     public ContentMap parent(ContentMap contentMap) throws RepositoryException {
253         return contentMap == null ? null : asContentMap(this.parent(contentMap.getJCRNode()));
254     }
255 
256     /**
257      * See {@link #parent(Node, String)} for details.
258      */
259     public ContentMap parent(ContentMap contentMap, String nodeTypeName) throws RepositoryException {
260         return contentMap == null ? null : asContentMap(this.parent(contentMap.getJCRNode(), nodeTypeName));
261     }
262 
263     /**
264      * Returns the direct parent of the node.
265      */
266     public Node parent(Node content) throws RepositoryException {
267         return this.parent(content, null);
268     }
269 
270     /**
271      * Returns the closest ancestor of the provided node which is of the specified nodeType.
272      * For example, can be used to find the parent page of component as in {{@link #page(Node)}.
273      */
274     public Node parent(Node content, String nodeTypeName) throws RepositoryException {
275         if (content == null) {
276             return null;
277         }
278         if (isRoot(content)) {
279             return null;
280         }
281         if (nodeTypeName == null) {
282             return content.getParent();
283         }
284         Node parent = content.getParent();
285         while (!parent.isNodeType(nodeTypeName)) {
286             if (isRoot(parent)) {
287                 return null;
288             }
289             parent = parent.getParent();
290         }
291         return parent;
292     }
293 
294     /**
295      * Returns the page's {@link ContentMap} of the passed {@link ContentMap}. If the passed {@link ContentMap} represents a page, the passed {@link ContentMap} will be returned.
296      * If the passed {@link ContentMap} has no parent page at all, null is returned.
297      *
298      * @param content The {@link ContentMap} to get the page's {@link ContentMap} from.
299      * @return The page {@link ContentMap} of the passed content {@link ContentMap}.
300      */
301     public ContentMap page(ContentMap content) throws RepositoryException {
302         return content == null ? null : asContentMap(page(content.getJCRNode()));
303     }
304 
305     /**
306      * Returns the page {@link Node} of the passed node. If the passed {@link Node} is a page, the passed {@link Node} will be returned.
307      * If the passed Node has no parent page at all, null is returned.
308      *
309      * @param content The {@link Node} to get the page from.
310      * @return The page {@link Node} of the passed content {@link Node}.
311      */
312     public Node page(Node content) throws RepositoryException {
313         if (content == null) {
314             return null;
315         }
316         if (content.isNodeType(NodeTypes.Page.NAME)) {
317             return content;
318         }
319         return parent(content, NodeTypes.Page.NAME);
320     }
321 
322     /**
323      * See {@link #ancestors(Node)} for details.
324      */
325     public List<ContentMap> ancestors(ContentMap contentMap) throws RepositoryException {
326         return ancestors(contentMap, null);
327     }
328 
329     /**
330      * See {@link #ancestors(Node, String)} for details.
331      */
332     public List<ContentMap> ancestors(ContentMap contentMap, String nodeTypeName) throws RepositoryException {
333         List<Node> ancestorsAsNodes = this.ancestors(contentMap.getJCRNode(), nodeTypeName);
334         return asContentMapList(ancestorsAsNodes);
335     }
336 
337     /**
338      * Return a list of ancestors of the provided node.
339      * The list does not include the root node and is ordered from *oldest* to *youngest*.
340      */
341     public List<Node> ancestors(Node content) throws RepositoryException {
342         return content == null ? null : this.ancestors(content, null);
343     }
344 
345     /**
346      * Return a list of ancestors of the provided node which have the provided nodeType.
347      * The list does not include the root node and is ordered from *oldest* to *youngest*.
348      */
349     public List<Node> ancestors(Node content, String nodeTypeName) throws RepositoryException {
350         if (content == null) {
351             return null;
352         }
353         List<Node> ancestors = new ArrayList<Node>();
354         int depth = content.getDepth();
355         for (int i = 1; i < depth; ++i) {
356             Node possibleAncestor = (Node) content.getAncestor(i);
357             if (nodeTypeName == null) {
358                 ancestors.add(possibleAncestor);
359             } else {
360                 if (possibleAncestor.isNodeType(nodeTypeName)) {
361                     ancestors.add(possibleAncestor);
362                 }
363             }
364         }
365         return ancestors;
366     }
367 
368     /**
369      * Wraps the provided node with an inheritance wrapper so that the node appears to
370      * contain the components and/or properties that exist in the equivalent nodes of the
371      * ancestors of the provided node's "anchor".
372      * <p>
373      * (The anchor is the first parent of type mgnl:content - typically the page node of the area). The inheritance is subject to the inheritance configuration of the node. (See {@link info.magnolia.rendering.template.configured.ConfiguredInheritance}.) <b>Note:</b> This is the most commonly used signature of the 'inherit' methods, but the inherit methods are seldom necessary because areas already support inheritance natively through configuration.
374      * </p>
375      */
376     public Node inherit(Node content) throws RepositoryException {
377         return inherit(content, null);
378     }
379 
380     /**
381      * Wraps the provided node with an inheritance wrapper so that the node appears to
382      * contain the components and/or properties that exist in the equivalent nodes of the
383      * ancestors of the provided node's "anchor".
384      * <p>
385      * (The anchor is the first parent of type mgnl:content - typically the page node of the area). The inheritance is subject to the inheritance configuration of the node. (See {@link info.magnolia.rendering.template.configured.ConfiguredInheritance}.)
386      * </p>
387      *
388      * @param relPath If not blank, the node will inherit the node specified by this relative path rather than the actual provided node.
389      * This could be used to grab a specific property from a subnode.
390      */
391     public Node inherit(Node content, String relPath) throws RepositoryException {
392         return inherit(content, relPath, StringUtils.EMPTY, ConfiguredInheritance.COMPONENTS_FILTERED, ConfiguredInheritance.PROPERTIES_ALL);
393     }
394 
395     /**
396      * Wraps the provided node with an inheritance wrapper so that the node appears to contain the components and/or
397      * properties that exist in the equivalent nodes of the ancestors of the provided node's "anchor" (The anchor is
398      * the first parent of type mgnl:content - typically the page node of the provided node).
399      *
400      * <p>The inheritance is subject to the inheritance configuration of the node, but can be overridden with the
401      * parameters of this method.</p>
402      * <p><b>Note</b>: The inherit methods are seldom necessary because areas already support inheritance natively
403      * through configuration.</p>
404      *
405      * <p>Node inheritance accepts:
406      * <dl><dt>{@link ConfiguredInheritance#COMPONENTS_ALL}</dt><dd>Inherit all components</dd>
407      * <dt>{@link ConfiguredInheritance#COMPONENTS_FILTERED}</dt><dd>Inherit only components with property
408      * <code>inheritable=true</code></dd>
409      * <dt>{@link ConfiguredInheritance#COMPONENTS_NONE}</dt><dd>Do not inherit components</dd></dl></p>
410      *
411      * <p>Property inheritance accepts:
412      * <dl><dt>{@link ConfiguredInheritance#PROPERTIES_ALL}</dt><dd>Inherit all properties of ancestors, if multiple
413      * ancestors provide the same property the one from the nearest ancestor is used</dd>
414      * <dt>{@link ConfiguredInheritance#PROPERTIES_NONE}</dt><dd>Do not inherit properties</dd></dl></p>
415      *
416      * @param relPath If not blank, the node will inherit the node specified by this relative path rather than the
417      * actual provided node. This can be used to grab a specific property from a subnode.
418      * @param nodeTypes If not blank, only ancestors of the anchor with one of the nodeTypes in this comma delimited
419      * string will be evaluated.
420      * @param nodeInheritance Specify which child components of the ancestors to inherit.
421      * @param propertyInheritance Specify which properties of the ancestors to inherit.
422      *
423      * @see info.magnolia.templating.inheritance.DefaultInheritanceContentDecorator
424      * @see info.magnolia.rendering.template.configured.ConfiguredInheritance
425      */
426     public Node inherit(Node content, String relPath, String nodeTypes, String nodeInheritance, String propertyInheritance) throws RepositoryException {
427         if (content == null) {
428             return null;
429         }
430 
431         ConfiguredInheritance configuredInheritance = new ConfiguredInheritance();
432         if (StringUtils.isNotEmpty(nodeTypes)) {
433             configuredInheritance.setNodeTypes(Arrays.asList(StringUtils.split(nodeTypes, ",")));
434         }
435         if (StringUtils.isNotEmpty(nodeInheritance)) {
436             configuredInheritance.setComponents(nodeInheritance);
437         }
438         if (StringUtils.isNotEmpty(propertyInheritance)) {
439             configuredInheritance.setProperties(propertyInheritance);
440         }
441 
442         Node inheritedNode = wrapForInheritance(content, configuredInheritance);
443 
444         if (StringUtils.isBlank(relPath)) {
445             return inheritedNode;
446         }
447 
448         try {
449             Node subNode = inheritedNode.getNode(relPath);
450             return NodeUtil.unwrap(subNode);
451         } catch (PathNotFoundException e) {
452             // TODO fgrilli: rethrow exception?
453         }
454         return null;
455     }
456 
457     /**
458      * See {@link #inherit(Node)} for details.
459      */
460     public ContentMap inherit(ContentMap content) throws RepositoryException {
461         return inherit(content, null);
462     }
463 
464     /**
465      * See {@link #inherit(Node, String)} for details.
466      */
467     public ContentMap inherit(ContentMap content, String relPath) throws RepositoryException {
468         if (content == null) {
469             return null;
470         }
471         Node node = inherit(content.getJCRNode(), relPath);
472         return node == null ? null : new ContentMap(node);
473     }
474 
475     /**
476      * See {@link #inherit(Node, String, String, String, String)} for details.
477      */
478     public ContentMap inherit(ContentMap content, String relPath, String nodeTypes, String nodeInheritance, String propertyInheritance) throws RepositoryException {
479         if (content == null) {
480             return null;
481         }
482         Node node = inherit(content.getJCRNode(), relPath, nodeTypes, nodeInheritance, propertyInheritance);
483         return node == null ? null : new ContentMap(node);
484     }
485 
486     /**
487      * Returns the inherited property of the node, subject to the standard rules of inheritance.
488      * (See {@link info.magnolia.rendering.template.configured.ConfiguredInheritance}.)
489      *
490      * @param relPath The path to the property. Can reference a property in the current (wrapped) node, or
491      * within subnodes via a slash syntax. (i.e. "myNode/mySubNode/myPropertyName")
492      */
493     public Property inheritProperty(Node content, String relPath) throws RepositoryException {
494         if (content == null) {
495             return null;
496         }
497         if (StringUtils.isBlank(relPath)) {
498             throw new IllegalArgumentException("relative path cannot be null or empty");
499         }
500         try {
501             Node inheritedNode = wrapForInheritance(content, new ConfiguredInheritance());
502             return inheritedNode.getProperty(relPath);
503 
504         } catch (PathNotFoundException e) {
505             // TODO fgrilli: rethrow exception?
506         } catch (RepositoryException e) {
507             // TODO fgrilli:rethrow exception?
508         }
509 
510         return null;
511     }
512 
513     /**
514      * See {@link #inheritProperty(Node, String)} for details.
515      */
516     public Property inheritProperty(ContentMap content, String relPath) throws RepositoryException {
517         if (content == null) {
518             return null;
519         }
520         return inheritProperty(content.getJCRNode(), relPath);
521     }
522 
523     /**
524      * Returns the children of the wrapped-for-inheritance subnode obtained as in {@link #inherit(Node, String)}.
525      */
526     public List<Node> inheritList(Node content, String relPath) throws RepositoryException {
527         if (content == null) {
528             return null;
529         }
530         if (StringUtils.isBlank(relPath)) {
531             throw new IllegalArgumentException("Relative path cannot be null or empty");
532         }
533         Node inheritedNode = wrapForInheritance(content, new ConfiguredInheritance());
534         Node subNode = inheritedNode.getNode(relPath);
535         return children(subNode);
536     }
537 
538     /**
539      * Returns the children of the wrapped-for-inheritance subnode obtained as in {@link #inherit(Node, String)}.
540      */
541     public List<ContentMap> inheritList(ContentMap content, String relPath) throws RepositoryException {
542         if (content == null) {
543             return null;
544         }
545         if (StringUtils.isBlank(relPath)) {
546             throw new IllegalArgumentException("Relative path cannot be null or empty");
547         }
548         Node node = asJCRNode(content);
549         Node inheritedNode = wrapForInheritance(node, new ConfiguredInheritance());
550         Node subNode = inheritedNode.getNode(relPath);
551         return children(new ContentMap(subNode));
552     }
553 
554     /**
555      * Returns whether the content is inherited from another node.
556      */
557     public boolean isInherited(Node content) {
558         if (content instanceof InheritanceNodeWrapper) {
559             return ((InheritanceNodeWrapper) content).isInherited();
560         }
561         return false;
562     }
563 
564     /**
565      * See {@link #isInherited(Node)} for details.
566      */
567     public boolean isInherited(ContentMap content) {
568         return isInherited(asJCRNode(content));
569     }
570 
571     /**
572      * Returns whether the content is an actual child of the current page -
573      * i.e. that it was not inherited from another page (or node).
574      */
575     public boolean isFromCurrentPage(Node content) {
576         return !isInherited(content);
577     }
578 
579     /**
580      * See {@link #isFromCurrentPage(Node)} for details.
581      */
582     public boolean isFromCurrentPage(ContentMap content) {
583         return isFromCurrentPage(asJCRNode(content));
584     }
585 
586     /**
587      * Returns a url for Node identified by nodeIdentifier in the specified workspace.
588      * Suitable for use in an html anchor.
589      */
590     public String link(String workspace, String nodeIdentifier) {
591         try {
592             return LinkUtil.createLink(workspace, nodeIdentifier);
593         } catch (RepositoryException e) {
594             handleRepositoryException(e, workspace);
595             return null;
596         }
597     }
598 
599     /**
600      * There should be no real reason to use this method except to produce link to binary content stored in jcr:data property in
601      * which case one should call {@link #link(Node)} while passing parent node as a parameter. In case you find other valid reason to use this method,
602      * please raise it in a forum discussion or create issue. Otherwise this method will be removed in the future.
603      *
604      * @deprecated since 4.5.4. There is no valid use case for this method.
605      */
606     @Deprecated
607     public String link(Property property) {
608         try {
609             Node parentNode = null;
610             String propertyName = null;
611             if (property.getType() == PropertyType.BINARY) {
612                 parentNode = property.getParent().getParent();
613                 propertyName = property.getParent().getName();
614             } else {
615                 parentNode = property.getParent();
616                 propertyName = property.getName();
617             }
618             NodeData equivNodeData = ContentUtil.asContent(parentNode).getNodeData(propertyName);
619             return LinkUtil.createLink(equivNodeData);
620         } catch (Exception e) {
621             return null;
622         }
623     }
624 
625     /**
626      * Returns a url for the provided node.
627      * Suitable for use in an html anchor.
628      */
629     public String link(Node content) {
630         return content == null ? null : LinkUtil.createLink(content);
631     }
632 
633     /**
634      * See {@link #link(Node)} for details.
635      */
636     public String link(ContentMap contentMap) throws RepositoryException {
637         return contentMap == null ? null : this.link(asJCRNode(contentMap));
638     }
639 
640     public String linkPrefix(Node content) {
641         if (!MgnlContext.isWebContext()) {
642             return MgnlContext.getContextPath();
643         }
644         String fullLinkToPage = link(content);
645         String pagePath = Components.getComponent(URI2RepositoryManager.class).getURI(LinkUtil.createLinkInstance(content));
646         int ndx = fullLinkToPage.length();
647         for (int cnt = StringUtils.countMatches(pagePath, "/"); cnt > 0; cnt--) {
648             ndx = fullLinkToPage.lastIndexOf('/', ndx - 1);
649         }
650         String linkPrefix = fullLinkToPage.substring(0, ndx);
651         return linkPrefix;
652     }
653 
654     /**
655      * See {@link #linkPrefix(Node)} for details.
656      */
657     public String linkPrefix(ContentMap content) {
658         return linkPrefix(asJCRNode(content));
659     }
660 
661     /**
662      * Returns the querystring and fragment of a url, or an empty string if there are none.
663      *
664      * @deprecated since 5.3.8 - use {@link #queryStringAndFragment(String) instead.
665      */
666     @Deprecated
667     public String getQueryStringAndFragment(String url) {
668         return queryStringAndFragment(url);
669     }
670 
671     /**
672      * Returns the querystring and fragment of a url, or an empty string if there are none.
673      */
674     public String queryStringAndFragment(String url) {
675         String result = StringUtils.EMPTY;
676         try {
677             URI uri = new URI(url);
678             String query = uri.getQuery();
679             String fragment = uri.getFragment();
680 
681             if (StringUtils.isNotEmpty(query)) {
682                 result += "?" + query;
683             }
684             if (StringUtils.isNotEmpty(fragment)) {
685                 result += "#" + fragment;
686             }
687         } catch (URISyntaxException e) {
688             log.warn("URL cannot be parsed. {0}, {1}", url, e.getMessage());
689             return StringUtils.EMPTY;
690         }
691         return result;
692     }
693 
694     /**
695      * Get the language used currently.
696      *
697      * @return The language as a String.
698      */
699     public String language() {
700         return i18nContentSupport.get().getLocale().toString();
701     }
702 
703     /**
704      * Returns an external link prepended with <code>http://</code> in case the protocol is missing or an empty String
705      * if the link does not exist.
706      *
707      * @param content The node where the link property is stored on.
708      * @param linkPropertyName The property where the link value is stored in.
709      * @return The link prepended with <code>http://</code>;
710      */
711     public String externalLink(Node content, String linkPropertyName) {
712         String externalLink = PropertyUtil.getString(content, linkPropertyName);
713         if (StringUtils.isBlank(externalLink)) {
714             return StringUtils.EMPTY;
715         }
716         // Anchors should not be prepended with protocol
717         if (!hasProtocol(externalLink) && !externalLink.startsWith("#")) {
718             externalLink = "http://" + externalLink;
719         }
720         return EscapeUtil.escapeXss(externalLink);
721     }
722 
723     /**
724      * Returns an external link prepended with <code>http://</code> in case the protocol is missing or an empty String
725      * if the link does not exist.
726      *
727      * @param content The node's map representation where the link property is stored on.
728      * @param linkPropertyName The property where the link value is stored in.
729      * @return The link prepended with <code>http://</code>;
730      */
731     public String externalLink(ContentMap content, String linkPropertyName) {
732         return externalLink(asJCRNode(content), linkPropertyName);
733     }
734 
735     /**
736      * Return a link title based on the @param linkTitlePropertyName. When property @param linkTitlePropertyName is
737      * empty or null, the link itself is provided as the linkTitle (prepended with <code>http://</code>).
738      *
739      * @param content The node where the link property is stored on.
740      * @param linkPropertyName The property where the link value is stored in.
741      * @param linkTitlePropertyName The property where the link title value is stored
742      * @return The resolved link title value
743      */
744     public String externalLinkTitle(Node content, String linkPropertyName, String linkTitlePropertyName) {
745         String linkTitle = PropertyUtil.getString(content, linkTitlePropertyName);
746         if (StringUtils.isNotEmpty(linkTitle)) {
747             return linkTitle;
748         }
749         return externalLink(content, linkPropertyName);
750     }
751 
752     /**
753      * Return a link title based on the @param linkTitlePropertyName. When property @param linkTitlePropertyName is
754      * empty or null, the link itself is provided as the linkTitle (prepended with <code>http://</code>).
755      *
756      * @param content The node where the link property is stored on.
757      * @param linkPropertyName The property where the link value is stored in.
758      * @param linkTitlePropertyName The property where the link title value is stored
759      * @return The resolved link title value
760      */
761     public String externalLinkTitle(ContentMap content, String linkPropertyName, String linkTitlePropertyName) {
762         return externalLinkTitle(asJCRNode(content), linkPropertyName, linkTitlePropertyName);
763     }
764 
765     /**
766      * Returns whether the request is from the Page editor edit mode.
767      * Specifically whether the request is on the author instance and not from the page editor in preview mode.
768      * Useful for providing messages in the page to assist the editors.
769      */
770     public boolean isEditMode() {
771         // TODO : see CmsFunctions.isEditMode, which checks a couple of other properties.
772         return isAuthorInstance() && !isPreviewMode();
773     }
774 
775     /**
776      * Returns whether the request is from the Page editor preview mode.
777      */
778     public boolean isPreviewMode() {
779         return this.webContextProvider.get().getAggregationState().isPreviewMode();
780     }
781 
782     /**
783      * Returns whether the request is from an author instance.
784      */
785     public boolean isAuthorInstance() {
786         return Components.getComponent(ServerConfiguration.class).isAdmin();
787     }
788 
789     /**
790      * Returns whether the request is from a public instance.
791      */
792     public boolean isPublicInstance() {
793         return !isAuthorInstance();
794     }
795 
796     /**
797      * Util method to create html attributes <code>name="value"</code>. If the value is empty an empty string will be returned.
798      * This is mainly helpful to avoid empty attributes.
799      */
800     public String createHtmlAttribute(String name, String value) {
801         value = StringUtils.trim(value);
802         if (StringUtils.isNotEmpty(value)) {
803             return new StringBuffer().append(name).append("=\"").append(value).append("\"").toString();
804         }
805         return StringUtils.EMPTY;
806     }
807 
808     /**
809      * Returns an instance of SiblingsHelper for the given node.
810      */
811     public SiblingsHelper siblings(Node node) throws RepositoryException {
812         return SiblingsHelper.of(ContentUtil.asContent(node));
813     }
814 
815     public SiblingsHelper siblings(ContentMap node) throws RepositoryException {
816         return siblings(asJCRNode(node));
817     }
818 
819     /**
820      * Return the Node for the given path from the website repository.
821      *
822      * @deprecated since 5.3.3 use {@link #contentByPath(String)} or {@link #nodeByPath(String)} instead
823      */
824     @Deprecated
825     public Node content(String path) {
826         return content(RepositoryConstants.WEBSITE, path);
827     }
828 
829     /**
830      * Return the Node for the given path from the given repository.
831      *
832      * @deprecated since 5.3.3 use {@link #contentByPath(String, String)} or {@link #nodeByPath(String, String)} instead
833      */
834     @Deprecated
835     public Node content(String repository, String path) {
836         return SessionUtil.getNode(repository, path);
837     }
838 
839     /**
840      * Return the Node by the given identifier from the website repository.
841      *
842      * @deprecated since 5.3.3 use {@link #contentById(String)} or {@link #nodeById(String) instead
843      */
844     @Deprecated
845     public Node contentByIdentifier(String id) {
846         return contentByIdentifier(RepositoryConstants.WEBSITE, id);
847     }
848 
849     /**
850      * Return the Node by the given identifier from the given repository.
851      *
852      * @deprecated since 5.3.3 use {@link #contentById(String, String)} or {@link #contentByPath(String, String)} instead
853      */
854     @Deprecated
855     public Node contentByIdentifier(String repository, String id) {
856         return SessionUtil.getNodeByIdentifier(repository, id);
857     }
858 
859     /**
860      * Return the ContentMap for the given path from the website repository.
861      */
862     public ContentMap contentByPath(String path) {
863         return contentByPath(path, RepositoryConstants.WEBSITE);
864     }
865 
866     /**
867      * Return the ContentMap for the given path from the given repository.
868      */
869     public ContentMap contentByPath(String path, String workspace) {
870         return asContentMap(nodeByPath(path, workspace));
871     }
872 
873     /**
874      * Return the ContentMap by the given identifier from the website repository.
875      */
876     public ContentMap contentById(String id) {
877         return contentById(id, RepositoryConstants.WEBSITE);
878     }
879 
880     /**
881      * Return the ContentMap by the given identifier from the given repository.
882      */
883     public ContentMap contentById(String id, String workspace) {
884         return asContentMap(nodeById(id, workspace));
885     }
886 
887     /**
888      * Return the Node for the given path from the website repository.
889      */
890     public Node nodeByPath(String path) {
891         return nodeByPath(path, RepositoryConstants.WEBSITE);
892     }
893 
894     /**
895      * Return the Node for the given path from the given repository.
896      */
897     public Node nodeByPath(String path, String workspace) {
898         try {
899             String pathToNode = new URI(path).getPath();
900             return webContextProvider.get().getJCRSession(workspace).getNode(pathToNode);
901         } catch (URISyntaxException e) {
902             log.warn("Path cannot be parsed. {0}, {1}", path, e.getMessage());
903             return null;
904         } catch (RepositoryException e) {
905             handleRepositoryException(e, workspace);
906             return null;
907         }
908     }
909 
910     /**
911      * Return the Node by the given identifier from the website repository.
912      */
913     public Node nodeById(String id) {
914         return nodeById(id, RepositoryConstants.WEBSITE);
915     }
916 
917     /**
918      * Return the Node by the given identifier from the given repository.
919      */
920     public Node nodeById(String id, String workspace) {
921         try {
922             return webContextProvider.get().getJCRSession(workspace).getNodeByIdentifier(id);
923         } catch (RepositoryException e) {
924             handleRepositoryException(e, workspace);
925             return null;
926         }
927     }
928 
929     private void handleRepositoryException(RepositoryException e, String workspace) {
930         final AggregationState aggregationState = webContextProvider.get().getAggregationState();
931         String template;
932         try {
933             template = NodeTypes.Renderable.getTemplate(aggregationState.getCurrentContentNode());
934         } catch (RepositoryException e1) {
935             template = "unknown";
936         }
937         log.debug("Exception in '{}' workspace when rendering template '{}' for URI '{}': {}", workspace, template, aggregationState.getOriginalBrowserURI(), e);
938 
939     }
940 
941     /**
942      * Returns a {@link Node} object which is referenced by its id, stored in
943      * the @param propertyName.
944      *
945      * @param content The node with a property containing the referenced id value.
946      * @param idPropertyName The name of the property which contains the id of the referenced {@link Node}.
947      * @param referencedWorkspace The workspace in which the referenced {@link Node} exists.
948      * @return the referenced {@link Node}
949      */
950     public Node contentByReference(Node content, String idPropertyName, String referencedWorkspace) throws RepositoryException {
951         if (content.hasProperty(idPropertyName)) {
952             final String identifier = PropertyUtil.getString(content, idPropertyName);
953             final Node node = NodeUtil.getNodeByIdentifier(referencedWorkspace, identifier);
954             return encode(wrapForI18n(node));
955         }
956         return null;
957     }
958 
959     /**
960      * See {@link #contentByReference(Node, String, String)} for details.
961      */
962     public ContentMap contentByReference(ContentMap content, String idPropertyName, String referencedWorkspace) throws RepositoryException {
963         Node node = asJCRNode(content);
964         return asContentMap(contentByReference(node, idPropertyName, referencedWorkspace));
965     }
966 
967     /**
968      * Returns a List of {@link ContentMap} objects for the provided collection of {@link Node} objects.
969      * Useful for working with a collection of Nodes in a template.
970      */
971     public List<ContentMap> asContentMapList(Collection<Node> nodeList) {
972         if (nodeList != null) {
973             List<ContentMap> contentMapList = new ArrayList<ContentMap>();
974             for (Node node : nodeList) {
975                 contentMapList.add(asContentMap(node));
976             }
977             return contentMapList;
978         }
979         return null;
980     }
981 
982     /**
983      * Returns a List of {@link Node} objects for the provided collection of {@link ContentMap} objects.
984      * Useful for working with a collection of ContentMaps in java code.
985      */
986     public List<Node> asNodeList(Collection<ContentMap> contentMapList) {
987         if (contentMapList != null) {
988             List<Node> nodeList = new ArrayList<Node>();
989             for (ContentMap node : contentMapList) {
990                 nodeList.add(node.getJCRNode());
991             }
992             return nodeList;
993         }
994         return null;
995     }
996 
997     // TODO fgrilli: should we unwrap children?
998     protected List<Node> asNodeList(Iterable<Node> nodes) {
999         List<Node> childList = new ArrayList<Node>();
1000         for (Node child : nodes) {
1001             childList.add(child);
1002         }
1003         return childList;
1004     }
1005 
1006     // TODO fgrilli: should we unwrap children?
1007     protected List<ContentMap> asContentMapList(Iterable<Node> nodes) {
1008         List<ContentMap> childList = new ArrayList<ContentMap>();
1009         for (Node child : nodes) {
1010             childList.add(new ContentMap(child));
1011         }
1012         return childList;
1013     }
1014 
1015     /**
1016      * Checks if passed string has a <code>http://</code> protocol.
1017      *
1018      * @param link The link to check
1019      * @return If @param link contains a <code>http://</code> protocol
1020      */
1021     private boolean hasProtocol(String link) {
1022         return link != null && link.contains("://");
1023     }
1024 
1025     /**
1026      * Checks if the passed {@link Node} is the jcr root '/' of the workspace.
1027      *
1028      * @param content {@link Node} to check if its root.
1029      * @return if @param content is the jcr workspace root.
1030      */
1031     private boolean isRoot(Node content) throws RepositoryException {
1032         return content.getDepth() == 0;
1033     }
1034 
1035     /**
1036      * Removes escaping of HTML on properties.
1037      */
1038     public ContentMap decode(ContentMap content) {
1039         return asContentMap(decode(content.getJCRNode()));
1040     }
1041 
1042     /**
1043      * Removes escaping of HTML on properties.
1044      */
1045     public Node decode(Node content) {
1046         return NodeUtil.deepUnwrap(content, HTMLEscapingNodeWrapper.class);
1047     }
1048 
1049     /**
1050      * Adds escaping of HTML on properties as well as changing line breaks into &lt;br/&gt; tags.
1051      */
1052     public ContentMap encode(ContentMap content) {
1053         return content != null ? new ContentMap(new HTMLEscapingNodeWrapper(content.getJCRNode(), true)) : null;
1054     }
1055 
1056     /**
1057      * Adds escaping of HTML on properties as well as changing line breaks into &lt;br/&gt; tags.
1058      */
1059     public Node encode(Node content) {
1060         return content != null ? new HTMLEscapingNodeWrapper(content, true) : null;
1061     }
1062 
1063     /**
1064      * Adds escaping of HTML on normal text as well as changing line breaks into &lt;br/&gt; tags.
1065      */
1066     public String encode(String text) {
1067         return new HTMLEscapingContentDecorator(true).decorate(text);
1068     }
1069 
1070     /**
1071      * Wraps content into {@link info.magnolia.jcr.wrapper.I18nNodeWrapper} so properties are in visitor's language.
1072      */
1073     public Node wrapForI18n(Node content) {
1074         return content != null ? new I18nNodeWrapper(content) : null;
1075     }
1076 
1077     /**
1078      * Wraps content into {@link info.magnolia.jcr.wrapper.I18nNodeWrapper} so properties are in visitor's language.
1079      */
1080     public ContentMap wrapForI18n(ContentMap content) {
1081         return content != null ? new ContentMap(wrapForI18n(content.getJCRNode())) : null;
1082     }
1083 
1084     /**
1085      * Wraps node in {@link DefaultInheritanceContentDecorator} so that the node will appear to contain
1086      * inherited components and/or properties of the ancestors of the template (typically the page).
1087      */
1088     private Node wrapForInheritance(Node destination, ConfiguredInheritance configuredInheritance) throws RepositoryException {
1089         configuredInheritance.setEnabled(true);
1090         return new DefaultInheritanceContentDecorator(destination, configuredInheritance).wrapNode(destination);
1091     }
1092 
1093     /**
1094      * Returns the string representation of a property from the metaData of the node or <code>null</code> if the node has no Magnolia metaData or if no matching property is found.
1095      */
1096     public String metaData(Node content, String property) {
1097 
1098         Object returnValue;
1099         try {
1100             if (property.equals(NodeTypes.Created.CREATED)) {
1101                 returnValue = NodeTypes.Created.getCreated(content);
1102             } else if (property.equals(NodeTypes.Created.CREATED_BY)) {
1103                 returnValue = NodeTypes.Created.getCreatedBy(content);
1104             } else if (property.equals(NodeTypes.LastModified.LAST_MODIFIED)) {
1105                 returnValue = NodeTypes.LastModified.getLastModified(content);
1106             } else if (property.equals(NodeTypes.LastModified.LAST_MODIFIED_BY)) {
1107                 returnValue = NodeTypes.LastModified.getLastModifiedBy(content);
1108             } else if (property.equals(NodeTypes.Renderable.TEMPLATE)) {
1109                 returnValue = NodeTypes.Renderable.getTemplate(content);
1110             } else if (property.equals(NodeTypes.Activatable.LAST_ACTIVATED)) {
1111                 returnValue = NodeTypes.Activatable.getLastActivated(content);
1112             } else if (property.equals(NodeTypes.Activatable.LAST_ACTIVATED_BY)) {
1113                 returnValue = NodeTypes.Activatable.getLastActivatedBy(content);
1114             } else if (property.equals(NodeTypes.Activatable.ACTIVATION_STATUS)) {
1115                 returnValue = NodeTypes.Activatable.getActivationStatus(content);
1116             } else if (property.equals(NodeTypes.Deleted.DELETED)) {
1117                 returnValue = NodeTypes.Deleted.getDeleted(content);
1118             } else if (property.equals(NodeTypes.Deleted.DELETED_BY)) {
1119                 returnValue = NodeTypes.Deleted.getDeletedBy(content);
1120             } else if (property.equals(NodeTypes.Deleted.COMMENT)) {
1121                 // Since NodeTypes.Deleted.COMMENT and NodeTypes.Versionable.COMMENT have identical names this will work for both
1122                 returnValue = NodeTypes.Deleted.getComment(content);
1123             } else {
1124 
1125                 // Try to get the value using one of the deprecated names in MetaData.
1126                 // This throws an IllegalArgumentException if its not one of those constants
1127                 returnValue = MetaDataUtil.getMetaData(content).getStringProperty(property);
1128 
1129                 // If no exception was thrown then warn that a legacy constant was used
1130                 log.warn("Deprecated constant [{}] used to query for meta data property on node [{}]", property, NodeUtil.getPathIfPossible(content));
1131             }
1132         } catch (RepositoryException e) {
1133             log.error("An error occured while trying to get [{}] property at [{}]", property, NodeUtil.getNodePathIfPossible(content), e);
1134             return null;
1135         }
1136 
1137         return returnValue instanceof Calendar ? ISO8601.format((Calendar) returnValue) : returnValue != null ? returnValue.toString() : null;
1138     }
1139 
1140     /**
1141      * See {@link #metaData(Node, String)} for details.
1142      */
1143     public String metaData(ContentMap content, String property) {
1144         return metaData(content.getJCRNode(), property);
1145     }
1146 
1147     /**
1148      * Executes query and returns result as Collection of Nodes.
1149      *
1150      * @deprecated since 5.4 - use <code>SearchTemplatingFunctions#searchContent(String, String, String, String, long, long)</code> from <code>info.magnolia.templating:magnolia-templating-essentials-models</code> instead.
1151      * @param statement has to be in formal form for chosen language
1152      */
1153     @Deprecated
1154     public Collection<Node> search(String workspace, String statement, String language, String returnItemType) {
1155         try {
1156             return NodeUtil.getCollectionFromNodeIterator(QueryUtil.search(workspace, statement, language, returnItemType));
1157         } catch (Exception e) {
1158             log.error(e.getMessage(), e);
1159         }
1160         return null;
1161     }
1162 
1163     /**
1164      * Executes simple SQL2 query and returns result as Collection of Nodes.
1165      *
1166      * @deprecated since 5.4 - use <code>SearchTemplatingFunctions#searchContent(String, String, String, String, long, long)</code> from <code>info.magnolia.templating:magnolia-templating-essentials-models</code> instead.
1167      * @param statement should be set of labels target has to contain inserted as one string each separated by comma
1168      * @param startPath can be inserted, for results without limitation set it to slash
1169      */
1170     @Deprecated
1171     public Collection<Node> simpleSearch(String workspace, String statement, String returnItemType, String startPath) {
1172         if (StringUtils.isEmpty(statement)) {
1173             log.error("Cannot search with empty statement.");
1174             return null;
1175         }
1176         String query = QueryUtil.buildQuery(statement, startPath);
1177         try {
1178             return NodeUtil.getCollectionFromNodeIterator(QueryUtil.search(workspace, query, "JCR-SQL2", returnItemType));
1179         } catch (Exception e) {
1180             log.error(e.getMessage(), e);
1181         }
1182         return null;
1183     }
1184 
1185     /**
1186      * Returns the site's root of the {@link Node}.
1187      * <p>
1188      * The root {@link Node} is defined as the page {@link Node} having template type {@link info.magnolia.rendering.template.type.DefaultTemplateTypes#SITE_ROOT}. If no ancestor page exists with type {@link info.magnolia.rendering.template.type.DefaultTemplateTypes#SITE_ROOT}, the JCR workspace root is returned.
1189      * </p>
1190      *
1191      * @param content The node to determine its site root
1192      * @return The site root {@link Node} of the passed content {@link Node}
1193      */
1194     public Node siteRoot(Node content) {
1195         return this.siteRoot(content, DefaultTemplateTypes.SITE_ROOT);
1196     }
1197 
1198     /**
1199      * See {@link #siteRoot(Node)} for details..
1200      */
1201     public ContentMap siteRoot(ContentMap content) {
1202         return this.siteRoot(content, DefaultTemplateTypes.SITE_ROOT);
1203     }
1204 
1205     /**
1206      * See {@link #siteRoot(Node, String)} for details.
1207      */
1208     public ContentMap siteRoot(ContentMap content, String siteRootTemplateType) {
1209         return asContentMap(siteRoot(content.getJCRNode(), siteRootTemplateType));
1210     }
1211 
1212     /**
1213      * Returns the site's root of the passed {@link Node}.
1214      * <p>
1215      * The root {@link Node} is defined as the page {@link Node} with the passed template type (see: {@link info.magnolia.rendering.template.type.DefaultTemplateTypes}). If no ancestor page exists with provided type, the JCR workspace root is returned.
1216      * </p>
1217      *
1218      * @param content The node to determine its site root
1219      * @param siteRootTemplateType The template type value of the site root to detect.
1220      * @return The site root {@link Node} of the passed content {@link Node}
1221      */
1222     public Node siteRoot(Node content, String siteRootTemplateType) {
1223         if (siteRootTemplateType == null) {
1224             siteRootTemplateType = DefaultTemplateTypes.SITE_ROOT;
1225         }
1226         try {
1227             final Node page = page(content);
1228             final Node root = parentWithTemplateType(page, siteRootTemplateType);
1229             return (root == null) ? (Node) page.getAncestor(0) : root;
1230         } catch (RepositoryException e) {
1231             throw new RuntimeException("Can't access site root.", e);
1232         }
1233     }
1234 
1235     /**
1236      * Returns the type of the template assigned to a node.
1237      * <p>
1238      * If the assigned template is not a {@link info.magnolia.rendering.template.TemplateDefinition} it defaults to {@link DefaultTemplateTypes#CONTENT} and if there is no template assigned or the assigned template doesn't exists it returns the empty string.
1239      * </p>
1240      */
1241     public String templateType(Node pageNode) {
1242         return templateTypeHelper.getTemplateTypeOrDefault(pageNode);
1243     }
1244 
1245     /**
1246      * See {@link #templateType(Node)}.
1247      */
1248     public String templateType(ContentMap page) {
1249         return templateType(asJCRNode(page));
1250     }
1251 
1252     /**
1253      * Returns the subtype of the template assigned to a node.
1254      * <p>
1255      * If the assigned template is not a {@link info.magnolia.rendering.template.TemplateDefinition} it defaults to {@link DefaultTemplateTypes#CONTENT} and if there is no template assigned or the assigned template doesn't exists it returns the empty string.
1256      * </p>
1257      */
1258     public String templateSubtype(Node pageNode) {
1259         return templateTypeHelper.getTemplateSubtypeOrDefault(pageNode);
1260     }
1261 
1262     /**
1263      * See {@link #templateSubtype(Node)} for details.
1264      */
1265     public String templateSubtype(ContentMap page) {
1266         return templateSubtype(asJCRNode(page));
1267     }
1268 
1269     /**
1270      * Checks whether the given page-{@link javax.jcr.Node} has the specified <code>templateType</code>.
1271      */
1272     public boolean hasTemplateOfType(Node pageNode, String templateType) {
1273         return templateTypeHelper.hasTemplateOfType(pageNode, templateType);
1274     }
1275 
1276     /**
1277      * See {@link #hasTemplateOfType(Node, String)} for details.
1278      */
1279     public boolean hasTemplateOfType(ContentMap page, String templateType) {
1280         return hasTemplateOfType(asJCRNode(page), templateType);
1281     }
1282 
1283     /**
1284      * Finds a parent {@link javax.jcr.Node} of given {@link javax.jcr.Node} with the specified <code>templateType</code>.
1285      */
1286     public Node parentWithTemplateType(Node pageNode, String templateType) throws RepositoryException {
1287         return templateTypeHelper.findParentWithTemplateType(pageNode, templateType);
1288     }
1289 
1290     /**
1291      * See {@link #parentWithTemplateType(Node, String)}.
1292      */
1293     public ContentMap parentWithTemplateType(ContentMap page, String templateType) throws RepositoryException {
1294         return asContentMap(parentWithTemplateType(asJCRNode(page), templateType));
1295     }
1296 
1297     /**
1298      * Find content objects with the given template type (and optionally subtype) below given search root.
1299      *
1300      * @param searchRoot the {@link javax.jcr.Node} to use as root of the search
1301      * @param templateType the TemplateType to search for
1302      * @param templateSubtype the TemplateSubtype to search for (optional)
1303      * @param maxResultSize setting this can drastically improve query performance, if you are interested only in a fixed number of leading result objects
1304      * @param andClause an additional "AND" clause in SQL syntax, excluding the "AND" itself, e.g. "date IS NOT NULL"
1305      * @param orderByClause an "ORDER BY" clause in SQL syntax, excluding the "ORDER BY" itself, e.g. "date desc" or "date asc"
1306      */
1307     public List<Node> contentListByTemplateType(Node searchRoot, String templateType, String templateSubtype, int maxResultSize, String andClause, String orderByClause) throws RepositoryException {
1308         return templateTypeHelper.getContentListByTemplateType(searchRoot, templateType, templateSubtype, maxResultSize, andClause, orderByClause);
1309     }
1310 
1311     /**
1312      * See {@link #contentListByTemplateType(Node, String, String, int, String, String)} for details.
1313      */
1314     public List<ContentMap> contentListByTemplateType(ContentMap searchRoot, String templateType, String templateSubtype, int maxResultSize, String andClause, String orderByClause) throws RepositoryException {
1315         return asContentMapList(contentListByTemplateType(asJCRNode(searchRoot), templateType, templateSubtype, maxResultSize, andClause, orderByClause));
1316     }
1317 
1318     /**
1319      * See {@link #contentListByTemplateType(Node, String, String, int, String, String)} for details.
1320      */
1321     public List<Node> contentListByTemplateType(Node siteRoot, String templateType, String templateSubtype) throws RepositoryException {
1322         return contentListByTemplateType(siteRoot, templateType, templateSubtype, Integer.MAX_VALUE, null, null);
1323     }
1324 
1325     /**
1326      * See {@link #contentListByTemplateType(Node, String, String, int, String, String)} for details.
1327      */
1328     public List<ContentMap> contentListByTemplateType(ContentMap siteRoot, String templateType, String templateSubtype) throws RepositoryException {
1329         return asContentMapList(contentListByTemplateType(asJCRNode(siteRoot), templateType, templateSubtype, Integer.MAX_VALUE, null, null));
1330     }
1331 
1332     /**
1333      * Find content objects with one of the given template IDs below a given search root.
1334      *
1335      * @param searchRoot the {@link javax.jcr.Node} to use as root of the search
1336      * @param templateIds a {@link java.util.Set} of template IDs to search for
1337      * @param maxResultSize setting this can drastically improve query performance, if you are interested only in a fixed number of leading result objects
1338      * @param andClause an additional "AND" clause in SQL syntax, excluding the "AND" itself, e.g. "date IS NOT NULL"
1339      * @param orderByClause an "ORDER BY" clause in SQL syntax, excluding the "ORDER BY" itself, e.g. "date desc" or "date asc"
1340      */
1341     public List<Node> contentListByTemplateIds(Node searchRoot, Set<String> templateIds, int maxResultSize, String andClause, String orderByClause) throws RepositoryException {
1342         return templateTypeHelper.getContentListByTemplateIds(searchRoot, templateIds, maxResultSize, andClause, orderByClause);
1343     }
1344 
1345     /**
1346      * See {@link #contentListByTemplateIds(Node, Set, int, String, String)} for details.
1347      */
1348     public List<ContentMap> contentListByTemplateIds(ContentMap searchRoot, Set<String> templateIds, int maxResultSize, String andClause, String orderByClause) throws RepositoryException {
1349         return asContentMapList(contentListByTemplateIds(asJCRNode(searchRoot), templateIds, maxResultSize, andClause, orderByClause));
1350     }
1351 
1352     /**
1353      * See {@link #contentListByTemplateId(Node, String, int, String, String)} for details.
1354      */
1355     public List<Node> contentListByTemplateId(Node searchRoot, String templateId) throws RepositoryException {
1356         return contentListByTemplateIds(searchRoot, Collections.singleton(templateId), Integer.MAX_VALUE, null, null);
1357     }
1358 
1359     /**
1360      * Find content objects with the given template ID below a given search root.
1361      *
1362      * @param searchRoot the {@link javax.jcr.Node} to use as root of the search
1363      * @param templateId a template ID to search for
1364      * @param maxResultSize setting this can drastically improve query performance, if you are interested only in a fixed number of leading result objects
1365      * @param andClause an additional "AND" clause in SQL syntax, excluding the "AND" itself, e.g. "date IS NOT NULL"
1366      * @param orderByClause an "ORDER BY" clause in SQL syntax, excluding the "ORDER BY" itself, e.g. "date desc" or "date asc"
1367      */
1368     public List<Node> contentListByTemplateId(Node searchRoot, String templateId, int maxResultSize, String andClause, String orderByClause) throws RepositoryException {
1369         return contentListByTemplateIds(searchRoot, Collections.singleton(templateId), maxResultSize, andClause, orderByClause);
1370     }
1371 
1372     /**
1373      * Shortens a {@link java.lang.String} to provided length and adds a custom suffix.
1374      */
1375     public String abbreviateString(String stringToAbbreviate, int length, String suffix) {
1376         if (stringToAbbreviate == null) {
1377             return null;
1378         }
1379 
1380         if (stringToAbbreviate.length() > length) {
1381             final int lengthMinusSuffix = length - suffix.length();
1382             String abbreviatedString = StringUtils.left(stringToAbbreviate, lengthMinusSuffix);
1383 
1384             // If the character after the cut was a space, no word was cut: no cutting on last space needed
1385             String firstCharAfterCut = stringToAbbreviate.substring(lengthMinusSuffix, lengthMinusSuffix + 1);
1386             if (!" ".equals(firstCharAfterCut)) {
1387                 abbreviatedString = StringUtils.substringBeforeLast(abbreviatedString, " ");
1388             }
1389 
1390             return abbreviatedString + suffix;
1391         }
1392 
1393         return stringToAbbreviate;
1394     }
1395 
1396     /**
1397      * Shortens a {@link java.lang.String} to provided length and adds suffix "<code> ...</code>".
1398      */
1399     public String abbreviateString(String stringToAbbreviate, int length) {
1400         return abbreviateString(stringToAbbreviate, length, " ...");
1401     }
1402 
1403     /**
1404      * Returns the file extension of a filename by returning the suffix after the last <code>.</code> (dot).
1405      */
1406     public String fileExtension(String fileName) {
1407         return PathUtil.getExtension(fileName);
1408     }
1409 
1410     /**
1411      * Returns a human friendly string for a file size.
1412      *
1413      * @see org.apache.commons.io.FileUtils#byteCountToDisplaySize(long)
1414      */
1415     public String readableFileSize(long sizeBytes) {
1416         return FileUtils.byteCountToDisplaySize(sizeBytes);
1417     }
1418 
1419     /**
1420      * Checks if given language is current locale (set in {@link AggregationState}).
1421      */
1422     public boolean isCurrentLocale(String language) {
1423         return StringUtils.equals(webContextProvider.get().getAggregationState().getLocale().toString(), language);
1424     }
1425 
1426     /**
1427      * Creates a link of current URI to given language.
1428      *
1429      * <p><i>Note</i>: This currently only works with links to pages in the website repository. It is not possible to
1430      * generate localized links for URIs that point to another workspace or links that are forwarded.</p>
1431      *
1432      * @see <a href="https://wiki.magnolia-cms.com/display/DEV/Concept+-+Link+API+overhaul">Concept+-+Link+API+overhaul</a>
1433      */
1434     public Map<String, String> localizedLinks() throws RepositoryException {
1435         final Node currentNode = webContextProvider.get().getAggregationState().getCurrentContentNode();
1436 
1437         return localizedLinks(currentNode);
1438     }
1439 
1440     /**
1441      * Creates a map of localized links to the node for all supported languages.
1442      *
1443      * See {@link #localizedLinks()} for details.
1444      */
1445     public Map<String, String> localizedLinks(Node content) throws RepositoryException {
1446         final Node pageNode = page(content);
1447 
1448         final String pageIdentifier = pageNode.getIdentifier();
1449         final Collection<Locale> locales = i18nContentSupport.get().getLocales();
1450         if (i18nContentSupport.get().isEnabled() && locales.size() > 1) {
1451             final Map<String, String> map = new LinkedHashMap<>();
1452             for (Locale locale : locales) {
1453                 final String uri = createURI(pageIdentifier, locale);
1454                 // Using toString() method in order to be able to create link to e.g.
1455                 // de_CH, de_DE, de_AT
1456                 final String label = locale.toString();
1457                 map.put(label, uri);
1458             }
1459             return map;
1460         }
1461 
1462         return Collections.emptyMap();
1463     }
1464 
1465     /**
1466      * Creates a localized link by setting/resetting the {@link Locale} in the {@link AggregationState}.
1467      *
1468      * @see LinkUtil
1469      */
1470     private String createURI(final String identifier, final Locale locale) {
1471         // we are going to change the context language, this is ugly but is safe
1472         // as only the current Thread is modified
1473         final Locale currentLocale = i18nContentSupport.get().getLocale();
1474         String uri = null;
1475         final AggregationState aggregationState;
1476 
1477         if (Components.getComponent(RenderingEngine.class).getEscapeHtml()) {
1478             aggregationState = new HTMLEscapingWebContextWrapper(webContextProvider.get()).getAggregationState();
1479         } else {
1480             aggregationState = webContextProvider.get().getAggregationState();
1481         }
1482         try {
1483             aggregationState.setLocale(locale);
1484             uri = LinkUtil.createAbsoluteLink(RepositoryConstants.WEBSITE, identifier);
1485         } catch (RepositoryException e) {
1486             log.error("Error creating a localized link to node with identifier {} for locale {}", identifier, locale.toString());
1487         } finally {
1488             // make sure that we always reset to the original locale
1489             aggregationState.setLocale(currentLocale);
1490         }
1491 
1492         final String selector = aggregationState.getSelector();
1493         if (StringUtils.isNotBlank(selector)) {
1494             final String defaultExtension = Components.getComponent(ServerConfiguration.class).getDefaultExtension();
1495             if (StringUtils.isNotBlank(defaultExtension)) {
1496                 uri = StringUtils.substringBeforeLast(uri, "." + defaultExtension) + SelectorUtil.SELECTOR_DELIMITER + selector + SelectorUtil.SELECTOR_DELIMITER + "." + defaultExtension;
1497             } else {
1498                 uri = uri + SelectorUtil.SELECTOR_DELIMITER + selector + SelectorUtil.SELECTOR_DELIMITER;
1499             }
1500         }
1501 
1502         return uri;
1503     }
1504 
1505 }