Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import jakarta.el.MethodExpression;
import jakarta.el.MethodInfo;
import jakarta.el.MethodNotFoundException;
import jakarta.el.PropertyNotFoundException;
import jakarta.el.ValueExpression;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
Expand Down Expand Up @@ -98,6 +99,7 @@ public class ContextualCompositeMethodExpression extends MethodExpression {

// Log instance for this class
private static final Logger LOGGER = FacesLogger.FACELETS_EL.getLogger();
private static final String INVOKE_ERROR = "faces.facelets.el.method.expression.invoke.error: {0} {1}";

private final MethodExpression delegate;
private final ValueExpression source;
Expand Down Expand Up @@ -144,51 +146,66 @@ public Object invoke(ELContext elContext, Object[] objects) {
}
}
} catch (ELException ele) {
/*
* If we got a validator exception it is actually correct to immediately bubble it up.
*/
if (ele.getCause() != null && ele.getCause() instanceof ValidatorException) {
throw (ValidatorException) ele.getCause();
}
rethrowValidatorException(ele);

if (source != null && ele instanceof MethodNotFoundException) {
// special handling when an ELException handling. This is necessary
// when there are multiple levels of composite component nesting.
// When this happens, we need to evaluate the source expression
// to find and invoke the MethodExpression at the next highest
// nesting level. Is there a cleaner way to detect this case?
try {
Object fallback = source.getValue(elContext);
if (fallback != null && fallback instanceof MethodExpression) {
return ((MethodExpression) fallback).invoke(elContext, objects);

}

} catch (ELException ex) {

/*
* If we got a validator exception it is actually correct to immediately bubble it up.
*/
if (ex.getCause() != null && ex.getCause() instanceof ValidatorException) {
throw (ValidatorException) ex.getCause();
}

if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, ele.toString());
LOGGER.log(Level.WARNING, "faces.facelets.el.method.expression.invoke.error: {0} {1}",
new Object[] { ex.toString(), source.getExpressionString() });
}

if (!(ex instanceof MethodNotFoundException)) {
throw ex;
}
}
return invokeOuterCompositeMethodExpression(elContext, source, objects, (MethodNotFoundException) ele);
}

throw ele;
}

}

/**
* Invokes the method expression which the source expression evaluates to. With nested composite components, that is
* the method expression at the next highest nesting level. When the source expression does not resolve, does not
* evaluate to a method expression, or when that method expression finds no method either, the given
* {@link MethodNotFoundException} is thrown, so that the caller can try another variant of the method. Any other
* exception of evaluating the source expression or of the invoked method propagates.
*/
static Object invokeOuterCompositeMethodExpression(ELContext elContext, ValueExpression source, Object[] params, MethodNotFoundException notFound) {
Object outer;

try {
outer = source.getValue(elContext);
} catch (PropertyNotFoundException e) {
logInvokeError(Level.FINE, notFound, e, source);
throw notFound;
} catch (ELException e) {
rethrowValidatorException(e);
logInvokeError(Level.WARNING, notFound, e, source);
throw e;
}

if (!(outer instanceof MethodExpression)) {
throw notFound;
}

try {
return ((MethodExpression) outer).invoke(elContext, params);
} catch (MethodNotFoundException e) {
throw notFound;
} catch (ELException e) {
rethrowValidatorException(e);
logInvokeError(Level.WARNING, notFound, e, source);
throw e;
}
}

private static void logInvokeError(Level level, MethodNotFoundException notFound, ELException e, ValueExpression source) {
if (LOGGER.isLoggable(level)) {
LOGGER.log(level, notFound.toString());
LOGGER.log(level, INVOKE_ERROR, new Object[] { e.toString(), source.getExpressionString() });
}
}

private static void rethrowValidatorException(ELException e) {
if (e.getCause() instanceof ValidatorException) {
throw (ValidatorException) e.getCause();
}
}

// ------------------------------------------------- Methods from Expression

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.sun.faces.facelets.tag.faces;


import jakarta.el.ExpressionFactory;
import jakarta.el.MethodExpression;
import jakarta.faces.component.ActionSource;
import jakarta.faces.component.ActionSource2;
Expand All @@ -39,7 +38,6 @@ final class ActionSourceRule extends MetaRule {
public final static Class[] ACTION_SIG = new Class[0];

public final static Class[] ACTION_LISTENER_SIG = new Class[] { ActionEvent.class };
public final static Class[] ACTION_LISTENER_ZEROARG_SIG = new Class[] {};

final static class ActionMapper2 extends Metadata {

Expand Down Expand Up @@ -67,12 +65,8 @@ public ActionListenerMapper2(TagAttribute attr) {
@Override
public void applyMetadata(FaceletContext ctx, Object instance) {

ExpressionFactory expressionFactory = ctx.getExpressionFactory();

MethodExpression methodExpressionOneArg = attr.getMethodExpression(ctx, null, ActionSourceRule.ACTION_LISTENER_SIG);

MethodExpression methodExpressionZeroArg = expressionFactory.createMethodExpression(ctx, methodExpressionOneArg.getExpressionString(), Void.class,
ActionSourceRule.ACTION_LISTENER_ZEROARG_SIG);
MethodExpression methodExpressionZeroArg = attr.getMethodExpression(ctx, null, ActionSourceRule.ACTION_SIG);

((ActionSource2) instance).addActionListener(new MethodExpressionActionListener(methodExpressionOneArg, methodExpressionZeroArg));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.sun.faces.facelets.tag.faces;


import jakarta.el.MethodExpression;
import jakarta.faces.component.EditableValueHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
Expand Down Expand Up @@ -58,8 +59,9 @@ public ValueChangedExpressionMetadata(TagAttribute attr) {

@Override
public void applyMetadata(FaceletContext ctx, Object instance) {
((EditableValueHolder) instance)
.addValueChangeListener(new MethodExpressionValueChangeListener(attr.getMethodExpression(ctx, null, VALUECHANGE_SIG)));
MethodExpression methodExpressionOneArg = attr.getMethodExpression(ctx, null, VALUECHANGE_SIG);
MethodExpression methodExpressionZeroArg = attr.getMethodExpression(ctx, null, VALUECHANGE_ZEROARG_SIG);
((EditableValueHolder) instance).addValueChangeListener(new MethodExpressionValueChangeListener(methodExpressionOneArg, methodExpressionZeroArg));
}
}

Expand All @@ -80,6 +82,8 @@ public void applyMetadata(FaceletContext ctx, Object instance) {

private final static Class<?>[] VALUECHANGE_SIG = new Class<?>[] { ValueChangeEvent.class };

private final static Class<?>[] VALUECHANGE_ZEROARG_SIG = new Class<?>[] {};

public final static EditableValueHolderRule Instance = new EditableValueHolderRule();

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
*
* 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 com.sun.faces.facelets.tag.faces;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.faces.util.FacesLogger;

import jakarta.el.ELContext;
import jakarta.el.MethodExpression;
import jakarta.el.MethodInfo;
import jakarta.el.MethodNotFoundException;
import jakarta.faces.application.ProjectStage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ActionEvent;
import jakarta.faces.event.MethodExpressionActionListener;

/**
* Invokes the <code>listener</code> method expression of <code>f:event</code> and <code>f:ajax</code>, which may point
* to a method taking the event or to a method taking no arguments.
* <p>
* Each invocation tries the variant taking the event first, in the same order as
* {@link MethodExpressionActionListener#processAction(ActionEvent)}. When that throws {@link MethodNotFoundException}
* or {@link IllegalArgumentException}, the variant taking no arguments is invoked.
* <p>
* In {@link ProjectStage#Development}, it checks once per listener, on the first invocation of the variant taking the
* event, whether the bean also has the method taking no arguments, and if so it logs a warning, because that method is
* then never invoked.
*/
public final class ListenerMethodInvoker {

private static final Logger LOGGER = FacesLogger.TAGLIB.getLogger();
private static final String AMBIGUOUS = "faces.facelets.listener.method.ambiguous";
private static final Object[] NO_ARGS = {};
private static final Set<String> CHECKED_LISTENERS = ConcurrentHashMap.newKeySet();

private ListenerMethodInvoker() {
}

/**
* Invokes the variant taking the event, and when that throws {@link MethodNotFoundException} or
* {@link IllegalArgumentException}, the variant taking no arguments. Any other exception propagates.
*
* @param context the Faces context.
* @param listener the listener as it appears in a warning, such as the location and the value of its tag attribute,
* or <code>null</code> to use the expression string.
* @param oneArgListener the variant taking the event.
* @param noArgListener the variant taking no arguments.
* @param event the event.
*/
public static void invoke(FacesContext context, String listener, MethodExpression oneArgListener, MethodExpression noArgListener, Object event) {
ELContext elContext = context.getELContext();

try {
oneArgListener.invoke(elContext, new Object[] { event });
} catch (MethodNotFoundException | IllegalArgumentException e) {
noArgListener.invoke(elContext, NO_ARGS);
return;
}

if (context.isProjectStage(ProjectStage.Development)) {
String description = listener != null ? listener : oneArgListener.getExpressionString();

if (CHECKED_LISTENERS.add(description)) {
warnIfBothMethodsExist(elContext, description, oneArgListener, noArgListener);
}
}
}

private static void warnIfBothMethodsExist(ELContext context, String description, MethodExpression oneArgListener, MethodExpression noArgListener) {
MethodInfo oneArgMethod;
MethodInfo noArgMethod;

try {
oneArgMethod = oneArgListener.getMethodInfo(context);
noArgMethod = noArgListener.getMethodInfo(context);
} catch (RuntimeException e) {
return;
}

if (hasParameterCount(oneArgMethod, 1) && hasParameterCount(noArgMethod, 0)) {
LOGGER.log(Level.WARNING, AMBIGUOUS, new Object[] { description, noArgMethod.getName() });
}
}

private static boolean hasParameterCount(MethodInfo method, int count) {
Class<?>[] parameterTypes = method == null ? null : method.getParamTypes();
return parameterTypes != null && parameterTypes.length == count;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
import java.util.List;
import java.util.TreeSet;

import jakarta.el.ELContext;
import jakarta.el.MethodExpression;
import jakarta.el.MethodNotFoundException;
import jakarta.el.ValueExpression;
import jakarta.faces.application.Application;
import jakarta.faces.application.ResourceHandler;
Expand All @@ -56,6 +54,7 @@
import com.sun.faces.facelets.tag.composite.BehaviorHolderWrapper;
import com.sun.faces.facelets.tag.composite.RetargetedAjaxBehavior;
import com.sun.faces.facelets.tag.faces.CompositeComponentTagHandler;
import com.sun.faces.facelets.tag.faces.ListenerMethodInvoker;
import com.sun.faces.renderkit.RenderKitUtils;

/**
Expand Down Expand Up @@ -313,7 +312,7 @@ private AjaxBehavior createAjaxBehavior(FaceletContext ctx, UIComponent parent,
if (null != listener) {
behavior.addAjaxBehaviorListener(
new AjaxBehaviorListenerImpl(listener.getMethodExpression(ctx, Object.class, new Class<?>[] { AjaxBehaviorEvent.class }),
listener.getMethodExpression(ctx, Object.class, new Class<?>[] {})));
listener.getMethodExpression(ctx, Object.class, new Class<?>[] {}), listener.toString()));
}

return behavior;
Expand Down Expand Up @@ -363,24 +362,20 @@ class AjaxBehaviorListenerImpl implements AjaxBehaviorListener, Serializable {

private MethodExpression oneArgListener;
private MethodExpression noArgListener;
private String listener;

// Necessary for state saving
public AjaxBehaviorListenerImpl() {
}

public AjaxBehaviorListenerImpl(MethodExpression oneArg, MethodExpression noArg) {
public AjaxBehaviorListenerImpl(MethodExpression oneArg, MethodExpression noArg, String listener) {
oneArgListener = oneArg;
noArgListener = noArg;
this.listener = listener;
}

@Override
public void processAjaxBehavior(AjaxBehaviorEvent event) throws AbortProcessingException {
final ELContext elContext = FacesContext.getCurrentInstance().getELContext();
try {
noArgListener.invoke(elContext, new Object[] {});
} catch (MethodNotFoundException | IllegalArgumentException mnfe) {
// Attempt to call public void method(AjaxBehaviorEvent event)
oneArgListener.invoke(elContext, new Object[] { event });
}
ListenerMethodInvoker.invoke(FacesContext.getCurrentInstance(), listener, oneArgListener, noArgListener, event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@
import java.io.Serializable;

import com.sun.faces.application.ApplicationAssociate;
import com.sun.faces.facelets.tag.faces.ListenerMethodInvoker;

import jakarta.el.ELContext;
import jakarta.el.MethodExpression;
import jakarta.el.MethodNotFoundException;
import jakarta.faces.FacesException;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIViewRoot;
Expand Down Expand Up @@ -66,7 +65,7 @@ public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
if (eventClass != null) {
parent.subscribeToEvent(eventClass,
new DeclarativeSystemEventListener(listener.getMethodExpression(ctx, Object.class, new Class<?>[] { ComponentSystemEvent.class }),
listener.getMethodExpression(ctx, Object.class, new Class<?>[] {})));
listener.getMethodExpression(ctx, Object.class, new Class<?>[] {}), listener.toString()));
}
}
}
Expand All @@ -88,25 +87,21 @@ class DeclarativeSystemEventListener implements ComponentSystemEventListener, Se

private MethodExpression oneArgListener;
private MethodExpression noArgListener;
private String listener;

// Necessary for state saving
public DeclarativeSystemEventListener() {
}

public DeclarativeSystemEventListener(MethodExpression oneArg, MethodExpression noArg) {
public DeclarativeSystemEventListener(MethodExpression oneArg, MethodExpression noArg, String listener) {
oneArgListener = oneArg;
noArgListener = noArg;
this.listener = listener;
}

@Override
public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
final ELContext elContext = FacesContext.getCurrentInstance().getELContext();
try {
noArgListener.invoke(elContext, new Object[] {});
} catch (MethodNotFoundException | IllegalArgumentException mnfe) {
// Attempt to call public void method(ComponentSystemEvent event)
oneArgListener.invoke(elContext, new Object[] { event });
}
ListenerMethodInvoker.invoke(FacesContext.getCurrentInstance(), listener, oneArgListener, noArgListener, event);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,4 @@ faces.component.binding.not.request.scoped=JSF1104: {0} {1}
faces.component.binding.not.request.scoped_summary=Component binding {0} resolved to a component that has survived across requests.
faces.component.binding.not.request.scoped_detail=Component bindings must be request scoped per the Jakarta Faces specification section 3.1.5: a UIComponent belongs to a single request''s view tree and depends on running inside a single thread, so binding it to a wider scope can cause duplicate component id errors, stale state and concurrency bugs. Either bind to a request scoped (or unscoped) target; or, if you only need to populate the component, populate it from an f:event type="postAddToView" listener through event.getComponent() without retaining a reference; or look it up on demand via UIViewRoot.findComponent(id).
faces.config.webconfig.param.development_only=JSF1109: [{0}] Context initialization parameter ''{1}'' only makes debugging easier and weakens the deployment, so it is ignored when the project stage is ''{2}''.
faces.facelets.listener.method.ambiguous=JSF1113: {0} resolves to a method taking the event and to a method {1}() taking no arguments. Only the method taking the event is invoked. Remove the unused method, or merge both into one.
Loading
Loading