Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically pass through all on* event attributes #5325

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f7b6a2d
Automatically pass through all on* event attributes
BalusC Sep 30, 2023
6286b9c
Fix compilation error caused by locally reverted refactoring
BalusC Sep 30, 2023
3bf2287
Catch up Body/HeadRendererTest with adjusted RenderKitUtils behavior and
BalusC Sep 30, 2023
0ba351d
Also fix AjaxBehaviors (the case when <f:ajax> wraps components)
BalusC Sep 30, 2023
959722b
Further improved to let "unoptimized" path to also support f:ajax event
BalusC Sep 30, 2023
69b16bb
Initial commit of new HtmlEvents class and adapted existing HTML
BalusC Sep 30, 2023
d77e716
Fixed compilation error caused by accidental usage of Java16 method
BalusC Sep 30, 2023
2944ff2
Refactored inline constant
BalusC Sep 30, 2023
3540ef9
Merge branch 'master' into faces_issue_1507_automatically_pass_throug…
BalusC Oct 7, 2023
ab3d229
Reverted temp change in AjaxBehaviors as HtmlEvents has been introduced
BalusC Oct 7, 2023
26e38c8
Merge branch 'master' into faces_issue_1507_automatically_pass_throug…
BalusC Oct 15, 2023
8ffb4fd
Add FacesComponentEvent enum with action and valueChange; refactored
BalusC Oct 15, 2023
c056558
Clean up after self-review
BalusC Oct 15, 2023
b550fe6
Merge remote-tracking branch 'origin/master' into faces_issue_1507_au…
BalusC Oct 22, 2023
acf3d47
Fixed failing tests
BalusC Oct 22, 2023
e3ead5c
Merge branch 'master' into faces_issue_1507_automatically_pass_throug…
BalusC Oct 28, 2023
0bc83fd
Renamed the enums to have Html prefix and changed appmap by enummap
BalusC Oct 28, 2023
7236717
Set in stone in spec
BalusC Oct 28, 2023
71ad216
Remove HTML5 specificity
BalusC Oct 28, 2023
12f9a98
Merge branch 'master' into faces_issue_1507_automatically_pass_throug…
BalusC Nov 4, 2023
52b935d
Fix typo
BalusC Nov 4, 2023
30b417c
Merge branch 'master' into
BalusC Nov 12, 2023
2f822ea
Merge branch 'master' into
BalusC Feb 10, 2024
c5c11ca
Merge branch 'master' into
BalusC Mar 24, 2024
12e79cd
Merge remote-tracking branch 'origin/master' into faces_issue_1507_au…
BalusC Mar 24, 2024
26237e1
Merge branch 'master' into faces_issue_1507_automatically_pass_throug…
BalusC Sep 2, 2024
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 @@ -16,12 +16,15 @@

package com.sun.faces.component;

import static jakarta.faces.component.html.HtmlEvents.getHtmlBodyElementEventNames;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import jakarta.el.ValueExpression;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.component.html.HtmlEvents.HtmlDocumentElementEvent;

/**
* <p>
Expand Down Expand Up @@ -355,17 +358,14 @@ public void setStyleClass(java.lang.String styleClass) {
getStateHelper().put(PropertyKeys.styleClass, styleClass);
}

private static final List<String> EVENT_NAMES = List.of(
"click", "dblclick", "keydown", "keypress", "keyup", "mousedown", "mousemove", "mouseout", "mouseover", "mouseup");

@Override
public Collection<String> getEventNames() {
return EVENT_NAMES;
return getHtmlBodyElementEventNames(getFacesContext());
}

@Override
public String getDefaultEventName() {
return "click";
return HtmlDocumentElementEvent.click.name();
}

// TODO The same as jakarta.faces.component.html.HtmlComponentUtils#handleAttribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.AjaxBehavior;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.component.html.HtmlEvents;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.AjaxBehaviorEvent;
Expand Down Expand Up @@ -352,7 +353,9 @@ private String getUnsupportedEventMessage(String eventName, Collection<String> e
}
}

builder.append(".");
builder.append(". In case you wish to add new ones, then you can specify them"
+ " as space-separated value of context-param with name "
+ HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);

return builder.toString();
}
Expand Down
138 changes: 93 additions & 45 deletions impl/src/main/java/com/sun/faces/renderkit/RenderKitUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static com.sun.faces.renderkit.RenderKitUtils.PredefinedPostbackParameter.PARTIAL_EVENT_PARAM;
import static jakarta.faces.application.ResourceHandler.FACES_SCRIPT_LIBRARY_NAME;
import static jakarta.faces.application.ResourceHandler.FACES_SCRIPT_RESOURCE_NAME;
import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.io.Writer;
Expand All @@ -30,9 +31,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -64,11 +67,13 @@
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.behavior.ClientBehaviorHint;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.component.html.HtmlEvents.HtmlDocumentElementEvent;
import jakarta.faces.component.html.HtmlMessages;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.PartialViewContext;
import jakarta.faces.context.ResponseWriter;
import jakarta.faces.event.BehaviorEvent.FacesComponentEvent;
import jakarta.faces.model.SelectItem;
import jakarta.faces.render.RenderKit;
import jakarta.faces.render.RenderKitFactory;
Expand Down Expand Up @@ -141,11 +146,7 @@ public class RenderKitUtils {
*/
private static final String ATTRIBUTES_THAT_ARE_SET_KEY = UIComponentBase.class.getName() + ".attributesThatAreSet";

/**
* UIViewRoot attribute key of a boolean value which remembers whether the view will be rendered with a HTML5 doctype.
*/
private static final String VIEW_ROOT_ATTRIBUTES_DOCTYPE_KEY = RenderKitUtils.class.getName() + ".isOutputHtml5Doctype";

private static final String BEHAVIOR_EVENT_ATTRIBUTE_PREFIX = "on";

protected static final Logger LOGGER = FacesLogger.RENDERKIT.getLogger();

Expand Down Expand Up @@ -330,18 +331,17 @@ public static void renderPassThruAttributes(FacesContext context, ResponseWriter
behaviors = Collections.emptyMap();
}

if (canBeOptimized(component, behaviors)) {
List<String> setAttributes = (List<String>) component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY);
if (setAttributes != null) {
renderPassThruAttributesOptimized(context, writer, component, attributes, setAttributes, behaviors);
}
List<String> setAttributes = (List<String>) component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY);

if (setAttributes != null && canBeOptimized(component, behaviors)) {
renderPassThruAttributesOptimized(context, writer, component, attributes, setAttributes, behaviors);
} else {

// this block should only be hit by custom components leveraging
// the RI's rendering code, or in cases where we have behaviors
// attached to multiple events. We make no assumptions and loop
// through
renderPassThruAttributesUnoptimized(context, writer, component, attributes, behaviors);
renderPassThruAttributesUnoptimized(context, writer, component, attributes, setAttributes, behaviors);
}
}

Expand All @@ -352,11 +352,12 @@ public static void renderOnchange(FacesContext context, UIComponent component, b

final String handlerName = "onchange";
final Object userHandler = component.getAttributes().get(handlerName);
String behaviorEventName = "valueChange";
String behaviorEventName = FacesComponentEvent.valueChange.name();
String domEventName = HtmlDocumentElementEvent.change.name();
if (component instanceof ClientBehaviorHolder) {
Map<?, ?> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
if (null != behaviors && behaviors.containsKey("change")) {
behaviorEventName = "change";
if (null != behaviors && behaviors.containsKey(domEventName)) {
behaviorEventName = domEventName;
}
}

Expand All @@ -375,11 +376,12 @@ public static void renderSelectOnclick(FacesContext context, UIComponent compone

final String handlerName = "onclick";
final Object userHandler = component.getAttributes().get(handlerName);
String behaviorEventName = "valueChange";
String behaviorEventName = FacesComponentEvent.valueChange.name();
String domEventName = HtmlDocumentElementEvent.click.name();
if (component instanceof ClientBehaviorHolder) {
Map<?, ?> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
if (null != behaviors && behaviors.containsKey("click")) {
behaviorEventName = "click";
if (null != behaviors && behaviors.containsKey(domEventName)) {
behaviorEventName = domEventName;
}
}

Expand All @@ -401,18 +403,19 @@ public static void renderOnclick(FacesContext context, UIComponent component, Co

final String handlerName = "onclick";
final Object userHandler = component.getAttributes().get(handlerName);
String behaviorEventName = "action";
String behaviorEventName = FacesComponentEvent.action.name();
String domEventName = HtmlDocumentElementEvent.click.name();
if (component instanceof ClientBehaviorHolder) {
Map<String, List<ClientBehavior>> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
boolean mixed = null != behaviors && behaviors.containsKey("click") && behaviors.containsKey("action");
boolean mixed = null != behaviors && behaviors.containsKey(domEventName) && behaviors.containsKey(behaviorEventName);
if (mixed) {
behaviorEventName = "click";
List<ClientBehavior> clickBehaviors = behaviors.get("click");
List<ClientBehavior> actionBehaviors = behaviors.get("action");
List<ClientBehavior> actionBehaviors = behaviors.get(behaviorEventName);
behaviorEventName = domEventName;
List<ClientBehavior> clickBehaviors = behaviors.get(domEventName);
clickBehaviors.addAll(actionBehaviors);
actionBehaviors.clear();
} else if (null != behaviors && behaviors.containsKey("click")) {
behaviorEventName = "click";
} else if (null != behaviors && behaviors.containsKey(domEventName)) {
behaviorEventName = domEventName;
}
}

Expand All @@ -423,7 +426,7 @@ public static void renderOnclick(FacesContext context, UIComponent component, Co
public static void renderFunction(FacesContext context, UIComponent component, Collection<ClientBehaviorContext.Parameter> params, String submitTarget)
throws IOException {

ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context, component, "action", submitTarget, params);
ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context, component, FacesComponentEvent.action.name(), submitTarget, params);
AjaxBehavior behavior = (AjaxBehavior) context.getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID);
mapAttributes(component, behavior, "execute", "render", "onerror", "onevent", "resetValues");

Expand Down Expand Up @@ -623,6 +626,18 @@ private static void renderPassThruAttributesOptimized(FacesContext context, Resp
if (isBehaviorEventAttribute(attr, behaviorEventName)) {
renderHandler(context, component, null, name, value, behaviorEventName, null, false, false);

renderedBehavior = true;
} else {
writer.writeAttribute(prefixAttribute(name, isXhtml), value, name);
}
}
}
else if (isBehaviorEventAttribute(name)) {
Object value = attrMap.get(name);
if (value != null && shouldRenderAttribute(value)) {
if (name.substring(2).equals(behaviorEventName)) {
renderHandler(context, component, null, name, value, behaviorEventName, null, false, false);

renderedBehavior = true;
} else {
writer.writeAttribute(prefixAttribute(name, isXhtml), value, name);
Expand All @@ -635,18 +650,27 @@ private static void renderPassThruAttributesOptimized(FacesContext context, Resp
// attribute rendering. Need to manually render it out now.
if (behaviorEventName != null && !renderedBehavior) {

List<String> behaviorAttributes = setAttributes.stream().filter(RenderKitUtils::isBehaviorEventAttribute).collect(toList());

for (String attrName : behaviorAttributes) {
String eventName = attrName.substring(2);
if (behaviorEventName.equals(eventName)) {
renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, attrName, behaviorEventName);
return;
}
}

// Note that we can optimize this search by providing
// an event name -> Attribute inverse look up map.
// This would change the search time from O(n) to O(1).
for (int i = 0; i < knownAttributes.length; i++) {
Attribute attr = knownAttributes[i];
String[] events = attr.getEvents();
if (events != null && events.length > 0 && behaviorEventName.equals(events[0])) {

renderHandler(context, component, null, attr.getName(), null, behaviorEventName, null, false, false);
for (Attribute attribute : knownAttributes) {
String attrName = attribute.getName();
String[] events = attribute.getEvents();
if (events != null && events.length > 0 && behaviorEventName.equals(events[0])) {
renderHandler(context, component, null, attrName, null, behaviorEventName, null, false, false);
}
}

}
}

Expand All @@ -658,35 +682,59 @@ private static void renderPassThruAttributesOptimized(FacesContext context, Resp
* @param writer the current writer
* @param component the component whose attributes we're rendering
* @param knownAttributes an array of pass-through attributes supported by this component
* @param setAttributes a <code>List</code> of attributes that have been set on the provided component
* @param behaviors the non-null behaviors map for this request.
* @throws IOException if an error occurs during the write
*/
private static void renderPassThruAttributesUnoptimized(FacesContext context, ResponseWriter writer, UIComponent component, Attribute[] knownAttributes,
Map<String, List<ClientBehavior>> behaviors) throws IOException {
List<String> setAttributes, Map<String, List<ClientBehavior>> behaviors) throws IOException {

boolean isXhtml = RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType());

Map<String, Object> attrMap = component.getAttributes();
Set<String> behaviorEventNames = new LinkedHashSet<>(behaviors.size() + 2);

behaviorEventNames.addAll(behaviors.keySet());

if (setAttributes != null) {
setAttributes.stream().filter(RenderKitUtils::isBehaviorEventAttribute).map(a -> a.substring(BEHAVIOR_EVENT_ATTRIBUTE_PREFIX.length())).forEach(behaviorEventNames::add);
}

for (Attribute attribute : knownAttributes) {
String attrName = attribute.getName();
String[] events = attribute.getEvents();
boolean hasBehavior = events != null && events.length > 0 && behaviors.containsKey(events[0]);
String eventName = events != null && events.length > 0 ? events[0] : null;
renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, attrName, eventName);
behaviorEventNames.remove(eventName);
}

Object value = attrMap.get(attrName);
for (String eventName : behaviorEventNames) {
renderPassthruAttribute(context, writer, component, behaviors, isXhtml, attrMap, BEHAVIOR_EVENT_ATTRIBUTE_PREFIX + eventName, eventName);
}
}

if (value != null && shouldRenderAttribute(value) && !hasBehavior) {
writer.writeAttribute(prefixAttribute(attrName, isXhtml), value, attrName);
} else if (hasBehavior) {
private static void renderPassthruAttribute(FacesContext context, ResponseWriter writer, UIComponent component,
Map<String, List<ClientBehavior>> behaviors, boolean isXhtml, Map<String, Object> attrMap, String attrName,
String eventName) throws IOException {
boolean hasBehavior = eventName != null && behaviors.containsKey(eventName);

// If we've got a behavior for this attribute,
// we may need to chain scripts together, so use
// renderHandler().
renderHandler(context, component, null, attrName, value, events[0], null, false, false);
}
Object value = attrMap.get(attrName);

if (value != null && shouldRenderAttribute(value) && !hasBehavior) {
writer.writeAttribute(prefixAttribute(attrName, isXhtml), value, attrName);
} else if (hasBehavior) {

// If we've got a behavior for this attribute,
// we may need to chain scripts together, so use
// renderHandler().
renderHandler(context, component, null, attrName, value, eventName, null, false, false);
}
}

public static boolean isBehaviorEventAttribute(String name) {
return name.startsWith(BEHAVIOR_EVENT_ATTRIBUTE_PREFIX) && name.length() > 2;
}

/**
* <p>
* Determines if an attribute should be rendered based on the specified #attributeVal.
Expand Down Expand Up @@ -1144,14 +1192,14 @@ public static boolean isPartialOrBehaviorAction(FacesContext context, String cli
// First check for a Behavior action event.
String behaviorEvent = BEHAVIOR_EVENT_PARAM.getValue(context);
if (null != behaviorEvent) {
return "action".equals(behaviorEvent);
return FacesComponentEvent.action.name().equals(behaviorEvent);
}

// Not a Behavior-related request. Check for faces.ajax.request()
// request params.
String partialEvent = PARTIAL_EVENT_PARAM.getValue(context);

return "click".equals(partialEvent);
return HtmlDocumentElementEvent.click.name().equals(partialEvent);
}

/**
Expand Down Expand Up @@ -1529,7 +1577,7 @@ private static String getChainedHandler(FacesContext context, UIComponent compon
// If we're submitting (either via a behavior, or by rendering
// a submit script), we need to return false to prevent the
// default button/link action.
if (submitting && ("action".equals(behaviorEventName) || "click".equals(behaviorEventName))) {
if (submitting && (FacesComponentEvent.action.name().equals(behaviorEventName) || HtmlDocumentElementEvent.click.name().equals(behaviorEventName))) {
builder.append(";return false");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.html.HtmlEvents.HtmlDocumentElementEvent;
import jakarta.faces.event.BehaviorEvent.FacesComponentEvent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import jakarta.faces.event.ActionEvent;
Expand Down Expand Up @@ -242,7 +244,7 @@ private static String getButtonType(UIComponent component) {
// which allows us to take a more optimized code path.
private static Map<String, List<ClientBehavior>> getNonOnClickBehaviors(UIComponent component) {

return getPassThruBehaviors(component, "click", "action");
return getPassThruBehaviors(component, HtmlDocumentElementEvent.click.name(), FacesComponentEvent.action.name());
}

} // end of class ButtonRenderer
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.html.HtmlEvents.HtmlDocumentElementEvent;
import jakarta.faces.event.BehaviorEvent.FacesComponentEvent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import jakarta.faces.event.ActionEvent;
Expand Down Expand Up @@ -199,7 +201,7 @@ private static boolean wasClicked(FacesContext context, UIComponent component, S
// which allows us to take a more optimized code path.
private static Map<String, List<ClientBehavior>> getNonOnClickBehaviors(UIComponent component) {

return getPassThruBehaviors(component, "click", "action");
return getPassThruBehaviors(component, HtmlDocumentElementEvent.click.name(), FacesComponentEvent.action.name());
}

} // end of class CommandLinkRenderer
Loading
Loading