/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
package org.apache.myfaces.renderkit.html.base; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.RandomAccess; | |
import javax.faces.component.UIComponent; | |
import javax.faces.component.behavior.ClientBehavior; | |
import javax.faces.component.behavior.ClientBehaviorContext; | |
import javax.faces.component.behavior.ClientBehaviorHint; | |
import javax.faces.component.behavior.ClientBehaviorHolder; | |
import javax.faces.context.FacesContext; | |
import org.apache.myfaces.config.MyfacesConfig; | |
import org.apache.myfaces.renderkit.RendererUtils; | |
import org.apache.myfaces.renderkit.html.util.JavascriptContext; | |
import org.apache.myfaces.util.lang.StringUtils; | |
public class ClientBehaviorRendererUtils | |
{ | |
private static final boolean RENDER_AS_STRING = false; | |
public static void decodeClientBehaviors(FacesContext facesContext, UIComponent component) | |
{ | |
if (!(component instanceof ClientBehaviorHolder)) | |
{ | |
return; | |
} | |
ClientBehaviorHolder clientBehaviorHolder = (ClientBehaviorHolder) component; | |
Map<String, List<ClientBehavior>> clientBehaviors = clientBehaviorHolder.getClientBehaviors(); | |
if (clientBehaviors != null && !clientBehaviors.isEmpty()) | |
{ | |
Map<String, String> paramMap = facesContext.getExternalContext().getRequestParameterMap(); | |
String behaviorEventName = paramMap.get(ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME); | |
if (behaviorEventName != null) | |
{ | |
List<ClientBehavior> clientBehaviorList = clientBehaviors.get(behaviorEventName); | |
if (clientBehaviorList != null && !clientBehaviorList.isEmpty()) | |
{ | |
String sourceId = paramMap.get(ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME); | |
String componentClientId = component.getClientId(facesContext); | |
String clientId = sourceId; | |
if (sourceId.startsWith(componentClientId) && | |
sourceId.length() > componentClientId.length()) | |
{ | |
String item = sourceId.substring(componentClientId.length()+1); | |
// If is item it should be an integer number, otherwise it can be related to a child | |
// component, because that could conflict with the clientId naming convention. | |
if (StringUtils.isInteger(item)) | |
{ | |
clientId = componentClientId; | |
} | |
} | |
if (component.getClientId(facesContext).equals(clientId)) | |
{ | |
if (clientBehaviorList instanceof RandomAccess) | |
{ | |
for (int i = 0, size = clientBehaviorList.size(); i < size; i++) | |
{ | |
ClientBehavior clientBehavior = clientBehaviorList.get(i); | |
clientBehavior.decode(facesContext, component); | |
} | |
} | |
else | |
{ | |
for (ClientBehavior clientBehavior : clientBehaviorList) | |
{ | |
clientBehavior.decode(facesContext, component); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Checks if the given component has a behavior attachment with a given name. | |
* | |
* @param eventName the event name to be checked for | |
* @param behaviors map of behaviors attached to the component | |
* @return true if client behavior with given name is attached, false otherwise | |
*/ | |
public static boolean hasClientBehavior(String eventName, Map<String, List<ClientBehavior>> behaviors) | |
{ | |
if (behaviors == null) | |
{ | |
return false; | |
} | |
return (behaviors.get(eventName) != null); | |
} | |
public static Collection<ClientBehaviorContext.Parameter> getClientBehaviorContextParameters( | |
Map<String, String> params) | |
{ | |
if (params == null || params.isEmpty()) | |
{ | |
return null; | |
} | |
List<ClientBehaviorContext.Parameter> paramList = new ArrayList<>(params.size()); | |
for (Map.Entry<String, String> paramEntry : params.entrySet()) | |
{ | |
paramList.add(new ClientBehaviorContext.Parameter(paramEntry.getKey(), paramEntry.getValue())); | |
} | |
return paramList; | |
} | |
/** | |
* builds the chained behavior script which then can be reused | |
* in following order by the other script building parts | |
* <p/> | |
* user defined event handling script | |
* behavior script | |
* renderer default script | |
* | |
* @param eventName event name ("onclick" etc...) | |
* @param config the {@link MyfacesConfig} | |
* @param uiComponent the component which has the attachement (or should have) | |
* @param facesContext the facesContext | |
* @param params params map of params which have to be dragged into the request | |
* @return a string representation of the javascripts for the attached event behavior, | |
* an empty string if none is present | |
*/ | |
private static boolean getClientBehaviorScript(FacesContext facesContext, | |
MyfacesConfig config, | |
UIComponent uiComponent, | |
String sourceId, String eventName, | |
Map<String, List<ClientBehavior>> clientBehaviors, | |
JavascriptContext target, | |
Collection<ClientBehaviorContext.Parameter> params) | |
{ | |
if (!(uiComponent instanceof ClientBehaviorHolder)) | |
{ | |
target.append(RendererUtils.EMPTY_STRING); | |
return false; | |
} | |
boolean renderClientBehavior = clientBehaviors != null && clientBehaviors.size() > 0; | |
if (!renderClientBehavior) | |
{ | |
target.append(RendererUtils.EMPTY_STRING); | |
return false; | |
} | |
List<ClientBehavior> attachedEventBehaviors = clientBehaviors.get(eventName); | |
if (attachedEventBehaviors == null || attachedEventBehaviors.isEmpty()) | |
{ | |
target.append(RendererUtils.EMPTY_STRING); | |
return false; | |
} | |
ClientBehaviorContext context = ClientBehaviorContext | |
.createClientBehaviorContext(facesContext, uiComponent, eventName, sourceId, params); | |
boolean submitting = false; | |
// List<ClientBehavior> attachedEventBehaviors is 99% _DeltaList created in | |
// javax.faces.component.UIComponentBase.addClientBehavior | |
if (attachedEventBehaviors instanceof RandomAccess) | |
{ | |
for (int i = 0, size = attachedEventBehaviors.size(); i < size; i++) | |
{ | |
ClientBehavior clientBehavior = attachedEventBehaviors.get(i); | |
submitting = appendClientBehaviourScript(target, context, | |
submitting, i < (size -1), clientBehavior, config); | |
} | |
} | |
else | |
{ | |
Iterator<ClientBehavior> clientIterator = attachedEventBehaviors.iterator(); | |
while (clientIterator.hasNext()) | |
{ | |
ClientBehavior clientBehavior = clientIterator.next(); | |
submitting = appendClientBehaviourScript(target, context, submitting, | |
clientIterator.hasNext(), clientBehavior, config); | |
} | |
} | |
return submitting; | |
} | |
private static boolean appendClientBehaviourScript(JavascriptContext target, ClientBehaviorContext context, | |
boolean submitting, boolean hasNext, ClientBehavior clientBehavior, MyfacesConfig config) | |
{ | |
String script = clientBehavior.getScript(context); | |
// The script _can_ be null, and in fact is for <f:ajax disabled="true" /> | |
if (script != null) | |
{ | |
addFunction(script, target, config); | |
if (hasNext) | |
{ | |
target.append(", "); | |
} | |
// MYFACES-3836 If no script provided by the client behavior, ignore the | |
// submitting hint because. it is evidence the client behavior is disabled. | |
if (!submitting) | |
{ | |
submitting = clientBehavior.getHints().contains(ClientBehaviorHint.SUBMITTING); | |
} | |
} | |
return submitting; | |
} | |
public static String buildBehaviorChain(FacesContext facesContext, | |
UIComponent uiComponent, | |
String eventName, | |
Collection<ClientBehaviorContext.Parameter> params, | |
Map<String, List<ClientBehavior>> clientBehaviors, | |
String userEventCode, String serverEventCode) | |
{ | |
return buildBehaviorChain(facesContext, uiComponent, | |
null, eventName, params, | |
clientBehaviors, userEventCode, serverEventCode); | |
} | |
public static String buildBehaviorChain(FacesContext facesContext, | |
UIComponent uiComponent, | |
String sourceId, String eventName, | |
Collection<ClientBehaviorContext.Parameter> params, | |
Map<String, List<ClientBehavior>> clientBehaviors, | |
String userEventCode, String serverEventCode) | |
{ | |
MyfacesConfig config = MyfacesConfig.getCurrentInstance(facesContext); | |
List<String> functions = new ArrayList<>(3); | |
if (StringUtils.isNotBlank(userEventCode)) | |
{ | |
addFunction(userEventCode, functions, config); | |
} | |
JavascriptContext chainContext = new JavascriptContext(); | |
JavascriptContext behaviorContext = new JavascriptContext(); | |
getClientBehaviorScript(facesContext, config, uiComponent, sourceId, | |
eventName, clientBehaviors, behaviorContext, params); | |
String behaviorScript = behaviorContext.toString(); | |
if (StringUtils.isNotBlank(behaviorScript)) | |
{ | |
functions.add(behaviorScript); | |
} | |
if (StringUtils.isNotBlank(serverEventCode)) | |
{ | |
addFunction(serverEventCode, functions, config); | |
} | |
// It's possible that there are no behaviors to render. | |
// For example, if we have <f:ajax disabled="true" /> as the only behavior. | |
int size = functions.size(); | |
if (size > 0) | |
{ | |
//according to the spec jsf.util.chain has to be used to build up the | |
//behavior and scripts | |
if (sourceId == null) | |
{ | |
chainContext.append("jsf.util.chain(this, event,"); | |
} | |
else | |
{ | |
chainContext.append("jsf.util.chain(document.getElementById('" + sourceId + "'), event,"); | |
} | |
for (int i = 0; i < size; i++) | |
{ | |
if (i != 0) | |
{ | |
chainContext.append(", "); | |
} | |
chainContext.append(functions.get(i)); | |
} | |
chainContext.append(");"); | |
} | |
return chainContext.toString(); | |
} | |
public static String buildBehaviorChain(FacesContext facesContext, | |
UIComponent uiComponent, | |
String eventName1, | |
Collection<ClientBehaviorContext.Parameter> params1, | |
String eventName2, | |
Collection<ClientBehaviorContext.Parameter> params2, | |
Map<String, List<ClientBehavior>> clientBehaviors, | |
String userEventCode, | |
String serverEventCode) | |
{ | |
return buildBehaviorChain(facesContext, | |
uiComponent, null, | |
eventName1, params1, | |
eventName2, params2, | |
clientBehaviors, userEventCode, serverEventCode); | |
} | |
public static String buildBehaviorChain(FacesContext facesContext, | |
UIComponent uiComponent, | |
String sourceId, | |
String eventName1, | |
Collection<ClientBehaviorContext.Parameter> params1, | |
String eventName2, | |
Collection<ClientBehaviorContext.Parameter> params2, | |
Map<String, List<ClientBehavior>> clientBehaviors, | |
String userEventCode, | |
String serverEventCode) | |
{ | |
MyfacesConfig config = MyfacesConfig.getCurrentInstance(facesContext); | |
List<String> functions = new ArrayList<>(3); | |
if (StringUtils.isNotBlank(userEventCode)) | |
{ | |
addFunction(userEventCode, functions, config); | |
} | |
JavascriptContext chainContext = new JavascriptContext(); | |
JavascriptContext behaviorContext1 = new JavascriptContext(); | |
boolean submitting1 = getClientBehaviorScript(facesContext, config, | |
uiComponent, sourceId, eventName1, clientBehaviors, | |
behaviorContext1, params1); | |
JavascriptContext behaviorContext2 = new JavascriptContext(); | |
boolean submitting2 = getClientBehaviorScript(facesContext, config, | |
uiComponent, sourceId, eventName2, clientBehaviors, | |
behaviorContext2, params2); | |
// ClientBehaviors for both events have to be checked for the Submitting hint | |
boolean submitting = submitting1 || submitting2; | |
String behaviorScript1 = behaviorContext1.toString(); | |
if (StringUtils.isNotBlank(behaviorScript1)) | |
{ | |
functions.add(behaviorScript1); | |
} | |
String behaviorScript2 = behaviorContext2.toString(); | |
if (StringUtils.isNotBlank(behaviorScript2)) | |
{ | |
functions.add(behaviorScript2); | |
} | |
if (StringUtils.isNotBlank(serverEventCode)) | |
{ | |
addFunction(serverEventCode, functions, config); | |
} | |
// It's possible that there are no behaviors to render. For example, if we have | |
// <f:ajax disabled="true" /> as the only behavior. | |
int size = functions.size(); | |
if (size > 0) | |
{ | |
if (!submitting) | |
{ | |
chainContext.append("return "); | |
} | |
//according to the spec jsf.util.chain has to be used to build up the | |
//behavior and scripts | |
if (sourceId == null) | |
{ | |
chainContext.append("jsf.util.chain(this, event,"); | |
} | |
else | |
{ | |
chainContext.append("jsf.util.chain(document.getElementById('" + sourceId + "'), event,"); | |
} | |
for (int i = 0; i < size; i++) | |
{ | |
if (i != 0) | |
{ | |
chainContext.append(", "); | |
} | |
chainContext.append(functions.get(i)); | |
} | |
chainContext.append(");"); | |
if (submitting) | |
{ | |
chainContext.append(" return false;"); | |
} | |
} | |
return chainContext.toString(); | |
} | |
/** | |
* This function correctly escapes the given JavaScript code | |
* for the use in the jsf.util.chain() JavaScript function. | |
* It also handles double-escaping correclty. | |
* | |
* @param javaScript | |
* @return | |
*/ | |
public static String escapeJavaScriptForChain(String javaScript) | |
{ | |
StringBuilder out = null; | |
for (int pos = 0; pos < javaScript.length(); pos++) | |
{ | |
char c = javaScript.charAt(pos); | |
if (c == '\\' || c == '\'') | |
{ | |
if (out == null) | |
{ | |
out = new StringBuilder(javaScript.length() + 8); | |
if (pos > 0) | |
{ | |
out.append(javaScript, 0, pos); | |
} | |
} | |
out.append('\\'); | |
} | |
if (out != null) | |
{ | |
out.append(c); | |
} | |
} | |
if (out == null) | |
{ | |
return javaScript; | |
} | |
else | |
{ | |
return out.toString(); | |
} | |
} | |
private static void addFunction(String function, List<String> functions, MyfacesConfig config) | |
{ | |
if (StringUtils.isNotBlank(function)) | |
{ | |
// either strings or functions are allowed | |
if (config.isRenderClientBehaviorScriptsAsString()) | |
{ | |
// escape every ' in the user event code since it will be a string attribute of jsf.util.chain | |
functions.add('\'' + escapeJavaScriptForChain(function) + '\''); | |
} | |
else | |
{ | |
functions.add("function(event){" + function + "}"); | |
} | |
} | |
} | |
private static void addFunction(String function, JavascriptContext target, MyfacesConfig config) | |
{ | |
if (StringUtils.isNotBlank(function)) | |
{ | |
// either strings or functions are allowed | |
if (config.isRenderClientBehaviorScriptsAsString()) | |
{ | |
// escape every ' in the user event code since it will be a string attribute of jsf.util.chain | |
target.append('\''); | |
target.append(escapeJavaScriptForChain(function)); | |
target.append('\''); | |
} | |
else | |
{ | |
target.append("function(event){"); | |
target.append(function); | |
target.append('}'); | |
} | |
} | |
} | |
} |