View Javadoc

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