/*
* Copyright 2017 OmniFaces
*
* Licensed 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.omnifaces.component.script;
import static java.lang.Boolean.FALSE;
import static java.lang.String.format;
import static org.omnifaces.util.Components.getParams;
import static org.omnifaces.util.Components.validateHasParent;
import static org.omnifaces.util.Utils.isEmpty;
import java.io.IOException;
import java.util.regex.Pattern;
import javax.faces.application.ResourceDependencies;
import javax.faces.application.ResourceDependency;
import javax.faces.component.FacesComponent;
import javax.faces.component.UICommand;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.ActionEvent;
import javax.faces.event.PhaseId;
import org.omnifaces.component.ParamHolder;
import org.omnifaces.util.Json;
import org.omnifaces.util.State;
/**
* <p>
* The <code><o:commandScript></code> is a component based on the standard <code><h:commandXxx></code> which
* generates a JavaScript function in the global JavaScript scope which allows the end-user to execute a JSF ajax
* request by just a function call <code>functionName()</code> in the JavaScript context.
* <p>
* The <code><o:commandScript></code> component is required to be enclosed in a {@link UIForm} component. The
* <code>name</code> attribute is required and it represents the JavaScript function name. The <code>execute</code>
* and <code>render</code> attributes work exactly the same as in <code><f:ajax></code>. The <code>onbegin</code>
* and <code>oncomplete</code> attributes must represent (valid!) JavaScript code which will be executed before sending
* the ajax request and after processing the ajax response respectively. The <code>action</code>,
* <code>actionListener</code> and <code>immediate</code> attributes work exactly the same as in
* <code><h:commandXxx></code>.
* <p>
* Basic usage example of <code><o:commandScript></code> which submits the entire form on click of a plain HTML
* button:
* <pre>
* <h:form>
* <h:inputText value="#{bean.input1}" ... />
* <h:inputText value="#{bean.input2}" ... />
* <h:inputText value="#{bean.input3}" ... />
* <o:commandScript name="submitForm" action="#{bean.submit}" render="@form" />
* </h:form>
* <input type="button" value="submit" onclick="submitForm()" />
* </pre>
* <p>
* Usage example which uses the <code><o:commandScript></code> as a poll function which updates every 3 seconds:
* <pre>
* <h:form>
* <h:dataTable id="data" value="#{bean.data}" ...>...</h:dataTable>
* <o:commandScript name="updateData" action="#{bean.reloadData}" render="data" />
* </h:form>
* <h:outputScript target="body">setInterval(updateData, 3000);</h:outputScript>
* </pre>
* <p>
* The component also supports nesting of <code><f:param></code>, <code><f:actionListener></code> and
* <code><f:setPropertyActionListener></code>, exactly like as in <code><h:commandXxx></code>. The function
* also supports a JS object as argument which will then end up in the HTTP request parameter map:
* <pre>
* functionName({ name1: "value1", name2: "value2" });
* </pre>
* <p>
* With the above example, the parameters are in the action method available as follows:
* <pre>
* String name1 = Faces.getRequestParameter("name1"); // value1
* String name2 = Faces.getRequestParameter("name2"); // value2
* </pre>
* <p>
* This is much similar to PrimeFaces <code><p:remoteCommand></code>,
* expect that the <code><o:commandScript></code> uses the standard JSF ajax API instead of the PrimeFaces/jQuery ajax API.
* So it wouldn't trigger jQuery-specific event listeners, but only JSF-specific event listeners
* (e.g. <code>jsf.ajax.addOnEvent()</code> and so on).
*
* @author Bauke Scholtz
* @since 1.3
*/
@FacesComponent(CommandScript.COMPONENT_TYPE)
@ResourceDependencies({
@ResourceDependency(library="javax.faces", name="jsf.js", target="head"), // Required for jsf.ajax.request.
@ResourceDependency(library="omnifaces", name="omnifaces.js", target="head") // Specifically util.js.
})
public class CommandScript extends UICommand {
// Public constants -----------------------------------------------------------------------------------------------
public static final String COMPONENT_TYPE = "org.omnifaces.component.script.CommandScript";
// Private constants ----------------------------------------------------------------------------------------------
private static final Pattern PATTERN_NAME = Pattern.compile("[$a-z_](\\.?[$\\w])*", Pattern.CASE_INSENSITIVE);
private static final String ERROR_MISSING_NAME =
"o:commandScript 'name' attribute must be specified.";
private static final String ERROR_ILLEGAL_NAME =
"o:commandScript 'name' attribute '%s' does not represent a valid script function name.";
private static final String ERROR_UNKNOWN_CLIENTID =
"o:commandScript execute/render client ID '%s' cannot be found relative to parent NamingContainer component"
+ " with client ID '%s'.";
private enum PropertyKeys {
// Cannot be uppercased. They have to exactly match the attribute names.
name, execute, render, onbegin, oncomplete, autorun;
}
// Variables ------------------------------------------------------------------------------------------------------
private final State state = new State(getStateHelper());
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Constructs the CommandScript component.
*/
public CommandScript() {
setRendererType(null);
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* Returns {@link ScriptFamily#COMPONENT_FAMILY}.
*/
@Override
public String getFamily() {
return ScriptFamily.COMPONENT_FAMILY;
}
/**
* If this command script was invoked, queue the {@link ActionEvent} accordingly.
*/
@Override
public void decode(FacesContext context) {
String source = context.getExternalContext().getRequestParameterMap().get("javax.faces.source");
if (getClientId(context).equals(source)) {
ActionEvent event = new ActionEvent(this);
event.setPhaseId(isImmediate() ? PhaseId.APPLY_REQUEST_VALUES : PhaseId.INVOKE_APPLICATION);
queueEvent(event);
}
}
/**
* Write a <code><span><script></code> with therein the script function which allows the end-user to
* execute a JSF ajax request by a just script function call <code>functionName()</code> in the JavaScript context.
* @throws IllegalStateException When there is no parent form.
* @throws IllegalArgumentException When the <code>name</code> attribute is missing, or when the <code>name</code>
* attribute does not represent a valid script function name.
*/
@Override
public void encodeBegin(FacesContext context) throws IOException {
validateHasParent(this, UIForm.class);
String name = getName();
if (name == null) {
throw new IllegalArgumentException(ERROR_MISSING_NAME);
}
if (!PATTERN_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(format(ERROR_ILLEGAL_NAME, name));
}
ResponseWriter writer = context.getResponseWriter();
writer.startElement("span", this);
writer.writeAttribute("id", getClientId(context), "id");
writer.startElement("script", this);
writer.writeAttribute("type", "text/javascript", "type");
encodeFunction(context, name);
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.endElement("script");
writer.endElement("span");
}
/**
* Encode the script function. It has the following syntax:
* <pre>
* var name = function() {
* jsf.ajax.request(clientId, null, options);
* });
* </pre>
* The first argument <code>clientId</code> is the client ID of the current component which will ultimately be sent
* as <code>javax.faces.source</code> request parameter, so that the {@link #decode(FacesContext)} can properly
* intercept on it. The second argument is the event type, which is irrelevant here. The third argument is a JS
* object which holds the jsf.ajax.request options, such as additional request parameters from
* <code><f:param></code>, the values of <code>execute</code> and <code>render</code> attributes and the
* <code>onevent</code> function which contains the <code>onbegin</code> and <code>oncomplete</code> scripts.
* @param context The faces context to work with.
* @param name The script function name.
* @throws IOException When something fails at I/O level.
*/
protected void encodeFunction(FacesContext context, String name) throws IOException {
ResponseWriter writer = context.getResponseWriter();
if (!name.contains(".")) {
writer.append("var ");
}
writer.append(name).append('=').append("function(o){var o=(typeof o==='object')&&o?o:{};");
encodeOptions(context);
writer.append("jsf.ajax.request('").append(getClientId(context)).append("',null,o)}");
if (isAutorun()) {
writer.append(";OmniFaces.Util.addOnloadListener(").append(name).append(")");
}
}
/**
* Encode the JS object which holds the jsf.ajax.request options, such as additional request parameters from
* <code><f:param></code>, the values of <code>execute</code> and <code>render</code> attributes and the
* <code>onevent</code> function which contains the <code>onbegin</code> and <code>oncomplete</code> scripts.
* @param context The faces context to work with.
* @throws IOException When something fails at I/O level.
*/
protected void encodeOptions(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
for (ParamHolder param : getParams(this)) {
writer.append("o[").append(Json.encode(param.getName())).append("]=")
.append(Json.encode(param.getValue())).append(";");
}
writer.append("o['javax.faces.behavior.event']='action';");
writer.append("o.execute='").append(resolveClientIds(context, getExecute())).append("';");
writer.append("o.render='").append(resolveClientIds(context, getRender())).append("';");
encodeOneventOption(context, getOnbegin(), getOncomplete());
}
/**
* Create an option for the <code>onevent</code> function which contains the <code>onbegin</code> and
* <code>oncomplete</code> scripts. This will return <code>null</code> when no scripts are been definied.
* @param context The faces context to work with.
* @param onbegin The onbegin script.
* @param oncomplete The oncomplete script.
* @throws IOException When something fails at I/O level.
*/
protected void encodeOneventOption(FacesContext context, String onbegin, String oncomplete) throws IOException {
if (onbegin == null && oncomplete == null) {
return;
}
ResponseWriter writer = context.getResponseWriter();
writer.write("o.onevent=function(data){");
if (onbegin != null) {
writer.append("if(data.status=='begin'){").append(onbegin).append('}');
}
if (oncomplete != null) {
writer.append("if(data.status=='success'){").append(oncomplete).append('}');
}
writer.write("};");
}
/**
* Resolve the given space separated collection of relative client IDs to absolute client IDs.
* @param context The faces context to work with.
* @param relativeClientIds The space separated collection of relative client IDs to be resolved.
* @return A space separated collection of absolute client IDs, or <code>null</code> if the given relative client
* IDs is empty.
*/
protected String resolveClientIds(FacesContext context, String relativeClientIds) {
if (isEmpty(relativeClientIds)) {
return null;
}
StringBuilder absoluteClientIds = new StringBuilder();
for (String relativeClientId : relativeClientIds.split("\\s+")) {
if (absoluteClientIds.length() > 0) {
absoluteClientIds.append(' ');
}
if (relativeClientId.charAt(0) == '@') {
absoluteClientIds.append(relativeClientId);
}
else{
UIComponent found = findComponent(relativeClientId);
if (found == null) {
throw new IllegalArgumentException(
format(ERROR_UNKNOWN_CLIENTID, relativeClientId, getNamingContainer().getClientId(context)));
}
absoluteClientIds.append(found.getClientId(context));
}
}
return absoluteClientIds.toString();
}
// Attribute getters/setters --------------------------------------------------------------------------------------
/**
* Returns the script function name.
* @return The script function name.
*/
public String getName() {
return state.get(PropertyKeys.name);
}
/**
* Sets the script function name.
* @param name The script function name.
*/
public void setName(String name) {
state.put(PropertyKeys.name, name);
}
/**
* Returns a space separated string of client IDs to process on ajax request.
* @return A space separated string of client IDs to process on ajax request.
*/
public String getExecute() {
return state.get(PropertyKeys.execute, "@this");
}
/**
* Sets a space separated string of client IDs to process on ajax request.
* @param execute A space separated string of client IDs to process on ajax request.
*/
public void setExecute(String execute) {
state.put(PropertyKeys.execute, execute);
}
/**
* Returns a space separated string of client IDs to update on ajax response.
* @return A space separated string of client IDs to update on ajax response.
*/
public String getRender() {
return state.get(PropertyKeys.render, "@none");
}
/**
* Sets a space separated string of client IDs to update on ajax response.
* @param render A space separated string of client IDs to update on ajax response.
*/
public void setRender(String render) {
state.put(PropertyKeys.render, render);
}
/**
* Returns a script to execute before ajax request is fired.
* @return A script to execute before ajax request is fired.
*/
public String getOnbegin() {
return state.get(PropertyKeys.onbegin);
}
/**
* Sets a script to execute before ajax request is fired.
* @param onbegin A script to execute before ajax request is fired.
*/
public void setOnbegin(String onbegin) {
state.put(PropertyKeys.onbegin, onbegin);
}
/**
* Returns a script to execute after ajax response is processed.
* @return A script to execute after ajax response is processed.
*/
public String getOncomplete() {
return state.get(PropertyKeys.oncomplete);
}
/**
* Sets a script to execute after ajax response is processed.
* @param oncomplete A script to execute after ajax response is processed.
*/
public void setOncomplete(String oncomplete) {
state.put(PropertyKeys.oncomplete, oncomplete);
}
/**
* Returns whether the command script should automatically run inline during page load.
* @return Whether the command script should automatically run inline during page load.
* @since 2.2
*/
public boolean isAutorun() {
return state.get(PropertyKeys.autorun, FALSE);
}
/**
* Sets whether the command script should automatically run inline during page load.
* @param autorun Whether the command script should automatically run inline during page load.
* @since 2.2
*/
public void setAutorun(boolean autorun) {
state.put(PropertyKeys.autorun, autorun);
}
}