/**
* This file Copyright (c) 2012 Magnolia International
* Ltd. (http://www.magnolia-cms.com). All rights reserved.
*
*
* This file is dual-licensed under both the Magnolia
* Network Agreement and the GNU General Public License.
* You may elect to use one or the other of these licenses.
*
* This file is distributed in the hope that it will be
* useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
* Redistribution, except as permitted by whichever of the GPL
* or MNA you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or
* modify this file under the terms of the GNU General
* Public License, Version 3, as published by the Free Software
* Foundation. You should have received a copy of the GNU
* General Public License, Version 3 along with this program;
* if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 2. For the Magnolia Network Agreement (MNA), this file
* and the accompanying materials are made available under the
* terms of the MNA which accompanies this distribution, and
* is available at http://www.magnolia-cms.com/mna.html
*
* Any modifications to this file must keep this entire header
* intact.
*
*/
package info.magnolia.templating.jsp.taglib;
import info.magnolia.cms.core.Content;
import info.magnolia.cms.core.Content.ContentFilter;
import info.magnolia.cms.core.ItemType;
import info.magnolia.cms.core.NodeData;
import info.magnolia.cms.i18n.I18nContentSupportFactory;
import info.magnolia.context.MgnlContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.jcr.RepositoryException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.TagSupport;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.NestableRuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tldgen.annotations.BodyContent;
import org.tldgen.annotations.Tag;
/**
* Draws a simple, css based, navigation menu. The menu layout can then be customized using css, and the default menu
* should be enough for most uses. Two following page properties will also be used in the menu:
* <ul>
* <li><code>navTitle</code>: a title to use for the navigation menu, if different from the real page title</li>
* <li><code>accessKey</code>: an optional access key which will be added to the link</li>
* <li><code>wrappingElement</code>: an optional html element (div, span, p, etc) to go within the <a> tag wrapping the anchor text
* </ul>
*
* @jsp.tag name="simpleNavigation" body-content="empty"
* @jsp.tag-example
* <pre>
* <cmsu:simpleNavigation startLevel="3" style="mystyle"/>
*
* Will output the following:
*
* <ul class="level3 mystyle">
* <li><a href="...">page 1 name </a></li>
* <li><a href="...">page 2 name </a></li>
* <li class="trail"><a href="...">page 3 name </a>
* <ul class="level3">
* <li><a href="...">subpage 1 name </a></li>
* <li><a href="...">subpage 2 name </a></li>
* <li><strong><a href="...">selected page name </a></strong></li>
* </ul>
* </li>
* <li><a href="...">page 4 name </a></li>
* </ul>
* </pre>
* @author Fabrizio Giustina
* @version $Revision$ ($Author$)
*/
@Tag(name = "simpleNavigation", bodyContent = BodyContent.EMPTY)
public class SimpleNavigationTag extends TagSupport {
/**
* Css class added to active page.
*/
private static final String CSS_LI_ACTIVE = "active";
/**
* Css class added to ancestor of the active page.
*/
private static final String CSS_LI_TRAIL = "trail";
/**
* Css class added to leaf pages.
*/
private static final String CSS_LI_LEAF = "leaf";
/**
* Css class added to open trees.
*/
private static final String CSS_LI_CLOSED = "closed";
/**
* Css class added to closed trees.
*/
private static final String CSS_LI_OPEN = "open";
/**
* Css class added to first li in ul.
*/
private static final String CSS_LI_FIRST = "first";
/**
* Css class added to last li in ul.
*/
private static final String CSS_LI_LAST = "last";
/**
* Page property: navigation title.
*/
private static final String NODEDATA_NAVIGATIONTITLE = "navTitle";
/**
* Page property: access key.
*/
public static final String NODEDATA_ACCESSKEY = "accessKey";
/**
* Default name for "open menu" nodeData.
*/
public static final String DEFAULT_OPENMENU_NODEDATA = "openMenu";
/**
* Default name for "hide in nav" nodeData.
*/
public static final String DEFAULT_HIDEINNAV_NODEDATA = "hideInNav";
/**
* Default name for "wrapperElement" nodeData.
*/
public static final String DEFAULT_WRAPPERELEMENT_NODEDATA = "";
/**
* Expand all expand all the nodes.
*/
public static final String EXPAND_ALL = "all";
/**
* Expand all expand only page that should be displayed in navigation.
*/
public static final String EXPAND_SHOW = "show";
/**
* Do not use expand functions.
*/
public static final String EXPAND_NONE = "none";
/**
* Stable serialVersionUID.
*/
private static final long serialVersionUID = 224L;
/**
* Logger.
*/
private static Logger log = LoggerFactory.getLogger(SimpleNavigationTag.class);
/**
* Start level.
*/
private int startLevel;
/**
* End level.
*/
private int endLevel;
/**
* Name for the "hide in nav" nodeData.
*/
private String hideInNav;
/**
* Name for the "open menu" nodeData.
*/
private String openMenu;
/**
* Style to apply to the menu.
*/
private String style;
/**
* html element to wrap the anchortext. (i.e. <a><wrapper>...</wrapper></a>)
*/
public String wrapperElement;
/**
* Expand all the nodes. (sitemap mode)
*/
private String expandAll = EXPAND_NONE;
private boolean relativeLevels = false;
/**
* Name for a page property which will be written to the css class attribute.
*/
private String classProperty;
/**
* Name for the "nofollow" hodeData (for link that must be ignored by search engines).
*/
private String nofollow;
/**
* Content Filter to use to evaluate if a page should be drawn.
*/
private ContentFilter filter;
private String contentFilter = "";
/**
* Flag to set if the first and last li in each ul should be marked with a special css class.
*/
private boolean markFirstAndLastElement = false;
/**
* The start level for navigation, defaults to 0.
*
* @jsp.attribute required="false" rtexprvalue="true" type="int"
*/
public void setStartLevel(int startLevel) {
this.startLevel = startLevel;
}
/**
* The end level for navigation, defaults to 0.
*
* @jsp.attribute required="false" rtexprvalue="true" type="int"
*/
public void setEndLevel(int endLevel) {
this.endLevel = endLevel;
}
/**
* The css class to be applied to the first ul. Default is empty.
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setStyle(String style) {
this.style = style;
}
/**
* Name for the "hide in nav" nodeData. If a page contains a boolean property with this name and
* it is set to true, the page is not shown in navigation. Defaults to "hideInNav".
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setHideInNav(String hideInNav) {
this.hideInNav = hideInNav;
}
/**
* Name for the "open menu" nodeData. If a page contains a boolean property with this name and
* it is set to true, subpages are always shown also if the page is not selected.
* Defaults to "openMenu".
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setOpenMenu(String openMenu) {
this.openMenu = openMenu;
}
/**
* Name for the "nofollow" nodeData. If a page contains a boolean property with this name
* and it is set to true, rel="nofollow" will be added to the generated link
* (for links the should be ignored by search engines).
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setNofollow(String nofollow) {
this.nofollow = nofollow;
}
/**
* A variable in the pageContext that contains a content filter, determining if a given page should be drawn or not.
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setContentFilter(String contentFilter) {
this.contentFilter = contentFilter;
}
/**
* Sitemap mode. Can be assigned the "show" value. Only showable pages will be displayed. Any other value will
* result in displaying all pages.
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setExpandAll(String expandAll) {
if (expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
this.expandAll = expandAll;
}
else {
this.expandAll = EXPAND_ALL;
}
}
/**
* If set to true, the startLevel and endLevel values are treated relatively to the current active page.
* The default value is false.
*
* @jsp.attribute required="false" rtexprvalue="true" type="boolean"
*/
public void setRelativeLevels(boolean relativeLevels) {
this.relativeLevels = relativeLevels;
}
/**
* Name for a page property that will hold a css class name which will be added to the html class attribute.
*
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setClassProperty(String classProperty) {
this.classProperty = classProperty;
}
/**
* When specified, all links will have the anchortext wrapped in the supplied element. (such as "span")
*
* @param wrapperElement name of an html element that will be included in the anchor, wrapping the anchortext
* @jsp.attribute required="false" rtexprvalue="true"
*/
public void setWrapperElement(String wrapperElement) {
this.wrapperElement = wrapperElement;
}
/**
* If set to true, a "first" or "last" css class will be added to the list of css classes of the
* first and the last li in each ul.
*
* @jsp.attribute required="false" rtexprvalue="true" type="boolean"
*/
public void setMarkFirstAndLastElement(boolean flag) {
markFirstAndLastElement = flag;
}
@Override
public int doEndTag() throws JspException {
Content activePage = getCurrentActivePage();
try {
while (!ItemType.PAGE.getSystemName().equals(activePage.getNodeTypeName()) && activePage.getLevel() != 0) {
activePage = activePage.getParent();
}
} catch (RepositoryException e) {
log.error("Failed to obtain parent page for " + getCurrentActivePage().getHandle(), e);
activePage = getCurrentActivePage();
}
JspWriter out = this.pageContext.getOut();
if (StringUtils.isNotEmpty(this.contentFilter)) {
try {
filter = (ContentFilter) this.pageContext.getAttribute(this.contentFilter);
} catch (ClassCastException e) {
log.error("contentFilter assigned was not a content filter", e);
}
} else {
filter = null;
}
if (startLevel > endLevel) {
endLevel = 0;
}
try {
final int activePageLevel = activePage.getLevel();
// if we are to treat the start and end level as relative
// to the active page, we adjust them here...
if (relativeLevels) {
this.startLevel += activePageLevel;
this.endLevel += activePageLevel;
}
if (this.startLevel <= activePageLevel) {
Content startContent = activePage.getAncestor(this.startLevel);
drawChildren(startContent, activePage, out);
}
} catch (RepositoryException e) {
log.error("RepositoryException caught while drawing navigation: " + e.getMessage(), e);
return EVAL_PAGE;
} catch (IOException e) {
// should never happen
throw new NestableRuntimeException(e);
}
return EVAL_PAGE;
}
@Override
public void release() {
this.startLevel = 0;
this.endLevel = 0;
this.hideInNav = null;
this.openMenu = null;
this.style = null;
this.classProperty = null;
this.expandAll = EXPAND_NONE;
this.relativeLevels = false;
this.wrapperElement = "";
this.contentFilter = "";
this.filter = null;
this.nofollow = null;
this.markFirstAndLastElement = false;
super.release();
}
/**
* Draws page children as an unordered list.
*
* @param page current page
* @param activePage active page
* @param out jsp writer
* @throws IOException jspwriter exception
* @throws RepositoryException any exception thrown during repository reading
*/
private void drawChildren(Content page, Content activePage, JspWriter out) throws IOException, RepositoryException {
Collection<Content> children = new ArrayList<Content>(page.getChildren(ItemType.CONTENT));
if (children.size() == 0) {
return;
}
out.print("<ul class=\"level");
out.print(page.getLevel());
if (style != null && page.getLevel() == startLevel) {
out.print(" ");
out.print(style);
}
out.print("\">");
Iterator<Content> iter = children.iterator();
// loop through all children and discard those we don't want to display
while (iter.hasNext()) {
final Content child = iter.next();
if (expandAll.equalsIgnoreCase(EXPAND_NONE) || expandAll.equalsIgnoreCase(EXPAND_SHOW)) {
if (child
.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
iter.remove();
continue;
}
// use a filter
if (filter != null) {
if (!filter.accept(child)) {
iter.remove();
continue;
}
}
} else {
if (child.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
iter.remove();
continue;
}
}
}
boolean isFirst = true;
Iterator<Content> visibleIt = children.iterator();
while (visibleIt.hasNext()) {
Content child = visibleIt.next();
List<String> cssClasses = new ArrayList<String>(4);
NodeData nodeData = I18nContentSupportFactory.getI18nSupport().getNodeData(child, NODEDATA_NAVIGATIONTITLE);
String title = null;
if(nodeData != null){
title = nodeData.getString(StringUtils.EMPTY);
}
// if nav title is not set, the main title is taken
if (StringUtils.isEmpty(title)) {
title = child.getTitle();
}
// if main title is not set, the name of the page is taken
if (StringUtils.isEmpty(title)) {
title = child.getName();
}
boolean showChildren = false;
boolean self = false;
if (!expandAll.equalsIgnoreCase(EXPAND_NONE)) {
showChildren = true;
}
if (activePage.getHandle().equals(child.getHandle())) {
// self
showChildren = true;
self = true;
cssClasses.add(CSS_LI_ACTIVE);
}
else if (!showChildren) {
showChildren = child.getLevel() <= activePage.getAncestors().size() && StringUtils.equals(activePage.getAncestor(child.getLevel()).getHandle(), child.getHandle());
}
if (!showChildren) {
showChildren = child
.getNodeData(StringUtils.defaultString(this.openMenu, DEFAULT_OPENMENU_NODEDATA))
.getBoolean();
}
if (endLevel > 0) {
showChildren &= child.getLevel() < endLevel;
}
cssClasses.add(hasVisibleChildren(child) ? showChildren ? CSS_LI_OPEN : CSS_LI_CLOSED : CSS_LI_LEAF);
if (child.getLevel() < activePage.getLevel()
&& activePage.getAncestor(child.getLevel()).getHandle().equals(child.getHandle())) {
cssClasses.add(CSS_LI_TRAIL);
}
if (StringUtils.isNotEmpty(classProperty) && child.hasNodeData(classProperty)) {
cssClasses.add(child.getNodeData(classProperty).getString(StringUtils.EMPTY));
}
if (markFirstAndLastElement && isFirst) {
cssClasses.add(CSS_LI_FIRST);
isFirst = false;
}
if (markFirstAndLastElement && !visibleIt.hasNext()) {
cssClasses.add(CSS_LI_LAST);
}
StringBuffer css = new StringBuffer(cssClasses.size() * 10);
Iterator<String> iterator = cssClasses.iterator();
while (iterator.hasNext()) {
css.append(iterator.next());
if (iterator.hasNext()) {
css.append(" ");
}
}
out.print("<li class=\"");
out.print(css.toString());
out.print("\">");
if (self) {
out.println("<strong>");
}
String accesskey = null;
if(child.getNodeData(NODEDATA_ACCESSKEY) != null){
accesskey = child.getNodeData(NODEDATA_ACCESSKEY).getString(StringUtils.EMPTY);
}
out.print("<a href=\"");
out.print(((HttpServletRequest) this.pageContext.getRequest()).getContextPath());
out.print(I18nContentSupportFactory.getI18nSupport().toI18NURI(child.getHandle()));
out.print(".html\"");
if (StringUtils.isNotEmpty(accesskey)) {
out.print(" accesskey=\"");
out.print(accesskey);
out.print("\"");
}
if (nofollow != null && child.getNodeData(this.nofollow).getBoolean())
{
out.print(" rel=\"nofollow\"");
}
out.print(">");
if (StringUtils.isNotEmpty(this.wrapperElement)) {
out.print("<" + this.wrapperElement + ">");
}
out.print(StringEscapeUtils.escapeHtml(title));
if (StringUtils.isNotEmpty(this.wrapperElement)) {
out.print("</" + this.wrapperElement + ">");
}
out.print(" </a>");
if (self) {
out.println("</strong>");
}
if (showChildren) {
drawChildren(child, activePage, out);
}
out.print("</li>");
}
out.print("</ul>");
}
/**
* Checks if the page has a visible children. Pages with the <code>hide in nav</code> attribute set to <code>true</code> are ignored.
*
* @param page root page
* @return <code>true</code> if the given page has at least one visible child.
*/
private boolean hasVisibleChildren(Content page) {
Collection<Content> children = page.getChildren();
if (children.size() > 0 && expandAll.equalsIgnoreCase(EXPAND_ALL)) {
return true;
}
for (Content ch : children) {
if (!ch.getNodeData(StringUtils.defaultString(this.hideInNav, DEFAULT_HIDEINNAV_NODEDATA)).getBoolean()) {
return true;
}
}
return false;
}
protected Content getCurrentActivePage() {
Content currentActpage = MgnlContext.getAggregationState().getCurrentContent();
if (currentActpage == null) {
currentActpage = MgnlContext.getAggregationState().getMainContent();
}
return currentActpage;
}
}