View Javadoc
1   /**
2    * This file Copyright (c) 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 static com.google.common.base.Preconditions.checkArgument;
37  
38  import info.magnolia.cms.core.Path;
39  import info.magnolia.jcr.util.ContentMap;
40  import info.magnolia.jcr.util.NodeTypes;
41  import info.magnolia.jcr.util.NodeUtil;
42  import info.magnolia.link.Link;
43  import info.magnolia.link.LinkTransformerManager;
44  import info.magnolia.link.LinkUtil;
45  import info.magnolia.templating.predicates.NavigationItemPredicate;
46  
47  import java.util.Collections;
48  import java.util.List;
49  
50  import javax.inject.Inject;
51  import javax.inject.Singleton;
52  import javax.jcr.Node;
53  import javax.jcr.RepositoryException;
54  
55  import org.apache.commons.lang3.StringUtils;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  /**
60   * Templating functions for creating page navigations. They return an instance of {@link ContentMap}
61   * (or a collection of it) which is more easily accessible in template scripts via . (dot) notation.
62   *
63   * <p>Functions are exposed in templates as <code>navfn</code>.</p>
64   *
65   * For some usage samples, please see MTE's and travel-demo's <code>navigation.ftl</code> macros or the documentation at
66   * https://documentation.magnolia-cms.com/display/DOCS55/navfn.
67   */
68  @Singleton
69  public class NavigationTemplatingFunctions {
70  
71      private static final Logger log = LoggerFactory.getLogger(NavigationTemplatingFunctions.class);
72  
73      public static final String HIDE_IN_NAVIGATION_PROPERTY_NAME = "hideInNav";
74  
75      private final TemplatingFunctions templatingFunctions;
76  
77      @Inject
78      public NavigationTemplatingFunctions(TemplatingFunctions templatingFunctions) {
79          this.templatingFunctions = templatingFunctions;
80      }
81  
82      /**
83       * Returns the <em>furthest</em> ancestor with node type <code>{@value info.magnolia.jcr.util.NodeTypes.Page#NAME}</code>
84       * of the provided page.
85       *
86       * <p>Useful to find the root or home page of your current page.</p>
87       *
88       * @see info.magnolia.jcr.util.NodeTypes.Page#NAME
89       */
90      public ContentMap rootPage(ContentMap page) throws RepositoryException {
91          if (page == null) {
92              return null;
93          }
94          Node node = page.getJCRNode();
95          if (node.getDepth() == 1) {
96              if (node.isNodeType(NodeTypes.Page.NAME)) {
97                  return page;
98              } else {
99                  return null;
100             }
101         }
102         return templatingFunctions.root(page, NodeTypes.Page.NAME);
103     }
104 
105     /**
106      * Returns the ancestor of the provided page at the specified depth only if the ancestor has node type
107      * <code>{@value info.magnolia.jcr.util.NodeTypes.Page#NAME}</code>.
108      *
109      * <p>E.g. <code>depth == 1</code> would return the root page to this page, <code>depth == 2</code> would return the
110      * child page of the root page, etc.</p>
111      *
112      * @param page The page against which depth is calculated.
113      * @param depth An ancestor of depth x is the page that is x levels down along the path from the root to this page.
114      */
115     public ContentMap ancestorPageAtLevel(ContentMap page, int depth) throws RepositoryException {
116         if (page == null) {
117             return null;
118         }
119         Node node = page.getJCRNode();
120         if (node.getDepth() >= depth) {
121             Node ancestor = (Node) node.getAncestor(depth);
122             if (ancestor.isNodeType(NodeTypes.Page.NAME)) {
123                 return templatingFunctions.asContentMap(ancestor);
124             }
125         }
126         return null;
127     }
128 
129     /**
130      * Returns a list of child nodes for the <code>{@value info.magnolia.jcr.util.NodeTypes.Page#NAME}</code> node type
131      * of the provided page.
132      *
133      * <p>Only non hidden in navigation nodes are returned. A node hidden in navigation is one which has
134      * <code>{@value #HIDE_IN_NAVIGATION_PROPERTY_NAME}</code> property set to <code>true</code>.</p>
135      */
136     public List<ContentMap> navItems(ContentMap page) throws RepositoryException {
137         if (page == null) {
138             return Collections.emptyList();
139         }
140         return templatingFunctions.asContentMapList(NodeUtil.getNodes(page.getJCRNode(), new NavigationItemPredicate()));
141     }
142 
143     /**
144      * Returns a list of child nodes for a given parent with a given node type in a given workspace.
145      * Only non hidden in navigation nodes are returned. A node hidden in navigation is one which has
146      * <code>{@value #HIDE_IN_NAVIGATION_PROPERTY_NAME}</code> property set to <code>true</code>.
147      *
148      * <p>A usage sample from travel-demo's <code>navigation.ftl</code> macro follows. Here tour types are retrieved
149      * from the <code>category</code> workspace and a link to a page capable of displaying such items is then created
150      * with {@link #linkWithSelector(ContentMap, ContentMap)}.</p>
151      *
152      * <pre>
153      * [#assign navContentItems = navfn.navItemsFromApp("category", "/tour-types", "mgnl:category")]
154      * [#list navContentItems as navContentItem]
155      *   &lt;li&gt;&lt;a href="${navfn.linkWithSelector(navItem, navContentItem)!"#"}"&gt;${navContentItem.displayName!navContentItem.@name}&lt/a&gt;&lt/li&gt;
156      * [/#list]
157      * </pre>
158      *
159      * @param workspace the JCR workspace for the content app
160      * @param parentPath the path to the node whose children we want to retrieve in order to build navigation items
161      * @param nodeType the JCR node type for the content app workspace
162      *
163      * @see #linkWithSelector(ContentMap, ContentMap)
164      */
165     public List<ContentMap> navItemsFromApp(String workspace, String parentPath, String nodeType) throws RepositoryException {
166         checkArgument(StringUtils.isNotBlank(workspace), "Workspace has to be specified.");
167         checkArgument(StringUtils.isNotBlank(parentPath), "ParentPath is not specified.");
168         checkArgument(StringUtils.isNotBlank(nodeType), "NodeType is not specified.");
169 
170         Node content = templatingFunctions.nodeByPath(parentPath, workspace);
171         if (content == null) {
172             log.warn("Can't resolve parent item for navigation from app: workspace [{}], path [{}]", workspace, parentPath);
173             return Collections.emptyList();
174         }
175         return templatingFunctions.asContentMapList(NodeUtil.getNodes(content, new NavigationItemPredicate(nodeType)));
176     }
177 
178     /**
179      * Checks whether the given page has the specified template.
180      */
181     public boolean hasTemplate(ContentMap page, String template) throws RepositoryException {
182         return template != null && page != null && template.equals(NodeTypes.Renderable.getTemplate(page.getJCRNode()));
183     }
184 
185     /**
186      * Checks whether the given page has the specified template type.
187      */
188     public boolean hasTemplateType(ContentMap page, String templateType) throws RepositoryException {
189         return templateType != null && page != null && templateType.equals(templatingFunctions.templateType(page));
190     }
191 
192     /**
193      * Checks whether the given page has the specified template subtype.
194      */
195     public boolean hasTemplateSubtype(ContentMap page, String templateSubtype) throws RepositoryException {
196         return templateSubtype != null && page != null && templateSubtype.equals(templatingFunctions.templateSubtype(page));
197     }
198 
199     /**
200      * Returns a page url with a <em>selector</em> (delimited by the <code>~</code> [tilde] character) identifying the
201      * content to be rendered.
202      * This relies on Magnolia's <a href="https://documentation.magnolia-cms.com/display/DOCS/Location,+location,+location#Location,location,location-Selectors">selector mechanism</a>.
203      *
204      * <p>A link of this type is produced <code>http://mysite/mypage~mycontent~.html</code> where 'mypage' is the node
205      * name of the target page and 'mycontent' the node name of the content.</p>
206      *
207      * @see #navItemsFromApp(String, String, String)
208      */
209     public String linkWithSelector(ContentMap targetPage, ContentMap content) throws RepositoryException {
210         if (targetPage == null || content == null) {
211             return null;
212         }
213         Node pageNode = targetPage.getJCRNode();
214         Node contentNode = content.getJCRNode();
215 
216         Link pageLinkInstance = LinkUtil.createLinkInstance(pageNode);
217         String pageLink = LinkTransformerManager.getInstance().getBrowserLink(pageNode.getPath()).transform(pageLinkInstance);
218         String extension = pageLinkInstance.getExtension();
219         if (StringUtils.isEmpty(extension)) {
220             return pageLink + Path.SELECTOR_DELIMITER + contentNode.getName() + Path.SELECTOR_DELIMITER;
221         } else {
222             return StringUtils.substringBeforeLast(pageLink, ".") + Path.SELECTOR_DELIMITER + contentNode.getName() + Path.SELECTOR_DELIMITER + "." + extension;
223         }
224     }
225 
226     /**
227      * Returns a page url with a parameter identifying the content to be rendered.
228      *
229      * <p>A link of this type is produced <code>http://mysite/mypage.html?workspaceName=contentNodeName</code> where
230      * 'mypage' is the node name of the target page, 'contentNodeName' the node name of the content and 'workspaceName'
231      * is where the latter comes from'.</p>
232      */
233     public String linkWithParameter(ContentMap targetPage, ContentMap content) throws RepositoryException {
234         if (content == null) {
235             return null;
236         }
237         return linkWithParameter(targetPage, content, content.getJCRNode().getSession().getWorkspace().getName());
238     }
239 
240     /**
241      * Returns a page url with a parameter identifying the content to be rendered.
242      *
243      * <p>A link of this type is produced <code>http://mysite/mypage.html?parameterName=contentNodeName</code> where
244      * 'mypage' is the node name of the target page, 'contentNodeName' the node name of the content.</p>
245      *
246      * @see #linkWithParameter(ContentMap, ContentMap)
247      */
248     public String linkWithParameter(ContentMap targetPage, ContentMap content, String parameterName) throws RepositoryException {
249         if (targetPage == null || content == null) {
250             return null;
251         }
252         Node pageNode = targetPage.getJCRNode();
253         Node contentNode = content.getJCRNode();
254 
255         Link pageLinkInstance = LinkUtil.createLinkInstance(pageNode);
256         pageLinkInstance.setParameters(parameterName + "=" + contentNode.getName());
257         return LinkTransformerManager.getInstance().getBrowserLink(pageNode.getPath()).transform(pageLinkInstance);
258     }
259 
260     /**
261      * Returns a url for the provided content.
262      */
263     public String link(ContentMap content) throws RepositoryException {
264         return templatingFunctions.link(content);
265     }
266 
267     /**
268      * Checks whether navigation item is the currently displayed content.
269      *
270      * @param content can be either a node from a content app or page proper.
271      */
272     public boolean isActive(ContentMap content, ContentMap navigationItem) throws RepositoryException {
273         if (content == null || navigationItem == null) {
274             return false;
275         }
276         Node activePageNode = templatingFunctions.page(content.getJCRNode());
277         return navigationItem.getJCRNode().isSame(activePageNode);
278     }
279 
280     /**
281      * Checks whether navigation item is the ancestor of current content.
282      *
283      * @param content can be either a node from a content app or page proper.
284      */
285     public boolean isOpen(ContentMap content, ContentMap navigationItem) throws RepositoryException {
286         if (content == null || navigationItem == null) {
287             return false;
288         }
289         String activePagePath = templatingFunctions.page(content.getJCRNode()).getPath();
290         String navigationItemPath = navigationItem.getJCRNode().getPath();
291         return !StringUtils.equals(activePagePath, navigationItemPath) && StringUtils.startsWith(activePagePath, navigationItemPath);
292     }
293 
294     /**
295      * Checks whether the given content is hidden in navigation.
296      *
297      * <p>A node hidden in navigation is one which has <code>{@value #HIDE_IN_NAVIGATION_PROPERTY_NAME}</code>
298      * property set to <code>true</code>.</p>
299      */
300     public boolean isHiddenInNav(ContentMap content) throws RepositoryException {
301         if (content == null) {
302             return false;
303         }
304         Node node = content.getJCRNode();
305         return node.hasProperty(HIDE_IN_NAVIGATION_PROPERTY_NAME) && node.getProperty(HIDE_IN_NAVIGATION_PROPERTY_NAME).getBoolean();
306     }
307 
308     /**
309      * Negates {@link #isHiddenInNav(ContentMap)}.
310      */
311     public boolean isNotHiddenInNav(ContentMap content) throws RepositoryException {
312         return !isHiddenInNav(content);
313     }
314 
315     /**
316      * A helper method checking whether a given property is true or false. It allows avoiding verbose and cumbersome
317      * checks like this one <code>[#if child.showInNav?has_content && child.showInNav?string == "true"]</code>.
318      */
319     public boolean isTrue(ContentMap content, String propertyName) throws RepositoryException {
320         if (content == null || StringUtils.isBlank(propertyName)) {
321             return false;
322         }
323         Node node = content.getJCRNode();
324         return node.hasProperty(propertyName) && node.getProperty(propertyName).getBoolean();
325     }
326 }