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