View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.jsp.taglib;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.Content.ContentFilter;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.core.NodeData;
40  import info.magnolia.cms.i18n.I18nContentSupportFactory;
41  import info.magnolia.cms.util.ContentUtil;
42  import info.magnolia.context.MgnlContext;
43  
44  import java.io.IOException;
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.Iterator;
48  import java.util.List;
49  
50  import javax.jcr.Node;
51  import javax.jcr.RepositoryException;
52  import javax.servlet.http.HttpServletRequest;
53  import javax.servlet.jsp.JspException;
54  import javax.servlet.jsp.JspWriter;
55  import javax.servlet.jsp.tagext.TagSupport;
56  
57  import org.apache.commons.lang3.StringEscapeUtils;
58  import org.apache.commons.lang3.StringUtils;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  import org.tldgen.annotations.BodyContent;
62  import org.tldgen.annotations.Tag;
63  
64  /**
65   * Draws a simple, css based, navigation menu. The menu layout can then be customized using css, and the default menu
66   * should be enough for most uses. Two following page properties will also be used in the menu:
67   * <ul>
68   * <li><code>navTitle</code>: a title to use for the navigation menu, if different from the real page title</li>
69   * <li><code>accessKey</code>: an optional access key which will be added to the link</li>
70   * <li><code>wrappingElement</code>: an optional html element (div, span, p, etc) to go within the &lt;a&gt; tag wrapping the anchor text
71   * </ul>
72   * 
73   * @jsp.tag name="simpleNavigation" body-content="empty"
74   * @jsp.tag-example
75   * <pre>
76   * &lt;cmsu:simpleNavigation startLevel="3" style="mystyle"/&gt;
77   * 
78   * Will output the following:
79   * 
80   * &lt;ul class="level3 mystyle"&gt;
81   *     &lt;li&gt;&lt;a href="..."&gt;page 1 name &lt;/a&gt;&lt;/li&gt;
82   *     &lt;li&gt;&lt;a href="..."&gt;page 2 name &lt;/a&gt;&lt;/li&gt;
83   *     &lt;li class="trail"&gt;&lt;a href="..."&gt;page 3 name &lt;/a&gt;
84   *         &lt;ul class="level3"&gt;
85   *             &lt;li&gt;&lt;a href="..."&gt;subpage 1 name &lt;/a&gt;&lt;/li&gt;
86   *             &lt;li&gt;&lt;a href="..."&gt;subpage 2 name &lt;/a&gt;&lt;/li&gt;
87   *             &lt;li&gt;&lt;strong&gt;&lt;a href="..."&gt;selected page name &lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
88   *         &lt;/ul&gt;
89   *     &lt;/li&gt;
90   *     &lt;li&gt;&lt;a href="..."&gt;page 4 name &lt;/a&gt;&lt;/li&gt;
91   * &lt;/ul&gt;
92   * </pre>
93   */
94  @Tag(name = "simpleNavigation", bodyContent = BodyContent.EMPTY)
95  public class SimpleNavigationTag extends TagSupport {
96  
97      /**
98       * Css class added to active page.
99       */
100     private static final String CSS_LI_ACTIVE = "active";
101 
102     /**
103      * Css class added to ancestor of the active page.
104      */
105     private static final String CSS_LI_TRAIL = "trail";
106 
107     /**
108      * Css class added to leaf pages.
109      */
110     private static final String CSS_LI_LEAF = "leaf";
111 
112     /**
113      * Css class added to open trees.
114      */
115     private static final String CSS_LI_CLOSED = "closed";
116 
117     /**
118      * Css class added to closed trees.
119      */
120     private static final String CSS_LI_OPEN = "open";
121 
122     /**
123      * Css class added to first li in ul.
124      */
125     private static final String CSS_LI_FIRST = "first";
126 
127     /**
128      * Css class added to last li in ul.
129      */
130     private static final String CSS_LI_LAST = "last";
131 
132     /**
133      * Page property: navigation title.
134      */
135     private static final String NODEDATA_NAVIGATIONTITLE = "navTitle";
136 
137     /**
138      * Page property: access key.
139      */
140     public static final String NODEDATA_ACCESSKEY = "accessKey";
141 
142     /**
143      * Default name for "open menu" nodeData.
144      */
145     public static final String DEFAULT_OPENMENU_NODEDATA = "openMenu";
146 
147     /**
148      * Default name for "hide in nav" nodeData.
149      */
150     public static final String DEFAULT_HIDEINNAV_NODEDATA = "hideInNav";
151 
152     /**
153      * Default name for "wrapperElement" nodeData.
154      */
155     public static final String DEFAULT_WRAPPERELEMENT_NODEDATA = "";
156 
157     /**
158      * Expand all expand all the nodes.
159      */
160     public static final String EXPAND_ALL = "all";
161 
162     /**
163      * Expand all expand only page that should be displayed in navigation.
164      */
165     public static final String EXPAND_SHOW = "show";
166 
167     /**
168      * Do not use expand functions.
169      */
170     public static final String EXPAND_NONE = "none";
171 
172     private static Logger log = LoggerFactory.getLogger(SimpleNavigationTag.class);
173 
174     /**
175      * Start level.
176      */
177     private int startLevel;
178 
179     /**
180      * End level.
181      */
182     private int endLevel;
183 
184     /**
185      * Name for the "hide in nav" nodeData.
186      */
187     private String hideInNav;
188 
189     /**
190      * Name for the "open menu" nodeData.
191      */
192     private String openMenu;
193 
194     /**
195      * Style to apply to the menu.
196      */
197     private String style;
198 
199     /**
200      * html element to wrap the anchortext. (i.e. &lt;a&gt;&lt;wrapper&gt;...&lt;/wrapper&gt;&lt;/a&gt;)
201      */
202     public String wrapperElement;
203 
204     /**
205      * Expand all the nodes. (sitemap mode)
206      */
207     private String expandAll = EXPAND_NONE;
208 
209     private boolean relativeLevels = false;
210 
211     /**
212      * Name for a page property which will be written to the css class attribute.
213      */
214     private String classProperty;
215 
216     /**
217      * Name for the "nofollow" hodeData (for link that must be ignored by search engines).
218      */
219     private String nofollow;
220 
221     /**
222      * Content Filter to use to evaluate if a page should be drawn.
223      */
224     private ContentFilter filter;
225 
226     private String contentFilter = "";
227 
228     /**
229      * Flag to set if the first and last li in each ul should be marked with a special css class.
230      */
231     private boolean markFirstAndLastElement = false;
232 
233     /**
234      * The start level for navigation, defaults to 0.
235      * 
236      * @jsp.attribute required="false" rtexprvalue="true" type="int"
237      */
238     public void setStartLevel(int startLevel) {
239         this.startLevel = startLevel;
240     }
241 
242     /**
243      * The end level for navigation, defaults to 0.
244      * 
245      * @jsp.attribute required="false" rtexprvalue="true" type="int"
246      */
247     public void setEndLevel(int endLevel) {
248         this.endLevel = endLevel;
249     }
250 
251     /**
252      * The css class to be applied to the first ul. Default is empty.
253      * 
254      * @jsp.attribute required="false" rtexprvalue="true"
255      */
256     public void setStyle(String style) {
257         this.style = style;
258     }
259 
260     /**
261      * Name for the "hide in nav" nodeData. If a page contains a boolean property with this name and
262      * it is set to true, the page is not shown in navigation. Defaults to "hideInNav".
263      * 
264      * @jsp.attribute required="false" rtexprvalue="true"
265      */
266     public void setHideInNav(String hideInNav) {
267         this.hideInNav = hideInNav;
268     }
269 
270     /**
271      * Name for the "open menu" nodeData. If a page contains a boolean property with this name and
272      * it is set to true, subpages are always shown also if the page is not selected.
273      * Defaults to "openMenu".
274      * 
275      * @jsp.attribute required="false" rtexprvalue="true"
276      */
277     public void setOpenMenu(String openMenu) {
278         this.openMenu = openMenu;
279     }
280 
281     /**
282      * Name for the "nofollow" nodeData. If a page contains a boolean property with this name
283      * and it is set to true, rel="nofollow" will be added to the generated link
284      * (for links the should be ignored by search engines).
285      * 
286      * @jsp.attribute required="false" rtexprvalue="true"
287      */
288     public void setNofollow(String nofollow) {
289         this.nofollow = nofollow;
290     }
291 
292     /**
293      * A variable in the pageContext that contains a content filter, determining if a given page should be drawn or not.
294      * 
295      * @jsp.attribute required="false" rtexprvalue="true"
296      */
297     public void setContentFilter(String contentFilter) {
298         this.contentFilter = contentFilter;
299     }
300 
301     /**
302      * Sitemap mode. Can be assigned the "show" value. Only showable pages will be displayed. Any other value will
303      * result in displaying all pages.
304      * 
305      * @jsp.attribute required="false" rtexprvalue="true"
306      */
307     public void setExpandAll(String expandAll) {
308         if (expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
309             this.expandAll = expandAll;
310         }
311         else {
312             this.expandAll = EXPAND_ALL;
313         }
314     }
315 
316     /**
317      * If set to true, the startLevel and endLevel values are treated relatively to the current active page.
318      * The default value is false.
319      * 
320      * @jsp.attribute required="false" rtexprvalue="true" type="boolean"
321      */
322     public void setRelativeLevels(boolean relativeLevels) {
323         this.relativeLevels = relativeLevels;
324     }
325 
326     /**
327      * Name for a page property that will hold a css class name which will be added to the html class attribute.
328      * 
329      * @jsp.attribute required="false" rtexprvalue="true"
330      */
331     public void setClassProperty(String classProperty) {
332         this.classProperty = classProperty;
333     }
334 
335     /**
336      * When specified, all links will have the anchortext wrapped in the supplied element. (such as "span")
337      * 
338      * @param wrapperElement name of an html element that will be included in the anchor, wrapping the anchortext
339      * @jsp.attribute required="false" rtexprvalue="true"
340      */
341     public void setWrapperElement(String wrapperElement) {
342         this.wrapperElement = wrapperElement;
343     }
344 
345     /**
346      * If set to true, a "first" or "last" css class will be added to the list of css classes of the
347      * first and the last li in each ul.
348      * 
349      * @jsp.attribute required="false" rtexprvalue="true" type="boolean"
350      */
351     public void setMarkFirstAndLastElement(boolean flag) {
352         markFirstAndLastElement = flag;
353     }
354 
355     @Override
356     public int doEndTag() throws JspException {
357         Content activePage = getCurrentActivePage();
358         try {
359             while (!ItemType.PAGE.getSystemName().equals(activePage.getNodeTypeName()) && activePage.getLevel() != 0) {
360                 activePage = activePage.getParent();
361             }
362         } catch (RepositoryException e) {
363             log.error("Failed to obtain parent page for {}", getCurrentActivePage().getHandle(), e);
364             activePage = getCurrentActivePage();
365         }
366         JspWriter out = this.pageContext.getOut();
367 
368         if (StringUtils.isNotEmpty(this.contentFilter)) {
369             try {
370                 filter = (ContentFilter) this.pageContext.getAttribute(this.contentFilter);
371             } catch (ClassCastException e) {
372                 log.error("contentFilter assigned was not a content filter", e);
373             }
374         } else {
375             filter = null;
376         }
377 
378         if (startLevel > endLevel) {
379             endLevel = 0;
380         }
381 
382         try {
383             final int activePageLevel = activePage.getLevel();
384             // if we are to treat the start and end level as relative
385             // to the active page, we adjust them here...
386             if (relativeLevels) {
387                 this.startLevel += activePageLevel;
388                 this.endLevel += activePageLevel;
389             }
390             if (this.startLevel <= activePageLevel) {
391                 Content startContent = activePage.getAncestor(this.startLevel);
392                 drawChildren(startContent, activePage, out);
393             }
394 
395         } catch (RepositoryException e) {
396             log.error("RepositoryException caught while drawing navigation: {}", e.getMessage(), e);
397             return EVAL_PAGE;
398         } catch (IOException e) {
399             // should never happen
400             throw new JspException(e);
401         }
402 
403         return EVAL_PAGE;
404     }
405 
406     @Override
407     public void release() {
408         this.startLevel = 0;
409         this.endLevel = 0;
410         this.hideInNav = null;
411         this.openMenu = null;
412         this.style = null;
413         this.classProperty = null;
414         this.expandAll = EXPAND_NONE;
415         this.relativeLevels = false;
416         this.wrapperElement = "";
417         this.contentFilter = "";
418         this.filter = null;
419         this.nofollow = null;
420         this.markFirstAndLastElement = false;
421         super.release();
422     }
423 
424     /**
425      * Draws page children as an unordered list.
426      * 
427      * @param page current page
428      * @param activePage active page
429      * @param out jsp writer
430      * @throws IOException jspwriter exception
431      * @throws RepositoryException any exception thrown during repository reading
432      */
433     private void drawChildren(Content page, Content activePage, JspWriter out) throws IOException, RepositoryException {
434 
435         Collection<Content> children = new ArrayList<Content>(page.getChildren(ItemType.CONTENT));
436 
437         if (children.size() == 0) {
438             return;
439         }
440 
441         out.print("<ul class=\"level");
442         out.print(page.getLevel());
443         if (style != null && page.getLevel() == startLevel) {
444             out.print(" ");
445             out.print(style);
446         }
447         out.print("\">");
448 
449         Iterator<Content> iter = children.iterator();
450         // loop through all children and discard those we don't want to display
451         while (iter.hasNext()) {
452             final Content child = iter.next();
453 
454             if (expandAll.equalsIgnoreCase(EXPAND_NONE) || expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
455                 if (child
456                         .getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
457                     iter.remove();
458                     continue;
459                 }
460                 // use a filter
461                 if (filter != null) {
462                     if (!filter.accept(child)) {
463                         iter.remove();
464                         continue;
465                     }
466                 }
467             } else {
468                 if (child.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
469                     iter.remove();
470                     continue;
471                 }
472             }
473         }
474 
475         boolean isFirst = true;
476         Iterator<Content> visibleIt = children.iterator();
477         while (visibleIt.hasNext()) {
478             Content child = visibleIt.next();
479             List<String> cssClasses = new ArrayList<String>(4);
480 
481             NodeData nodeData = I18nContentSupportFactory.getI18nSupport().getNodeData(child, NODEDATA_NAVIGATIONTITLE);
482             String title = null;
483             if(nodeData != null){
484                 title = nodeData.getString(StringUtils.EMPTY);
485             }
486 
487             // if nav title is not set, the main title is taken
488             if (StringUtils.isEmpty(title)) {
489                 title = child.getTitle();
490             }
491 
492             // if main title is not set, the name of the page is taken
493             if (StringUtils.isEmpty(title)) {
494                 title = child.getName();
495             }
496 
497             boolean showChildren = false;
498             boolean self = false;
499 
500             if (!expandAll.equalsIgnoreCase(EXPAND_NONE)) {
501                 showChildren = true;
502             }
503 
504             if (activePage.getHandle().equals(child.getHandle())) {
505                 // self
506                 showChildren = true;
507                 self = true;
508                 cssClasses.add(CSS_LI_ACTIVE);
509             }
510             else if (!showChildren) {
511                 showChildren = child.getLevel() <= activePage.getAncestors().size() && StringUtils.equals(activePage.getAncestor(child.getLevel()).getHandle(), child.getHandle());
512             }
513 
514             if (!showChildren) {
515                 showChildren = child
516                         .getNodeData(StringUtils.defaultString(this.openMenu, DEFAULT_OPENMENU_NODEDATA))
517                         .getBoolean();
518             }
519 
520             if (endLevel > 0) {
521                 showChildren &= child.getLevel() < endLevel;
522             }
523 
524             cssClasses.add(hasVisibleChildren(child) ? showChildren ? CSS_LI_OPEN : CSS_LI_CLOSED : CSS_LI_LEAF);
525 
526             if (child.getLevel() < activePage.getLevel()
527                     && activePage.getAncestor(child.getLevel()).getHandle().equals(child.getHandle())) {
528                 cssClasses.add(CSS_LI_TRAIL);
529             }
530 
531             if (StringUtils.isNotEmpty(classProperty) && child.hasNodeData(classProperty)) {
532                 cssClasses.add(child.getNodeData(classProperty).getString(StringUtils.EMPTY));
533             }
534 
535             if (markFirstAndLastElement && isFirst) {
536                 cssClasses.add(CSS_LI_FIRST);
537                 isFirst = false;
538             }
539 
540             if (markFirstAndLastElement && !visibleIt.hasNext()) {
541                 cssClasses.add(CSS_LI_LAST);
542             }
543 
544             StringBuffer css = new StringBuffer(cssClasses.size() * 10);
545             Iterator<String> iterator = cssClasses.iterator();
546             while (iterator.hasNext()) {
547                 css.append(iterator.next());
548                 if (iterator.hasNext()) {
549                     css.append(" ");
550                 }
551             }
552 
553             out.print("<li class=\"");
554             out.print(css.toString());
555             out.print("\">");
556 
557             if (self) {
558                 out.println("<strong>");
559             }
560 
561             String accesskey = null;
562             if(child.getNodeData(NODEDATA_ACCESSKEY) != null){
563                 accesskey = child.getNodeData(NODEDATA_ACCESSKEY).getString(StringUtils.EMPTY);
564             }
565 
566             out.print("<a href=\"");
567             out.print(((HttpServletRequest) this.pageContext.getRequest()).getContextPath());
568             out.print(I18nContentSupportFactory.getI18nSupport().toI18NURI(child.getHandle()));
569             out.print(".html\"");
570 
571             if (StringUtils.isNotEmpty(accesskey)) {
572                 out.print(" accesskey=\"");
573                 out.print(accesskey);
574                 out.print("\"");
575             }
576 
577             if (nofollow != null && child.getNodeData(this.nofollow).getBoolean())
578             {
579                 out.print(" rel=\"nofollow\"");
580             }
581 
582             out.print(">");
583 
584             if (StringUtils.isNotEmpty(this.wrapperElement)) {
585                 out.print("<" + this.wrapperElement + ">");
586             }
587 
588             out.print(StringEscapeUtils.escapeHtml4(title));
589 
590             if (StringUtils.isNotEmpty(this.wrapperElement)) {
591                 out.print("</" + this.wrapperElement + ">");
592             }
593 
594             out.print(" </a>");
595 
596             if (self) {
597                 out.println("</strong>");
598             }
599 
600             if (showChildren) {
601                 drawChildren(child, activePage, out);
602             }
603             out.print("</li>");
604         }
605 
606         out.print("</ul>");
607     }
608 
609     /**
610      * Checks if the page has a visible children. Pages with the <code>hide in nav</code> attribute set to <code>true</code> are ignored.
611      * 
612      * @param page root page
613      * @return <code>true</code> if the given page has at least one visible child.
614      */
615     private boolean hasVisibleChildren(Content page) {
616         Collection<Content> children = page.getChildren();
617         if (children.size() > 0 && expandAll.equalsIgnoreCase(EXPAND_ALL)) {
618             return true;
619         }
620         for (Content ch : children) {
621             if (!ch.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
622                 return true;
623             }
624         }
625         return false;
626     }
627 
628     protected Node getCurrentActivePageNode() {
629         Node currentActpage = MgnlContext.getAggregationState().getCurrentContentNode();
630         if (currentActpage == null) {
631             currentActpage = MgnlContext.getAggregationState().getMainContentNode();
632         }
633         return currentActpage;
634     }
635 
636     /**
637      * @deprecated since 4.5 - use #getCurrentActivePageNode instead.
638      */
639     protected Content getCurrentActivePage() {
640         return ContentUtil.asContent(getCurrentActivePageNode());
641     }
642 }