/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.katari.core.web;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.i18n.LocaleContextHolder;
import com.globant.katari.core.spring.KatariMessageSource;
/** This represents a node item in a menu, a node item could be either a
* container or a leaf.
*
* A container means that this menu item can hold either other containers or
* leaves inside, creating a tree.
*
* @author mariano.nardi
*/
public class MenuNode implements Comparable<MenuNode> {
/** The class logger.
*/
private final Logger log = LoggerFactory.getLogger(MenuNode.class);
/** The message source associated with the menu node.
*
* If specified, this message source is used to obtain the display name of
* the menu node. It is null if the menu is not internationalized.
*/
private KatariMessageSource messageSource;
/** The node display name to the customer.
*
* It cannot be null.
*/
private String displayName = "";
/** The node identifier name.
*
* it cannot be null and cannot contain empty spaces.
*/
private String name = "";
/** The node position in the container.
*
* It only applies for non top level nodes and can be either negative or
* positive.
*/
private int position = 0;
/** The node tool tip text.
*
* A null tooltip means that no tooltip will be rendered.
*/
private String toolTip = null;
/** The path this node is linked to.
*
* It will be only null for containers.
*/
private String linkPath = null;
/** false if this node allows children on it, true if it is at the bottom
* level.
*/
private boolean isLeaf = false;
/** A List containing all the child nodes for this node.
*
* It is never null, just empty for leaf nodes.
*/
private final List<MenuNode> childNodes = new ArrayList<MenuNode>();
/** The node parent, null means a top level node but this will be only valid
* for menu bars.
*/
private MenuNode parent = null;
/** The home node, this will be selected option when the container is
* selected.
*
* It is null only for leafs and empty containers.
*/
private MenuNode home = null;
/**
* Creates a new <code>MenuNode</code> container.
*
* @param theParent the parent for this node, it cannot be null. if you need
* a top level container a MenuBar should be used.
* @param theDisplayName the node display name to the customer. It cannot be
* null.
* @param theName the node identifier name. It cannot be null and cannot
* contain empty spaces.
* @param thePosition the node position in the container. It only applies for
* non top level nodes and can be either negative or positive.
* @param theToolTip the menu tooltip, a null tooltip means that no tooltip
* will be rendered.
*/
public MenuNode(final MenuNode theParent, final String theDisplayName,
final String theName, final int thePosition, final String theToolTip) {
Validate.notNull(theParent, "The parent cannot be null, if you need "
+ "a top level node use a MenuBar");
Validate.notEmpty(theDisplayName, "You have to specify the display name");
Validate.notEmpty(theName, "You have to specify the identifier name");
Validate.isTrue(theName.indexOf(' ') == -1,
"the name cannot contain empty spaces");
this.isLeaf = false;
this.parent = theParent;
this.parent.childNodes.add(this);
this.displayName = theDisplayName;
this.name = theName;
this.position = thePosition;
this.toolTip = theToolTip;
}
/**
* Creates a new <code>MenuNode</code> leaf.
*
* @param theParent the parent for this node, since is a leaf it cannot be
* null.
* @param theDisplayName the node display name to the client.
* @param theName the node identifier name. It cannot be null and cannot
* contain empty spaces.
* @param thePosition the node position in the container. It only applies for
* non top level nodes and can be either negative or positive.
* @param theToolTip the menu tooltip, a null tooltip means that no tooltip
* will be rendered.
* @param theLinkPath the leaf link, since is a leaf it cannot be null.
*/
public MenuNode(final MenuNode theParent, final String theDisplayName,
final String theName, final int thePosition,
final String theToolTip, final String theLinkPath) {
Validate.notNull(theParent, "A Leaf should always have a parent");
Validate.notEmpty(theDisplayName, "You have to specify the display name");
Validate.notEmpty(theName, "You have to specify the identifier name");
Validate.isTrue(theName.indexOf(' ') == -1,
"the name cannot contain empty spaces");
Validate.notNull(theLinkPath, "A Leaf should always have a link path");
this.isLeaf = true;
this.parent = theParent;
this.parent.childNodes.add(this);
this.parent.setHome(this);
this.displayName = theDisplayName;
this.name = theName;
this.position = thePosition;
this.toolTip = theToolTip;
this.linkPath = theLinkPath;
}
/**
* Returns the node display name.
*
* @return the node display name, it is never null.
*/
public String getDisplayName() {
Locale locale = LocaleContextHolder.getLocale();
if (messageSource != null) {
return messageSource.getMessage(getPath(), null, displayName, locale);
} else {
return displayName;
}
}
/**
* Returns the node identifier name.
*
* @return the node identifier name, it is never null and cannot contain
* empty spaces.
*/
public String getName() {
return this.name;
}
/** Returns the node position in the container.
*
* It only applies for non top level nodes and can be either negative or
* positive. The menu is shown in the screen in ascending position order.
*
* @return the node position;
*/
public int getPosition() {
return position;
}
/** Returns the node link path where this menu element links to.
*
* @return the link path for this node. If this is a container, it returns
* the link of the home menu, if specified. Otherwise, it returns null.
*/
public String getLinkPath() {
if (this.isLeaf) {
return this.linkPath;
} else if (this.home != null) {
return this.home.getLinkPath();
}
return null;
}
/**
* Returns the node tooltip.
*
* @return the menu tooltip. A null tooltip means that no tooltip will be
* rendered.
*/
public String getToolTip() {
return this.toolTip;
}
/**
* Returns the parent <code>MenuNode</code> of the receiver.
*
* @return the parent container for this node. A null parent means a top
* level node (a MenuBar).
*/
public MenuNode getParent() {
return this.parent;
}
/**
* Returns the home <code>MenuNode</code> of the container.
*
* @return the home leaf of this container. the first added child will be
* assumed as home until a new home leaf is explicitly added. Null if the
* node has no chidren.
*/
public MenuNode getHome() {
return this.home;
}
/** Sets the home <code>MenuNode</code> of the container and all it's
* ancestors.
*
* This operation guarantees that every node in the menu hierarchy has a
* home. If the home was already set, this operation does nothing.
*
* @param theHome the home leaf of this container. the first added child will
* be assumed as home until a new home leaf is explicitly added. It cannot be
* specified as null but it will be null while this node contains no
* children.
*/
private void setHome(final MenuNode theHome) {
Validate.notNull(theHome, "you can't specify a null home node");
if (home == null) {
home = theHome;
if (parent != null) {
parent.setHome(theHome);
}
}
}
/**
* Returns true if this menu represents a leaf.
*
* @return true if this is a leaf. That means that no child can be added to
* this node.
*/
public boolean isLeaf() {
return this.isLeaf;
}
/** Returns the children of the receiver as an <code>List</code>.
*
* This is only valid if the node is a container, means not a leaf.
*
* @return a List containing the children of this node.
*/
public List<MenuNode> getChildNodes() {
Validate.isTrue(!this.isLeaf,
"This node is a leaf, so it contains no children");
return Collections.unmodifiableList(childNodes);
}
/**
* Returns the node's path signature.
*
* The path creation is delegated into the parents recursively.
*
* @return the node's path signature. This is not null.
*/
public String getPath() {
if (this.getParent() == null) {
return "/" + this.name;
} else {
return this.getParent().getPath() + "/" + this.name;
}
}
/** Merge all the children of the other node into this node.
*
* The merge operation consists of taking all the children of the other node
* and adding them to this node. If two nodes have the same name and one is a
* leaf node, the merge fails. Otherwise, the merge is reapplied between both
* nodes.
*
* Notice that the other node is not included in the merge, only its
* children.
*
* Also, given that merging adds child nodes, this node cannot be a leaf.
*
* @param other the node whose children need to be merged with the ones on
* this node that should be merged. It cannot be null.
*
* @param variables a map of variable names to variables. These variables can
* be referenced in the menu link as ${variable-name}. It cannot be null.
*
* @param prefix a prefix to add to relative menu link. It cannot be null.
*/
public void merge(final MenuNode other,
final Map<String, String> variables, final String prefix) {
log.trace("Entering <{}>->merge('{}', ...", name, other.name);
Validate.notNull(other, "The menu to merge cannot be null");
Validate.notNull(variables, "The variables map cannot be null");
Validate.notNull(prefix, "The prefix cannot be null");
Validate.isTrue(!isLeaf, "You cannot merge with a leaf");
other.transformAllLinks(variables, prefix);
if (this.childNodes.size() > 0) {
for (MenuNode otherCurrNode : other.childNodes) {
int pos = this.childNodes.indexOf(otherCurrNode);
if (pos != -1) {
MenuNode thisCurrNode = this.childNodes.get(pos);
Validate.isTrue(!thisCurrNode.isLeaf() && !otherCurrNode.isLeaf(),
"You cannot have a leaf node with the same name as other menu"
+ " node. The problematic menu is " + otherCurrNode.getPath());
if (thisCurrNode.messageSource == null) {
// Make sure that if somebody specified a message source, the node
// always has a message source.
thisCurrNode.messageSource = otherCurrNode.messageSource;
}
thisCurrNode.merge(otherCurrNode, variables, prefix);
// thisCurrNode.home = null;
} else {
this.childNodes.add(otherCurrNode);
}
}
} else {
this.childNodes.addAll(other.getChildNodes());
}
Collections.sort(this.childNodes);
boolean mustSetHome = (home == null && childNodes.size() > 0);
if (mustSetHome) {
home = childNodes.get(0);
}
log.trace("Leaving merge");
}
/** Transforms all the links in the menu node (the current node and all its
* children.
*
* @param variables a map of variable names to variables. These variables can
* be referenced in the menu link as ${variable-name}. It cannot be null.
*
* @param prefix a prefix to add to relative menu link. It cannot be null.
*/
private void transformAllLinks(final Map<String, String> variables,
final String prefix) {
Validate.notNull(variables, "The variables map cannot be null");
Validate.notNull(prefix, "The prefix cannot be null");
if (this.isLeaf) {
transformLink(variables, prefix);
} else {
for (MenuNode node : childNodes) {
node.transformAllLinks(variables, prefix);
}
}
}
/** Transforms the menu link replacing all variables and adding the prefix if
* necessary.
*
* The prefix is added when the link is relative. If a variable could not be
* replace, this method throws an exception. It can only be called on leaf
* nodes.
*
* @param variables a map of variable names to variables. These variables can
* be referenced in the menu link as ${variable-name}. It cannot be null.
*
* @param prefix a prefix to add to relative menu links. It cannot be null.
*/
private void transformLink(final Map<String, String> variables,
final String prefix) {
log.trace("Entering transformLink(..., '{}')", prefix);
Validate.notNull(variables, "The variables map cannot be null");
Validate.notNull(prefix, "The prefix cannot be null");
Validate.isTrue(isLeaf, "transformLink can only be called on leaf nodes");
StringBuffer result = new StringBuffer();
// Replace the variables. \$\{([^}]+)\} matches ${variable-name}.
Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
Matcher matcher = pattern.matcher(linkPath);
log.debug("Matching {} against var", linkPath);
while (matcher.find()) {
String variableName = matcher.group(1);
String value = variables.get(variableName);
Validate.notNull(value, "Could not find variable " + variableName
+ " transforming menu linkPath " + linkPath);
matcher.appendReplacement(result, "/" + value);
}
matcher.appendTail(result);
String finalResult = result.toString();
// Check if it is an absolute linkPath. An absolute linkPath starts with /
// or a protocol specification.
if (!finalResult.startsWith("/") && !finalResult.matches("\\w+//:")) {
// We must add the prefix
if (finalResult.length() == 0) {
finalResult = "/" + prefix + "/";
} else {
finalResult = "/" + prefix + "/" + finalResult;
}
}
linkPath = finalResult;
log.trace("Leaving transformLink setting linkpath to '{}'", linkPath);
}
/**
* Overriding to make the two node comparation by using the node path.
*
* @see java.lang.Object#equals(java.lang.Object)
*
* @param obj The object to compare to. If null, it returns false.
*
* @return true if the nodes have the same path.
*/
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof MenuNode)) {
return false;
}
MenuNode other = (MenuNode) obj;
return getPath().equals(other.getPath());
}
/** The node hashcode.
*
* It returns the hascode of its path, to be consistent with equals.
*
* @return the node hashcode.
*/
@Override
public int hashCode() {
return getPath().hashCode();
}
/**
* Creates a top level MenuNode.
*
* This is used to support the MenuBar class. Beeing a top level node means
* that it does not have a parent and public access is granted. It also have
* no tooltip.
*
* @param theDisplayName the node display name to the customer. It cannot be
* null.
* @param theName the node identifier name. It cannot be null and cannot
* contain empty spaces.
*/
protected MenuNode(final String theDisplayName, final String theName) {
Validate.notEmpty(theDisplayName, "You have to specify the display name");
Validate.notEmpty(theName, "You have to specify the identifier name");
Validate.isTrue(theName.indexOf(' ') == -1,
"the name cannot contain empty spaces");
displayName = theDisplayName;
name = theName;
}
/**
* Compares this object with the specified object for order.
*
* Returns a negative integer, zero, or a positive integer as this object is
* less than, equal to, or greater than the specified object.<p>
*
* @param theOther the other menu node to be compared.
* @see java.lang.Comparable#compareTo(java.lang.Object)
*
* @return an integer specifying how this compares to the other.
*/
public int compareTo(final MenuNode theOther) {
return this.position - theOther.position;
}
/** Sets the message source to use to display the label of this menu node.
*
* Call this only if the menu node is internationalized.
*
* @param theMessageSource the message source. It cannot be null.
*/
void setMessageSource(final KatariMessageSource theMessageSource) {
Validate.notNull(theMessageSource, "The message source cannot be null.");
messageSource = theMessageSource;
for(MenuNode node : childNodes) {
node.setMessageSource(theMessageSource);
}
}
}