/*
* Copyright 2012 OmniFaces.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.component.tree;
import static org.omnifaces.util.Components.getClosestParent;
import java.util.HashMap;
import java.util.Map;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesEvent;
import javax.faces.event.PhaseId;
import org.omnifaces.component.EditableValueHolderStateHelper;
import org.omnifaces.event.FacesEventWrapper;
import org.omnifaces.model.tree.AbstractTreeModel;
import org.omnifaces.model.tree.ListTreeModel;
import org.omnifaces.model.tree.SortedTreeModel;
import org.omnifaces.model.tree.TreeModel;
import org.omnifaces.util.Callback;
import org.omnifaces.util.State;
/**
* <p>
* The <code><o:tree></code> allows the developers to have full control over the markup of a tree
* hierarchy by declaring the appropriate JSF components or HTML elements in the markup. The <code><o:tree></code>
* does namely not render any HTML markup by itself.
* <p>
* The component value must point to a tree of data objects represented by a {@link TreeModel} instance, typically
* established via a {@link ValueExpression}. During iterative processing over the nodes of tree in the tree model,
* the object for the current node is exposed as a request attribute under the key specified by the <code>var</code>
* attribute. The node itself is exposed as a request attribute under the key specified by the <code>varNode</code>
* attribute.
* <p>
* Only children of type {@link TreeNode} are allowed and processed by this component.
* <p>
* Here is a basic usage example:
* <pre>
* <o:tree value="#{bean.treeModel}" var="item" varNode="node">
* <o:treeNode>
* <ul>
* <o:treeNodeItem>
* <li>
* #{node.index} #{item.someProperty}
* <o:treeInsertChildren />
* </li>
* </o:treeNodeItem>
* </ul>
* </o:treeNode>
* </o:tree>
* </pre>
*
* @author Bauke Scholtz
* @see TreeNode
* @see TreeNodeItem
* @see TreeInsertChildren
* @see TreeFamily
* @see TreeModel
* @see AbstractTreeModel
* @see ListTreeModel
* @see SortedTreeModel
*/
@FacesComponent(Tree.COMPONENT_TYPE)
@SuppressWarnings("rawtypes") // For TreeModel. We don't care about its actual type anyway.
public class Tree extends TreeFamily implements NamingContainer {
// Public constants -----------------------------------------------------------------------------------------------
/** The standard component type. */
public static final String COMPONENT_TYPE = "org.omnifaces.component.tree.Tree";
// Private constants ----------------------------------------------------------------------------------------------
private static final String ERROR_EXPRESSION_DISALLOWED =
"A value expression is disallowed on 'var' and 'varNode' attributes of Tree.";
private static final String ERROR_INVALID_MODEL =
"Tree accepts only model of type TreeModel. Encountered model of type '%s'.";
private static final String ERROR_NESTING_DISALLOWED =
"Nesting Tree components is disallowed. Use TreeNode instead to markup specific levels.";
private static final String ERROR_NO_CHILDREN =
"Tree must have children of type TreeNode. Currently none are encountered.";
private static final String ERROR_INVALID_CHILD =
"Tree accepts only children of type TreeNode. Encountered child of type '%s'.";
private static final String ERROR_DUPLICATE_NODE =
"TreeNode with level '%s' is already declared. Choose a different level or remove it.";
private enum PropertyKeys {
// Cannot be uppercased. They have to exactly match the attribute names.
value, var, varNode;
}
// Variables ------------------------------------------------------------------------------------------------------
private final State state = new State(getStateHelper());
private TreeModel model;
private Map<Integer, TreeNode> nodes;
private TreeModel currentModelNode;
// Actions --------------------------------------------------------------------------------------------------------
/**
* An override which appends the index of the current model node to the client ID chain, if any available.
* @see TreeModel#getIndex()
*/
@Override
public String getContainerClientId(FacesContext context) {
String containerClientId = super.getContainerClientId(context);
String currentModelNodeIndex = (currentModelNode != null) ? currentModelNode.getIndex() : null;
if (currentModelNodeIndex != null) {
containerClientId = new StringBuilder(containerClientId)
.append(UINamingContainer.getSeparatorChar(context))
.append(currentModelNodeIndex)
.toString();
}
return containerClientId;
}
/**
* An override which checks if this isn't been invoked on <code>var</code> or <code>varNode</code> attribute.
* Finally it delegates to the super method.
* @throws IllegalArgumentException When this value expression is been set on <code>var</code> or
* <code>varNode</code> attribute.
*/
@Override
public void setValueExpression(String name, ValueExpression binding) {
if (PropertyKeys.var.toString().equals(name) || PropertyKeys.varNode.toString().equals(name)) {
throw new IllegalArgumentException(ERROR_EXPRESSION_DISALLOWED);
}
super.setValueExpression(name, binding);
}
/**
* An override which wraps the given faces event in a specific faces event which remembers the current model node.
*/
@Override
public void queueEvent(FacesEvent event) {
super.queueEvent(new TreeFacesEvent(event, this, getCurrentModelNode()));
}
/**
* Validate the component hierarchy.
* @throws IllegalArgumentException When this component is nested in another {@link Tree}, or when there aren't any
* children of type {@link TreeNode}.
*/
@Override
protected void validateHierarchy() {
if (getClosestParent(this, Tree.class) != null) {
throw new IllegalArgumentException(ERROR_NESTING_DISALLOWED);
}
if (getChildCount() == 0) {
throw new IllegalArgumentException(ERROR_NO_CHILDREN);
}
}
/**
* Set the root node as current node and delegate the call to {@link #processTreeNode(FacesContext, PhaseId)}.
* @param context The faces context to work with.
* @param phaseId The current phase ID.
*/
@Override
protected void process(final FacesContext context, final PhaseId phaseId) {
if (!isRendered()) {
return;
}
process(context, phaseId, getModel(phaseId), new Callback.Returning<Void>() {
@Override
public Void invoke() {
processTreeNode(context, phaseId);
return null;
}
});
}
/**
* Set the root node as current node and delegate the call to {@link #visitTreeNode(VisitContext, VisitCallback)}.
* @param context The visit context to work with.
* @param callback The visit callback to work with.
* @return The visit result.
*/
@Override
public boolean visitTree(final VisitContext context, final VisitCallback callback) {
if (!isVisitable(context)) {
return false;
}
PhaseId phaseId = PhaseId.ANY_PHASE;
return process(context.getFacesContext(), phaseId, getModel(phaseId), new Callback.Returning<Boolean>() {
@Override
public Boolean invoke() {
VisitResult result = context.invokeVisitCallback(Tree.this, callback);
if (result == VisitResult.COMPLETE) {
return true;
}
if (result == VisitResult.ACCEPT && !context.getSubtreeIdsToVisit(Tree.this).isEmpty()) {
return visitTreeNode(context, callback);
}
return false;
}
});
}
/**
* If the given event is an instance of the specific faces event which was created during our
* {@link #queueEvent(FacesEvent)}, then extract the node from it and set it as current node and delegate the call
* to the wrapped faces event.
*/
@Override
public void broadcast(FacesEvent event) throws AbortProcessingException {
if (event instanceof TreeFacesEvent) {
FacesContext context = FacesContext.getCurrentInstance();
TreeFacesEvent treeEvent = (TreeFacesEvent) event;
final FacesEvent wrapped = treeEvent.getWrapped();
process(context, event.getPhaseId(), treeEvent.getNode(), new Callback.Returning<Void>() {
@Override
public Void invoke() {
wrapped.getComponent().broadcast(wrapped);
return null;
}
});
}
else {
super.broadcast(event);
}
}
/**
* If the current model node isn't a leaf (i.e. it has any children), then obtain the {@link TreeNode} associated
* with the level of the current model node. If it isn't null, then process it according to the rules of the given
* phase ID. This method is also called by {@link TreeInsertChildren#process(FacesContext, PhaseId)}.
* @param context The faces context to work with.
* @param phaseId The current phase ID.
* @see TreeModel#isLeaf()
* @see TreeModel#getLevel()
* @see TreeInsertChildren
*/
protected void processTreeNode(final FacesContext context, final PhaseId phaseId) {
processTreeNode(phaseId, new Callback.ReturningWithArgument<Void, TreeNode>() {
@Override
public Void invoke(TreeNode treeNode) {
if (treeNode != null) {
treeNode.process(context, phaseId);
}
return null;
}
});
}
/**
* If the current model node isn't a leaf (i.e. it has any children), then obtain the {@link TreeNode} associated
* with the level of the current model node. If it isn't null, then visit it according to the given visit context
* and callback. This method is also called by {@link TreeInsertChildren#visitTree(VisitContext, VisitCallback)}.
* @param context The visit context to work with.
* @param callback The visit callback to work with.
* @return <code>true</code> if the visit is complete.
* @see TreeModel#isLeaf()
* @see TreeModel#getLevel()
* @see TreeInsertChildren
*/
protected boolean visitTreeNode(final VisitContext context, final VisitCallback callback) {
return processTreeNode(PhaseId.ANY_PHASE, new Callback.ReturningWithArgument<Boolean, TreeNode>() {
@Override
public Boolean invoke(TreeNode treeNode) {
if (treeNode != null) {
return treeNode.visitTree(context, callback);
}
return false;
}
});
}
/**
* Convenience method to handle {@link #process(FacesContext, PhaseId)},
* {@link #visitTree(VisitContext, VisitCallback)} and {@link #broadcast(FacesEvent)} without code duplication.
* @param context The faces context to work with.
* @param phaseId The current phase ID.
* @param node The current tree model node.
* @param callback The callback to be invoked.
* @return The callback result.
*/
private <R> R process(FacesContext context, PhaseId phaseId, TreeModel node, Callback.Returning<R> callback) {
Object[] originalVars = captureOriginalVars(context);
TreeModel originalModelNode = currentModelNode;
pushComponentToEL(context, null);
try {
setCurrentModelNode(context, node);
return callback.invoke();
}
finally {
popComponentFromEL(context);
setCurrentModelNode(context, originalModelNode);
setVars(context, originalVars);
}
}
/**
* Convenience method to handle both {@link #processTreeNode(FacesContext, PhaseId)} and
* {@link #visitTreeNode(VisitContext, VisitCallback)} without code duplication.
* @param phaseId The current phase ID.
* @param callback The callback to be invoked.
* @return The callback result.
*/
private <R> R processTreeNode(PhaseId phaseId, Callback.ReturningWithArgument<R, TreeNode> callback) {
TreeNode treeNode = null;
if (!currentModelNode.isLeaf()) {
Map<Integer, TreeNode> nodes = getNodes(phaseId);
treeNode = nodes.get(currentModelNode.getLevel());
if (treeNode == null) {
treeNode = nodes.get(null);
}
}
return callback.invoke(treeNode);
}
/**
* Returns the tree nodes by finding direct {@link TreeNode} children and collecting them by their level attribute.
* @param phaseId The current phase ID.
* @return The tree nodes.
* @throws IllegalArgumentException When a direct child component isn't of type {@link TreeNode}, or when there are
* multiple {@link TreeNode} components with the same level.
*/
private Map<Integer, TreeNode> getNodes(PhaseId phaseId) {
if (phaseId == PhaseId.RENDER_RESPONSE || nodes == null) {
nodes = new HashMap<>(getChildCount());
for (UIComponent child : getChildren()) {
if (child instanceof TreeNode) {
TreeNode node = (TreeNode) child;
if (nodes.put(node.getLevel(), node) != null) {
throw new IllegalArgumentException(String.format(ERROR_DUPLICATE_NODE, node.getLevel()));
}
}
else {
throw new IllegalArgumentException(String.format(ERROR_INVALID_CHILD, child.getClass().getName()));
}
}
}
return nodes;
}
/**
* Returns the tree model associated with the <code>value</code> attribute.
* @param phaseId The current phase ID.
* @return The tree model.
* @throws IllegalArgumentException When the <code>value</code> isn't of type {@link TreeModel}.
*/
private TreeModel getModel(PhaseId phaseId) {
if (phaseId == PhaseId.RENDER_RESPONSE || model == null) {
Object value = getValue();
if (value == null) {
model = new ListTreeModel();
}
else if (value instanceof TreeModel) {
model = (TreeModel) value;
}
else {
throw new IllegalArgumentException(String.format(ERROR_INVALID_MODEL, value.getClass().getName()));
}
}
return model;
}
/**
* Capture the original values of the request attributes associated with the component attributes <code>var</code>
* and <code>varNode</code>.
* @param context The faces context to work with.
* @return An object array with the two values.
*/
private Object[] captureOriginalVars(FacesContext context) {
Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
String[] names = { getVar(), getVarNode() };
Object[] vars = new Object[names.length];
for (int i = 0; i < names.length; i++) {
if (names[i] != null) {
vars[i] = requestMap.get(names[i]);
}
}
return vars;
}
/**
* Set the values associated with the <code>var</code> and <code>varNode</code> attributes.
* @param context The faces context to work with.
* @param vars An object array with the two values.
*/
private void setVars(FacesContext context, Object... vars) {
Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
String[] names = { getVar(), getVarNode() };
for (int i = 0; i < names.length; i++) {
if (names[i] != null) {
if (vars[i] != null) {
requestMap.put(names[i], vars[i]);
}
else {
requestMap.remove(names[i]);
}
}
}
}
// Internal getters/setters ---------------------------------------------------------------------------------------
/**
* Sets the current node of the tree model. Its wrapped data will be set as request attribute associated with the
* <code>var</code> attribute, if any. The node itself will also be set as request attribute associated with the
* <code>varNode</code> attribute, if any.
* @param context The faces context to work with.
* @param currentModelNode The current node of the tree model.
*/
protected void setCurrentModelNode(FacesContext context, TreeModel currentModelNode) {
// Save state of any child input fields of previous model node before setting new model node.
EditableValueHolderStateHelper.save(context, getStateHelper(), getFacetsAndChildren());
this.currentModelNode = currentModelNode;
setVars(context, (currentModelNode != null ? currentModelNode.getData() : null), currentModelNode);
// Restore any saved state of any child input fields of current model node before continuing.
EditableValueHolderStateHelper.restore(context, getStateHelper(), getFacetsAndChildren());
}
/**
* Returns the current node of the tree model.
* @return The current node of the tree model.
*/
protected TreeModel getCurrentModelNode() {
return currentModelNode;
}
// Attribute getters/setters --------------------------------------------------------------------------------------
/**
* Returns the tree model.
* @return The tree model
*/
public Object getValue() {
return state.get(PropertyKeys.value);
}
/**
* Sets the tree model.
* @param value The tree model.
*/
public void setValue(Object value) {
state.put(PropertyKeys.value, value);
}
/**
* Returns the name of the request attribute which exposes the wrapped data of the current node of the tree model.
* @return The name of the request attribute which exposes the wrapped data of the current node of the tree model.
*/
public String getVar() {
return state.get(PropertyKeys.var);
}
/**
* Sets the name of the request attribute which exposes the wrapped data of the current node of the tree model.
* @param var The name of the request attribute which exposes the wrapped data of the current node of the tree
* model.
*/
public void setVar(String var) {
state.put(PropertyKeys.var, var);
}
/**
* Returns the name of the request attribute which exposes the current node of the tree model.
* @return The name of the request attribute which exposes the current node of the tree model.
*/
public String getVarNode() {
return state.get(PropertyKeys.varNode);
}
/**
* Sets the name of the request attribute which exposes the current node of the tree model.
* @param varNode The name of the request attribute which exposes the current node of the tree model.
*/
public void setVarNode(String varNode) {
state.put(PropertyKeys.varNode, varNode);
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
* This faces event implementation remembers the current model node at the moment the faces event was queued.
*
* @author Bauke Scholtz
*/
private static class TreeFacesEvent extends FacesEventWrapper {
private static final long serialVersionUID = -7751061713837227515L;
private TreeModel node;
public TreeFacesEvent(FacesEvent wrapped, Tree tree, TreeModel node) {
super(wrapped, tree);
this.node = node;
}
public TreeModel getNode() {
return node;
}
}
}