jakarta.faces.component.behavior.AjaxBehavior Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.faces.component.behavior;
import static jakarta.faces.component.behavior.ClientBehaviorHint.SUBMITTING;
import static java.util.Collections.unmodifiableSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import jakarta.el.ELContext;
import jakarta.el.ELException;
import jakarta.el.ValueExpression;
import jakarta.faces.FacesException;
import jakarta.faces.component.UIComponentBase;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AjaxBehaviorListener;
/**
*
* An instance of this class is added as a
* {@link ClientBehavior} to a component using the
* {@link jakarta.faces.component.behavior.ClientBehaviorHolder#addClientBehavior} contract that components implement.
* The presence of this {@link ClientBehavior} will cause the rendering of JavaScript that produces an Ajax
* request using the specification public JavaScript API when the component is rendered.
*
*
*
* If the component is an instance of {@link jakarta.faces.component.EditableValueHolder}, Where at all possible, the
* component must have the UI register the ajax event when the initial value is changed, not when focus is lost on the
* component.
*
*
*
*
* @since 2.0
*/
public class AjaxBehavior extends ClientBehaviorBase {
/**
*
* The standard id for this behavior.
*
*/
public static final String BEHAVIOR_ID = "jakarta.faces.behavior.Ajax";
private static final Set HINTS = unmodifiableSet(EnumSet.of(SUBMITTING));
private String onerror;
private String onevent;
private String delay;
private List execute;
private List render;
private Boolean disabled;
private Boolean immediate;
private Boolean resetValues;
private Map bindings;
/**
* Default constructor that just creates this instance.
*/
public AjaxBehavior() {
}
// ---------------------------------------------------------- Public Methods
@Override
public String getRendererType() {
// We use the same sring for both the behavior id and the renderer
// type.
return BEHAVIOR_ID;
}
/**
*
* This method returns an unmodifiable Set
containing the {@link ClientBehaviorHint}
* SUBMITTING
.
*
*
* @return unmodifiable set containing the hint {@link ClientBehaviorHint} SUBMITTING
.
*
* @since 2.0
*/
@Override
public Set getHints() {
return HINTS;
}
/**
*
* Return the String
of JavaScript function name that will be used to identify the client callback function
* that should be run in the event of an error.
*
* @return the JavaScript function name of ONERROR
.
*
* @since 2.0
*/
public String getOnerror() {
return (String) eval(ONERROR, onerror);
}
/**
*
* Sets the JavaScript function name that will be used to identify the client callback function that should be run in
* the event of an error.
*
* @param onerror the error handling function name
*
* @since 2.0
*/
public void setOnerror(String onerror) {
this.onerror = onerror;
clearInitialState();
}
/**
*
* Return the String
of JavaScript function name that will be used to identify the client callback function
* that should be run on the occurance of a client-side event.
*
* @return the JavaScript function name of ONEVENT
.
*
* @since 2.0
*/
public String getOnevent() {
return (String) eval(ONEVENT, onevent);
}
/**
*
* Sets the JavaScript function name that will be used to identify the client callback function that should be run in
* response to event activity.
*
* @param onevent the event handling function name
*
* @since 2.0
*/
public void setOnevent(String onevent) {
this.onevent = onevent;
clearInitialState();
}
/**
*
* Return a non-empty Collection<String>
of component identifiers that will be used to identify
* components that should be processed during the execute
phase of the request processing lifecycle.
*
*
* Note that the returned collection may be unmodifiable. Modifications should be performed by calling
* {@link #setExecute}.
*
*
* @return the JavaScript function name of EXECUTE
.
*
* @since 2.0
*/
public Collection getExecute() {
return getCollectionValue(EXECUTE, execute);
}
/**
*
* Sets the component identifiers that will be used to identify components that should be processed during the
* execute
phase of the request processing lifecycle.
*
*
* @param execute the ids of components to execute
*
* @since 2.0
*/
public void setExecute(Collection execute) {
this.execute = copyToList(execute);
clearInitialState();
}
/**
*
* Returns the delay value, or null
if no value was set.
*
*
* @return the delay value.
*
* @since 2.2
*/
public String getDelay() {
return (String) eval(DELAY, delay);
}
/**
*
* If less than delay milliseconds elapses between calls to request() only the most recent one is sent
* and all other requests are discarded. The default value of this option is 300. If the value of delay is the
* literal string 'none'
without the quotes, no delay is used.
*
*
* @param delay the ajax delay value
*
* @since 2.2
*/
public void setDelay(String delay) {
this.delay = delay;
clearInitialState();
}
/**
*
* Return a non-empty Collection<String>
of component identifiers that will be used to identify
* components that should be processed during the render
phase of the request processing lifecycle.
*
*
* Note that the returned collection may be unmodifiable. Modifications should be performed by calling
* {@link #setRender}.
*
*
* @return the ids of components to render.
*
* @since 2.0
*/
public Collection getRender() {
return getCollectionValue(RENDER, render);
}
/**
*
* Sets the component identifiers that will be used to identify components that should be processed during the
* render
phase of the request processing lifecycle.
*
*
* @param render the ids of components to render
*
* @since 2.0
*/
public void setRender(Collection render) {
this.render = copyToList(render);
clearInitialState();
}
/**
*
* Return the resetValues status of this behavior.
*
*
* @return the resetValues status.
*
* @since 2.2
*/
public boolean isResetValues() {
Boolean result = (Boolean) eval(RESET_VALUES, resetValues);
return result != null ? result : false;
}
/**
*
* Set the resetValues status of this behavior.
*
*
* @param resetValues the resetValues status.
*
* @since 2.2
*/
public void setResetValues(boolean resetValues) {
this.resetValues = resetValues;
clearInitialState();
}
/**
*
* Return the disabled status of this behavior.
*
*
* @return the disabled status of this behavior.
*
* @since 2.0
*/
public boolean isDisabled() {
Boolean result = (Boolean) eval(DISABLED, disabled);
return result != null ? result : false;
}
/**
*
* Sets the disabled status of this behavior.
*
*
* @param disabled the flag to be set.
*
* @since 2.0
*/
public void setDisabled(boolean disabled) {
this.disabled = disabled;
clearInitialState();
}
/**
*
* Return the immediate status of this behavior.
*
*
* @return the immediate status.
*
* @since 2.0
*/
public boolean isImmediate() {
Boolean result = (Boolean) eval(IMMEDIATE, immediate);
return result != null ? result : false;
}
/**
*
* Sets the immediate status of this behavior.
*
*
* @param immediate the flag to be set.
*
* @since 2.0
*/
public void setImmediate(boolean immediate) {
this.immediate = immediate;
clearInitialState();
}
/**
*
* Tests whether the immediate attribute is specified. Returns true if the immediate attribute is specified, either as a
* locally set property or as a value expression. This information allows an associated client behavior renderer to fall
* back on the parent component's immediate status when immediate is not explicitly specified on the
* AjaxBehavior
.
*
*
* @return the flag whether the immediate attribute is specified.
*
* @since 2.0
*/
public boolean isImmediateSet() {
return immediate != null || getValueExpression(IMMEDIATE) != null;
}
/**
*
* Tests whether the resetValues attribute is specified. Returns true if the resetValues attribute is specified, either
* as a locally set property or as a value expression.
*
*
* @return the flag whether the resetValues attribute is specified.
*
* @since 2.2
*/
public boolean isResetValuesSet() {
return resetValues != null || getValueExpression(RESET_VALUES) != null;
}
/**
*
* Returns the {@link ValueExpression} used to calculate the value for the specified property name, if any.
*
*
* @param name Name of the property for which to retrieve a {@link ValueExpression}
*
* @return the {@link ValueExpression}.
*
* @throws NullPointerException if name
is null
*/
public ValueExpression getValueExpression(String name) {
if (name == null) {
throw new NullPointerException();
}
return bindings == null ? null : bindings.get(name);
}
/**
*
* Sets the {@link ValueExpression} used to calculate the value for the specified property name.
*
*
* @param name Name of the property for which to set a {@link ValueExpression}
* @param binding The {@link ValueExpression} to set, or null
to remove any currently set
* {@link ValueExpression}
*
* @throws NullPointerException if name
is null
*/
public void setValueExpression(String name, ValueExpression binding) {
if (name == null) {
throw new NullPointerException();
}
if (binding != null) {
if (binding.isLiteralText()) {
setLiteralValue(name, binding);
} else {
if (bindings == null) {
// We use a very small initial capacity on this HashMap.
// The goal is not to reduce collisions, but to keep the
// memory footprint small. It is very unlikely that an
// an AjaxBehavior would have more than 1 or 2 bound
// properties - and even if more are present, it's okay
// if we have some collisions - will still be fast.
bindings = new HashMap<>(6, 1.0f);
}
bindings.put(name, binding);
}
} else {
if (bindings != null) {
bindings.remove(name);
if (bindings.isEmpty()) {
bindings = null;
}
}
}
clearInitialState();
}
/**
*
* Add the specified {@link AjaxBehaviorListener} to the set of listeners registered to receive event notifications from
* this {@link AjaxBehavior}.
*
*
* @param listener The {@link AjaxBehaviorListener} to be registered
*
* @throws NullPointerException if listener
is null
*
* @since 2.0
*/
public void addAjaxBehaviorListener(AjaxBehaviorListener listener) {
addBehaviorListener(listener);
}
/**
*
* Remove the specified {@link AjaxBehaviorListener} from the set of listeners registered to receive event notifications
* from this {@link AjaxBehavior}.
*
*
* @param listener The {@link AjaxBehaviorListener} to be removed
*
* @throws NullPointerException if listener
is null
*
* @since 2.0
*/
public void removeAjaxBehaviorListener(AjaxBehaviorListener listener) {
removeBehaviorListener(listener);
}
@Override
public Object saveState(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
Object[] values;
Object superState = super.saveState(context);
if (initialStateMarked()) {
if (superState == null) {
values = null;
} else {
values = new Object[] { superState };
}
} else {
values = new Object[10];
values[0] = superState;
values[1] = onerror;
values[2] = onevent;
values[3] = disabled;
values[4] = immediate;
values[5] = resetValues;
values[6] = delay;
values[7] = saveList(execute);
values[8] = saveList(render);
values[9] = saveBindings(context, bindings);
}
return values;
}
@Override
public void restoreState(FacesContext context, Object state) {
if (context == null) {
throw new NullPointerException();
}
if (state != null) {
Object[] values = (Object[]) state;
super.restoreState(context, values[0]);
if (values.length != 1) {
onerror = (String) values[1];
onevent = (String) values[2];
disabled = (Boolean) values[3];
immediate = (Boolean) values[4];
resetValues = (Boolean) values[5];
delay = (String) values[6];
execute = restoreList(EXECUTE, values[7]);
render = restoreList(RENDER, values[8]);
bindings = restoreBindings(context, values[9]);
// If we saved state last time, save state again next time.
clearInitialState();
}
}
}
// --------------------------------------------------------- Private Methods
// Utility for saving bindings state
private static Object saveBindings(FacesContext context, Map bindings) {
// Note: This code is copied from UIComponentBase. In a future
// version of the Jakarta Faces Specification, it would be useful to define a
// attribute/property/bindings/state helper object that can be
// shared across components/behaviors/validaters/converters.
if (bindings == null) {
return null;
}
Object values[] = new Object[2];
values[0] = bindings.keySet().toArray(new String[bindings.size()]);
Object[] bindingValues = bindings.values().toArray();
for (int i = 0; i < bindingValues.length; i++) {
bindingValues[i] = UIComponentBase.saveAttachedState(context, bindingValues[i]);
}
values[1] = bindingValues;
return values;
}
// Utility for restoring bindings from state
private static Map restoreBindings(FacesContext context, Object state) {
// Note: This code is copied from UIComponentBase. See note above
// in saveBindings().
if (state == null) {
return null;
}
Object values[] = (Object[]) state;
String names[] = (String[]) values[0];
Object states[] = (Object[]) values[1];
Map bindings = new HashMap<>(names.length);
for (int i = 0; i < names.length; i++) {
bindings.put(names[i], (ValueExpression) UIComponentBase.restoreAttachedState(context, states[i]));
}
return bindings;
}
// Save the List, either as a String (single element) or as
// a String[] (multiple elements.
private static Object saveList(List list) {
if (list == null || list.isEmpty()) {
return null;
}
int size = list.size();
if (size == 1) {
return list.get(0);
}
return list.toArray(new String[size]);
}
// Restore the list from a String (single element) or a String[]
// (multiple elements)
private static List restoreList(String propertyName, Object state) {
if (state == null) {
return null;
}
List list = null;
if (state instanceof String) {
list = toSingletonList(propertyName, (String) state);
} else if (state instanceof String[]) {
list = Collections.unmodifiableList(Arrays.asList((String[]) state));
}
return list;
}
private Object eval(String propertyName, Object value) {
if (value != null) {
return value;
}
ValueExpression expression = getValueExpression(propertyName);
if (expression != null) {
FacesContext ctx = FacesContext.getCurrentInstance();
return expression.getValue(ctx.getELContext());
}
return null;
}
@SuppressWarnings("unchecked")
private Collection getCollectionValue(String propertyName, Collection collection) {
if (collection != null) {
return collection;
}
Collection result = null;
ValueExpression expression = getValueExpression(propertyName);
if (expression != null) {
FacesContext ctx = FacesContext.getCurrentInstance();
Object value = expression.getValue(ctx.getELContext());
if (value != null) {
if (value instanceof Collection) {
// Unchecked cast to Collection
return (Collection) value;
}
result = toList(propertyName, expression, value);
}
}
return result == null ? Collections.emptyList() : result;
}
// Sets a property, converting it from a literal
private void setLiteralValue(String propertyName, ValueExpression expression) {
assert expression.isLiteralText();
Object value;
ELContext context = FacesContext.getCurrentInstance().getELContext();
try {
value = expression.getValue(context);
} catch (ELException ele) {
throw new FacesException(ele);
}
if (null != propertyName) {
switch (propertyName) {
case ONEVENT:
onevent = (String) value;
break;
case DELAY:
delay = (String) value;
break;
case ONERROR:
onerror = (String) value;
break;
case IMMEDIATE:
immediate = (Boolean) value;
break;
case RESET_VALUES:
resetValues = (Boolean) value;
break;
case DISABLED:
disabled = (Boolean) value;
break;
case EXECUTE:
execute = toList(propertyName, expression, value);
break;
case RENDER:
render = toList(propertyName, expression, value);
break;
}
}
}
// Converts the specified object to a List
private static List toList(String propertyName, ValueExpression expression, Object value) {
if (value instanceof String) {
String strValue = (String) value;
// If the value contains no spaces, we can optimize.
// This is worthwhile, since the execute/render lists
// will often only contain a single value.
if (strValue.indexOf(' ') == -1) {
return toSingletonList(propertyName, strValue);
}
// We're stuck splitting up the string.
String[] values = SPLIT_PATTERN.split(strValue);
if (values == null || values.length == 0) {
return null;
}
// Note that we could create a Set out of the values if
// we care about removing duplicates. However, the
// presence of duplicates does not real harm. They will
// be consolidated during the partial view traversal. So,
// just create an list - garbage in, garbage out.
return Collections.unmodifiableList(Arrays.asList(values));
}
// RELEASE_PENDING i18n ;
throw new FacesException(expression.toString() + " : '" + propertyName + "' attribute value must be either a String or a Collection");
}
// Converts a String with no spaces to a singleton list
private static List toSingletonList(String propertyName, String value) {
if (null == value || value.length() == 0) {
return null;
}
if (value.charAt(0) == '@') {
// These are very common, so we use shared copies
// of these collections instead of re-creating.
if (ALL.equals(value)) {
return ALL_LIST;
} else if (FORM.equals(value)) {
return FORM_LIST;
} else if (THIS.equals(value)) {
return THIS_LIST;
} else if (NONE.equals(value)) {
return NONE_LIST;
}
}
return Collections.singletonList(value);
}
// Makes a defensive copy of the collection, converting to a List
// (to make state saving a bit easier).
private List copyToList(Collection collection) {
if (collection == null || collection.isEmpty()) {
return null;
}
return Collections.unmodifiableList(new ArrayList<>(collection));
}
// Property name constants
private static final String ONEVENT = "onevent";
private static final String ONERROR = "onerror";
private static final String IMMEDIATE = "immediate";
private static final String RESET_VALUES = "resetValues";
private static final String DISABLED = "disabled";
private static final String EXECUTE = "execute";
private static final String RENDER = "render";
private static final String DELAY = "delay";
// Id keyword constants
private static String ALL = "@all";
private static String FORM = "@form";
private static String THIS = "@this";
private static String NONE = "@none";
// Shared execute/render collections
private static List ALL_LIST = Collections.singletonList("@all");
private static List FORM_LIST = Collections.singletonList("@form");
private static List THIS_LIST = Collections.singletonList("@this");
private static List NONE_LIST = Collections.singletonList("@none");
// Pattern used for execute/render string splitting
private static Pattern SPLIT_PATTERN = Pattern.compile(" ");
}