blob: 42e26199bb3524b0a8bd4838b718c6f737b20e89 [file] [log] [blame]
/*
* 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.util;
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.util.lang.StringUtils;
public class ClientBehaviorRendererUtils
{
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('}');
}
}
}
}