View Javadoc

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