Demo Code

Check http://jolira-tools.googlecode.com/svn/wicket-stateless/trunk/ for a demo and an elaboration on the approach mentioned here.

Wicket currently (1.4.6) supports a StatelessForm, but no stateless Ajax components. Because I really, really need stateless Ajax behavior for a mobile application for a very, very large online retailer, I have written the following class:

StatelessAjaxFallbackLink.java
import static org.apache.wicket.protocol.http.request.WebRequestCodingStrategy.BOOKMARKABLE_PAGE_PARAMETER_NAME;

import org.apache.wicket.Page;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.IAjaxCallDecorator;
import org.apache.wicket.ajax.calldecorator.CancelEventIfNoAjaxDecorator;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.ajax.markup.html.IAjaxLink;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.IModel;

/**
 * Just like {@link AjaxFallbackLink}, but stateless.
 * 
 */
public abstract class StatelessAjaxFallbackLink<T> extends Link<T> implements
        IAjaxLink {
    private static final long serialVersionUID = -133600842398684777L;

    public StatelessAjaxFallbackLink(final String id) {
        this(id, null);
    }

    public StatelessAjaxFallbackLink(final String id, final IModel<T> model) {
        super(id, model);

        add(new AjaxEventBehavior("onclick") {
            private static final long serialVersionUID = 1L;

            @Override
            protected IAjaxCallDecorator getAjaxCallDecorator() {
                return new CancelEventIfNoAjaxDecorator(
                        StatelessAjaxFallbackLink.this.getAjaxCallDecorator());
            }

            @Override
            public final CharSequence getCallbackUrl(
                    final boolean onlyTargetActivePage) {
                final CharSequence callbackUrl = super.getCallbackUrl(false);

                return decorateCallbackUrl(callbackUrl);
            }

            @Override
            @SuppressWarnings("synthetic-access")
            protected void onComponentTag(final ComponentTag tag) {
                // only render handler if link is enabled
                if (isLinkEnabled()) {
                    super.onComponentTag(tag);
                }
            }

            @Override
            protected void onEvent(final AjaxRequestTarget target) {
                onClick(target);
            }
        });
    }

    protected final CharSequence decorateCallbackUrl(
            final CharSequence callbackUrl) {
        final Page page = getPage();
        final Class<? extends Page> pageCls = page.getClass();
        final String pageClsName = pageCls.getName();
        final StringBuilder buf = new StringBuilder();

        buf.append(callbackUrl);
        buf.append('&');
        buf.append(BOOKMARKABLE_PAGE_PARAMETER_NAME);
        buf.append("=");
        buf.append(PATH_SEPARATOR);
        buf.append(pageClsName);

        return buf; 
    }

    /**
     * 
     * @return call decorator to use or null if none
     */
    protected IAjaxCallDecorator getAjaxCallDecorator() {
        return null;
    }

    /**
     * Hints that this component is stateless.
     * 
     * @return always {@literal true}
     * @see Link#getStatelessHint()
     */
    @Override
    protected boolean getStatelessHint() {
        return true;
    }

    /**
     * @see Link#onClick()
     */
    @Override
    public final void onClick() {
        onClick(null);
    }

    /**
     * Callback for the onClick event. If ajax failed and this event was
     * generated via a normal link the target argument will be null
     * 
     * @param target
     *            ajax target if this linked was invoked using ajax, null
     *            otherwise
     */
    public abstract void onClick(final AjaxRequestTarget target);
}

This code is almost identical to what can be found in the AjaxFallbackLink. The only main difference is that it adds another parameter to the generated URL, which identifies the class the page targets using the standard "wicket:bookmarkablePage" parameter.

This alone does not make the solution work yet as the default request process is confused about the newly generated request target. In order to rectify this situation, I have added the following code to my application class:

WicketApplication.java
package com.jolira.stateless;

import java.util.Map;

import org.apache.wicket.Component;
import org.apache.wicket.IPageFactory;
import org.apache.wicket.IRequestTarget;
import org.apache.wicket.Page;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.RequestListenerInterface;
import org.apache.wicket.Session;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.IRequestCodingStrategy;
import org.apache.wicket.request.IRequestCycleProcessor;
import org.apache.wicket.request.RequestParameters;
import org.apache.wicket.request.target.component.listener.BehaviorRequestTarget;
import org.apache.wicket.settings.ISessionSettings;
import org.apache.wicket.util.string.Strings;

/**
 * Application object for your web application. If you want to run this
 * application without deploying, run the Start class.
 * 
 * @see com.jolira.stateless.Start#main(String[])
 */
public class WicketApplication extends WebApplication {
    /**
     * @see org.apache.wicket.Application#getHomePage()
     */
    @Override
    public Class<HomePage> getHomePage() {
        return HomePage.class;
    }

    @SuppressWarnings("unchecked")
    private Class<? extends Page> getPageClass(final Session session,
            final String clsName) {
        try {
            return (Class<? extends Page>) session.getClassResolver()
                    .resolveClass(clsName);
        } catch (final ClassNotFoundException e) {
            throw new WicketRuntimeException(
                    "Unable to load Bookmarkable Page", e);
        }
    }

    // lots of irrelevant code omitted .... }
    private <C extends Page> Page newPage(final Class<C> pageClass,
            final RequestCycle cycle, final PageParameters pageParameters) {
        final ISessionSettings settings = getSessionSettings();
        final IPageFactory pageFactory = settings.getPageFactory();

        final Map<String, String[]> requestMap = cycle.getRequest()
                .getParameterMap();
        final Map<String, String[]> reqParams = pageParameters
                .toRequestParameters();

        requestMap.putAll(reqParams);

        return pageFactory.newPage(pageClass, pageParameters);
    }

    /**
     * Install a new request processor to handle requests generated by any
     * {@link StatelessAjaxFallbackLink}.
     * 
     * @see WebApplication#newRequestCycleProcessor()
     */
    @Override
    protected IRequestCycleProcessor newRequestCycleProcessor() {
        final IRequestCycleProcessor processor = super
                .newRequestCycleProcessor();

        return new IRequestCycleProcessor() {
            public IRequestCodingStrategy getRequestCodingStrategy() {
                return processor.getRequestCodingStrategy();
            }

            public void processEvents(final RequestCycle requestCycle) {
                processor.processEvents(requestCycle);
            }

            public IRequestTarget resolve(final RequestCycle requestCycle,
                    final RequestParameters requestParameters) {
                return WicketApplication.this.resolve(processor, requestCycle,
                        requestParameters);
            }

            public void respond(final RequestCycle requestCycle) {
                processor.respond(requestCycle);
            }

            public void respond(final RuntimeException e,
                    final RequestCycle requestCycle) {
                processor.respond(e, requestCycle);
            }
        };
    }

    protected IRequestTarget resolve(final IRequestCycleProcessor processor,
            final RequestCycle cycle, final RequestParameters requestParameters) {
        final RequestListenerInterface iface = requestParameters.getInterface();
        final String behaviorId = requestParameters.getBehaviorId();
        final String pageName = requestParameters.getBookmarkablePageClass();

        if (iface == null || behaviorId == null || pageName == null) {
            return processor.resolve(cycle, requestParameters);
        }

        final Map<String, ?> _params = requestParameters.getParameters();
        final PageParameters params = new PageParameters(_params);
        final Session session = cycle.getSession();
        final Class<? extends Page> pageClass = getPageClass(session, pageName);
        final Page page = newPage(pageClass, cycle, params);
        final String componentPath = requestParameters.getComponentPath();
        final String pageRelativeComponentPath = Strings
                .afterFirstPathComponent(componentPath,
                        Component.PATH_SEPARATOR);
        final Component component = page.get(pageRelativeComponentPath);

        // See {@link
        // BookmarkableListenerInterfaceRequestTarget#processEvents(RequestCycle)}
        // We make have to try to look for the component twice, if we hit the
        // same condition.
        if (component == null) {
            throw new WicketRuntimeException(
                    "unable to find component with path "
                            + pageRelativeComponentPath
                            + " on stateless page "
                            + page
                            + " it could be that the component is inside a repeater make your component return false in getStatelessHint()");
        }

        final String interfaceName = requestParameters.getInterfaceName();
        final RequestListenerInterface listenerInterface = RequestListenerInterface
                .forName(interfaceName);

        if (listenerInterface == null) {
            throw new WicketRuntimeException(
                    "unable to find listener interface " + interfaceName);
        }

        return new BehaviorRequestTarget(page, component, listenerInterface,
                requestParameters);
    }

}
  • No labels