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(); } }