/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.richfaces.component;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UpdateModelException;
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.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ExceptionQueuedEvent;
import javax.faces.event.ExceptionQueuedEventContext;
import javax.faces.event.FacesEvent;
import javax.faces.event.PhaseId;
import org.ajax4jsf.model.DataComponentState;
import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.richfaces.application.FacesMessages;
import org.richfaces.application.MessageFactory;
import org.richfaces.application.ServiceTracker;
import org.richfaces.cdk.annotations.Attribute;
import org.richfaces.cdk.annotations.EventName;
import org.richfaces.cdk.annotations.JsfComponent;
import org.richfaces.cdk.annotations.JsfRenderer;
import org.richfaces.cdk.annotations.Tag;
import org.richfaces.component.attribute.AjaxProps;
import org.richfaces.component.attribute.CoreProps;
import org.richfaces.component.attribute.EventsKeyProps;
import org.richfaces.component.attribute.EventsMouseProps;
import org.richfaces.component.attribute.I18nProps;
import org.richfaces.component.attribute.ImmediateProps;
import org.richfaces.component.attribute.SequenceProps;
import org.richfaces.component.attribute.TreeCommonProps;
import org.richfaces.component.attribute.TreeProps;
import org.richfaces.component.util.MessageUtil;
import org.richfaces.context.ExtendedVisitContext;
import org.richfaces.context.ExtendedVisitContextMode;
import org.richfaces.event.TreeSelectionChangeEvent;
import org.richfaces.event.TreeSelectionChangeListener;
import org.richfaces.event.TreeSelectionChangeSource;
import org.richfaces.event.TreeToggleEvent;
import org.richfaces.event.TreeToggleListener;
import org.richfaces.event.TreeToggleSource;
import org.richfaces.model.ClassicTreeNodeDataModelImpl;
import org.richfaces.model.DeclarativeModelKey;
import org.richfaces.model.DeclarativeTreeDataModelImpl;
import org.richfaces.model.DeclarativeTreeModel;
import org.richfaces.model.SwingTreeNodeDataModelImpl;
import org.richfaces.model.TreeDataModel;
import org.richfaces.model.TreeDataModelTuple;
import org.richfaces.model.TreeDataVisitor;
import org.richfaces.model.TreeNode;
import org.richfaces.renderkit.MetaComponentRenderer;
import org.richfaces.view.facelets.TreeHandler;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
/**
* <p>The <rich:tree> component provides a hierarchical tree control. Each <rich:tree> component typically
* consists of <rich:treeNode> child components. The appearance and behavior of the tree and its nodes can be
* fully customized.</p>
*
* @author Nick Belaevski
*/
@JsfComponent(type = AbstractTree.COMPONENT_TYPE, family = AbstractTree.COMPONENT_FAMILY,
tag = @Tag(name = "tree", handlerClass = TreeHandler.class),
renderer = @JsfRenderer(type = "org.richfaces.TreeRenderer"))
// TODO add rowData caching for wrapper events
public abstract class AbstractTree extends UIDataAdaptor implements MetaComponentResolver, MetaComponentEncoder, TreeSelectionChangeSource, TreeToggleSource, AjaxProps, CoreProps, EventsKeyProps, EventsMouseProps, ImmediateProps, I18nProps, SequenceProps, TreeProps, TreeCommonProps {
public static final String COMPONENT_TYPE = "org.richfaces.Tree";
public static final String COMPONENT_FAMILY = "org.richfaces.Tree";
public static final String SELECTION_META_COMPONENT_ID = "selection";
public static final String DEFAULT_TREE_NODE_ID = "__defaultTreeNode";
public static final String DEFAULT_TREE_NODE_FACET_NAME = "defaultNode";
private static final String COMPONENT_FOR_MODEL_UNAVAILABLE = "Component is not available for model {0}";
private static final String CONVERTER_FOR_MODEL_UNAVAILABLE = "Row key converter is not available for model {0}";
private static final class MatchingTreeNodePredicate implements Predicate<UIComponent> {
private String type;
public MatchingTreeNodePredicate(String type) {
super();
this.type = type;
}
public boolean apply(UIComponent input) {
if (!(input instanceof AbstractTreeNode)) {
return false;
}
String nodeType = ((AbstractTreeNode) input).getType();
if (type == null && nodeType == null) {
return true;
}
return type != null && type.equals(nodeType);
}
}
;
private enum PropertyKeys {
selection
}
private transient TreeRange treeRange;
private transient UIComponent currentComponent = this;
private transient Map<String, UIComponent> declatariveModelsMap = null;
public AbstractTree() {
setKeepSaved(true);
setRendererType("org.richfaces.TreeRenderer");
}
protected TreeRange getTreeRange() {
if (treeRange == null) {
treeRange = new TreeRange(this);
}
return treeRange;
}
/**
* Points to the data model
*/
@Attribute
public abstract Object getValue();
@Attribute
public abstract String getNodeClass();
@Attribute(events = @EventName("nodetoggle"))
public abstract String getOnnodetoggle();
@Attribute(events = @EventName("beforenodetoggle"))
public abstract String getOnbeforenodetoggle();
@Attribute(events = @EventName("selectionchange"))
public abstract String getOnselectionchange();
@Attribute(events = @EventName("beforeselectionchange"))
public abstract String getOnbeforeselectionchange();
@Attribute
public abstract SwitchType getToggleType();
@Attribute
public abstract SwitchType getSelectionType();
@Attribute
public abstract String getNodeType();
@Attribute
public abstract String getToggleNodeEvent();
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
@Attribute
public Collection<Object> getSelection() {
@SuppressWarnings("unchecked")
Collection<Object> selection = (Collection<Object>) getStateHelper().eval(PropertyKeys.selection);
if (selection == null) {
selection = new HashSet<Object>();
ValueExpression ve = getValueExpression(PropertyKeys.selection.toString());
if (ve != null) {
ve.setValue(getFacesContext().getELContext(), selection);
} else {
getStateHelper().put(PropertyKeys.selection, selection);
}
}
return selection;
}
public void setSelection(Collection<Object> selection) {
getStateHelper().put(PropertyKeys.selection, selection);
}
@Override
protected DataComponentState createComponentState() {
// TODO Auto-generated method stub
return null;
}
@Override
@Attribute
public Converter getRowKeyConverter() {
Converter converter = super.getRowKeyConverter();
if (converter == null) {
converter = getTreeDataModel().getRowKeyConverter();
}
return converter;
}
protected Iterator<UIComponent> findMatchingTreeNodeComponent(String nodeType, UIComponent parentComponent) {
Iterator<UIComponent> children = parentComponent.getChildren().iterator();
if (parentComponent != this) {
children = Iterators.concat(children, this.getChildren().iterator());
}
return Iterators.filter(children, new MatchingTreeNodePredicate(nodeType));
}
protected void setupCurrentComponent() {
ExtendedDataModel<?> dataModel = getExtendedDataModel();
if (dataModel instanceof DeclarativeTreeModel) {
currentComponent = ((DeclarativeTreeModel) dataModel).getCurrentComponent();
} else {
currentComponent = this;
}
}
public AbstractTreeNode findTreeNodeComponent() {
String nodeType = getNodeType();
Iterator<UIComponent> nodesItr = findMatchingTreeNodeComponent(nodeType, currentComponent);
if (nodesItr.hasNext()) {
while (nodesItr.hasNext()) {
AbstractTreeNode node = (AbstractTreeNode) nodesItr.next();
if (node.isRendered()) {
return node;
}
}
} else if (Strings.isNullOrEmpty(nodeType) || isUseDefaultNode()) {
return (AbstractTreeNode) getFacet(DEFAULT_TREE_NODE_FACET_NAME);
}
return null;
}
@Override
public void broadcast(FacesEvent event) throws AbortProcessingException {
super.broadcast(event);
if (event instanceof TreeSelectionChangeEvent) {
TreeSelectionChangeEvent selectionEvent = (TreeSelectionChangeEvent) event;
final Collection<Object> newSelection = selectionEvent.getNewSelection();
Collection<Object> selectionCollection = getSelection();
Iterables.removeIf(selectionCollection, new Predicate<Object>() {
public boolean apply(Object input) {
return !newSelection.contains(input);
}
;
});
if (!newSelection.isEmpty()) {
Iterables.addAll(selectionCollection, newSelection);
}
} else if (event instanceof TreeToggleEvent) {
TreeToggleEvent toggleEvent = (TreeToggleEvent) event;
AbstractTreeNode treeNodeComponent = findTreeNodeComponent();
boolean newExpandedValue = toggleEvent.isExpanded();
FacesContext context = getFacesContext();
ValueExpression expression = treeNodeComponent
.getValueExpression(AbstractTreeNode.PropertyKeys.expanded.toString());
if (expression != null) {
ELContext elContext = context.getELContext();
Exception caught = null;
FacesMessage message = null;
try {
expression.setValue(elContext, newExpandedValue);
} catch (ELException e) {
caught = e;
String messageStr = e.getMessage();
Throwable result = e.getCause();
while (null != result && result.getClass().isAssignableFrom(ELException.class)) {
messageStr = result.getMessage();
result = result.getCause();
}
if (null == messageStr) {
MessageFactory messageFactory = ServiceTracker.getService(MessageFactory.class);
message = messageFactory.createMessage(context, FacesMessages.UIINPUT_UPDATE,
MessageUtil.getLabel(context, this));
} else {
message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr);
}
} catch (Exception e) {
caught = e;
MessageFactory messageFactory = ServiceTracker.getService(MessageFactory.class);
message = messageFactory.createMessage(context, FacesMessages.UIINPUT_UPDATE,
MessageUtil.getLabel(context, this));
}
if (caught != null) {
assert (message != null);
UpdateModelException toQueue = new UpdateModelException(message, caught);
ExceptionQueuedEventContext eventContext = new ExceptionQueuedEventContext(context, toQueue, this,
PhaseId.UPDATE_MODEL_VALUES);
context.getApplication().publishEvent(context, ExceptionQueuedEvent.class, eventContext);
}
} else {
treeNodeComponent.setExpanded(newExpandedValue);
}
}
}
@Override
protected boolean visitFixedChildren(VisitContext visitContext, VisitCallback callback) {
if (visitContext instanceof ExtendedVisitContext) {
ExtendedVisitContext extendedVisitContext = (ExtendedVisitContext) visitContext;
if (ExtendedVisitContextMode.RENDER == extendedVisitContext.getVisitMode()) {
VisitResult result = extendedVisitContext.invokeMetaComponentVisitCallback(this, callback,
SELECTION_META_COMPONENT_ID);
if (result != VisitResult.ACCEPT) {
return result == VisitResult.COMPLETE;
}
}
}
return super.visitFixedChildren(visitContext, callback);
}
void decodeMetaComponent(FacesContext context, String metaComponentId) {
((MetaComponentRenderer) getRenderer(context)).decodeMetaComponent(context, this, metaComponentId);
}
public void encodeMetaComponent(FacesContext context, String metaComponentId) throws IOException {
((MetaComponentRenderer) getRenderer(context)).encodeMetaComponent(context, this, metaComponentId);
}
public String resolveClientId(FacesContext facesContext, UIComponent contextComponent, String metaComponentId) {
if (SELECTION_META_COMPONENT_ID.equals(metaComponentId)) {
return getClientId(facesContext) + MetaComponentResolver.META_COMPONENT_SEPARATOR_CHAR + metaComponentId;
}
return null;
}
public String substituteUnresolvedClientId(FacesContext facesContext, UIComponent contextComponent, String metaComponentId) {
return null;
}
@Override
protected Iterator<UIComponent> dataChildren() {
AbstractTreeNode treeNodeComponent = findTreeNodeComponent();
if (treeNodeComponent != null) {
return Iterators.<UIComponent> singletonIterator(treeNodeComponent);
} else {
return ImmutableSet.<UIComponent>of().iterator();
}
}
public void addTreeSelectionChangeListener(TreeSelectionChangeListener listener) {
addFacesListener(listener);
}
@Attribute(hidden = true)
public TreeSelectionChangeListener[] getTreeSelectionChangeListeners() {
return (TreeSelectionChangeListener[]) getFacesListeners(TreeSelectionChangeListener.class);
}
public void removeTreeSelectionChangeListener(TreeSelectionChangeListener listener) {
removeFacesListener(listener);
}
public void addTreeToggleListener(TreeToggleListener listener) {
addFacesListener(listener);
}
@Attribute(hidden = true)
public TreeToggleListener[] getTreeToggleListeners() {
return (TreeToggleListener[]) getFacesListeners(TreeToggleListener.class);
}
public void removeTreeToggleListener(TreeToggleListener listener) {
removeFacesListener(listener);
}
@Attribute(hidden = true)
public boolean isExpanded() {
if (getRowKey() == null) {
return true;
}
AbstractTreeNode treeNode = findTreeNodeComponent();
if (treeNode == null) {
return false;
}
return treeNode.isExpanded();
}
// TODO review
protected TreeDataModel<?> getTreeDataModel() {
return (TreeDataModel<?>) getExtendedDataModel();
}
@Attribute(hidden = true)
public boolean isLeaf() {
return getTreeDataModel().isLeaf();
}
@Override
public void walk(final FacesContext faces, final DataVisitor visitor, final Object argument) {
walkModel(faces, new TreeDataVisitor() {
public void enterNode() {
visitor.process(faces, getRowKey(), argument);
}
public void exitNode() {
}
public void beforeChildrenVisit() {
}
public void afterChildrenVisit() {
}
});
}
@Override
protected ExtendedDataModel<?> createExtendedDataModel() {
ExtendedDataModel<?> dataModel;
Object value = getValue();
if (value == null) {
dataModel = new DeclarativeTreeDataModelImpl(this);
} else if (value instanceof TreeNode) {
dataModel = new ClassicTreeNodeDataModelImpl();
dataModel.setWrappedData(value);
} else if (value instanceof TreeDataModel<?>) {
if (value instanceof ExtendedDataModel<?>) {
dataModel = (ExtendedDataModel<?>) value;
} else {
throw new IllegalArgumentException(MessageFormat.format(
"TreeDataModel implementation {0} is not a subclass of ExtendedDataModel", value.getClass().getName()));
}
} else {
dataModel = new SwingTreeNodeDataModelImpl();
dataModel.setWrappedData(value);
}
return dataModel;
}
public void walkModel(FacesContext context, TreeDataVisitor dataVisitor) {
TreeDataModel<?> model = getTreeDataModel();
if (!getTreeRange().shouldProcessNode()) {
return;
}
boolean isRootNode = (getRowKey() == null);
if (!isRootNode) {
dataVisitor.enterNode();
}
walkModelChildren(context, dataVisitor, model);
if (!isRootNode) {
dataVisitor.exitNode();
}
}
private void walkModelChildren(FacesContext context, TreeDataVisitor dataVisitor, TreeDataModel<?> model) {
if (!getTreeRange().shouldIterateChildren()) {
return;
}
dataVisitor.beforeChildrenVisit();
Iterator<TreeDataModelTuple> childrenTuples = model.children();
while (childrenTuples.hasNext()) {
TreeDataModelTuple tuple = childrenTuples.next();
restoreFromSnapshot(context, tuple);
if (!getTreeRange().shouldProcessNode()) {
continue;
}
dataVisitor.enterNode();
walkModelChildren(context, dataVisitor, model);
dataVisitor.exitNode();
}
dataVisitor.afterChildrenVisit();
}
@Override
protected void resetDataModel() {
super.resetDataModel();
treeRange = null;
declatariveModelsMap = null;
}
public TreeDataModelTuple createSnapshot() {
return getTreeDataModel().createSnapshot();
}
public void restoreFromSnapshot(FacesContext context, TreeDataModelTuple tuple) {
getTreeDataModel().restoreFromSnapshot(tuple);
setRowKey(context, tuple.getRowKey());
}
@Override
protected void restoreChildState(FacesContext facesContext) {
setupCurrentComponent();
super.restoreChildState(facesContext);
}
protected UIComponent findDeclarativeModel(String modelId) {
if (declatariveModelsMap == null) {
declatariveModelsMap = Maps.newHashMap();
}
UIComponent adaptor = declatariveModelsMap.get(modelId);
if (adaptor == null) {
adaptor = findComponent(modelId);
if (adaptor != null) {
declatariveModelsMap.put(modelId, adaptor);
}
}
if (adaptor == null) {
throw new IllegalStateException(MessageFormat.format(COMPONENT_FOR_MODEL_UNAVAILABLE, modelId));
}
return adaptor;
}
public String convertDeclarativeKeyToString(FacesContext context, DeclarativeModelKey declarativeKey)
throws ConverterException {
try {
UIComponent component = findDeclarativeModel(declarativeKey.getModelId());
TreeModelAdaptor adaptor = (TreeModelAdaptor) component;
Converter rowKeyConverter = adaptor.getRowKeyConverter();
if (rowKeyConverter == null) {
throw new ConverterException(MessageFormat.format(CONVERTER_FOR_MODEL_UNAVAILABLE, declarativeKey.getModelId()));
}
return rowKeyConverter.getAsString(context, (UIComponent) adaptor, declarativeKey.getModelKey());
} catch (ConverterException e) {
throw e;
} catch (Exception e) {
throw new ConverterException(e.getMessage(), e);
}
}
public DeclarativeModelKey convertDeclarativeKeyFromString(FacesContext context, String modelId, String modelKeyAsString)
throws ConverterException {
try {
UIComponent component = findDeclarativeModel(modelId);
TreeModelAdaptor adaptor = (TreeModelAdaptor) component;
Converter rowKeyConverter = adaptor.getRowKeyConverter();
if (rowKeyConverter == null) {
throw new ConverterException(MessageFormat.format(CONVERTER_FOR_MODEL_UNAVAILABLE, modelId));
}
Object modelKey = rowKeyConverter.getAsObject(context, (UIComponent) adaptor, modelKeyAsString);
return new DeclarativeModelKey(modelId, modelKey);
} catch (ConverterException e) {
throw e;
} catch (Exception e) {
throw new ConverterException(e.getMessage(), e);
}
}
}