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