com.sun.faces.renderkit.RenderKitUtils Maven / Gradle / Ivy
Show all versions of jakarta.faces Show documentation
/*
* 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 com.sun.faces.renderkit;
import static com.sun.faces.renderkit.RenderKitUtils.PredefinedPostbackParameter.BEHAVIOR_EVENT_PARAM;
import static com.sun.faces.renderkit.RenderKitUtils.PredefinedPostbackParameter.BEHAVIOR_SOURCE_PARAM;
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 java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.faces.RIConstants;
import com.sun.faces.application.ApplicationAssociate;
import com.sun.faces.config.WebConfiguration;
import com.sun.faces.el.ELUtils;
import com.sun.faces.facelets.util.DevTools;
import com.sun.faces.util.FacesLogger;
import com.sun.faces.util.RequestStateManager;
import com.sun.faces.util.Util;
import jakarta.el.ValueExpression;
import jakarta.faces.FacesException;
import jakarta.faces.FactoryFinder;
import jakarta.faces.application.Application;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.application.ProjectStage;
import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceHandler;
import jakarta.faces.component.ActionSource;
import jakarta.faces.component.Doctype;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIComponentBase;
import jakarta.faces.component.UIForm;
import jakarta.faces.component.UIOutput;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.component.behavior.AjaxBehavior;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.behavior.ClientBehaviorHint;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
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.model.SelectItem;
import jakarta.faces.render.RenderKit;
import jakarta.faces.render.RenderKitFactory;
import jakarta.faces.render.Renderer;
import jakarta.faces.render.ResponseStateManager;
/**
*
* A set of utilities for use in {@link RenderKit}s.
*
*/
public class RenderKitUtils {
/**
*
* The prefix to append to certain attributes when renderking XHTML Transitional
content.
*/
private static final String XHTML_ATTR_PREFIX = "xml:";
/**
*
* Boolean
attributes to be rendered using XHMTL
semantics.
*/
private static final String[] BOOLEAN_ATTRIBUTES = { "disabled", "ismap", "readonly", "multiple" };
/**
*
* An array of attributes that must be prefixed by {@link #XHTML_ATTR_PREFIX} when rendering
* XHTML Transitional
content.
*/
private static final String[] XHTML_PREFIX_ATTRIBUTES = { "lang" };
/**
*
* The maximum number of content type parts. For example: for the type: "text/html; level=1; q=0.5" The parts of this
* type would be: "text" - type "html; level=1" - subtype "0.5" - quality value "1" - level value
*
*/
private final static int MAX_CONTENT_TYPE_PARTS = 4;
/**
* The character that is used to delimit content types in an accept String.
*
*/
private final static String CONTENT_TYPE_DELIMITER = ",";
/**
* The character that is used to delimit the type and subtype portions of a content type in an accept String. Example:
* text/html
*
*/
private final static String CONTENT_TYPE_SUBTYPE_DELIMITER = "/";
/**
* This represents the base package that can leverage the attributesThatAreSet
List for optimized attribute
* rendering.
*
* IMPLEMENTATION NOTE: This must be kept in sync with the array in UIComponentBase$AttributesMap and
* HtmlComponentGenerator.
*
* Hopefully Faces X will remove the need for this.
*/
private static final String OPTIMIZED_PACKAGE = "jakarta.faces.component.";
/**
* IMPLEMENTATION NOTE: This must be kept in sync with the Key in UIComponentBase$AttributesMap and
* HtmlComponentGenerator.
*
* Hopefully Faces X will remove the need for this.
*/
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";
protected static final Logger LOGGER = FacesLogger.RENDERKIT.getLogger();
public static final String DEVELOPMENT_STAGE_MESSAGES_ID = "jakarta_faces_developmentstage_messages";
/**
* @see UIViewRoot#encodeChildren(FacesContext)
*/
public enum PredefinedPostbackParameter {
VIEW_STATE_PARAM(ResponseStateManager.VIEW_STATE_PARAM), CLIENT_WINDOW_PARAM(ResponseStateManager.CLIENT_WINDOW_PARAM),
RENDER_KIT_ID_PARAM(ResponseStateManager.RENDER_KIT_ID_PARAM), BEHAVIOR_SOURCE_PARAM(ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME),
BEHAVIOR_EVENT_PARAM(ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME), PARTIAL_EVENT_PARAM(PartialViewContext.PARTIAL_EVENT_PARAM_NAME),
PARTIAL_EXECUTE_PARAM(PartialViewContext.PARTIAL_EXECUTE_PARAM_NAME), PARTIAL_RENDER_PARAM(PartialViewContext.PARTIAL_RENDER_PARAM_NAME),
PARTIAL_RESET_VALUES_PARAM(PartialViewContext.RESET_VALUES_PARAM_NAME);
private String name;
private PredefinedPostbackParameter(String name) {
this.name = name;
}
public String getValue(FacesContext context) {
return context.getExternalContext().getRequestParameterMap().get(getName(context));
}
public String getName(FacesContext context) {
return getParameterName(context, name);
}
}
// ------------------------------------------------------------ Constructors
private RenderKitUtils() {
}
// ---------------------------------------------------------- Public Methods
/**
*
* Return the {@link RenderKit} for the current request.
*
*
* @param context the {@link FacesContext} of the current request
* @return the {@link RenderKit} for the current request.
*/
public static RenderKit getCurrentRenderKit(FacesContext context) {
RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
return renderKitFactory.getRenderKit(context, context.getViewRoot().getRenderKitId());
}
/**
*
* Obtain and return the {@link ResponseStateManager} for the specified #renderKitId.
*
*
* @param context the {@link FacesContext} of the current request
* @param renderKitId {@link RenderKit} ID
* @return the {@link ResponseStateManager} for the specified #renderKitId
* @throws FacesException if an exception occurs while trying to obtain the ResponseStateManager
*/
public static ResponseStateManager getResponseStateManager(FacesContext context, String renderKitId) throws FacesException {
assert null != renderKitId;
assert null != context;
RenderKit renderKit = context.getRenderKit();
if (renderKit == null) {
// check request scope for a RenderKitFactory implementation
RenderKitFactory factory = (RenderKitFactory) RequestStateManager.get(context, RequestStateManager.RENDER_KIT_IMPL_REQ);
if (factory != null) {
renderKit = factory.getRenderKit(context, renderKitId);
} else {
factory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
if (factory == null) {
throw new FacesException("Unable to locate RenderKitFactory for " + FactoryFinder.RENDER_KIT_FACTORY);
} else {
RequestStateManager.set(context, RequestStateManager.RENDER_KIT_IMPL_REQ, factory);
}
renderKit = factory.getRenderKit(context, renderKitId);
if (renderKit == null) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, "Unable to locate renderkit " + "instance for render-kit-id {0}. Using {1} instead.",
new String[] { renderKitId, RenderKitFactory.HTML_BASIC_RENDER_KIT });
}
renderKitId = RenderKitFactory.HTML_BASIC_RENDER_KIT;
UIViewRoot root = context.getViewRoot();
if (null != root) {
root.setRenderKitId(renderKitId);
}
}
renderKit = factory.getRenderKit(context, renderKitId);
if (renderKit == null) {
throw new FacesException("Unable to locate renderkit instance for render-kit-id " + renderKitId);
}
}
}
return renderKit.getResponseStateManager();
}
/**
*
* Return a List of {@link jakarta.faces.model.SelectItem} instances representing the available options for this
* component, assembled from the set of {@link jakarta.faces.component.UISelectItem} and/or
* {@link jakarta.faces.component.UISelectItems} components that are direct children of this component. If there are no
* such children, an empty Iterator
is returned.
*
*
* @param context The {@link jakarta.faces.context.FacesContext} for the current request. If null, the UISelectItems
* behavior will not work.
* @param component the component
* @throws IllegalArgumentException if context
is null
* @return a List of the select items for the specified component
*/
public static SelectItemsIterator getSelectItems(FacesContext context, UIComponent component) {
Util.notNull("context", context);
Util.notNull("component", component);
return new SelectItemsIterator<>(context, component);
}
/**
*
* Render any "passthru" attributes, where we simply just output the raw name and value of the attribute. This method is
* aware of the set of HTML4 attributes that fall into this bucket. Examples are all the javascript attributes, alt,
* rows, cols, etc.
*
*
* @param context the FacesContext for this request
* @param writer writer the {@link jakarta.faces.context.ResponseWriter} to be used when writing the attributes
* @param component the component
* @param attributes an array of attributes to be processed
* @throws IOException if an error occurs writing the attributes
*/
public static void renderPassThruAttributes(FacesContext context, ResponseWriter writer, UIComponent component, Attribute[] attributes) throws IOException {
assert null != context;
assert null != writer;
assert null != component;
Map> behaviors = null;
if (component instanceof ClientBehaviorHolder) {
behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
}
// Don't render behavior scripts if component is disabled
if (null != behaviors && behaviors.size() > 0 && Util.componentIsDisabled(component)) {
behaviors = null;
}
renderPassThruAttributes(context, writer, component, attributes, behaviors);
}
/**
*
* Render any "passthru" attributes, where we simply just output the raw name and value of the attribute. This method is
* aware of the set of HTML4 attributes that fall into this bucket. Examples are all the javascript attributes, alt,
* rows, cols, etc.
*
*
* @param context the FacesContext for this request
* @param writer writer the {@link jakarta.faces.context.ResponseWriter} to be used when writing the attributes
* @param component the component
* @param attributes an array of attributes to be processed
* @param behaviors the behaviors for this component, or null if component is not a ClientBehaviorHolder
* @throws IOException if an error occurs writing the attributes
*/
@SuppressWarnings("unchecked")
public static void renderPassThruAttributes(FacesContext context, ResponseWriter writer, UIComponent component, Attribute[] attributes,
Map> behaviors) throws IOException {
assert null != writer;
assert null != component;
if (behaviors == null) {
behaviors = Collections.emptyMap();
}
if (canBeOptimized(component, behaviors)) {
List setAttributes = (List) component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY);
if (setAttributes != null) {
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);
}
}
// Renders the onchange handler for input components. Handles
// chaining together the user-provided onchange handler with
// any Behavior scripts.
public static void renderOnchange(FacesContext context, UIComponent component, boolean incExec) throws IOException {
final String handlerName = "onchange";
final Object userHandler = component.getAttributes().get(handlerName);
String behaviorEventName = "valueChange";
if (component instanceof ClientBehaviorHolder) {
Map, ?> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
if (null != behaviors && behaviors.containsKey("change")) {
behaviorEventName = "change";
}
}
List params;
if (!incExec) {
params = Collections.emptyList();
} else {
params = new LinkedList<>();
params.add(new ClientBehaviorContext.Parameter("incExec", true));
}
renderHandler(context, component, params, handlerName, userHandler, behaviorEventName, null, false, incExec);
}
// Renders onclick handler for SelectRaidio and SelectCheckbox
public static void renderSelectOnclick(FacesContext context, UIComponent component, boolean incExec) throws IOException {
final String handlerName = "onclick";
final Object userHandler = component.getAttributes().get(handlerName);
String behaviorEventName = "valueChange";
if (component instanceof ClientBehaviorHolder) {
Map, ?> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
if (null != behaviors && behaviors.containsKey("click")) {
behaviorEventName = "click";
}
}
List params;
if (!incExec) {
params = Collections.emptyList();
} else {
params = new LinkedList<>();
params.add(new ClientBehaviorContext.Parameter("incExec", true));
}
renderHandler(context, component, params, handlerName, userHandler, behaviorEventName, null, false, incExec);
}
// Renders the onclick handler for command buttons. Handles
// chaining together the user-provided onclick handler, any
// Behavior scripts, plus the default button submit script.
public static void renderOnclick(FacesContext context, UIComponent component, Collection params, String submitTarget,
boolean needsSubmit) throws IOException {
final String handlerName = "onclick";
final Object userHandler = component.getAttributes().get(handlerName);
String behaviorEventName = "action";
if (component instanceof ClientBehaviorHolder) {
Map> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
boolean mixed = null != behaviors && behaviors.containsKey("click") && behaviors.containsKey("action");
if (mixed) {
behaviorEventName = "click";
List clickBehaviors = behaviors.get("click");
List actionBehaviors = behaviors.get("action");
clickBehaviors.addAll(actionBehaviors);
actionBehaviors.clear();
} else if (null != behaviors && behaviors.containsKey("click")) {
behaviorEventName = "click";
}
}
renderHandler(context, component, params, handlerName, userHandler, behaviorEventName, submitTarget, needsSubmit, false);
}
// Renders the script function for command scripts.
public static void renderFunction(FacesContext context, UIComponent component, Collection params, String submitTarget)
throws IOException {
ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context, component, "action", submitTarget, params);
AjaxBehavior behavior = (AjaxBehavior) context.getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID);
mapAttributes(component, behavior, "execute", "render", "onerror", "onevent", "resetValues");
context.getResponseWriter().append(behavior.getScript(behaviorContext));
}
private static void mapAttributes(UIComponent component, AjaxBehavior behavior, String... attributeNames) {
for (String attributeName : attributeNames) {
ValueExpression binding = component.getValueExpression(attributeName);
if (binding == null) {
Object value = component.getAttributes().get(attributeName);
if (value != null) {
binding = ELUtils.createValueExpression(value.toString(), value.getClass());
}
}
behavior.setValueExpression(attributeName, binding);
}
}
public static String prefixAttribute(final String attrName, final ResponseWriter writer) {
return prefixAttribute(attrName, RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType()));
}
public static String prefixAttribute(final String attrName, boolean isXhtml) {
if (isXhtml) {
if (Arrays.binarySearch(XHTML_PREFIX_ATTRIBUTES, attrName) > -1) {
return XHTML_ATTR_PREFIX + attrName;
} else {
return attrName;
}
} else {
return attrName;
}
}
/**
*
* Renders the attributes from {@link #BOOLEAN_ATTRIBUTES} using XHMTL
semantics (i.e.,
* disabled="disabled").
*
*
* @param writer writer the {@link ResponseWriter} to be used when writing the attributes
* @param component the component
* @throws IOException if an error occurs writing the attributes
*/
public static void renderXHTMLStyleBooleanAttributes(ResponseWriter writer, UIComponent component) throws IOException {
assert writer != null;
assert component != null;
List excludedAttributes = null;
renderXHTMLStyleBooleanAttributes(writer, component, excludedAttributes);
}
/**
*
* Renders the attributes from {@link #BOOLEAN_ATTRIBUTES} using XHMTL
semantics (i.e.,
* disabled="disabled").
*
*
* @param writer writer the {@link ResponseWriter} to be used when writing the attributes
* @param component the component
* @param excludedAttributes a List
of attributes that are to be excluded from rendering
* @throws IOException if an error occurs writing the attributes
*/
public static void renderXHTMLStyleBooleanAttributes(ResponseWriter writer, UIComponent component, List excludedAttributes) throws IOException {
assert writer != null;
assert component != null;
Map, ?> attrMap = component.getAttributes();
for (String attrName : BOOLEAN_ATTRIBUTES) {
if (isExcludedAttribute(attrName, excludedAttributes)) {
continue;
}
Object val = attrMap.get(attrName);
if (val == null) {
continue;
}
if (Boolean.valueOf(val.toString())) {
writer.writeAttribute(attrName, true, attrName);
}
}
}
/**
*
* Given an accept String from the client, and a String
of server supported content types, determine the
* best qualified content type for the client. If no match is found, or either of the arguments are null
,
* null
is returned.
*
*
* @param accept The client accept String
* @param serverSupportedTypes The types that the server supports
* @param preferredType The preferred content type if another type is found with the same highest quality factor.
* @return The content type String
*/
public static String determineContentType(String accept, String serverSupportedTypes, String preferredType) {
String contentType = null;
if (null == accept || null == serverSupportedTypes) {
return contentType;
}
String[][] clientContentTypes = buildTypeArrayFromString(accept);
String[][] serverContentTypes = buildTypeArrayFromString(serverSupportedTypes);
String[][] preferredContentType = buildTypeArrayFromString(preferredType);
String[][] matchedInfo = findMatch(clientContentTypes, serverContentTypes, preferredContentType);
// if best match exits and best match is not some wildcard,
// return best match
if (matchedInfo[0][1] != null && !matchedInfo[0][2].equals("*")) {
contentType = matchedInfo[0][1] + CONTENT_TYPE_SUBTYPE_DELIMITER + matchedInfo[0][2];
}
return contentType;
}
/**
* @param contentType the content type in question
* @return true
if the content type is a known XML-based content type, otherwise, false
*/
public static boolean isXml(String contentType) {
return RIConstants.XHTML_CONTENT_TYPE.equals(contentType) || RIConstants.APPLICATION_XML_CONTENT_TYPE.equals(contentType)
|| RIConstants.TEXT_XML_CONTENT_TYPE.equals(contentType);
}
// --------------------------------------------------------- Private Methods
/**
* @param component the UIComponent in question
* @return true
if the component is within the jakarta.faces.component
or
* jakarta.faces.component.html
packages, otherwise return false
*/
private static boolean canBeOptimized(UIComponent component, Map> behaviors) {
assert component != null;
assert behaviors != null;
String name = component.getClass().getName();
if (name != null && name.startsWith(OPTIMIZED_PACKAGE)) {
// If we've got behaviors attached to multiple events
// it is difficult to optimize, so fall back to the
// non-optimized code path. Behaviors attached to
// multiple event handlers should be a fairly rare case.
return behaviors.size() < 2;
}
return false;
}
/**
*
* For each attribute in setAttributes
, perform a binary search against the array of
* knownAttributes
If a match is found and the value is not null
, render the attribute.
*
* @param context the {@link FacesContext} of the current request
* @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 List
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 renderPassThruAttributesOptimized(FacesContext context, ResponseWriter writer, UIComponent component, Attribute[] knownAttributes,
List setAttributes, Map> behaviors) throws IOException {
// We should only come in here if we've got zero or one behavior event
assert behaviors != null && behaviors.size() < 2;
String behaviorEventName = getSingleBehaviorEventName(behaviors);
boolean renderedBehavior = false;
Collections.sort(setAttributes);
boolean isXhtml = RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType());
Map attrMap = component.getAttributes();
for (String name : setAttributes) {
// Note that this search can be optimized by switching from
// an array to a Map. This would change
// the search time from O(log n) to O(1).
int index = Arrays.binarySearch(knownAttributes, Attribute.attr(name));
if (index >= 0) {
Object value = attrMap.get(name);
if (value != null && shouldRenderAttribute(value)) {
Attribute attr = knownAttributes[index];
if (isBehaviorEventAttribute(attr, behaviorEventName)) {
renderHandler(context, component, null, name, value, behaviorEventName, null, false, false);
renderedBehavior = true;
} else {
writer.writeAttribute(prefixAttribute(name, isXhtml), value, name);
}
}
}
}
// We did not render out the behavior as part of our optimized
// attribute rendering. Need to manually render it out now.
if (behaviorEventName != null && !renderedBehavior) {
// 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);
}
}
}
}
/**
*
* Loops over all known attributes and attempts to render each one.
*
* @param context the {@link FacesContext} of the current request
* @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 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> behaviors) throws IOException {
boolean isXhtml = RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType());
Map attrMap = component.getAttributes();
for (Attribute attribute : knownAttributes) {
String attrName = attribute.getName();
String[] events = attribute.getEvents();
boolean hasBehavior = events != null && events.length > 0 && behaviors.containsKey(events[0]);
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, events[0], null, false, false);
}
}
}
/**
*
* Determines if an attribute should be rendered based on the specified #attributeVal.
*
*
* @param attributeVal the attribute value
* @return true
if and only if #attributeVal is an instance of a wrapper for a primitive type and its value
* is equal to the default value for that type as given in the specification.
*/
private static boolean shouldRenderAttribute(Object attributeVal) {
if (attributeVal instanceof String) {
return true;
} else if (attributeVal instanceof Boolean && Boolean.FALSE.equals(attributeVal)) {
return false;
} else if (attributeVal instanceof Integer && (Integer) attributeVal == Integer.MIN_VALUE) {
return false;
} else if (attributeVal instanceof Double && (Double) attributeVal == Double.MIN_VALUE) {
return false;
} else if (attributeVal instanceof Character && (Character) attributeVal == Character.MIN_VALUE) {
return false;
} else if (attributeVal instanceof Float && (Float) attributeVal == Float.MIN_VALUE) {
return false;
} else if (attributeVal instanceof Short && (Short) attributeVal == Short.MIN_VALUE) {
return false;
} else if (attributeVal instanceof Byte && (Byte) attributeVal == Byte.MIN_VALUE) {
return false;
} else if (attributeVal instanceof Long && (Long) attributeVal == Long.MIN_VALUE) {
return false;
}
return true;
}
/**
*
* This method expects a List
of attribute names that are to be excluded from rendering. A
* Renderer
may include an attribute name in this list for exclusion. For example, h:link
may
* use the disabled
attribute with a value of true
. However we don't want
* disabled
passed through and rendered on the span
element as it is invalid HTML.
*
*
* @param attributeName the attribute name that is to be tested for exclusion
* @param excludedAttributes the list of attribute names that are to be excluded from rendering
* @return true
if the attribute name is not in the exclude list.
*/
private static boolean isExcludedAttribute(String attributeName, List excludedAttributes) {
if (null == excludedAttributes) {
return false;
}
if (excludedAttributes.contains(attributeName)) {
return true;
}
return false;
}
/**
*
* This method builds a two element array structure as follows: Example: Given the following accept string: text/html;
* level=1, text/plain; q=0.5 [0][0] 1 (quality is 1 if none specified) [0][1] "text" (type) [0][2] "html; level=1"
* (subtype) [0][3] 1 (level, if specified; null if not)
*
* [1][0] .5 [1][1] "text" [1][2] "plain" [1][3] (level, if specified; null if not)
*
* The array is used for comparison purposes in the findMatch method.
*
*
* @param accept An accept String
* @return an two dimensional array containing content-type/quality info
*/
private static String[][] buildTypeArrayFromString(String accept) {
// return if empty
if (accept == null || accept.length() == 0) {
return new String[0][0];
}
// some helper variables
StringBuilder typeSubType;
String type;
String subtype;
String level = null;
String quality = null;
Map appMap = FacesContext.getCurrentInstance().getExternalContext().getApplicationMap();
// Parse "types"
String[] types = Util.split(appMap, accept, CONTENT_TYPE_DELIMITER);
String[][] arrayAccept = new String[types.length][MAX_CONTENT_TYPE_PARTS];
int index = -1;
for (int i = 0; i < types.length; i++) {
String token = types[i].trim();
index += 1;
// Check to see if our accept string contains the delimiter that is used
// to add uniqueness to a type/subtype, and/or delimits a qualifier value:
// Example: text/html;level=1,text/html;level=2; q=.5
if (token.contains(";")) {
String[] typeParts = Util.split(appMap, token, ";");
typeSubType = new StringBuilder(typeParts[0].trim());
for (int j = 1; j < typeParts.length; j++) {
quality = "not set";
token = typeParts[j].trim();
// if "level" is present, make sure it gets included in the "type/subtype"
if (token.contains("level")) {
typeSubType.append(';').append(token);
String[] levelParts = Util.split(appMap, token, "=");
level = levelParts[0].trim();
if (level.equalsIgnoreCase("level")) {
level = levelParts[1].trim();
}
} else {
quality = token;
String[] qualityParts = Util.split(appMap, quality, "=");
quality = qualityParts[0].trim();
if (quality.equalsIgnoreCase("q")) {
quality = qualityParts[1].trim();
break;
} else {
quality = "not set"; // to identifiy that no quality was supplied
}
}
}
} else {
typeSubType = new StringBuilder(token);
quality = "not set"; // to identifiy that no quality was supplied
}
// now split type and subtype
if (typeSubType.indexOf(CONTENT_TYPE_SUBTYPE_DELIMITER) >= 0) {
String[] typeSubTypeParts = Util.split(appMap, typeSubType.toString(), CONTENT_TYPE_SUBTYPE_DELIMITER);
// Apparently there are user-agents that send invalid
// Accept headers containing no subtype (i.e. text/).
// For those cases, assume "*" for the subtype.
if (typeSubTypeParts.length == 1) {
type = typeSubTypeParts[0].trim();
subtype = "*";
} else if (typeSubTypeParts.length == 0) {
type = typeSubType.toString();
subtype = "";
} else {
type = typeSubTypeParts[0].trim();
subtype = typeSubTypeParts[1].trim();
}
} else {
type = typeSubType.toString();
subtype = "";
}
// check quality and assign values
if ("not set".equals(quality)) {
if (type.equals("*") && subtype.equals("*")) {
quality = "0.01";
} else if (!type.equals("*") && subtype.equals("*")) {
quality = "0.02";
} else if (type.equals("*") && subtype.length() == 0) {
quality = "0.01";
} else {
quality = "1";
}
}
arrayAccept[index][0] = quality;
arrayAccept[index][1] = type;
arrayAccept[index][2] = subtype;
arrayAccept[index][3] = level;
}
return arrayAccept;
}
/**
*
* For each server supported type, compare client (browser) specified types. If a match is found, keep track of the
* highest quality factor. The end result is that for all matches, only the one with the highest quality will be
* returned.
*
*
* @param clientContentTypes An array
of accept String
information for the client built
* from @{link #buildTypeArrayFromString}.
* @param serverSupportedContentTypes An array
of accept String
information for the server
* supported types built from @{link #buildTypeArrayFromString}.
* @param preferredContentType An array
of preferred content type information.
* @return An array
containing the parts of the preferred content type for the client. The information is
* stored as outlined in @{link #buildTypeArrayFromString}.
*/
private static String[][] findMatch(String[][] clientContentTypes, String[][] serverSupportedContentTypes, String[][] preferredContentType) {
List resultList = new ArrayList<>(serverSupportedContentTypes.length);
// the highest quality
double highestQFactor = 0;
// the record with the highest quality
int idx = 0;
for (int sidx = 0, slen = serverSupportedContentTypes.length; sidx < slen; sidx++) {
// get server type
String serverType = serverSupportedContentTypes[sidx][1];
if (serverType != null) {
for (int cidx = 0, clen = clientContentTypes.length; cidx < clen; cidx++) {
// get browser type
String browserType = clientContentTypes[cidx][1];
if (browserType != null) {
// compare them and check for wildcard
if (browserType.equalsIgnoreCase(serverType) || browserType.equals("*")) {
// types are equal or browser type is wildcard - compare subtypes
if (clientContentTypes[cidx][2].equalsIgnoreCase(serverSupportedContentTypes[sidx][2])
|| clientContentTypes[cidx][2].equals("*")) {
// subtypes are equal or browser subtype is wildcard
// found match: multiplicate qualities and add to result array
// if there was a level associated, this gets higher precedence, so
// factor in the level in the calculation.
double cLevel = 0.0;
double sLevel = 0.0;
if (clientContentTypes[cidx][3] != null) {
cLevel = Double.parseDouble(clientContentTypes[cidx][3]) * .10;
}
if (serverSupportedContentTypes[sidx][3] != null) {
sLevel = Double.parseDouble(serverSupportedContentTypes[sidx][3]) * .10;
}
double cQfactor = Double.parseDouble(clientContentTypes[cidx][0]) + cLevel;
double sQfactor = Double.parseDouble(serverSupportedContentTypes[sidx][0]) + sLevel;
double resultQuality = cQfactor * sQfactor;
String[] curResult = new String[MAX_CONTENT_TYPE_PARTS];
resultList.add(curResult);
curResult[0] = String.valueOf(resultQuality);
if (clientContentTypes[cidx][2].equals("*")) {
// browser subtype is wildcard
// return type and subtype (wildcard)
curResult[1] = clientContentTypes[cidx][1];
curResult[2] = clientContentTypes[cidx][2];
} else {
// return server type and subtype
curResult[1] = serverSupportedContentTypes[sidx][1];
curResult[2] = serverSupportedContentTypes[sidx][2];
curResult[3] = serverSupportedContentTypes[sidx][3];
}
// check if this was the highest factor
if (resultQuality > highestQFactor) {
idx = resultList.size() - 1;
highestQFactor = resultQuality;
}
}
}
}
}
}
}
// First, determine if we have a type that has the highest quality factor that
// also matches the preferred type (if there is one):
String[][] match = new String[1][3];
if (preferredContentType.length != 0 && preferredContentType[0][0] != null) {
BigDecimal highestQual = BigDecimal.valueOf(highestQFactor);
for (int i = 0, len = resultList.size(); i < len; i++) {
String[] result = resultList.get(i);
if (BigDecimal.valueOf(Double.parseDouble(result[0])).compareTo(highestQual) == 0 && result[1].equals(preferredContentType[0][1])
&& result[2].equals(preferredContentType[0][2])) {
match[0][0] = result[0];
match[0][1] = result[1];
match[0][2] = result[2];
return match;
}
}
}
if (!resultList.isEmpty()) {
String[] fallBack = resultList.get(idx);
match[0][0] = fallBack[0];
match[0][1] = fallBack[1];
match[0][2] = fallBack[2];
}
return match;
}
/**
*
* Replaces all occurrences of -
with $_
.
*
*
* @param origIdentifier the original identifer that needs to be 'ECMA-ized'
* @return an ECMA valid identifer
*/
public static String createValidECMAIdentifier(String origIdentifier) {
return origIdentifier.replace("-", "$_");
}
private static UIComponent createFacesJs() {
UIOutput output = new UIOutput();
output.setRendererType("jakarta.faces.resource.Script");
output.getAttributes().put("name", FACES_SCRIPT_RESOURCE_NAME);
output.getAttributes().put("library", FACES_SCRIPT_LIBRARY_NAME);
return output;
}
/**
*
* Only install the Faces script resource if it doesn't exist. The resource component will be installed with the target
* "head".
*
* @param context the FacesContext
for the current request
*/
public static void installFacesJsIfNecessary(FacesContext context) {
if (isFacesJsInstalled(context)) {
return;
}
ResourceHandler resourceHandler = context.getApplication().getResourceHandler();
if (resourceHandler.isResourceRendered(context, FACES_SCRIPT_RESOURCE_NAME, FACES_SCRIPT_LIBRARY_NAME)) {
return;
}
context.getViewRoot().addComponentResource(context, createFacesJs(), "head");
}
/**
*
* Renders the Javascript necessary to add and remove request parameters to the current form.
*
*
* @param context the FacesContext
for the current request
* @throws java.io.IOException if an error occurs writing to the response
*/
public static void renderFacesJsIfNecessary(FacesContext context) throws IOException {
if (isFacesJsInstalled(context)) {
return;
}
ResourceHandler resourceHandler = context.getApplication().getResourceHandler();
if (resourceHandler.isResourceRendered(context, FACES_SCRIPT_RESOURCE_NAME, FACES_SCRIPT_LIBRARY_NAME)) {
return;
}
// Since we've now determined that it's not in the page, we need to manually render it.
createFacesJs().encodeAll(context);
resourceHandler.markResourceRendered(context, FACES_SCRIPT_RESOURCE_NAME, FACES_SCRIPT_LIBRARY_NAME);
}
public static boolean isFacesJsInstalled(FacesContext context) {
if (RequestStateManager.containsKey(context, RequestStateManager.SCRIPT_STATE)) {
return true;
}
UIViewRoot viewRoot = context.getViewRoot();
for (UIComponent resource : viewRoot.getComponentResources(context)) {
Object name = resource.getAttributes().get("name");
Object library = resource.getAttributes().get("library");
if (FACES_SCRIPT_RESOURCE_NAME.equals(name) && FACES_SCRIPT_LIBRARY_NAME.equals(library)) {
RequestStateManager.set(context, RequestStateManager.SCRIPT_STATE, true);
return true;
}
}
return false;
}
public static void renderUnhandledMessages(FacesContext ctx) {
if (ctx.isProjectStage(ProjectStage.Development)) {
Application app = ctx.getApplication();
HtmlMessages messages = (HtmlMessages) app.createComponent(HtmlMessages.COMPONENT_TYPE);
messages.setId(DEVELOPMENT_STAGE_MESSAGES_ID);
Renderer messagesRenderer = ctx.getRenderKit().getRenderer(HtmlMessages.COMPONENT_FAMILY, "jakarta.faces.Messages");
messages.setErrorStyle("Color: red");
messages.setWarnStyle("Color: orange");
messages.setInfoStyle("Color: blue");
messages.setFatalStyle("Color: red");
messages.setTooltip(true);
messages.setTitle("Project Stage[Development]: Unhandled Messages");
messages.setRedisplay(false);
try {
messagesRenderer.encodeBegin(ctx, messages);
messagesRenderer.encodeEnd(ctx, messages);
} catch (IOException ioe) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, ioe.toString(), ioe);
}
}
} else {
Iterator clientIds = ctx.getClientIdsWithMessages();
int messageCount = 0;
if (clientIds.hasNext()) {
// Display each message possibly not displayed.
StringBuilder builder = new StringBuilder();
while (clientIds.hasNext()) {
String clientId = clientIds.next();
Iterator messages = ctx.getMessages(clientId);
while (messages.hasNext()) {
FacesMessage message = messages.next();
if (message.isRendered()) {
continue;
}
messageCount++;
builder.append("\n");
builder.append("sourceId=").append(clientId);
builder.append("[severity=(").append(message.getSeverity());
builder.append("), summary=(").append(message.getSummary());
builder.append("), detail=(").append(message.getDetail()).append(")]");
}
}
if (messageCount > 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "faces.non_displayed_message", builder.toString());
}
}
}
}
}
public static void renderHtmlErrorPage(FacesContext ctx, FacesException fe) {
ExternalContext extContext = ctx.getExternalContext();
if (!extContext.isResponseCommitted()) {
extContext.setResponseContentType("text/html; charset=UTF-8");
try {
Writer w = extContext.getResponseOutputWriter();
if (ctx.isProjectStage(ProjectStage.Development)) {
DevTools.debugHtml(w, ctx, fe.getCause());
} else {
w.write("Please see your server log for the actual error");
}
w.flush();
} catch (IOException ioe) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, "Unable to generate Facelets error page.", ioe);
}
}
ctx.responseComplete();
} else {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "faces.facelets.error.page.response.committed");
}
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, fe.toString(), fe);
}
}
}
// Check the request parameters to see whether an action event has
// been triggered either via faces.ajax.request() or via a submitting
// behavior.
public static boolean isPartialOrBehaviorAction(FacesContext context, String clientId) {
if (clientId == null || clientId.length() == 0) {
return false;
}
String source = BEHAVIOR_SOURCE_PARAM.getValue(context);
if (!clientId.equals(source)) {
return false;
}
// First check for a Behavior action event.
String behaviorEvent = BEHAVIOR_EVENT_PARAM.getValue(context);
if (null != behaviorEvent) {
return "action".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);
}
/**
*
* Utility method to return the client ID of the parent form.
*
*
* @param component typically a command component
* @param context the FacesContext
for the current request
*
* @return the client ID of the parent form, if any
*/
public static String getFormClientId(UIComponent component, FacesContext context) {
UIForm form = getForm(component, context);
if (form != null) {
return form.getClientId(context);
}
return null;
}
/**
*
* Utility method to return the client ID of the parent form.
*
*
* @param component typically a command component
* @param context the FacesContext
for the current request
*
* @return the parent form, if any
*/
public static UIForm getForm(UIComponent component, FacesContext context) {
UIComponent parent = component.getParent();
while (parent != null) {
if (parent instanceof UIForm) {
break;
}
parent = parent.getParent();
}
UIForm form = (UIForm) parent;
if (form != null) {
return form;
}
return null;
}
/**
*
* Determine the path value of an image value for a component such as UIGraphic or UICommand.
*
*
* @param context the {@link FacesContext} for the current request.
* @param component the component to obtain the image information from
* @param attrName the attribute name that needs to be queried if the name and library attributes are not specified
*
* @return the encoded path to the image source
*/
public static String getImageSource(FacesContext context, UIComponent component, String attrName) {
String resName = (String) component.getAttributes().get("name");
ResourceHandler handler = context.getApplication().getResourceHandler();
if (resName != null) {
String libName = (String) component.getAttributes().get("library");
if (libName == null && ApplicationAssociate.getInstance(context).getResourceManager().isContractsResource(resName)) {
if (context.isProjectStage(ProjectStage.Development)) {
String msg = "Illegal path, direct contract references are not allowed: " + resName;
context.addMessage(component.getClientId(context), new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
}
return "RES_NOT_FOUND";
}
Resource res = handler.createResource(resName, libName);
if (res == null) {
if (context.isProjectStage(ProjectStage.Development)) {
String msg = "Unable to find resource " + (libName == null ? "" : libName + ", ") + resName;
context.addMessage(component.getClientId(context), new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
}
return "RES_NOT_FOUND";
} else {
String requestPath = res.getRequestPath();
return context.getExternalContext().encodeResourceURL(requestPath);
}
} else {
String value = (String) component.getAttributes().get(attrName);
if (value == null || value.length() == 0) {
return "";
}
WebConfiguration webConfig = WebConfiguration.getInstance();
if (value.startsWith(webConfig.getOptionValue(WebConfiguration.WebContextInitParameter.WebAppContractsDirectory))) {
if (context.isProjectStage(ProjectStage.Development)) {
String msg = "Illegal path, direct contract references are not allowed: " + value;
context.addMessage(component.getClientId(context), new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
}
return "RES_NOT_FOUND";
}
if (handler.isResourceURL(value)) {
return value;
} else {
value = context.getApplication().getViewHandler().getResourceURL(context, value);
return context.getExternalContext().encodeResourceURL(value);
}
}
}
/**
* If view root is instance of naming container, prepend its container client id to namespace given parameter name.
*
* @param context Involved faces context.
* @param name Request parameter name.
* @return The request parameter name, if necessary namespaced.
*/
public static String getParameterName(FacesContext context, String name) {
return Util.getNamingContainerPrefix(context) + name;
}
/**
* Returns true
if the view root associated with the given faces context will be rendered with a HTML5 doctype.
* @param context Involved faces context.
* @return true
if the view root associated with the given faces context will be rendered with a HTML5 doctype.
*/
public static boolean isOutputHtml5Doctype(FacesContext context) {
UIViewRoot viewRoot = context.getViewRoot();
if (viewRoot == null) {
return false;
}
Doctype doctype = viewRoot.getDoctype();
if (doctype == null) {
return false;
}
return "html".equalsIgnoreCase(doctype.getRootElement())
&& doctype.getPublic() == null
&& doctype.getSystem() == null;
}
// --------------------------------------------------------- Private Methods
// Appends a script to a faces.util.chain() call
private static void appendScriptToChain(StringBuilder builder, String script) {
if (script == null || script.length() == 0) {
return;
}
if (builder.length() == 0) {
builder.append("faces.util.chain(this,event,");
}
if (builder.charAt(builder.length() - 1) != ',') {
builder.append(',');
}
appendQuotedValue(builder, script);
}
// Appends an name/value property pair to a JSON object. Assumes
// object has already been opened by the caller. The value will
// be quoted (ie. wrapped in single quotes and escaped appropriately).
public static void appendProperty(StringBuilder builder, String name, Object value) {
appendProperty(builder, name, value, true);
}
// Appends an name/value property pair to a JSON object. Assumes
// object has already been opened by the caller.
public static void appendProperty(StringBuilder builder, String name, Object value, boolean quoteValue) {
if (null == name || name.length() == 0) {
throw new IllegalArgumentException();
}
char lastChar = builder.charAt(builder.length() - 1);
if (lastChar != ',' && lastChar != '{') {
builder.append(',');
}
RenderKitUtils.appendQuotedValue(builder, name);
builder.append(":");
if (value == null) {
builder.append("''");
} else if (quoteValue) {
RenderKitUtils.appendQuotedValue(builder, value.toString());
} else {
builder.append(value.toString());
}
}
// Append a script to the chain, escaping any single quotes, since
// our script content is itself nested within single quotes.
public static void appendQuotedValue(StringBuilder builder, String script) {
builder.append("'");
int length = script.length();
for (int i = 0; i < length; i++) {
char c = script.charAt(i);
if (c == '\'' || c == '\\') {
builder.append('\\');
}
builder.append(c);
}
builder.append("'");
}
// Appends one or more behavior scripts a faces.util.chain() call
private static boolean appendBehaviorsToChain(StringBuilder builder, FacesContext context, UIComponent component, List behaviors,
String behaviorEventName, Collection params) {
if (behaviors == null || behaviors.isEmpty()) {
return false;
}
ClientBehaviorContext bContext = createClientBehaviorContext(context, component, behaviorEventName, params);
boolean submitting = false;
for (ClientBehavior behavior : behaviors) {
String script = behavior.getScript(bContext);
if (script != null && script.length() > 0) {
appendScriptToChain(builder, script);
if (isSubmitting(behavior)) {
submitting = true;
}
}
}
return submitting;
}
// Given a behaviors Map with a single entry, returns the event name
// for that entry. Or, if no entries, returns null. Used by
// renderPassThruAttributesOptimized.
private static String getSingleBehaviorEventName(Map> behaviors) {
assert behaviors != null;
int size = behaviors.size();
if (size == 0) {
return null;
}
// If we made it this far, we should have a single
// entry in the behaviors map.
assert size == 1;
Iterator keys = behaviors.keySet().iterator();
assert keys.hasNext();
return keys.next();
}
// Tests whether the specified Attribute matches to specified
// behavior event name. Used by renderPassThruAttributesOptimized.
private static boolean isBehaviorEventAttribute(Attribute attr, String behaviorEventName) {
String[] events = attr.getEvents();
return behaviorEventName != null && events != null && events.length > 0 && behaviorEventName.equals(events[0]);
}
// Ensures that the user-specified DOM event handler script
// is non-empty, and trimmed if necessary.
private static String getNonEmptyUserHandler(Object handlerObject) {
String handler = null;
if (null != handlerObject) {
handler = handlerObject.toString();
handler = handler.trim();
if (handler.length() == 0) {
handler = null;
}
}
return handler;
}
// Returns the Behaviors for the specified component/event name,
// or null if no Behaviors are available
private static List getClientBehaviors(UIComponent component, String behaviorEventName) {
if (component instanceof ClientBehaviorHolder) {
ClientBehaviorHolder bHolder = (ClientBehaviorHolder) component;
Map> behaviors = bHolder.getClientBehaviors();
if (null != behaviors) {
return behaviors.get(behaviorEventName);
}
}
return null;
}
// Returns a submit handler - ie. a script that calls
// mojara.cljs()
private static String getSubmitHandler(FacesContext context, UIComponent component, Collection params, String submitTarget,
boolean preventDefault) {
StringBuilder builder = new StringBuilder(256);
String formClientId = getFormClientId(component, context);
String componentClientId = component.getClientId(context);
builder.append("mojarra.cljs(document.getElementById('");
builder.append(formClientId);
builder.append("'),{");
appendProperty(builder, componentClientId, componentClientId);
if (null != params && !params.isEmpty()) {
for (ClientBehaviorContext.Parameter param : params) {
appendProperty(builder, getParameterName(context, param.getName()), param.getValue());
}
}
builder.append("},'");
if (submitTarget != null) {
builder.append(submitTarget);
}
builder.append("')");
if (preventDefault) {
builder.append(";return false");
}
return builder.toString();
}
// Chains together a number of Behavior scripts with a user handler
// script.
private static String getChainedHandler(FacesContext context, UIComponent component, List behaviors,
Collection params, String behaviorEventName, String userHandler, String submitTarget, boolean needsSubmit) {
// Hard to pre-compute builder initial capacity
StringBuilder builder = new StringBuilder(100);
appendScriptToChain(builder, userHandler);
boolean submitting = appendBehaviorsToChain(builder, context, component, behaviors, behaviorEventName, params);
boolean hasParams = null != params && !params.isEmpty();
// If we've got parameters but we didn't render a "submitting"
// behavior script, we need to explicitly render a submit script.
if (!submitting && (hasParams || needsSubmit)) {
String submitHandler = getSubmitHandler(context, component, params, submitTarget, false);
appendScriptToChain(builder, submitHandler);
// We are now submitting since we've rendered a submit script.
submitting = true;
}
if (builder.length() == 0) {
return null;
}
builder.append(")");
// 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))) {
builder.append(";return false");
}
return builder.toString();
}
// Returns the script for a single Behavior
private static String getSingleBehaviorHandler(FacesContext context, UIComponent component, ClientBehavior behavior,
Collection params, String behaviorEventName, String submitTarget, boolean needsSubmit) {
ClientBehaviorContext bContext = createClientBehaviorContext(context, component, behaviorEventName, params);
String script = behavior.getScript(bContext);
boolean preventDefault = (needsSubmit || isSubmitting(behavior)) && (component instanceof ActionSource);
if (script == null) {
if (needsSubmit) {
script = getSubmitHandler(context, component, params, submitTarget, preventDefault);
}
} else if (preventDefault) {
script = script + ";return false";
}
return script;
}
// Creates a ClientBehaviorContext with the specified properties.
private static ClientBehaviorContext createClientBehaviorContext(FacesContext context, UIComponent component, String behaviorEventName,
Collection params) {
return ClientBehaviorContext.createClientBehaviorContext(context, component, behaviorEventName, null, params);
}
// Tests whether the specified behavior is submitting
private static boolean isSubmitting(ClientBehavior behavior) {
return behavior.getHints().contains(ClientBehaviorHint.SUBMITTING);
}
/**
* Renders a handler script, which may require chaining together the user-specified event handler, any scripts required
* by attached Behaviors, and also possibly the mojarra.cljs() "submit" script.
*
* @param context the FacesContext for this request.
* @param component the UIComponent that we are rendering
* @param params any parameters that should be included by "submitting" scripts.
* @param handlerName the name of the handler attribute to render (eg. "onclick" or "ommouseover")
* @param handlerValue the user-specified value for the handler attribute
* @param behaviorEventName the name of the behavior event that corresponds to this handler (eg. "action" or
* "mouseover").
* @param needsSubmit indicates whether the mojarra.cljs() "submit" script is required by the component. Most
* components do not need this, either because they submit themselves (eg. commandButton), or because they do not
* perform submits (eg. non-command components). This flag is mainly here for the commandLink case, where we need to
* render the submit script to make the link submit.
*/
private static void renderHandler(FacesContext context, UIComponent component, Collection params, String handlerName,
Object handlerValue, String behaviorEventName, String submitTarget, boolean needsSubmit, boolean includeExec) throws IOException {
ResponseWriter writer = context.getResponseWriter();
String userHandler = getNonEmptyUserHandler(handlerValue);
List behaviors = getClientBehaviors(component, behaviorEventName);
// Don't render behavior scripts if component is disabled
if (null != behaviors && behaviors.size() > 0 && Util.componentIsDisabled(component)) {
behaviors = null;
}
if (params == null) {
params = Collections.emptyList();
}
String handler = null;
switch (getHandlerType(behaviors, params, userHandler, needsSubmit, includeExec)) {
case USER_HANDLER_ONLY:
handler = userHandler;
break;
case SINGLE_BEHAVIOR_ONLY:
handler = getSingleBehaviorHandler(context, component, behaviors.get(0), params, behaviorEventName, submitTarget, needsSubmit);
break;
case SUBMIT_ONLY:
handler = getSubmitHandler(context, component, params, submitTarget, true);
break;
case CHAIN:
handler = getChainedHandler(context, component, behaviors, params, behaviorEventName, userHandler, submitTarget, needsSubmit);
break;
default:
assert false;
}
writer.writeAttribute(handlerName, handler, null);
}
// Determines the type of handler to render based on what sorts of
// scripts we need to render/chain.
private static HandlerType getHandlerType(List behaviors, Collection params, String userHandler,
boolean needsSubmit, boolean includeExec) {
if (behaviors == null || behaviors.isEmpty()) {
// No behaviors and no params means user handler only,
// if we have a param only because of includeExec while having
// no behaviors, also, user handler only
if (params.isEmpty() && !needsSubmit || includeExec) {
return HandlerType.USER_HANDLER_ONLY;
}
// We've got params. If we've also got a user handler, we need
// to chain. Otherwise, we only render the submit script.
return userHandler == null ? HandlerType.SUBMIT_ONLY : HandlerType.CHAIN;
}
// We've got behaviors. See if we can optimize for the single
// behavior case. We can only do this if we don't have a user
// handler.
if (behaviors.size() == 1 && userHandler == null) {
ClientBehavior behavior = behaviors.get(0);
// If we've got a submitting behavior, then it will handle
// submitting the params. If not, then we need to use
// a submit script to handle the params.
if (isSubmitting(behavior) || params.isEmpty() && !needsSubmit) {
return HandlerType.SINGLE_BEHAVIOR_ONLY;
}
}
return HandlerType.CHAIN;
}
// Little utility enum that we use to identify the type of
// handler that we are going to render.
private enum HandlerType {
// Indicates that we only have a user handler - nothing else
USER_HANDLER_ONLY,
// Indicates that we only have a single behavior - no chaining
SINGLE_BEHAVIOR_ONLY,
// Indicates that we only render the mojarra.cljs() script
SUBMIT_ONLY,
// Indicates that we've got a chain
CHAIN
}
// ---------------------------------------------------------- Nested Classes
} // END RenderKitUtils