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