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