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: 41137 $ ($Author: gjoseph $)
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     public int doEndTag() throws JspException {
353         Content activePage = Resource.getCurrentActivePage();
354         try {
355             while (!ItemType.CONTENT.getSystemName().equals(activePage.getNodeTypeName()) && activePage.getParent() != null) {
356                 activePage = activePage.getParent();
357             }
358         } catch (RepositoryException e) {
359             log.error("Failed to obtain parent page for " + Resource.getCurrentActivePage().getHandle(), e);
360             activePage = Resource.getCurrentActivePage();
361         }
362         JspWriter out = this.pageContext.getOut();
363 
364         if (StringUtils.isNotEmpty(this.contentFilter)) {
365             try {
366                 filter = (ContentFilter) this.pageContext.getAttribute(this.contentFilter);
367             }
368             catch(ClassCastException e) {
369                 log.error("contentFilter assigned was not a content filter", e);
370             }
371         } else {
372             filter = null;
373         }
374 
375         if (startLevel > endLevel) {
376             endLevel = 0;
377         }
378 
379         try {
380             final int activePageLevel = activePage.getLevel();
381             // if we are to treat the start and end level as relative
382             // to the active page, we adjust them here...
383             if (relativeLevels) {
384                 this.startLevel += activePageLevel;
385                 this.endLevel += activePageLevel;
386             }
387             if (this.startLevel <= activePageLevel) {
388                 Content startContent = activePage.getAncestor(this.startLevel);
389                 drawChildren(startContent, activePage, out);
390             }
391 
392         }
393         catch (RepositoryException e) {
394             log.error("RepositoryException caught while drawing navigation: " + e.getMessage(), e); //$NON-NLS-1$
395             return EVAL_PAGE;
396         }
397         catch (IOException e) {
398             // should never happen
399             throw new NestableRuntimeException(e);
400         }
401 
402         return EVAL_PAGE;
403     }
404 
405     public void release() {
406         this.startLevel = 0;
407         this.endLevel = 0;
408         this.hideInNav = null;
409         this.openMenu = null;
410         this.style = null;
411         this.classProperty = null;
412         this.expandAll = EXPAND_NONE;
413         this.relativeLevels = false;
414         this.wrapperElement = "";
415         this.contentFilter = "";
416         this.filter = null;
417         this.nofollow = null;
418         this.markFirstAndLastElement = false;
419         super.release();
420     }
421 
422     /**
423      * Draws page children as an unordered list.
424      * @param page current page
425      * @param activePage active page
426      * @param out jsp writer
427      * @throws IOException jspwriter exception
428      * @throws RepositoryException any exception thrown during repository reading
429      */
430     private void drawChildren(Content page, Content activePage, JspWriter out) throws IOException, RepositoryException {
431 
432         Collection<Content> children = new ArrayList<Content>(page.getChildren(ItemType.CONTENT));
433 
434         if (children.size() == 0) {
435             return;
436         }
437 
438         out.print("<ul class=\"level"); //$NON-NLS-1$
439         out.print(page.getLevel());
440         if (style != null && page.getLevel() == startLevel) {
441             out.print(" ");
442             out.print(style);
443         }
444         out.print("\">"); //$NON-NLS-1$
445 
446         Iterator<Content> iter = children.iterator();
447         // loop through all children and discard those we don't want to display
448         while(iter.hasNext()) {
449             final Content child = iter.next();
450 
451             if (expandAll.equalsIgnoreCase(EXPAND_NONE) || expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
452                 if (child
453                     .getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
454                     iter.remove();
455                     continue;
456                 }
457                 // use a filter
458                 if (filter != null) {
459                     if (!filter.accept(child)) {
460                         iter.remove();
461                         continue;
462                     }
463                 }
464             } else {
465                 if (child.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
466                     iter.remove();
467                     continue;
468                 }
469             }
470         }
471 
472         boolean isFirst = true;
473         Iterator<Content> visibleIt = children.iterator();
474         while (visibleIt.hasNext()) {
475             Content child = visibleIt.next();
476             List<String> cssClasses = new ArrayList<String>(4);
477 
478             NodeData nodeData = I18nContentSupportFactory.getI18nSupport().getNodeData(child, NODEDATA_NAVIGATIONTITLE);
479             String title = nodeData.getString(StringUtils.EMPTY);
480 
481             // if nav title is not set, the main title is taken
482             if (StringUtils.isEmpty(title)) {
483                 title = child.getTitle();
484             }
485 
486             // if main title is not set, the name of the page is taken
487             if (StringUtils.isEmpty(title)) {
488                 title = child.getName();
489             }
490 
491             boolean showChildren = false;
492             boolean self = false;
493 
494             if (!expandAll.equalsIgnoreCase(EXPAND_NONE)) {
495                 showChildren = true;
496             }
497 
498             if (activePage.getHandle().equals(child.getHandle())) {
499                 // self
500                 showChildren = true;
501                 self = true;
502                 cssClasses.add(CSS_LI_ACTIVE);
503             }
504             else if (!showChildren) {
505                 showChildren = (child.getLevel() <= activePage.getAncestors().size() && activePage.getAncestor(
506                     child.getLevel()).getHandle().equals(child.getHandle()));
507             }
508 
509             if (!showChildren) {
510                 showChildren = child
511                     .getNodeData(StringUtils.defaultString(this.openMenu, DEFAULT_OPENMENU_NODEDATA))
512                     .getBoolean();
513             }
514 
515             if (endLevel > 0) {
516                 showChildren &= child.getLevel() < endLevel;
517             }
518 
519             cssClasses.add(hasVisibleChildren(child) ? (showChildren ? CSS_LI_OPEN : CSS_LI_CLOSED) : CSS_LI_LEAF);
520 
521             if (child.getLevel() < activePage.getLevel()
522                 && activePage.getAncestor(child.getLevel()).getHandle().equals(child.getHandle())) {
523                 cssClasses.add(CSS_LI_TRAIL);
524             }
525 
526             if (StringUtils.isNotEmpty(classProperty) && child.hasNodeData(classProperty)) {
527                 cssClasses.add(child.getNodeData(classProperty).getString(StringUtils.EMPTY));
528             }
529 
530             if (markFirstAndLastElement && isFirst) {
531                 cssClasses.add(CSS_LI_FIRST);
532                 isFirst = false;
533             }
534 
535             if (markFirstAndLastElement && !visibleIt.hasNext()) {
536                 cssClasses.add(CSS_LI_LAST);
537             }
538 
539             StringBuffer css = new StringBuffer(cssClasses.size() * 10);
540             Iterator<String> iterator = cssClasses.iterator();
541             while (iterator.hasNext()) {
542                 css.append(iterator.next());
543                 if (iterator.hasNext()) {
544                     css.append(" "); //$NON-NLS-1$
545                 }
546             }
547 
548             out.print("<li class=\""); //$NON-NLS-1$
549             out.print(css.toString());
550             out.print("\">"); //$NON-NLS-1$
551 
552             if (self) {
553                 out.println("<strong>"); //$NON-NLS-1$
554             }
555 
556             String accesskey = child.getNodeData(NODEDATA_ACCESSKEY).getString(StringUtils.EMPTY);
557 
558             out.print("<a href=\""); //$NON-NLS-1$
559             out.print(((HttpServletRequest) this.pageContext.getRequest()).getContextPath());
560             out.print(I18nContentSupportFactory.getI18nSupport().toI18NURI(child.getHandle()));
561             out.print(".html\""); //$NON-NLS-1$
562 
563             if (StringUtils.isNotEmpty(accesskey)) {
564                 out.print(" accesskey=\""); //$NON-NLS-1$
565                 out.print(accesskey);
566                 out.print("\""); //$NON-NLS-1$
567             }
568 
569             if (nofollow != null && child.getNodeData(this.nofollow).getBoolean())
570             {
571                 out.print(" rel=\"nofollow\""); //$NON-NLS-1$
572             }
573 
574             out.print(">"); //$NON-NLS-1$
575 
576             if (StringUtils.isNotEmpty(this.wrapperElement)) {
577                 out.print("<" + this.wrapperElement + ">"); //$NON-NLS-1$
578             }
579 
580             out.print(StringEscapeUtils.escapeHtml(title));
581 
582             if (StringUtils.isNotEmpty(this.wrapperElement)) {
583                 out.print("</" + this.wrapperElement + ">"); //$NON-NLS-1$
584             }
585 
586             out.print(" </a>"); //$NON-NLS-1$
587 
588             if (self) {
589                 out.println("</strong>"); //$NON-NLS-1$
590             }
591 
592             if (showChildren) {
593                 drawChildren(child, activePage, out);
594             }
595             out.print("</li>"); //$NON-NLS-1$
596         }
597 
598         out.print("</ul>"); //$NON-NLS-1$
599     }
600 
601     /**
602      * Checks if the page has a visible children. Pages with the <code>hide in nav</code> attribute set to
603      * <code>true</code> are ignored.
604      * @param page root page
605      * @return <code>true</code> if the given page has at least one visible child.
606      */
607     private boolean hasVisibleChildren(Content page) {
608         Collection<Content> children = page.getChildren();
609         if (children.size() > 0 && expandAll.equalsIgnoreCase(EXPAND_ALL)) {
610             return true;
611         }
612         for (Content ch : children) {
613             if (!ch.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
614                 return true;
615             }
616         }
617         return false;
618     }
619 
620 }