View Javadoc

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