/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.myfaces.view.facelets.tag.jsf.core;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.el.MethodExpression;
import javax.faces.component.StateHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UniqueIdVendor;
import javax.faces.component.behavior.AjaxBehavior;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.AjaxBehaviorListener;
import javax.faces.view.BehaviorHolderAttachedObjectHandler;
import javax.faces.view.facelets.ComponentHandler;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.FaceletHandler;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributeException;
import javax.faces.view.facelets.TagConfig;
import javax.faces.view.facelets.TagException;
import javax.faces.view.facelets.TagHandler;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletAttribute;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletTag;
import org.apache.myfaces.shared_impl.renderkit.JSFAttr;
import org.apache.myfaces.shared_impl.renderkit.html.util.ResourceUtils;
import org.apache.myfaces.view.facelets.AbstractFaceletContext;
import org.apache.myfaces.view.facelets.FaceletCompositionContext;
import org.apache.myfaces.view.facelets.tag.TagHandlerUtils;
import org.apache.myfaces.view.facelets.tag.composite.InsertChildrenHandler;
import org.apache.myfaces.view.facelets.tag.jsf.ComponentSupport;
import org.apache.myfaces.view.facelets.tag.ui.DecorateHandler;
import org.apache.myfaces.view.facelets.tag.ui.IncludeHandler;
import org.apache.myfaces.view.facelets.tag.ui.InsertHandler;
/**
* This tag creates an instance of AjaxBehavior, and associates it with the nearest
* parent UIComponent that implements ClientBehaviorHolder interface. This tag can
* be used on single or composite components.
* <p>
* Unless otherwise specified, all attributes accept static values or EL expressions.
* </p>
* <p>
* According to the documentation, the tag handler implementing this tag should meet
* the following conditions:
* </p>
* <ul>
* <li>Since this tag attach objects to UIComponent instances, and those instances
* implements Behavior interface, this component should implement
* BehaviorHolderAttachedObjectHandler interface.</li>
* <li>f:ajax does not support binding property. In theory we should do something similar
* to f:convertDateTime tag does: extends from ConverterHandler and override setAttributes
* method, but in this case BehaviorTagHandlerDelegate has binding property defined, so
* if we extend from BehaviorHandler we add binding support to f:ajax.</li>
* <li>This tag works as a attached object handler, but note on the api there is no component
* to define a target for a behavior. See comment inside apply() method.</li>
* </ul>
* @author Leonardo Uribe (latest modification by $Author: lu4242 $)
* @version $Revision: 1037967 $ $Date: 2010-11-22 21:02:19 -0500 (Mon, 22 Nov 2010) $
*/
@JSFFaceletTag(name = "f:ajax")
public class AjaxHandler extends TagHandler implements
BehaviorHolderAttachedObjectHandler
{
public final static Class<?>[] AJAX_BEHAVIOR_LISTENER_SIG = new Class<?>[] { AjaxBehaviorEvent.class };
/**
* Constant used to check if in the current build view it has been rendered the standard jsf javascript
* library. It is necessary to remove this key from facesContext attribute map after build, to keep
* working this code for next views to be built.
*/
public final static String STANDARD_JSF_AJAX_LIBRARY_LOADED = "org.apache.myfaces.STANDARD_JSF_AJAX_LIBRARY_LOADED";
/**
*
*/
@JSFFaceletAttribute(name = "disabled", className = "javax.el.ValueExpression", deferredValueType = "java.lang.Boolean")
private final TagAttribute _disabled;
/**
*
*/
@JSFFaceletAttribute(name = "event", className = "javax.el.ValueExpression", deferredValueType = "java.lang.String")
private final TagAttribute _event;
/**
*
*/
@JSFFaceletAttribute(name = "execute", className = "javax.el.ValueExpression", deferredValueType = "java.lang.Object")
private final TagAttribute _execute;
/**
*
*/
@JSFFaceletAttribute(name = "immediate", className = "javax.el.ValueExpression", deferredValueType = "java.lang.Boolean")
private final TagAttribute _immediate;
/**
*
*/
@JSFFaceletAttribute(name = "listener", className = "javax.el.MethodExpression", deferredMethodSignature = "public void m(javax.faces.event.AjaxBehaviorEvent evt) throws javax.faces.event.AbortProcessingException")
private final TagAttribute _listener;
/**
*
*/
@JSFFaceletAttribute(name = "onevent", className = "javax.el.ValueExpression", deferredValueType = "java.lang.String")
private final TagAttribute _onevent;
/**
*
*/
@JSFFaceletAttribute(name = "onerror", className = "javax.el.ValueExpression", deferredValueType = "java.lang.String")
private final TagAttribute _onerror;
/**
*
*/
@JSFFaceletAttribute(name = "render", className = "javax.el.ValueExpression", deferredValueType = "java.lang.Object")
private final TagAttribute _render;
private final boolean _wrapMode;
public AjaxHandler(TagConfig config)
{
super(config);
_disabled = getAttribute("disabled");
_event = getAttribute("event");
_execute = getAttribute("execute");
_immediate = getAttribute("immediate");
_listener = getAttribute("listener");
_onerror = getAttribute("onerror");
_onevent = getAttribute("onevent");
_render = getAttribute("render");
// According to the spec, this tag works in two different ways:
// 1. Apply an ajax behavior for a selected component in this way
// <x:component><f:ajax ..../></x:component>
// 2. Apply an ajax behavior for a group of components inside it
// <f:ajax ....><x:componentA .../><x:componentB .../></f:ajax>
//
// The first problem is how to discriminate if f:ajax tag is on a
// "leaf" or if contain other components.
//
// One option is use the same strategy to cache instance for
// <composite:interface> handler: traverse the tree for instances of
// ComponentHandler. If it is found, wrapMode is used otherwise
// suppose f:ajax is the one wrapped by a component.
Collection<FaceletHandler> compHandlerList =
TagHandlerUtils.findNextByType(nextHandler, ComponentHandler.class,
InsertChildrenHandler.class, InsertHandler.class, DecorateHandler.class, IncludeHandler.class);
_wrapMode = !compHandlerList.isEmpty();
}
public void apply(FaceletContext ctx, UIComponent parent)
throws IOException
{
//Apply only if we are creating a new component
if (!ComponentHandler.isNew(parent))
{
if (_wrapMode)
{
AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
// In this case it will be only applied to components inserted by
// c:if or related tags, in other cases, ComponentTagHandlerDelegate should
// not reapply ajax tag.
actx.pushAjaxHandlerToStack(this);
nextHandler.apply(ctx, parent);
actx.popAjaxHandlerToStack();
}
return;
}
if (_wrapMode)
{
AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
// Push and pop this ajax handler to the stack, to delegate the
// call to applyAttachedObject to ComponentTagHandlerDelegate
// TODO: The spec is not clear about how to deal with
// composite component instances. The default one proposed here is
// use a different stack on DefaultFaceletContext.applyCompositeComponent,
// so components inside composite:implementation tag will not be
// affected by f:ajax outsider handlers.
actx.pushAjaxHandlerToStack(this);
nextHandler.apply(ctx, parent);
actx.popAjaxHandlerToStack();
}
else
{
if (parent instanceof ClientBehaviorHolder)
{
//Apply this handler directly over the parent
applyAttachedObject(ctx.getFacesContext(), parent);
}
else if (UIComponent.isCompositeComponent(parent))
{
FaceletCompositionContext mctx = FaceletCompositionContext.getCurrentInstance(ctx);
// It is supposed that for composite components, this tag should
// add itself as a target, but note that on whole api does not exists
// some tag that expose client behaviors as targets for composite
// components. In RI, there exists a tag called composite:clientBehavior,
// but does not appear on spec or javadoc, maybe because this could be
// understand as an implementation detail, after all there exists a key
// called AttachedObjectTarget.ATTACHED_OBJECT_TARGETS_KEY that could be
// used to create a tag outside jsf implementation to attach targets.
mctx.addAttachedObjectHandler(
parent, this);
}
else
{
throw new TagException(this.tag,
"Parent is not composite component or of type ClientBehaviorHolder, type is: "
+ parent);
}
}
registerJsfAjaxDefaultResource(ctx, parent);
}
public static void registerJsfAjaxDefaultResource(FaceletContext ctx, UIComponent parent)
{
// Register the standard ajax library on the current page in this way:
//
// <h:outputScript name="jsf.js" library="javax.faces" target="head"/>
//
// If no h:head component is in the page, we must anyway render the script inline,
// so the only way to make sure we are doing this is add a outputScript component.
// Note that call directly UIViewRoot.addComponentResource or use a listener
// does not work in this case, because check this condition will requires
// traverse the whole tree looking for h:head component.
FacesContext facesContext = ctx.getFacesContext();
if (!facesContext.getAttributes().containsKey(STANDARD_JSF_AJAX_LIBRARY_LOADED))
{
UIComponent outputScript = facesContext.getApplication().
createComponent(facesContext, "javax.faces.Output", "javax.faces.resource.Script");
outputScript.getAttributes().put(JSFAttr.NAME_ATTR, ResourceUtils.JSF_JS_RESOURCE_NAME);
outputScript.getAttributes().put(JSFAttr.LIBRARY_ATTR, ResourceUtils.JAVAX_FACES_LIBRARY_NAME);
outputScript.getAttributes().put(JSFAttr.TARGET_ATTR, "head");
//AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
// Since this component will be relocated, we need a generated clientId from the
// viewRoot, so when this one is relocated, its parent will be this UIViewRoot instance
// and prevent a duplicate id exception.
UniqueIdVendor uniqueIdVendor = ComponentSupport.getViewRoot(ctx, parent);
// UIViewRoot implements UniqueIdVendor, so there is no need to cast to UIViewRoot
// and call createUniqueId()
String uid = uniqueIdVendor.createUniqueId(ctx.getFacesContext(),null);
outputScript.setId(uid);
parent.getChildren().add(outputScript);
if (FaceletCompositionContext.getCurrentInstance(ctx).isMarkInitialState())
{
//Call it only if we are using partial state saving
outputScript.markInitialState();
}
facesContext.getAttributes().put(STANDARD_JSF_AJAX_LIBRARY_LOADED, Boolean.TRUE);
}
}
/**
* ViewDeclarationLanguage.retargetAttachedObjects uses it to check
* if the the target to be processed is applicable for this handler
*/
public String getEventName()
{
if (_event == null)
{
return null;
}
else
{
return _event.getValue();
}
}
/**
* This method should create an AjaxBehavior object and attach it to the
* parent component.
*
* Also, it should check if the parent can apply the selected AjaxBehavior
* to the selected component through ClientBehaviorHolder.getEventNames() or
* ClientBehaviorHolder.getDefaultEventName()
*/
public void applyAttachedObject(FacesContext context, UIComponent parent)
{
// Retrieve the current FaceletContext from FacesContext object
FaceletContext faceletContext = (FaceletContext) context
.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
// cast to a ClientBehaviorHolder
ClientBehaviorHolder cvh = (ClientBehaviorHolder) parent;
String eventName = getEventName();
if (eventName == null)
{
eventName = cvh.getDefaultEventName();
if (eventName == null)
{
if (_wrapMode)
{
// No eventName defined, we can't apply this tag to this component, because
// there is no event defined to attach it, but since we are in wrap mode
// we have here the case that the component could not be the target
// for this attached object.
return;
}
else
{
throw new TagAttributeException(_event, "eventName could not be defined for f:ajax tag with no wrap mode.");
}
}
}
else if (!cvh.getEventNames().contains(eventName))
{
if (_wrapMode)
{
// The current component does not implement the event selected,
// this ajax behavior cannot be applied, but we can't throw any exception
// since we are in wrap mode and we have here the case that the
// component could not be the target for this attached object.
return;
}
else
{
throw new TagAttributeException(_event, "event it is not a valid eventName defined for this component");
}
}
Map<String, List<ClientBehavior>> clientBehaviors = cvh.getClientBehaviors();
List<ClientBehavior> clientBehaviorList = clientBehaviors.get(eventName);
if (clientBehaviorList != null && !clientBehaviorList.isEmpty())
{
for (ClientBehavior cb : clientBehaviorList)
{
if (cb instanceof AjaxBehavior)
{
// The most inner one has been applied, so according to
// jsf 2.0 spec section 10.4.1.1 it is not necessary to apply
// this one, because the inner one has precendece over
// the outer one.
return;
}
}
}
AjaxBehavior ajaxBehavior = (AjaxBehavior) context.getApplication()
.createBehavior(AjaxBehavior.BEHAVIOR_ID);
if (_disabled != null)
{
if (_disabled.isLiteral())
{
ajaxBehavior.setDisabled(_disabled.getBoolean(faceletContext));
}
else
{
ajaxBehavior.setValueExpression("disabled", _disabled
.getValueExpression(faceletContext, Boolean.class));
}
}
if (_execute != null)
{
ajaxBehavior.setValueExpression("execute", _execute
.getValueExpression(faceletContext, Object.class));
}
if (_immediate != null)
{
if (_immediate.isLiteral())
{
ajaxBehavior
.setImmediate(_immediate.getBoolean(faceletContext));
}
else
{
ajaxBehavior.setValueExpression("immediate", _immediate
.getValueExpression(faceletContext, Boolean.class));
}
}
if (_listener != null)
{
MethodExpression expr = _listener.getMethodExpression(
faceletContext, Void.TYPE, AJAX_BEHAVIOR_LISTENER_SIG);
AjaxBehaviorListener abl = new AjaxBehaviorListenerImpl(expr);
ajaxBehavior.addAjaxBehaviorListener(abl);
}
if (_onerror != null)
{
if (_onerror.isLiteral())
{
ajaxBehavior.setOnerror(_onerror.getValue(faceletContext));
}
else
{
ajaxBehavior.setValueExpression("onerror", _onerror
.getValueExpression(faceletContext, String.class));
}
}
if (_onevent != null)
{
if (_onevent.isLiteral())
{
ajaxBehavior.setOnevent(_onevent.getValue(faceletContext));
}
else
{
ajaxBehavior.setValueExpression("onevent", _onevent
.getValueExpression(faceletContext, String.class));
}
}
if (_render != null)
{
ajaxBehavior.setValueExpression("render", _render
.getValueExpression(faceletContext, Object.class));
}
cvh.addClientBehavior(eventName, ajaxBehavior);
}
/**
* The documentation says this attribute should not be used since it is not
* taken into account. Instead, getEventName is used on
* ViewDeclarationLanguage.retargetAttachedObjects.
*/
public String getFor()
{
return null;
}
/**
* Wraps a method expression in a AjaxBehaviorListener
*/
public final static class AjaxBehaviorListenerImpl implements
AjaxBehaviorListener, StateHolder
{
private MethodExpression _expr;
private boolean _transient;
public AjaxBehaviorListenerImpl ()
{
}
public AjaxBehaviorListenerImpl(MethodExpression expr)
{
_expr = expr;
}
public void processAjaxBehavior(AjaxBehaviorEvent event)
throws AbortProcessingException
{
_expr.invoke(FacesContext.getCurrentInstance().getELContext(),
new Object[] { event });
}
public boolean isTransient()
{
return _transient;
}
public void restoreState(FacesContext context, Object state)
{
_expr = (MethodExpression) state;
}
public Object saveState(FacesContext context) {
return _expr;
}
public void setTransient(boolean newTransientValue)
{
_transient = newTransientValue;
}
}
}