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