View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 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.cms.taglibs.util;
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.taglibs.Resource;
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  
62  /**
63   * Draws a simple, css based, navigation menu. The menu layout can then be customized using css, and the default menu
64   * should be enough for most uses. Two following page properties will also be used in the menu:
65   * <ul>
66   * <li><code>navTitle</code>: a title to use for the navigation menu, if different from the real page title</li>
67   * <li><code>accessKey</code>: an optional access key which will be added to the link</li>
68   * <li><code>wrappingElement</code>: an optional html element (div, span, p, etc) to go within the &lt;a&gt; tag wrapping the anchor text
69   * </ul>
70   *
71   * @jsp.tag name="simpleNavigation" body-content="empty"
72   * @jsp.tag-example
73   * <pre>
74   * &lt;cmsu:simpleNavigation startLevel="3" style="mystyle"/&gt;
75   *
76   * Will output the following:
77   *
78   * &lt;ul class="level3 mystyle"&gt;
79   *     &lt;li&gt;&lt;a href="..."&gt;page 1 name &lt;/a&gt;&lt;/li&gt;
80   *     &lt;li&gt;&lt;a href="..."&gt;page 2 name &lt;/a&gt;&lt;/li&gt;
81   *     &lt;li class="trail"&gt;&lt;a href="..."&gt;page 3 name &lt;/a&gt;
82   *         &lt;ul class="level3"&gt;
83   *             &lt;li&gt;&lt;a href="..."&gt;subpage 1 name &lt;/a&gt;&lt;/li&gt;
84   *             &lt;li&gt;&lt;a href="..."&gt;subpage 2 name &lt;/a&gt;&lt;/li&gt;
85   *             &lt;li&gt;&lt;strong&gt;&lt;a href="..."&gt;selected page name &lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
86   *         &lt;/ul&gt;
87   *     &lt;/li&gt;
88   *     &lt;li&gt;&lt;a href="..."&gt;page 4 name &lt;/a&gt;&lt;/li&gt;
89   * &lt;/ul&gt;
90   *</pre>
91   *
92   * @author Fabrizio Giustina
93   * @version $Revision: 45983 $ ($Author: pbaerfuss $)
94   */
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"; //$NON-NLS-1$
111 
112     /**
113      * Css class added to open trees.
114      */
115     private static final String CSS_LI_CLOSED = "closed"; //$NON-NLS-1$
116 
117     /**
118      * Css class added to closed trees.
119      */
120     private static final String CSS_LI_OPEN = "open"; //$NON-NLS-1$
121 
122     /**
123      * Css class added to first li in ul.
124      */
125     private static final String CSS_LI_FIRST = "first"; //$NON-NLS-1$
126 
127     /**
128      * Css class added to last li in ul.
129      */
130     private static final String CSS_LI_LAST = "last"; //$NON-NLS-1$
131 
132     /**
133      * Page property: navigation title.
134      */
135     private static final String NODEDATA_NAVIGATIONTITLE = "navTitle"; //$NON-NLS-1$
136 
137     /**
138      * Page property: access key.
139      */
140     public static final String NODEDATA_ACCESSKEY = "accessKey"; //$NON-NLS-1$
141 
142     /**
143      * Default name for "open menu" nodeData.
144      */
145     public static final String DEFAULT_OPENMENU_NODEDATA = "openMenu"; //$NON-NLS-1$
146 
147     /**
148      * Default name for "hide in nav" nodeData.
149      */
150     public static final String DEFAULT_HIDEINNAV_NODEDATA = "hideInNav"; //$NON-NLS-1$
151 
152     /**
153      * Default name for "wrapperElement" nodeData.
154      */
155     public static final String DEFAULT_WRAPPERELEMENT_NODEDATA = ""; //$NON-NLS-1$
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     /**
173      * Stable serialVersionUID.
174      */
175     private static final long serialVersionUID = 224L;
176 
177     /**
178      * Logger.
179      */
180     private static Logger log = LoggerFactory.getLogger(SimpleNavigationTag.class);
181 
182     /**
183      * Start level.
184      */
185     private int startLevel;
186 
187     /**
188      * End level.
189      */
190     private int endLevel;
191 
192     /**
193      * Name for the "hide in nav" nodeData.
194      */
195     private String hideInNav;
196 
197     /**
198      * Name for the "open menu" nodeData.
199      */
200     private String openMenu;
201 
202     /**
203      * Style to apply to the menu.
204      */
205     private String style;
206 
207     /**
208      * html element to wrap the anchortext. (i.e. &lt;a&gt;&lt;wrapper&gt;...&lt;/wrapper&gt;&lt;/a&gt;)
209      */
210     public String wrapperElement;
211 
212     /**
213      * Expand all the nodes. (sitemap mode)
214      */
215     private String expandAll = EXPAND_NONE;
216 
217     private boolean relativeLevels = false;
218 
219     /**
220      * Name for a page property which will be written to the css class attribute.
221      */
222     private String classProperty;
223 
224     /**
225      * Name for the "nofollow" hodeData (for link that must be ignored by search engines).
226      */
227     private String nofollow;
228 
229 
230     /**
231      * Content Filter to use to evaluate if a page should be drawn.
232      */
233     private ContentFilter filter;
234 
235     private String contentFilter = "";
236 
237     /**
238      * Flag to set if the first and last li in each ul should be marked with a special css class.
239      */
240     private boolean markFirstAndLastElement = false;
241 
242     /**
243      * The start level for navigation, defaults to 0.
244      * @jsp.attribute required="false" rtexprvalue="true" type="int"
245      */
246     public void setStartLevel(int startLevel) {
247         this.startLevel = startLevel;
248     }
249 
250     /**
251      * The end level for navigation, defaults to 0.
252      * @jsp.attribute required="false" rtexprvalue="true" type="int"
253      */
254     public void setEndLevel(int endLevel) {
255         this.endLevel = endLevel;
256     }
257 
258     /**
259      * The css class to be applied to the first ul. Default is empty.
260      * @jsp.attribute required="false" rtexprvalue="true"
261      */
262     public void setStyle(String style) {
263         this.style = style;
264     }
265 
266     /**
267      * Name for the "hide in nav" nodeData. If a page contains a boolean property with this name and
268      * it is set to true, the page is not shown in navigation. Defaults to "hideInNav".
269      * @jsp.attribute required="false" rtexprvalue="true"
270      */
271     public void setHideInNav(String hideInNav) {
272         this.hideInNav = hideInNav;
273     }
274 
275     /**
276      * Name for the "open menu" nodeData. If a page contains a boolean property with this name and
277      * it is set to true, subpages are always shown also if the page is not selected.
278      * Defaults to "openMenu".
279      * @jsp.attribute required="false" rtexprvalue="true"
280      */
281     public void setOpenMenu(String openMenu) {
282         this.openMenu = openMenu;
283     }
284 
285     /**
286      * Name for the "nofollow" nodeData. If a page contains a boolean property with this name
287      * and it is set to true, rel="nofollow" will be added to the generated link
288      * (for links the should be ignored by search engines).
289      * @jsp.attribute required="false" rtexprvalue="true"
290      */
291     public void setNofollow(String nofollow) {
292         this.nofollow = nofollow;
293     }
294 
295     /**
296      * A variable in the pageContext that contains a content filter, determining if a given page should be drawn or not.
297      * @jsp.attribute required="false" rtexprvalue="true"
298      */
299     public void setContentFilter(String contentFilter) {
300         this.contentFilter = contentFilter;
301     }
302 
303     /**
304      * Sitemap mode. Can be assigned the "show" value. Only showable pages will be displayed. Any other value will
305      * result in displaying all pages.
306      * @jsp.attribute required="false" rtexprvalue="true"
307      */
308     public void setExpandAll(String expandAll) {
309         if (expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
310             this.expandAll = expandAll;
311         }
312         else {
313             this.expandAll = EXPAND_ALL;
314         }
315     }
316 
317     /**
318      * If set to true, the startLevel and endLevel values are treated relatively to the current active page.
319      * The default value is false.
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      * @jsp.attribute required="false" rtexprvalue="true"
329      */
330     public void setClassProperty(String classProperty) {
331         this.classProperty = classProperty;
332     }
333 
334     /**
335      * When specified, all links will have the anchortext wrapped in the supplied element. (such as "span")
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      * @jsp.attribute required="false" rtexprvalue="true" type="boolean"
347      */
348     public void setMarkFirstAndLastElement(boolean flag) {
349         markFirstAndLastElement = flag;
350     }
351 
352     @Override
353     public int doEndTag() throws JspException {
354         Content activePage = Resource.getCurrentActivePage();
355         try {
356             while (!ItemType.CONTENT.getSystemName().equals(activePage.getNodeTypeName()) && activePage.getParent() != null) {
357                 activePage = activePage.getParent();
358             }
359         } catch (RepositoryException e) {
360             log.error("Failed to obtain parent page for " + Resource.getCurrentActivePage().getHandle(), e);
361             activePage = Resource.getCurrentActivePage();
362         }
363         JspWriter out = this.pageContext.getOut();
364 
365         if (StringUtils.isNotEmpty(this.contentFilter)) {
366             try {
367                 filter = (ContentFilter) this.pageContext.getAttribute(this.contentFilter);
368             }
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         }
394         catch (RepositoryException e) {
395             log.error("RepositoryException caught while drawing navigation: " + e.getMessage(), e); //$NON-NLS-1$
396             return EVAL_PAGE;
397         }
398         catch (IOException e) {
399             // should never happen
400             throw new NestableRuntimeException(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      * @param page current page
427      * @param activePage active page
428      * @param out jsp writer
429      * @throws IOException jspwriter exception
430      * @throws RepositoryException any exception thrown during repository reading
431      */
432     private void drawChildren(Content page, Content activePage, JspWriter out) throws IOException, RepositoryException {
433 
434         Collection<Content> children = new ArrayList<Content>(page.getChildren(ItemType.CONTENT));
435 
436         if (children.size() == 0) {
437             return;
438         }
439 
440         out.print("<ul class=\"level"); //$NON-NLS-1$
441         out.print(page.getLevel());
442         if (style != null && page.getLevel() == startLevel) {
443             out.print(" ");
444             out.print(style);
445         }
446         out.print("\">"); //$NON-NLS-1$
447 
448         Iterator<Content> iter = children.iterator();
449         // loop through all children and discard those we don't want to display
450         while(iter.hasNext()) {
451             final Content child = iter.next();
452 
453             if (expandAll.equalsIgnoreCase(EXPAND_NONE) || expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
454                 if (child
455                     .getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
456                     iter.remove();
457                     continue;
458                 }
459                 // use a filter
460                 if (filter != null) {
461                     if (!filter.accept(child)) {
462                         iter.remove();
463                         continue;
464                     }
465                 }
466             } else {
467                 if (child.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
468                     iter.remove();
469                     continue;
470                 }
471             }
472         }
473 
474         boolean isFirst = true;
475         Iterator<Content> visibleIt = children.iterator();
476         while (visibleIt.hasNext()) {
477             Content child = visibleIt.next();
478             List<String> cssClasses = new ArrayList<String>(4);
479 
480             NodeData nodeData = I18nContentSupportFactory.getI18nSupport().getNodeData(child, NODEDATA_NAVIGATIONTITLE);
481             String title = nodeData.getString(StringUtils.EMPTY);
482 
483             // if nav title is not set, the main title is taken
484             if (StringUtils.isEmpty(title)) {
485                 title = child.getTitle();
486             }
487 
488             // if main title is not set, the name of the page is taken
489             if (StringUtils.isEmpty(title)) {
490                 title = child.getName();
491             }
492 
493             boolean showChildren = false;
494             boolean self = false;
495 
496             if (!expandAll.equalsIgnoreCase(EXPAND_NONE)) {
497                 showChildren = true;
498             }
499 
500             if (activePage.getHandle().equals(child.getHandle())) {
501                 // self
502                 showChildren = true;
503                 self = true;
504                 cssClasses.add(CSS_LI_ACTIVE);
505             }
506             else if (!showChildren) {
507                 showChildren = (child.getLevel() <= activePage.getAncestors().size() && activePage.getAncestor(
508                     child.getLevel()).getHandle().equals(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(" "); //$NON-NLS-1$
547                 }
548             }
549 
550             out.print("<li class=\""); //$NON-NLS-1$
551             out.print(css.toString());
552             out.print("\">"); //$NON-NLS-1$
553 
554             if (self) {
555                 out.println("<strong>"); //$NON-NLS-1$
556             }
557 
558             String accesskey = child.getNodeData(NODEDATA_ACCESSKEY).getString(StringUtils.EMPTY);
559 
560             out.print("<a href=\""); //$NON-NLS-1$
561             out.print(((HttpServletRequest) this.pageContext.getRequest()).getContextPath());
562             out.print(I18nContentSupportFactory.getI18nSupport().toI18NURI(child.getHandle()));
563             out.print(".html\""); //$NON-NLS-1$
564 
565             if (StringUtils.isNotEmpty(accesskey)) {
566                 out.print(" accesskey=\""); //$NON-NLS-1$
567                 out.print(accesskey);
568                 out.print("\""); //$NON-NLS-1$
569             }
570 
571             if (nofollow != null && child.getNodeData(this.nofollow).getBoolean())
572             {
573                 out.print(" rel=\"nofollow\""); //$NON-NLS-1$
574             }
575 
576             out.print(">"); //$NON-NLS-1$
577 
578             if (StringUtils.isNotEmpty(this.wrapperElement)) {
579                 out.print("<" + this.wrapperElement + ">"); //$NON-NLS-1$
580             }
581 
582             out.print(StringEscapeUtils.escapeHtml(title));
583 
584             if (StringUtils.isNotEmpty(this.wrapperElement)) {
585                 out.print("</" + this.wrapperElement + ">"); //$NON-NLS-1$
586             }
587 
588             out.print(" </a>"); //$NON-NLS-1$
589 
590             if (self) {
591                 out.println("</strong>"); //$NON-NLS-1$
592             }
593 
594             if (showChildren) {
595                 drawChildren(child, activePage, out);
596             }
597             out.print("</li>"); //$NON-NLS-1$
598         }
599 
600         out.print("</ul>"); //$NON-NLS-1$
601     }
602 
603     /**
604      * Checks if the page has a visible children. Pages with the <code>hide in nav</code> attribute set to
605      * <code>true</code> are ignored.
606      * @param page root page
607      * @return <code>true</code> if the given page has at least one visible child.
608      */
609     private boolean hasVisibleChildren(Content page) {
610         Collection<Content> children = page.getChildren();
611         if (children.size() > 0 && expandAll.equalsIgnoreCase(EXPAND_ALL)) {
612             return true;
613         }
614         for (Content ch : children) {
615             if (!ch.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
616                 return true;
617             }
618         }
619         return false;
620     }
621 
622 }