blob: 9bdda56772afcd4655862d8d69ad12bc933132b6 [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.wicket.ajax;
import java.time.Duration;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.Page;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.attributes.AjaxAttributeName;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.Method;
import org.apache.wicket.ajax.attributes.CallbackParameter;
import org.apache.wicket.ajax.attributes.IAjaxCallListener;
import org.apache.wicket.ajax.attributes.ThrottlingSettings;
import org.apache.wicket.ajax.json.JSONFunction;
import org.apache.wicket.ajax.json.JsonUtils;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.html.IComponentAwareHeaderContributor;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.resource.CoreLibrariesContributor;
import org.apache.wicket.util.string.Strings;
import com.github.openjson.JSONArray;
import com.github.openjson.JSONException;
import com.github.openjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.servlet.http.HttpServletRequest;
/**
* The base class for Wicket's default AJAX implementation.
*
* @since 1.2
*
* @author Igor Vaynberg (ivaynberg)
*
*/
public abstract class AbstractDefaultAjaxBehavior extends AbstractAjaxBehavior
{
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(AbstractDefaultAjaxBehavior.class);
/** reference to the default indicator gif file. */
public static final ResourceReference INDICATOR = new PackageResourceReference(
AbstractDefaultAjaxBehavior.class, "indicator.gif");
private static final String DYNAMIC_PARAMETER_FUNCTION_SIGNATURE = "function(attrs)";
private static final String PRECONDITION_FUNCTION_SIGNATURE = "function(attrs)";
private static final String COMPLETE_HANDLER_FUNCTION_SIGNATURE = "function(attrs, jqXHR, textStatus)";
private static final String FAILURE_HANDLER_FUNCTION_SIGNATURE = "function(attrs, jqXHR, errorMessage, textStatus)";
private static final String SUCCESS_HANDLER_FUNCTION_SIGNATURE = "function(attrs, jqXHR, data, textStatus)";
private static final String AFTER_HANDLER_FUNCTION_SIGNATURE = "function(attrs)";
private static final String BEFORE_SEND_HANDLER_FUNCTION_SIGNATURE = "function(attrs, jqXHR, settings)";
private static final String BEFORE_HANDLER_FUNCTION_SIGNATURE = "function(attrs)";
private static final String INIT_HANDLER_FUNCTION_SIGNATURE = "function(attrs)";
private static final String DONE_HANDLER_FUNCTION_SIGNATURE = "function(attrs)";
/**
* Subclasses should call super.onBind()
*
* @see org.apache.wicket.behavior.AbstractAjaxBehavior#onBind()
*/
@Override
protected void onBind()
{
final Component component = getComponent();
component.setOutputMarkupId(true);
if (getStatelessHint(component))
{
//generate behavior id
component.getBehaviorId(this);
}
}
/**
* @see org.apache.wicket.behavior.AbstractAjaxBehavior#renderHead(Component,
* org.apache.wicket.markup.head.IHeaderResponse)
*/
@Override
public void renderHead(final Component component, final IHeaderResponse response)
{
super.renderHead(component, response);
CoreLibrariesContributor.contributeAjax(component.getApplication(), response);
RequestCycle requestCycle = component.getRequestCycle();
Url baseUrl = requestCycle.getUrlRenderer().getBaseUrl();
CharSequence ajaxBaseUrl = Strings.escapeMarkup(baseUrl.toString());
response.render(JavaScriptHeaderItem.forScript("Wicket.Ajax.baseUrl=\"" + ajaxBaseUrl
+ "\";", "wicket-ajax-base-url"));
renderExtraHeaderContributors(component, response);
}
/**
* Renders header contribution by IAjaxCallListener instances which additionally implement
* IComponentAwareHeaderContributor interface.
*
* @param component
* the component assigned to this behavior
* @param response
* the current header response
*/
private void renderExtraHeaderContributors(final Component component,
final IHeaderResponse response)
{
AjaxRequestAttributes attributes = getAttributes();
List<IAjaxCallListener> ajaxCallListeners = attributes.getAjaxCallListeners();
for (IAjaxCallListener ajaxCallListener : ajaxCallListeners)
{
if (ajaxCallListener instanceof IComponentAwareHeaderContributor)
{
IComponentAwareHeaderContributor contributor = (IComponentAwareHeaderContributor)ajaxCallListener;
contributor.renderHead(component, response);
}
}
}
/**
* @return the Ajax settings for this behavior
* @since 6.0
*/
protected final AjaxRequestAttributes getAttributes()
{
AjaxRequestAttributes attributes = new AjaxRequestAttributes();
WebApplication application = (WebApplication)getComponent().getApplication();
AjaxRequestTargetListenerCollection ajaxRequestTargetListeners = application
.getAjaxRequestTargetListeners();
for (AjaxRequestTarget.IListener listener : ajaxRequestTargetListeners)
{
listener.updateAjaxAttributes(this, attributes);
}
updateAjaxAttributes(attributes);
return attributes;
}
/**
* This method decides whether to continue processing or to abort the Ajax request when the method
* is different than the {@link AjaxRequestAttributes#getMethod()}'s method.
*
* @return response that can either abort or continue the processing of the Ajax request
*/
protected Form.MethodMismatchResponse onMethodMismatch()
{
return Form.MethodMismatchResponse.CONTINUE;
}
/**
* Gives a chance to the specializations to modify the attributes.
*
* @param attributes
* @since 6.0
*/
protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
{
}
/**
* <pre>
* {
* u: 'editable-label?6-1.IBehaviorListener.0-text1-label', // url
* m: 'POST', // method name. Default: 'GET'
* c: 'label7', // component id (String) or window for page
* e: 'click', // event name
* sh: [], // list of success handlers
* fh: [], // list of failure handlers
* pre: [], // list of preconditions. If empty set default : Wicket.$(settings{c}) !== null
* ep: {}, // extra parameters
* async: true|false, // asynchronous XHR or not
* ch: 'someName|d', // AjaxChannel
* i: 'indicatorId', // indicator component id
* ad: true, // allow default
* }
* </pre>
*
* @param component
* the component with that behavior
* @return the attributes as string in JSON format
*/
protected final CharSequence renderAjaxAttributes(final Component component)
{
AjaxRequestAttributes attributes = getAttributes();
return renderAjaxAttributes(component, attributes);
}
/**
*
* @param component
* @param attributes
* @return the attributes as string in JSON format
*/
protected final CharSequence renderAjaxAttributes(final Component component,
AjaxRequestAttributes attributes)
{
JSONObject attributesJson = new JSONObject();
try
{
attributesJson.put(AjaxAttributeName.URL.jsonName(), getCallbackUrl());
Method method = attributes.getMethod();
if (Method.POST == method)
{
attributesJson.put(AjaxAttributeName.METHOD.jsonName(), method);
}
if (component instanceof Page == false)
{
String componentId = component.getMarkupId();
attributesJson.put(AjaxAttributeName.MARKUP_ID.jsonName(), componentId);
}
String formId = attributes.getFormId();
if (Strings.isEmpty(formId) == false)
{
attributesJson.put(AjaxAttributeName.FORM_ID.jsonName(), formId);
}
if (attributes.isMultipart())
{
attributesJson.put(AjaxAttributeName.IS_MULTIPART.jsonName(), true);
}
String submittingComponentId = attributes.getSubmittingComponentName();
if (Strings.isEmpty(submittingComponentId) == false)
{
attributesJson.put(AjaxAttributeName.SUBMITTING_COMPONENT_NAME.jsonName(),
submittingComponentId);
}
CharSequence childSelector = attributes.getChildSelector();
if (Strings.isEmpty(childSelector) == false)
{
attributesJson.put(AjaxAttributeName.CHILD_SELECTOR.jsonName(),
childSelector);
}
if (attributes.isSerializeRecursively())
{
attributesJson.put(AjaxAttributeName.SERIALIZE_RECURSIVELY.jsonName(), true);
}
String indicatorId = findIndicatorId();
if (Strings.isEmpty(indicatorId) == false)
{
attributesJson.put(AjaxAttributeName.INDICATOR_ID.jsonName(), indicatorId);
}
for (IAjaxCallListener ajaxCallListener : attributes.getAjaxCallListeners())
{
if (ajaxCallListener != null)
{
CharSequence initHandler = ajaxCallListener.getInitHandler(component);
appendListenerHandler(initHandler, attributesJson,
AjaxAttributeName.INIT_HANDLER.jsonName(),
INIT_HANDLER_FUNCTION_SIGNATURE);
CharSequence beforeHandler = ajaxCallListener.getBeforeHandler(component);
appendListenerHandler(beforeHandler, attributesJson,
AjaxAttributeName.BEFORE_HANDLER.jsonName(),
BEFORE_HANDLER_FUNCTION_SIGNATURE);
CharSequence beforeSendHandler = ajaxCallListener
.getBeforeSendHandler(component);
appendListenerHandler(beforeSendHandler, attributesJson,
AjaxAttributeName.BEFORE_SEND_HANDLER.jsonName(),
BEFORE_SEND_HANDLER_FUNCTION_SIGNATURE);
CharSequence afterHandler = ajaxCallListener.getAfterHandler(component);
appendListenerHandler(afterHandler, attributesJson,
AjaxAttributeName.AFTER_HANDLER.jsonName(), AFTER_HANDLER_FUNCTION_SIGNATURE);
CharSequence successHandler = ajaxCallListener.getSuccessHandler(component);
appendListenerHandler(successHandler, attributesJson,
AjaxAttributeName.SUCCESS_HANDLER.jsonName(),
SUCCESS_HANDLER_FUNCTION_SIGNATURE);
CharSequence failureHandler = ajaxCallListener.getFailureHandler(component);
appendListenerHandler(failureHandler, attributesJson,
AjaxAttributeName.FAILURE_HANDLER.jsonName(),
FAILURE_HANDLER_FUNCTION_SIGNATURE);
CharSequence completeHandler = ajaxCallListener.getCompleteHandler(component);
appendListenerHandler(completeHandler, attributesJson,
AjaxAttributeName.COMPLETE_HANDLER.jsonName(),
COMPLETE_HANDLER_FUNCTION_SIGNATURE);
CharSequence precondition = ajaxCallListener.getPrecondition(component);
appendListenerHandler(precondition, attributesJson,
AjaxAttributeName.PRECONDITION.jsonName(), PRECONDITION_FUNCTION_SIGNATURE);
CharSequence doneHandler = ajaxCallListener.getDoneHandler(component);
appendListenerHandler(doneHandler, attributesJson,
AjaxAttributeName.DONE_HANDLER.jsonName(),
DONE_HANDLER_FUNCTION_SIGNATURE);
}
}
JSONArray extraParameters = JsonUtils.asArray(attributes.getExtraParameters());
if (extraParameters.length() > 0)
{
attributesJson.put(AjaxAttributeName.EXTRA_PARAMETERS.jsonName(), extraParameters);
}
List<CharSequence> dynamicExtraParameters = attributes.getDynamicExtraParameters();
if (dynamicExtraParameters != null)
{
for (CharSequence dynamicExtraParameter : dynamicExtraParameters)
{
JSONFunction function = getJsonFunction(DYNAMIC_PARAMETER_FUNCTION_SIGNATURE, dynamicExtraParameter);
attributesJson.append(AjaxAttributeName.DYNAMIC_PARAMETER_FUNCTION.jsonName(),
function);
}
}
if (attributes.isAsynchronous() == false)
{
attributesJson.put(AjaxAttributeName.IS_ASYNC.jsonName(), false);
}
String[] eventNames = attributes.getEventNames();
if (eventNames.length == 1)
{
attributesJson.put(AjaxAttributeName.EVENT_NAME.jsonName(), eventNames[0]);
}
else
{
for (String eventName : eventNames)
{
attributesJson.append(AjaxAttributeName.EVENT_NAME.jsonName(), eventName);
}
}
AjaxChannel channel = attributes.getChannel();
if (channel != null && channel.equals(AjaxChannel.DEFAULT) == false)
{
attributesJson.put(AjaxAttributeName.CHANNEL.jsonName(), channel);
}
if (attributes.isPreventDefault())
{
attributesJson.put(AjaxAttributeName.IS_PREVENT_DEFAULT.jsonName(), true);
}
if (AjaxRequestAttributes.EventPropagation.STOP
.equals(attributes.getEventPropagation()))
{
attributesJson.put(AjaxAttributeName.EVENT_PROPAGATION.jsonName(), "stop");
}
else if (AjaxRequestAttributes.EventPropagation.STOP_IMMEDIATE.equals(attributes
.getEventPropagation()))
{
attributesJson.put(AjaxAttributeName.EVENT_PROPAGATION.jsonName(), "stopImmediate");
}
Duration requestTimeout = attributes.getRequestTimeout();
if (requestTimeout != null)
{
attributesJson.put(AjaxAttributeName.REQUEST_TIMEOUT.jsonName(),
requestTimeout.toMillis());
}
boolean wicketAjaxResponse = attributes.isWicketAjaxResponse();
if (wicketAjaxResponse == false)
{
attributesJson.put(AjaxAttributeName.IS_WICKET_AJAX_RESPONSE.jsonName(), false);
}
String dataType = attributes.getDataType();
if (AjaxRequestAttributes.XML_DATA_TYPE.equals(dataType) == false)
{
attributesJson.put(AjaxAttributeName.DATATYPE.jsonName(), dataType);
}
ThrottlingSettings throttlingSettings = attributes.getThrottlingSettings();
if (throttlingSettings != null)
{
JSONObject throttlingSettingsJson = new JSONObject();
String throttleId = throttlingSettings.getId();
if (throttleId == null)
{
throttleId = component.getMarkupId();
}
throttlingSettingsJson.put(AjaxAttributeName.THROTTLING_ID.jsonName(), throttleId);
throttlingSettingsJson.put(AjaxAttributeName.THROTTLING_DELAY.jsonName(),
throttlingSettings.getDelay().toMillis());
if (throttlingSettings.getPostponeTimerOnUpdate())
{
throttlingSettingsJson.put(
AjaxAttributeName.THROTTLING_POSTPONE_ON_UPDATE.jsonName(), true);
}
attributesJson.put(AjaxAttributeName.THROTTLING.jsonName(), throttlingSettingsJson);
}
postprocessConfiguration(attributesJson, component);
}
catch (JSONException e)
{
throw new WicketRuntimeException(e);
}
String attributesAsJson = attributesJson.toString();
return attributesAsJson;
}
private void appendListenerHandler(final CharSequence handler, final JSONObject attributesJson,
final String propertyName, final String signature) throws JSONException
{
if (Strings.isEmpty(handler) == false)
{
final JSONFunction function;
if (handler instanceof JSONFunction)
{
function = (JSONFunction)handler;
}
else
{
function = getJsonFunction(signature, handler);
}
attributesJson.append(propertyName, function);
}
}
private JSONFunction getJsonFunction(String signature, CharSequence body) {
String func = signature + "{" + body + "}";
return new JSONFunction(func);
}
/**
* Gives a chance to modify the JSON attributesJson that is going to be used as attributes for
* the Ajax call.
*
* @param attributesJson
* the JSON object created by #renderAjaxAttributes()
* @param component
* the component with the attached Ajax behavior
* @throws JSONException
* thrown if an error occurs while modifying {@literal attributesJson} argument
*/
protected void postprocessConfiguration(JSONObject attributesJson, Component component)
throws JSONException
{
}
/**
* @return javascript that will generate an ajax GET request to this behavior with its assigned
* component
*/
public CharSequence getCallbackScript()
{
return getCallbackScript(getComponent());
}
/**
* @param component
* the component to use when generating the attributes
* @return script that can be used to execute this Ajax behavior.
*/
// 'protected' because this method is intended to be called by other Behavior methods which
// accept the component as parameter
protected CharSequence getCallbackScript(final Component component)
{
CharSequence ajaxAttributes = renderAjaxAttributes(component);
return "Wicket.Ajax.ajax(" + ajaxAttributes + ");";
}
/**
* Generates a javascript function that can take parameters and performs an AJAX call which
* includes these parameters. The generated code looks like this:
*
* <pre>
* function(param1, param2) {
* var attrs = attrsJson;
* var params = {'param1': param1, 'param2': param2};
* attrs.ep = jQuery.extend(attrs.ep, params);
* Wicket.Ajax.ajax(attrs);
* }
* </pre>
*
* @param extraParameters
* @return A function that can be used as a callback function in javascript
*/
public CharSequence getCallbackFunction(CallbackParameter... extraParameters)
{
StringBuilder sb = new StringBuilder();
sb.append("function (");
boolean first = true;
for (CallbackParameter curExtraParameter : extraParameters)
{
if (curExtraParameter.getFunctionParameterName() != null)
{
if (!first)
sb.append(',');
else
first = false;
sb.append(curExtraParameter.getFunctionParameterName());
}
}
sb.append(") {\n");
sb.append(getCallbackFunctionBody(extraParameters));
sb.append("}\n");
return sb;
}
/**
* Generates the body the {@linkplain #getCallbackFunction(CallbackParameter...) callback
* function}. To embed this code directly into a piece of javascript, make sure any context
* parameters are available as local variables, global variables or within the closure.
*
* @param extraParameters
* @return The body of the {@linkplain #getCallbackFunction(CallbackParameter...) callback
* function}.
*/
public CharSequence getCallbackFunctionBody(CallbackParameter... extraParameters)
{
AjaxRequestAttributes attributes = getAttributes();
attributes.setEventNames();
CharSequence attrsJson = renderAjaxAttributes(getComponent(), attributes);
StringBuilder sb = new StringBuilder();
sb.append("var attrs = ");
sb.append(attrsJson);
sb.append(";\n");
JSONArray jsonArray = new JSONArray();
for (CallbackParameter curExtraParameter : extraParameters)
{
if (curExtraParameter.getAjaxParameterName() != null)
{
try
{
JSONObject object = new JSONObject();
object.put("name", curExtraParameter.getAjaxParameterName());
object.put("value", new JSONFunction(curExtraParameter.getAjaxParameterCode()));
jsonArray.put(object);
}
catch (JSONException e)
{
throw new WicketRuntimeException(e);
}
}
}
sb.append("var params = ").append(jsonArray).append(";\n");
sb.append("attrs.").append(AjaxAttributeName.EXTRA_PARAMETERS)
.append(" = params.concat(attrs.")
.append(AjaxAttributeName.EXTRA_PARAMETERS).append(" || []);\n");
sb.append("Wicket.Ajax.ajax(attrs);\n");
return sb;
}
/**
* Finds the markup id of the indicator. The default search order is: component, behavior,
* component's parent hierarchy.
*
* @return markup id or <code>null</code> if no indicator found
*/
protected String findIndicatorId()
{
if (getComponent() instanceof IAjaxIndicatorAware)
{
return ((IAjaxIndicatorAware)getComponent()).getAjaxIndicatorMarkupId();
}
if (this instanceof IAjaxIndicatorAware)
{
return ((IAjaxIndicatorAware)this).getAjaxIndicatorMarkupId();
}
Component parent = getComponent().getParent();
while (parent != null)
{
if (parent instanceof IAjaxIndicatorAware)
{
return ((IAjaxIndicatorAware)parent).getAjaxIndicatorMarkupId();
}
parent = parent.getParent();
}
return null;
}
@Override
public final void onRequest()
{
Form.MethodMismatchResponse methodMismatch = onMethodMismatch();
if (methodMismatch == Form.MethodMismatchResponse.ABORT)
{
AjaxRequestAttributes attrs = getAttributes();
String desiredMethod = attrs.getMethod().toString();
String actualMethod = ((HttpServletRequest) RequestCycle.get().getRequest().getContainerRequest()).getMethod();
if (!desiredMethod.equalsIgnoreCase(actualMethod))
{
LOG.debug("Ignoring the Ajax request because its method '{}' is different than the expected one '{}",
actualMethod, desiredMethod);
return;
}
}
WebApplication app = (WebApplication)getComponent().getApplication();
AjaxRequestTarget target = app.newAjaxRequestTarget(getComponent().getPage());
RequestCycle requestCycle = RequestCycle.get();
requestCycle.scheduleRequestHandlerAfterCurrent(target);
respond(target);
}
/**
* @param target
* The AJAX target
*/
protected abstract void respond(AjaxRequestTarget target);
}