/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2005-2007 The Apache Software Foundation
*
* 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 com.sun.faces.facelets.tag.jsf.core;
import com.sun.faces.renderkit.RenderKitUtils;
import com.sun.faces.component.behavior.AjaxBehaviors;
import com.sun.faces.facelets.tag.TagHandlerImpl;
import com.sun.faces.facelets.tag.jsf.CompositeComponentTagHandler;
import javax.el.ELContext;
import javax.el.MethodExpression;
import javax.el.MethodNotFoundException;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.component.behavior.AjaxBehavior;
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.AttachedObjectTarget;
import javax.faces.view.BehaviorHolderAttachedObjectHandler;
import javax.faces.view.BehaviorHolderAttachedObjectTarget;
import javax.faces.view.facelets.*;
import java.beans.BeanDescriptor;
import java.beans.BeanInfo;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
/**
* <p class="changed_added_2_0">Enable one or more components in the view
* to perform Ajax operations. This tag handler must create an instance
* of {@link javax.faces.component.behavior.AjaxBehavior} using the tag attribute
* values. If this tag is nested within a single
* {@link ClientBehaviorHolder} component:
* <ul>
* <li>If the <code>events</code> attribute value is not specified,
* obtain the default event name by calling {@link ClientBehaviorHolder#getDefaultEventName}.
* If that returns <code>null</code> throw an <code>exception</code>.</li>
* <li>If the <code>events</code> attribute value is specified, ensure it
* that it exists in the <code>Collection</code> returned from a call to
* {@link ClientBehaviorHolder#getEventNames} and throw an <code>exception</code> if
* it doesn't exist.</li>
* <li>Add the {@link AjaxBehavior} instance to the {@link ClientBehaviorHolder}
* component by calling {@link ClientBehaviorHolder#addClientBehavior} passing <code>event</code>
* and the {@link AjaxBehavior} instance.</li>
* </ul>
* <br/><br/>
* Check for the existence of the Ajax resource by calling
* <code>UIViewRoot.getComponentResources()</code>. If
* the Ajax resource does not exist, create a <code>UIOutput</code> component
* instance and set the renderer type to <code>javax.faces.resource.Script</code>.
* Set the the following attributes in the component's attribute <code>Map</code>:
* <code>library</code> with the value <code>javax.faces</code> and
* <code>name</code> with the value <code>jsf.js</code>. Install the component
* resource using <code>UIViewRoot.addComponentResource()</code> and specifying
* <code>head</code> as the <code>target</code> argument.</p>
*
* If this tag has component children, add the {@link AjaxBehavior} to
* {@link AjaxBehaviors} by calling {@link AjaxBehaviors#pushBehavior}. As
* subsequent child components that implement the {@link ClientBehaviorHolder} interface
* are evaluated this {@link AjaxBehavior} instance must be added as a behavior to
* the component.
* </p>
* @version $Id: AjaxHandler.java 5369 2008-09-08 19:53:45Z rogerk $
*/
public final class AjaxHandler extends TagHandlerImpl implements BehaviorHolderAttachedObjectHandler {
private final TagAttribute event;
private final TagAttribute execute;
private final TagAttribute render;
private final TagAttribute onevent;
private final TagAttribute onerror;
private final TagAttribute disabled;
private final TagAttribute immediate;
private final TagAttribute listener;
private final boolean wrapping;
/**
* @param config
*/
public AjaxHandler(TagConfig config) {
super(config);
this.event = this.getAttribute("event");
this.execute = this.getAttribute("execute");
this.render = this.getAttribute("render");
this.onevent = this.getAttribute("onevent");
this.onerror = this.getAttribute("onerror");
this.disabled = this.getAttribute("disabled");
this.immediate = this.getAttribute("immediate");
this.listener = this.getAttribute("listener");
this.wrapping = isWrapping();
}
/*
* (non-Javadoc)
*
* @see com.sun.facelets.FaceletHandler#apply(com.sun.facelets.FaceletContext,
* javax.faces.component.UIComponent)
*/
public void apply(FaceletContext ctx, UIComponent parent)
throws IOException {
String eventName = getEventName();
if (this.wrapping) {
applyWrapping(ctx, parent, eventName);
} else {
applyNested(ctx, parent, eventName);
}
}
public void applyAttachedObject(FacesContext context, UIComponent parent) {
FaceletContext ctx = (FaceletContext) context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
applyAttachedObject(ctx, parent, getEventName());
}
/* (non-Javadoc)
* @see javax.faces.view.AttachedObjectHandler#getFor()
*/
public String getFor() {
return null;
}
/* (non-Javadoc)
* @see javax.faces.view.BehaviorHolderAttachedObjectHandler#getEventName()
*/
public String getEventName() {
return (this.event != null) ? this.event.getValue() : null;
}
// Tests whether the <f:ajax> is wrapping other tags.
private boolean isWrapping() {
// Would be nice if there was some easy way to determine whether
// we are a leaf handler. However, even leaf handlers have a
// non-null nextHandler - the CompilationUnit.LEAF instance.
// We assume that if we've got a TagHandler or CompositeFaceletHandler
// as our nextHandler, we are not a leaf.
return ((this.nextHandler instanceof TagHandler) ||
(this.nextHandler instanceof CompositeFaceletHandler));
}
// Applies a wrapping AjaxHandler by pushing/popping the AjaxBehavior
// to the AjaxBeahviors object.
private void applyWrapping(FaceletContext ctx,
UIComponent parent,
String eventName) throws IOException {
// In the wrapping case, we assume that some wrapped component
// is going to be Ajax enabled and install the Ajax resource.
installAjaxResourceIfNecessary();
AjaxBehavior ajaxBehavior = createAjaxBehavior(ctx, eventName);
// We leverage AjaxBehaviors to support the wrapping case. We
// push/pop the AjaxBehavior instance on AjaxBehaviors so that
// child tags will have access to it.
FacesContext context = ctx.getFacesContext();
AjaxBehaviors ajaxBehaviors = AjaxBehaviors.getAjaxBehaviors(context, true);
ajaxBehaviors.pushBehavior(context, ajaxBehavior, eventName);
nextHandler.apply(ctx, parent);
ajaxBehaviors.popBehavior();
}
// Applies a nested AjaxHandler by adding the AjaxBehavior to the
// parent component.
private void applyNested(FaceletContext ctx,
UIComponent parent,
String eventName) {
if (!ComponentHandler.isNew(parent)) {
return;
}
// Composite component case
if (UIComponent.isCompositeComponent(parent)) {
// Check composite component event name:
boolean tagApplied = false;
if (parent instanceof ClientBehaviorHolder) {
applyAttachedObject(ctx, parent, eventName); // error here will propagate up
tagApplied = true;
}
BeanInfo componentBeanInfo = (BeanInfo) parent.getAttributes().get(
UIComponent.BEANINFO_KEY);
if (null == componentBeanInfo) {
throw new TagException(
tag,
"Error: enclosing composite component does not have BeanInfo attribute");
}
BeanDescriptor componentDescriptor = componentBeanInfo.getBeanDescriptor();
if (null == componentDescriptor) {
throw new TagException(
tag,
"Error: enclosing composite component BeanInfo does not have BeanDescriptor");
}
List<AttachedObjectTarget> targetList = (List<AttachedObjectTarget>)
componentDescriptor
.getValue(AttachedObjectTarget.ATTACHED_OBJECT_TARGETS_KEY);
if (null == targetList && !tagApplied) {
throw new TagException(
tag,
"Error: enclosing composite component does not support behavior events");
}
boolean supportedEvent = false;
for (AttachedObjectTarget target : targetList) {
if (target instanceof BehaviorHolderAttachedObjectTarget) {
BehaviorHolderAttachedObjectTarget behaviorTarget = (BehaviorHolderAttachedObjectTarget) target;
if ((null != eventName && eventName.equals(behaviorTarget.getName()))
|| (null == eventName && behaviorTarget.isDefaultEvent())) {
supportedEvent = true;
break;
}
}
}
if (supportedEvent) {
CompositeComponentTagHandler.getAttachedObjectHandlers(parent)
.add(this);
} else {
if (!tagApplied) {
throw new TagException(
tag,
"Error: enclosing composite component does not support event "
+ eventName);
}
}
} else if (parent instanceof ClientBehaviorHolder) {
applyAttachedObject(ctx, parent, eventName);
} else {
throw new TagException(this.tag,
"Unable to attach <f:ajax> to non-ClientBehaviorHolder parent");
}
}
/**
* <p class="changed_added_2_0"></p>
* @param ctx
* @param parent
* @param eventName
*/
private void applyAttachedObject(FaceletContext ctx,
UIComponent parent,
String eventName) {
ClientBehaviorHolder bHolder = (ClientBehaviorHolder) parent;
if (null == eventName) {
eventName = bHolder.getDefaultEventName();
if (null == eventName) {
throw new TagException(this.tag,
"Event attribute could not be determined: "
+ eventName);
}
} else {
Collection<String> eventNames = bHolder.getEventNames();
if (!eventNames.contains(eventName)) {
throw new TagException(this.tag,
getUnsupportedEventMessage(eventName, eventNames, parent));
}
}
AjaxBehavior ajaxBehavior = createAjaxBehavior(ctx, eventName);
bHolder.addClientBehavior(eventName, ajaxBehavior);
installAjaxResourceIfNecessary();
}
// Construct our AjaxBehavior from tag parameters.
private AjaxBehavior createAjaxBehavior(FaceletContext ctx, String eventName) {
Application application = ctx.getFacesContext().getApplication();
AjaxBehavior behavior = (AjaxBehavior)application.createBehavior(
AjaxBehavior.BEHAVIOR_ID);
setBehaviorAttribute(ctx, behavior, this.onevent, String.class);
setBehaviorAttribute(ctx, behavior, this.onerror, String.class);
setBehaviorAttribute(ctx, behavior, this.disabled, Boolean.class);
setBehaviorAttribute(ctx, behavior, this.immediate, Boolean.class);
setBehaviorAttribute(ctx, behavior, this.execute, Object.class);
setBehaviorAttribute(ctx, behavior, this.render, Object.class);
if (null != listener) {
behavior.addAjaxBehaviorListener(new AjaxBehaviorListenerImpl(
this.listener.getMethodExpression(ctx, Object.class, new Class[] { AjaxBehaviorEvent.class }),
this.listener.getMethodExpression(ctx, Object.class, new Class[] { })));
}
return behavior;
}
// Sets the value from the TagAttribute on the behavior
private void setBehaviorAttribute(FaceletContext ctx,
AjaxBehavior behavior,
TagAttribute attr,
Class type) {
if (attr != null) {
behavior.setValueExpression(attr.getLocalName(),
attr.getValueExpression(ctx, type));
}
}
// Only install the Ajax resource if it doesn't exist.
// The resource component will be installed with the target "head".
//
private void installAjaxResourceIfNecessary() {
FacesContext context = FacesContext.getCurrentInstance();
if (RenderKitUtils.hasScriptBeenRendered(context)) {
// Already included, return
return;
}
final String name = "jsf.js";
final String library = "javax.faces";
if (RenderKitUtils.hasResourceBeenInstalled(context, name, library)) {
RenderKitUtils.setScriptAsRendered(context);
return;
}
UIOutput output = new UIOutput();
output.setRendererType("javax.faces.resource.Script");
output.getAttributes().put("name", name);
output.getAttributes().put("library", library);
context.getViewRoot().addComponentResource(context, output, "head");
// Set the context to record script as included
RenderKitUtils.setScriptAsRendered(context);
}
// Returns an error message for the case where the <f:ajax> event
// attribute specified an unknown/unsupported event.
private String getUnsupportedEventMessage(String eventName,
Collection<String> eventNames,
UIComponent parent) {
StringBuilder builder = new StringBuilder(100);
builder.append("'");
builder.append(eventName);
builder.append("' is not a supported event for ");
builder.append(parent.getClass().getSimpleName());
builder.append(". Please specify one of these supported event names: ");
// Might as well sort the event names for a cleaner error message.
Collection<String> sortedEventNames = new TreeSet<String>(eventNames);
Iterator<String> iter = sortedEventNames.iterator();
boolean hasNext = iter.hasNext();
while (hasNext) {
builder.append(iter.next());
hasNext = iter.hasNext();
if (hasNext) {
builder.append(", ");
}
}
builder.append(".");
return builder.toString();
}
}
class AjaxBehaviorListenerImpl implements AjaxBehaviorListener, Serializable {
private static final long serialVersionUID = -6056525197409773897L;
private MethodExpression oneArgListener;
private MethodExpression noArgListener;
// Necessary for state saving
public AjaxBehaviorListenerImpl() {}
public AjaxBehaviorListenerImpl(MethodExpression oneArg, MethodExpression noArg) {
this.oneArgListener = oneArg;
this.noArgListener = noArg;
}
public void processAjaxBehavior(AjaxBehaviorEvent event) throws AbortProcessingException {
final ELContext elContext = FacesContext.getCurrentInstance().getELContext();
try{
noArgListener.invoke(elContext, new Object[]{});
} catch (MethodNotFoundException mnfe) {
// Attempt to call public void method(AjaxBehaviorEvent event)
oneArgListener.invoke(elContext, new Object[]{event});
} catch (IllegalArgumentException iae) {
// Attempt to call public void method(AjaxBehaviorEvent event)
oneArgListener.invoke(elContext, new Object[]{event});
}
}
}