package net.bootsfaces.component.ajax;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.el.MethodExpression;
import javax.faces.FacesException;
import javax.faces.component.ActionSource;
import javax.faces.component.ActionSource2;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.component.UIForm;
import javax.faces.component.UIParameter;
import javax.faces.component.behavior.AjaxBehavior;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
import javax.faces.event.FacesEvent;
import javax.faces.event.PhaseId;
import net.bootsfaces.component.commandButton.CommandButton;
import net.bootsfaces.component.navCommandLink.NavCommandLink;
import net.bootsfaces.component.navLink.NavLink;
import net.bootsfaces.component.tabView.TabView;
import net.bootsfaces.expressions.ExpressionResolver;
import net.bootsfaces.render.CoreRenderer;
public class AJAXRenderer extends CoreRenderer {
private static final Logger LOGGER = Logger.getLogger("net.bootsfaces.component.ajax.AJAXRenderer");
// local constants
public static final String BSF_EVENT_PREFIX = "BsFEvent=";
public static final String AJAX_EVENT_PREFIX = "ajax:";
public void decode(FacesContext context, UIComponent component) {
String id = component.getClientId(context);
decode(context, component, id);
}
public void decode(FacesContext context, UIComponent component, String componentId) {
if (componentIsDisabledOrReadonly(component)) {
return;
}
String source = (String) context.getExternalContext().getRequestParameterMap().get("javax.faces.source");
if (component instanceof TabView && source != null) {
for (UIComponent tab : component.getChildren()) {
String tabId = tab.getClientId().replace(":", "_") + "_tab";
if (source.equals(tabId)) {
component = tab;
componentId = tabId;
break;
}
}
}
if (source == null) {
// check for non-ajax call
if (context.getExternalContext().getRequestParameterMap().containsKey(componentId)) {
source = componentId;
}
}
if (source != null && (source.equals(componentId) || source.equals("input_"+componentId) || source.equals(componentId+"Inner"))) {
String event = context.getExternalContext().getRequestParameterMap().get("javax.faces.partial.event");
String realEvent = (String) context.getExternalContext().getRequestParameterMap().get("params");
if (null != realEvent && realEvent.startsWith(BSF_EVENT_PREFIX)) {
realEvent = realEvent.substring(BSF_EVENT_PREFIX.length());
if (!realEvent.equals(event)) {
// System.out.println("Difference between event and
// realEvent:" + event + " vs. " + realEvent
// + " Component: " + component.getClass().getSimpleName());
event = realEvent;
}
}
String nameOfGetter = "getOn" + event;
try {
Method[] methods = component.getClass().getMethods();
for (Method m : methods) {
if (m.getParameterTypes().length == 0) {
if (m.getReturnType() == String.class) {
if (m.getName().equalsIgnoreCase(nameOfGetter)) {
String jsCallback = (String) m.invoke(component);
if (jsCallback != null && jsCallback.contains(AJAX_EVENT_PREFIX)) {
if (component instanceof CommandButton && "action".equals(event)) {
component.queueEvent(new ActionEvent(component));
} else {
FacesEvent ajaxEvent = new BootsFacesAJAXEvent(
new AJAXBroadcastComponent(component), event, jsCallback);
ajaxEvent.setPhaseId(PhaseId.INVOKE_APPLICATION);
if (component instanceof ActionSource) {
if (((ActionSource) component).isImmediate())
ajaxEvent.setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
} else if (component instanceof IAJAXComponent) {
if (((IAJAXComponent) component).isImmediate())
ajaxEvent.setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
}
component.queueEvent(ajaxEvent);
}
}
break;
}
}
}
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Couldn't invoke method " + nameOfGetter);
}
if (null != event) {
UIComponentBase bb = (UIComponentBase) component;
Map<String, List<ClientBehavior>> clientBehaviors = bb.getClientBehaviors();
for (Entry<String, List<ClientBehavior>> entry : clientBehaviors.entrySet()) {
if (event.equals(entry.getKey())) {
List<ClientBehavior> value = entry.getValue();
for (ClientBehavior bh : value) {
if (bh instanceof AjaxBehavior) {
// String delay = ((AjaxBehavior)
// bh).getDelay();
bh.decode(context, component);
}
}
}
}
}
boolean addEventToQueue = false;
if (component instanceof ActionSource) {
ActionSource b = (ActionSource) component;
ActionListener[] actionListeners = b.getActionListeners();
if (null != actionListeners && actionListeners.length > 0) {
addEventToQueue = true;
}
}
if (component instanceof ActionSource2) {
MethodExpression actionExpression = ((ActionSource2) component).getActionExpression();
if (null != actionExpression) {
addEventToQueue = true;
}
}
if (addEventToQueue) {
ActionEvent ae = new ActionEvent(component);
if (component instanceof ActionSource) {
if (((ActionSource) component).isImmediate()) {
ae.setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
} else {
ae.setPhaseId(PhaseId.INVOKE_APPLICATION);
}
}
component.queueEvent(ae );
}
}
}
/**
* Public API for every input component (effectively everything except the
* command button).
*
* @param context
* @param component
* @param rw
* @param suppressAJAX replaces the AJAX request by a BsF.submitForm(), but only if there are parameters. Used by b:navCommandRenderer to implement
* and action or an actionListener instead of rendering a simple link.
* @throws IOException
*/
public static void generateBootsFacesAJAXAndJavaScript(FacesContext context, ClientBehaviorHolder component,
ResponseWriter rw, boolean suppressAJAX) throws IOException {
generateBootsFacesAJAXAndJavaScript(context, component, rw, null, null, false, suppressAJAX);
}
public static void generateBootsFacesAJAXAndJavaScript(FacesContext context, ClientBehaviorHolder component,
ResponseWriter rw, String specialEvent, String specialEventHandler, boolean isJQueryCallback, boolean suppressAJAX)
throws IOException {
boolean generatedAJAXCall = false;
Collection<String> eventNames = component.getEventNames();
Map<String, String> jQueryEvents = ((IAJAXComponent) component).getJQueryEvents();
if (null != eventNames) {
for (String keyClientBehavior : eventNames) {
if (null != jQueryEvents)
if (jQueryEvents.containsKey(keyClientBehavior))
continue;
generatedAJAXCall |= generateAJAXCallForASingleEvent(context, component, rw, null, specialEvent,
specialEventHandler, isJQueryCallback, keyClientBehavior, null, null);
}
}
if (!generatedAJAXCall) {
// should we generate AJAX nonetheless?
boolean ajax = ((IAJAXComponent) component).isAjax();
ajax |= null != ((IAJAXComponent) component).getUpdate();
if (ajax) {
// before generating an AJAX default handler, check if there's an jQuery handler that's generated later
if (null != jQueryEvents) {
Set<String> events = jQueryEvents.keySet();
for (String event : events) {
String nameOfGetter = "getOn" + event;
try {
Method[] methods = component.getClass().getMethods();
for (Method m : methods) {
if (m.getParameterTypes().length == 0) {
if (m.getReturnType() == String.class) {
if (m.getName().equalsIgnoreCase(nameOfGetter)) {
String jsCallback = (String) m.invoke(component);
if (jsCallback != null && jsCallback.contains(AJAX_EVENT_PREFIX)) {
ajax=false;
}
break;
}
}
}
}
} catch (Exception e) {
}
}
}
if (ajax) {
StringBuilder s = generateAJAXCallForClientBehavior(context, (IAJAXComponent) component,
(ClientBehavior) null);
String script = s.toString() + ";";
String defaultEvent = ((IAJAXComponent) component).getDefaultEventName();
if (component instanceof CommandButton)
if (script.length() > 0 && "click".equals(defaultEvent))
script += ";return false;";
rw.writeAttribute("on" + defaultEvent, script, null);
}
} else if (component instanceof CommandButton) {
encodeFormSubmit((UIComponent)component, rw, false);
}
else {
// b:navCommandLink doesn't submit the form, so we need to use
// AJAX
boolean generateNonAJAXCommand = false;
if (component instanceof ActionSource) {
ActionSource b = (ActionSource) component;
ActionListener[] actionListeners = b.getActionListeners();
if (null != actionListeners && actionListeners.length > 0) {
generateNonAJAXCommand = true;
}
}
if (component instanceof ActionSource2) {
MethodExpression actionExpression = ((ActionSource2) component).getActionExpression();
if (null != actionExpression) {
generateNonAJAXCommand = true;
}
}
if (generateNonAJAXCommand && component instanceof IAJAXComponent) {
// rw.writeAttribute("id", getClientId() + "_a", null);
generateOnClickHandler(context, rw, (IAJAXComponent) component, suppressAJAX);
}
}
// TODO: what about composite components?
}
}
private static void encodeFormSubmit(UIComponent component, ResponseWriter rw, boolean evenWithoutParameters) throws IOException {
String parameterList="";
List<UIComponent> children = ((UIComponent)component).getChildren();
for (UIComponent parameter: children) {
if (parameter instanceof UIParameter) {
String value=String.valueOf(((UIParameter) parameter).getValue());
String name = ((UIParameter) parameter).getName();
if (null!=value) {
parameterList += ",'" + name + "':'" + value + "'";
}
}
}
if (evenWithoutParameters || parameterList.length()>0) {
UIForm currentForm = getSurroundingForm((UIComponent)component, false);
parameterList = "'" + currentForm.getClientId() + "',{'" +component.getClientId() + "':'" + component.getClientId() + "'" + parameterList+ "}";
rw.writeAttribute("onclick", encodeClick((UIComponent) component) + "BsF.submitForm(" + parameterList + ");return false;", null);
}
}
private static void generateOnClickHandler(FacesContext context, ResponseWriter rw, IAJAXComponent component, boolean suppressAJAX)
throws IOException {
StringBuilder cJS = new StringBuilder(150); // optimize int
if (suppressAJAX) {
encodeFormSubmit((UIComponent)component, rw, true);
} else {
cJS.append(encodeClick((UIComponent)component)).append("return BsF.ajax.cb(this, event);");
}
rw.writeAttribute("onclick", cJS.toString(), null);
}
public static boolean generateAJAXCallForASingleEvent(FacesContext context, ClientBehaviorHolder component,
ResponseWriter rw, StringBuffer code, String specialEvent, String specialEventHandler, boolean isJQueryCallback,
String keyClientBehavior, StringBuilder generatedJSCode, String optionalParameterList) throws IOException {
boolean generatedAJAXCall = false;
String jsCallback = "";
String nameOfGetter = "getOn" + keyClientBehavior;
try {
Method[] methods = component.getClass().getMethods();
for (Method m : methods) {
if (m.getParameterTypes().length == 0) {
if (m.getReturnType() == String.class) {
if (m.getName().equalsIgnoreCase(nameOfGetter)) {
jsCallback = (String) m.invoke(component);
if (specialEventHandler != null && keyClientBehavior.equals(specialEvent)) {
if (null == jsCallback || jsCallback.length() == 0)
jsCallback = specialEventHandler;
else
jsCallback = jsCallback + ";javascript:" + specialEventHandler;
}
jsCallback = convertAJAXToJavascript(context, jsCallback, component, keyClientBehavior, optionalParameterList);
if (null != code) {
code.append(jsCallback);
}
if ("dragstart".equals(keyClientBehavior)) {
if (null != rw) {
rw.writeAttribute("draggable", "true", "draggable");
}
}
break;
}
}
}
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Couldn't invoke method " + nameOfGetter);
}
// TODO behaviors don't render correctly?
// SR 19.09.2015: looks a bit odd, indeed. The method generateAJAXCall()
// generates an onclick handler -
// regardless of which event we currently deal with
String script = "";
Map<String, List<ClientBehavior>> clientBehaviors = component.getClientBehaviors();
List<ClientBehavior> behaviors = clientBehaviors.get(keyClientBehavior);
if (null != behaviors) {
for (ClientBehavior cb : behaviors) {
if (cb instanceof AjaxBehavior) {
StringBuilder s = generateAJAXCallForClientBehavior(context, (IAJAXComponent) component,
(AjaxBehavior) cb);
script += s.toString() + ";";
} else if (cb.getClass().getSimpleName().equals("AjaxBehavior")) {
AjaxBehavior ab = new AjaxBehavior();
Object disabled = readBeanAttribute(cb, "isDisabled");
ab.setDisabled((Boolean) disabled);
ab.setOnerror((String) readBeanAttribute(cb, "getOnerror"));
ab.setRender((Collection<String>) readBeanAttributeAsCollection(cb, "getUpdate"));
ab.setExecute((Collection<String>) readBeanAttributeAsCollection(cb, "getProcess"));
ab.setOnevent(keyClientBehavior);
StringBuilder s = generateAJAXCallForClientBehavior(context, (IAJAXComponent) component, ab);
script += s.toString() + ";";
}
}
}
// TODO end
if (jsCallback.contains("BsF.ajax.") || script.contains("BsF.ajax.")) {
generatedAJAXCall = true;
}
if (!isJQueryCallback) {
if (jsCallback.length() > 0 || script.length() > 0) {
if (component instanceof CommandButton)
if (generatedAJAXCall && "click".equals(keyClientBehavior))
script += ";return false;";
if (null != rw) {
rw.writeAttribute("on" + keyClientBehavior, jsCallback + script, null);
}
if (null != code) {
code.append(jsCallback + script);
}
}
}
if (null != generatedJSCode) {
generatedJSCode.setLength(0);
if (jsCallback.length() > 0)
generatedJSCode.append(jsCallback);
if (script.length() > 0)
generatedJSCode.append(script);
}
return generatedAJAXCall;
}
private static Object readBeanAttribute(Object bean, String getter) {
try {
Method method = bean.getClass().getMethod(getter);
Object result = method.invoke(bean);
return result;
} catch (Exception e) {
throw new FacesException("An error occured when reading the property " + getter + " from the bean "
+ bean.getClass().getName(), e);
}
}
private static Collection<String> readBeanAttributeAsCollection(Object bean, String getter) {
Collection<String> result = null;
try {
Method method = bean.getClass().getMethod(getter);
Object value = method.invoke(bean);
if (null != value) {
String[] partials = ((String) value).split(" ");
result = new ArrayList<String>();
for (String p : partials) {
result.add(p);
}
}
return result;
} catch (Exception e) {
throw new FacesException("An error occured when reading the property " + getter + " from the bean "
+ bean.getClass().getName(), e);
}
}
private static String convertAJAXToJavascript(FacesContext context, String jsCallback,
ClientBehaviorHolder component, String event, String optionalParameterList) {
if (jsCallback == null)
jsCallback = "";
else {
if (jsCallback.contains(AJAX_EVENT_PREFIX)) {
int pos = jsCallback.indexOf(AJAX_EVENT_PREFIX);
String rest = "";
int end = jsCallback.indexOf(";javascript:", pos);
if (end >= 0) {
rest = jsCallback.substring(end + ";javascript:".length());
jsCallback = jsCallback.substring(0, end);
}
StringBuilder ajax = generateAJAXCall(context, (IAJAXComponent) component, event, optionalParameterList);
jsCallback = jsCallback.substring(0, pos) + ";" + ajax + rest;
}
if (!jsCallback.endsWith(";"))
jsCallback += ";";
}
return jsCallback;
}
public static StringBuilder generateAJAXCall(FacesContext context, IAJAXComponent component, String event, String optionalParameterList) {
String complete = component.getOncomplete();
String onError = null;
String onSuccess = null;
if (component instanceof IAJAXComponent2) {
onError = ((IAJAXComponent2) component).getOnerror();
onSuccess = ((IAJAXComponent2) component).getOnsuccess();
}
StringBuilder cJS = new StringBuilder(150);
String update = component.getUpdate();
if (null == update) {
update = "@none";
}
update = ExpressionResolver.getComponentIDs(context, (UIComponent) component, update);
String process = component.getProcess();
if (null == process) {
// see https://github.com/TheCoder4eu/BootsFaces-OSP/issues/371
process = "@all";
}
if ("@all".equals(process) || "@none".equals(process)) {
// these expressions are evaluated on the client side
} else {
process = ExpressionResolver.getComponentIDs(context, (UIComponent) component, process);
}
cJS.append("BsF.ajax.callAjax(this, event").append(",'" + update + "'").append(",'").append(process)
.append("'");
if (complete != null) {
cJS.append(",function(){" + complete + "}");
} else
cJS.append(", null");
if (onError != null) {
cJS.append(",function(){" + onError + "}");
} else
cJS.append(", null");
if (onSuccess != null) {
cJS.append(",function(){" + onSuccess + "}");
} else
cJS.append(", null");
if ((event != null) && (event.length() > 0)) {
cJS.append(", '" + event + "'");
} else
cJS.append(", null");
String parameterList="";
List<UIComponent> children = ((UIComponent)component).getChildren();
for (UIComponent parameter: children) {
if (parameter instanceof UIParameter) {
String value=String.valueOf(((UIParameter) parameter).getValue());
String name = ((UIParameter) parameter).getName();
if (null!=value) {
parameterList += ",'" + name + "':'" + value + "'";
}
}
}
if (null != optionalParameterList) {
parameterList += "," + optionalParameterList;
}
if (parameterList.length()>0) {
String json=",{" + parameterList.substring(1) + "}";
cJS.append(json);
}
cJS.append(");");
return cJS;
}
private static StringBuilder generateAJAXCallForClientBehavior(FacesContext context, IAJAXComponent component,
ClientBehavior ajaxBehavior) {
StringBuilder cJS = new StringBuilder(150);
// find default values
String update = component.getUpdate();
String oncomplete = component.getOncomplete();
String process = component.getProcess();
String onevent = "";
String onError = null;
String onSuccess = null;
if (component instanceof IAJAXComponent2) {
onError = ((IAJAXComponent2) component).getOnerror();
onSuccess = ((IAJAXComponent2) component).getOnsuccess();
}
if (ajaxBehavior != null) {
// the default values can be overridden by the AJAX behavior
if (ajaxBehavior instanceof AjaxBehavior) {
boolean disabled = ((AjaxBehavior) ajaxBehavior).isDisabled();
if (!disabled) {
// String onerror = ((AjaxBehavior)
// ajaxBehavior).getOnerror(); // todo
onevent = ((AjaxBehavior) ajaxBehavior).getOnevent();
if (onevent == null)
onevent = "";
Collection<String> execute = ((AjaxBehavior) ajaxBehavior).getExecute();
if (null != execute && (!execute.isEmpty())) {
for (String u : execute) {
if (null == process)
process = u;
else
process += " " + u;
}
} else {
process = "@this";
}
Collection<String> render = ((AjaxBehavior) ajaxBehavior).getRender();
if (null != render && (!render.isEmpty())) {
update = "";
for (String u : render) {
update += u + " ";
}
}
oncomplete = component.getOncomplete();
}
}
}
if ("@all".equals(process) || "@none".equals(process) ) {
// these expressions are evaluated on the client side
} else {
process = ExpressionResolver.getComponentIDs(context, (UIComponent) component, process);
}
if (update==null) {
update="";
} else {
update = ExpressionResolver.getComponentIDs(context, (UIComponent) component, update);
}
cJS.append(encodeClick((UIComponent)component)).append("BsF.ajax.callAjax(this, event")
.append(update == null ? ",''" : (",'" + update + "'"))
.append(process == null ? ",'@this'" : (",'" + process.trim() + "'"));
if (oncomplete != null) {
cJS.append(",function(){" + oncomplete + "}");
} else
cJS.append(",null");
if (onError != null) {
cJS.append(",function(){" + onError + "}");
} else
cJS.append(",null");
if (onSuccess != null) {
cJS.append(",function(){" + onSuccess + "}");
} else
cJS.append(",null");
if ((onevent != null) && (onevent.length() > 0)) {
cJS.append(", '" + onevent + "'");
} else {
cJS.append(",null");
}
String parameterList="";
List<UIComponent> children = ((UIComponent)component).getChildren();
for (UIComponent parameter: children) {
if (parameter instanceof UIParameter) {
String value=String.valueOf(((UIParameter) parameter).getValue());
String name = ((UIParameter) parameter).getName();
if (null!=value) {
parameterList += ",'" + name + "':'" + value + "'";
}
}
}
if (parameterList.length()>0) {
String json=",{" + parameterList.substring(1) + "}";
cJS.append(json);
}
cJS.append(");");
return cJS;
}
private static String encodeClick(UIComponent component) {
String js;
String oc=null;
if (component instanceof IAJAXComponent) {
oc = (String) ((IAJAXComponent)component).getOnclick();
}
if (component instanceof NavLink) {
oc = (String) ((NavLink)component).getOnclick();
}
if (component instanceof NavCommandLink) {
oc = (String) ((NavCommandLink)component).getOnclick();
}
if (oc != null) {
js = oc.endsWith(";") ? oc : oc + ";";
} else {
js = "";
}
return js;
}
/**
* Registers a callback with jQuery.
*
* @param context
* @param component
* @param rw
* @param jQueryExpressionOfTargetElement
* @param additionalEventHandlers
* @throws IOException
*/
public void generateBootsFacesAJAXAndJavaScriptForJQuery(FacesContext context, UIComponent component,
ResponseWriter rw, String jQueryExpressionOfTargetElement, Map<String, String> additionalEventHandlers) throws IOException {
generateBootsFacesAJAXAndJavaScriptForJQuery(context, component, rw, jQueryExpressionOfTargetElement, additionalEventHandlers, false);
}
/**
* Registers a callback with jQuery.
*
* @param context
* @param component
* @param rw
* @param jQueryExpressionOfTargetElement
* @param additionalEventHandlers
* @param attachOnReady
* @throws IOException
*/
public void generateBootsFacesAJAXAndJavaScriptForJQuery(FacesContext context, UIComponent component,
ResponseWriter rw, String jQueryExpressionOfTargetElement, Map<String, String> additionalEventHandlers, boolean attachOnReady) throws IOException {
if (jQueryExpressionOfTargetElement.contains(":")) {
if (!jQueryExpressionOfTargetElement.contains("\\\\:")) { // avoid escaping twice
jQueryExpressionOfTargetElement=jQueryExpressionOfTargetElement.replace(":", "\\\\:");
}
}
IAJAXComponent ajaxComponent = (IAJAXComponent) component;
Set<String> events = ajaxComponent.getJQueryEvents().keySet();
for (String event : events) {
StringBuilder code = new StringBuilder();
String additionalEventHandler = null;
if (null != additionalEventHandlers)
additionalEventHandler = additionalEventHandlers.get(event);
String parameterList = null;
if (null != ajaxComponent.getJQueryEventParameterListsForAjax()) {
if (null != ajaxComponent.getJQueryEventParameterListsForAjax().get(event))
parameterList = ajaxComponent.getJQueryEventParameterListsForAjax().get(event);
}
generateAJAXCallForASingleEvent(context, (ClientBehaviorHolder) component, rw, null, event,
additionalEventHandler, true, event, code, parameterList);
if (code.length() > 0) {
rw.startElement("script", component);
parameterList = "event";
if (null != ajaxComponent.getJQueryEventParameterLists()) {
if (null != ajaxComponent.getJQueryEventParameterLists().get(event))
parameterList = ajaxComponent.getJQueryEventParameterLists().get(event);
}
String js = "$('" + jQueryExpressionOfTargetElement + "').on('" + ajaxComponent.getJQueryEvents().get(event)
+ "', function(" + parameterList + "){" + code.toString() + "});";
if (attachOnReady) js = "$(function() { " + js + " })";
rw.writeText(js, null);
rw.endElement("script");
}
}
}
public String generateBootsFacesAJAXAndJavaScriptForAnMobileEvent(FacesContext context,
ClientBehaviorHolder component, ResponseWriter rw, String clientId, String event) throws IOException {
StringBuilder code = new StringBuilder();
String additionalEventHandler = null;
generateAJAXCallForASingleEvent(context, component, rw, null, event, additionalEventHandler, true, event, code, null);
return code.toString();
}
}