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