Letsgetdugg

Random tech jargon

Browsing the tag wicket

I hate expired sessions, death to all expired sessions. Traditionally a Java servlet container has a fixed session time, a flood of traffic can potentially cause JVM OOM errors if the session time is set too high. I wanted a smart session container that can hold onto sessions for as long as possible and expire sessions only when it is absolutely necessary; A Memcached store would be perfect for this wildstar gold.

There for I recently open sourced the jetty-session-store to solve this problem. With the jetty-session-store you can save your session state to Ehcache, Memcached or the database. State should not be bound to a single JVM, Viva Shared Session Stores!

So now that jetty-session-store is out in the wild you can technically cluster Wicket using just the HttpSessionStore. However, it isn’t very efficient with the way Memcached allocates data in fixed sized cache buckets.

1. Wicket sessions under the HttpSessionStore can get quite large, well over 1Mb in size. A Wicket session not only stores the session state but also the previous serialized pages the user has visited.

2. Serializing and de-serializing a large data structure can get expensive. The HttpSessionStore retains an AccessStackPageMap, which is a list data structure consisting of multiple page map revisions.

So instead of saving one large AccessStackPageMap, I wrote a SecondLevelCacheSessionStore that saves a page map revision per cache entry. This leads to much better cache utilization and a whole lot less serialization on the wire. Not to mention this avoids the whole 1Mb Memcached size limit.

Before you go willy nilly with clustering, read the Wicket render strategies page. Wicket requires session affinity for buffered responses with the default rendering strategy.

Clustering Wicket has never been easier.

Here is an example on how to offload page maps to a hybrid EhCache/Memcached cache. Memcached for long term shared storage while EhCache for short-lived fast cache look ups.

public class WebApp extends WebApplication {
        @Override
        protected ISessionStore newSessionStore() {
                // localhost:11211 — memcached server
                // "fabpagestore" — unique appender to avoid key clashes.
                // 300 — 5 minute TTL for local ehcache.
                return new SecondLevelCacheSessionStore(this,
                                new CachePageStore(Arrays.asList("localhost:11211"),"fabpagestore",300));
        }
}

Here is an example on how to offload page maps to the database.

public class WebApp extends WebApplication {
        @Override
        protected ISessionStore newSessionStore() {
                // "fabpagestore" — unique appender to avoid key clashes.
                return new SecondLevelCacheSessionStore(this,new CachePageStore(
                                new DBCache("jdbc:mysql://foo/mydb", "myname", "mypass", "com.driver.Name", "fabpagestore")));
        }
}

Here is my CachePageStore;

package com.base.pagestore;

import com.base.cache.AsyncMemcache;
import com.base.cache.ICache;
import org.apache.wicket.Page;
import org.apache.wicket.protocol.http.SecondLevelCacheSessionStore.IClusteredPageStore;
import org.apache.wicket.protocol.http.pagestore.AbstractPageStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class CachePageStore extends AbstractPageStore implements IClusteredPageStore {
    private ICache cache;
    private Logger logger = LoggerFactory.getLogger(CachePageStore.class);

    public CachePageStore(final List<String> servers, final String poolName, final int ttl) {
        this(servers, poolName, true, ttl);
    }

    public CachePageStore(final List<String> servers, final String poolName, boolean async, final int ttl) {
        this(new AsyncMemcache(servers, poolName, async, ttl));
    }

    public CachePageStore(final ICache cache) {
        this.cache = cache;
    }

    // If pageVersion -1 must return highest page version.
    protected String getKey(final String sessId, final String pageMapName, final int pageId, final int pageVersion) {
        int pageVer = (pageVersion == -1) ? 0 : pageVersion;
        if(pageVersion == -1) {
            String[] meta = getMeta(sessId, pageMapName, pageId);
            pageVer = Integer.valueOf(meta[0]);
        }

        return sessId + ":" + pageMapName + ":" + pageId + ":" + pageVer;
    }

    // If pageVersion -1 must return highest page version.
    // If ajaxVersion -1 must return highest version.
    public String getKey(final String sessId, final String pageMapName, final int pageId, final int pageVersion, final int ajaxVersion) {
        // Default it to 0 initially
        int ajaxVer = (ajaxVersion == -1) ? 0 : ajaxVersion;
        int pageVer = (pageVersion == -1) ? 0 : pageVersion;

        if(pageVersion == -1 || ajaxVersion == -1) {
            String[] meta = getMeta(sessId, pageMapName, pageId);
            if(pageVersion == -1) {
                pageVer = Integer.valueOf(meta[0]);
            }
            if(ajaxVersion == -1) {
                ajaxVer = Integer.valueOf(meta[1]);
            }
        }

        return sessId + ":" + pageMapName + ":" + pageId + ":" + pageVer + ":" + ajaxVer;
    }

    protected String storeKey(final String sessionId, final Page page) {
        return sessionId + ":" + page.getPageMapName() + ":" + page.getId() + ":" + page.getCurrentVersionNumber() + ":" + page.getAjaxVersionNumber();
    }

    protected String getBaseKey(String sessionId, Page page) {
        return sessionId + ":" + page.getPageMapName() + ":" + page.getId();
    }

    protected String getMetaKey(String sessionId, String pageMap, int id) {
        return getBaseKey(sessionId,pageMap,id)+"_meta";
    }

    protected String getMetaKey(String sessionId, Page page) {
        return getBaseKey(sessionId,page)+"_meta";
    }

    protected String getBaseKey(String sessionId, String pageMap, int id) {
        if(id == -1) {
            return sessionId + ":" + pageMap;
        } else {
            return sessionId + ":" + pageMap + ":" + id;
        }
    }
    public boolean containsPage(final String sessionId, final String pageMapName, final int pageId, final int pageVersion) {
        String key = getKey(sessionId, pageMapName, pageId, pageVersion, -1);
        if (logger.isDebugEnabled()) {
            logger.debug("CheckExists: " + key);
        }
        return cache.keyExists(key);
    }

    public void destroy() {
    }

    public <T> Page getPage(final String sessionId, final String pagemap, final int id, final int versionNumber, final int ajaxVersionNumber) {
        String key = getKey(sessionId, pagemap, id, versionNumber, ajaxVersionNumber);
        if (logger.isDebugEnabled()) {
            logger.debug("GetPage: " + key);
        }
        return (Page) cache.get(key);
    }

    public void pageAccessed(final String sessionId, final Page page) {
    }

    // If ID == -1 remove the entire pagemap; getBaseKey() takes care of this.
    public void removePage(final String sessionId, final String pagemap, final int id) {
        String key = getBaseKey(sessionId, pagemap, id);

        if (logger.isDebugEnabled()) {
            logger.debug("RemovePage: " + key);
        }

        cache.remove(getMetaKey(sessionId, pagemap, id));
        for (String k : cache.getKeys()) {
            if (k.startsWith(key)) {
                cache.remove(k);
            }
        }
    }

    protected String[] getMeta(final String sessionId, String pageMap, int pageId) {
        String metaKey = getMetaKey(sessionId,pageMap,pageId);
        Object ret = cache.get(metaKey);

        if (logger.isDebugEnabled()) {
            logger.debug("GetMeta: " + metaKey);
        }

        if(ret == null) {
            return new String[] {"0","0"};
        } else {
            return String.valueOf(ret).split(":");
        }
    }

    protected void storeMeta(final String sessionId, final Page page) {
        String metaKey = getMetaKey(sessionId, page);
        Object ret = cache.get(metaKey);

        if (logger.isDebugEnabled()) {
            logger.debug("StoreMeta: " + metaKey);
        }

        if(ret == null) {
            cache.put(metaKey,page.getCurrentVersionNumber()+":"+page.getAjaxVersionNumber());
        } else {
            String[] vals = String.valueOf(ret).split(":");
            int currPage = Integer.valueOf(vals[0]);
            int currAjax = Integer.valueOf(vals[1]);

            if(page.getCurrentVersionNumber() > currPage) {
                currPage = page.getCurrentVersionNumber();
            }
            if(page.getAjaxVersionNumber() > currAjax) {
                currAjax = page.getAjaxVersionNumber();
            }
            cache.put(metaKey,currPage+":"+currAjax);
        }
    }

    public void storePage(final String sessionId, final Page page) {
        String sKey = storeKey(sessionId, page);

        if (logger.isDebugEnabled()) {
            logger.debug("StorePage: " + sKey);
        }

        cache.put(sKey, page);
        storeMeta(sessionId,page);
    }

    public void unbind(final String sessionId) {
        if (logger.isDebugEnabled()) {
            logger.debug("Unbind: " + sessionId);
        }

        for (String key : cache.getKeys()) {
            if (key.startsWith(sessionId)) {
                cache.remove(key);
            }
        }
    }

}

I’ll start this post off with a quote from IRC

ivaynberg: you cant build good looking sites with wicket victori: lies ivaynberg: or public-facing sites

I have to admit that Wicket appeals more to the “backend” programmer than to the front-end design conscious developer. For every good-looking Wicket site out there, there are ten abysmal looking Wicket sites. Just look at the Wicket Wiki, it is littered with some dreadfully designed sites (Sorry Guys, this isn’t personal). You can tell right off the bat that the developers behind the sites care more about OO and clean code rather than clean design. Well to be frank, I don’t even know if the code behind the listed sites is even elegant. However, the fact that the sites are written on Wicket, tells me that the developers care about things such as separation of concerns and object oriented programming.

So to combat against the whole mentality that Wicket can’t scale and any site done in Wicket must look atrocious. I have decided to compile a list of some awesomely kick ass public-facing / good-looking Wicket sites.

If you don’t see your site and you feel that it should have made the list, feel free to leave a comment with your site’s URL.

High Traffic Wicket Sites

adscale.de

This site has an Alexa 1,700 traffic rank and runs on a single Tomcat servlet container. No proxy caches, no fancy clustering just Tomcat.



vegas.com

Next time someone states that no public facing sites are ever written in wicket, point them to vegas.com.





Clean Wicket Sites

kontain.com

The design behind this site is quite good and sets the design bar in my book.



meetmoi.com

Ah, I remember when the developer behind meetmoi dropped by #wicket and stated that he is officially working on it full time with a million dollars in venture capital seed money.



songtexte.com

Don’t know much about this site, aside that it looks clean and the author did the original b-side wicket site that got replaced with wordpress.



memolio.com



fabulously40.com

Disclaimer: this is the site I developed and I think it looks good ;-)


fab40site.jpg

winerevolution.com



islamicdesignhouse.com



Tagged with ,

I finally got around to open sourcing our scala memcached implementation that we use at fabulously40 for session storage.

Since wicket sessions can vary greatly in size, using the standard memcached server implementation became impractical due to the slab allocator.

The current code on github lacks the ehcache store and an Actor IoHandler adapter. The internal SMemcached application at fabulously40 uses a private caching API so we can hook up various caching backend storage implementations such as mysql, postgresql, ehcache or even another memcached server. You can grab the TCache project on github that SMemcached uses to unify caching under a single API. This gives SMemcached a lot of flexibility when it comes to caching your data.

fyi. TCache stands for “Tanek” Cache, Tanek means cache in russian.

The project works quite well, but don’t use it in production just yet since there is no data expiration for cached data in the HashMap storage implementation. This is just a technical preview. Do use it in production, this is what we use at Fabulously40 ;-)

http://github.com/victori/smemcached

Tagged with , , ,

One of the first few things I have tackled while creating fabulously40 was stateless pagination. I wanted to keep all the public facing pages stateless to avoid session overhead. Wicket makes pagination brain dead simple at the cost of session use. I wanted to keep the simple programming model yet be completely stateless. Here is my solution to the problem.

Wicket a stateful framework, is actually really good at being stateless.

Example code on how to use it.

update: I would suggest using MixedParamHybridCodingStrategy with the stateless paging navigation to keep links clean.

StatelessDataView<Photo> dv;
add(dv = new StatelessDataView<Photo>("pics", getPhotoProvider(), amount, pageParams) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        protected void populateItem(final Item<Photo> arg0) {
              }
});

// call back page class, page paramaters passed, the dataview and 12 pagination links.
add(new StatelessSimplePagingNavigator("nav", PhotosPage.class, pageParams, dataView, 12));

The dataview

package com.base.components;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.IDataProvider;

public abstract class StatelessDataView<T> extends DataView<T> {
        private static final long serialVersionUID = 1L;
        private PageParameters pp;

        @Override
        protected boolean getStatelessHint() {
                return true;
        }

        protected int getPageNumber(final String param) {
                String numResult = param;
                if(numResult.contains(".wicket-")) {
                        numResult = numResult.substring(0,numResult.indexOf(".wicket-"));
                }
                return Integer.valueOf(numResult);
        }

        public StatelessDataView(final String id, final IDataProvider dataProvider, final PageParameters pp) {
                super(id, dataProvider);
                this.pp = pp;
                if (pp.getString(id) != null) {
                        int pageNum = getPageNumber(pp.getString(id));
                        if(pageNum != -1 && pageNum >= 0 && pageNum <= getPageCount()) {
                                setCurrentPage(getPageNumber(pp.getString(id)));
                        }
                }
        }

        public StatelessDataView(final String id, final IDataProvider dataProvider, final int itemsPerPage,
                        final PageParameters pp) {
                super(id, dataProvider, itemsPerPage);
                this.pp = pp;
                if (pp.getString(id) != null) {
                        int pageNum = getPageNumber(pp.getString(id));
                        if(pageNum != -1 && pageNum >= 0 && pageNum <= getPageCount()) {
                                setCurrentPage(getPageNumber(pp.getString(id)));
                        }
                }
        }

}

The paging navigator

package com.base.components;

import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.navigation.paging.IPageable;
import org.apache.wicket.markup.html.navigation.paging.IPagingLabelProvider;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigation;

public class StatelessSimplePagingNavigator extends SimplePagingNavigator {
        private static final long serialVersionUID = 1L;
        private Class clazz;
        private PageParameters pp;
        private int current_page;
        private int page_count;
        private String pageable_id;
        private String anchor = null;

        @Override
        protected boolean getStatelessHint() {
                return true;
        }

        public StatelessSimplePagingNavigator(final String id, final Class clazz, final PageParameters pp,
                        final IPageable pageable, final int viewsize) {
                this(id, clazz, pp, pageable, viewsize, false);
        }

        public StatelessSimplePagingNavigator(final String id, final Class clazz, final PageParameters pp,
                        final IPageable pageable, final int viewsize, final boolean anchorSelf) {
                super(id, pageable, viewsize, anchorSelf);
                this.clazz = clazz;
                this.pp = pp;
                this.current_page = pageable.getCurrentPage();
                this.page_count = pageable.getPageCount();
                this.pageable_id = ((Component) pageable).getId();
        }

        public StatelessSimplePagingNavigator(final String id, final Class clazz, final PageParameters pp,
                        final IPageable pageable, final int viewsize, final String anchor) {
                super(id, pageable, viewsize, false);
                this.clazz = clazz;
                this.pp = pp;
                this.current_page = pageable.getCurrentPage();
                this.page_count = pageable.getPageCount();
                this.pageable_id = ((Component) pageable).getId();
                this.anchor = anchor;
        }

        @Override
        protected void onBeforeRender() {
                super.onBeforeRender();
                addOrReplace(newStatelessPagingNavigationLink("next", pageable_id, current_page, 1));
                addOrReplace(newStatelessPagingNavigationLink("prev", pageable_id, current_page, -1));
        }

        @Override
        protected PagingNavigation newNavigation(final IPageable pageable, final IPagingLabelProvider labelProvider) {
                PagingNavigation pg = new PagingNavigation("navigation", pageable, labelProvider) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        protected Link newPagingNavigationLink(final String id, final IPageable pageable, final int pageIndex) {
                                Component c = (Component) pageable;
                                // PageParameters p1 = new PageParameters();
                                // p1.putAll(pp);
                                pp.put(c.getId(), String.valueOf(pageIndex));
                                BookmarkablePageLink<Object> lnk = new BookmarkablePageLink<Object>(id, clazz, pp) {
                                        private static final long serialVersionUID = 1L;

                                        @Override
                                        protected void onComponentTag(final ComponentTag arg0) {
                                                super.onComponentTag(arg0);
                                                if (anchor != null) {
                                                        if(arg0.getString("href") != null) {
                                                                String href = arg0.getString("href").toString();
                                                                String atag = anchor.contains("#") ? anchor : "#" + anchor;
                                                                arg0.put("href", href + atag);
                                                        }
                                                }
                                        }

                                        @Override
                                        public boolean isEnabled() {
                                                return (current_page != pageIndex);
                                        }
                                };
                                if (isAnchorSelf()) {
                                        lnk.setAnchor(StatelessSimplePagingNavigator.this);
                                }
                                return lnk;
                        }
                };
                pg.setViewSize(getViewsize());
                return pg;
        }

        @Override
        protected Link<Object> newPagingNavigationIncrementLink(final String id, final IPageable pageable, final int increment) {
                Component c = (Component) pageable;
                final int page = Integer.valueOf(pp.getString(c.getId()));
                pp.put(c.getId(), String.valueOf(page + increment));
                return new BookmarkablePageLink<Object>(id, clazz, pp) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        public boolean isVisible() {
                                return (page + increment) >= 0;
                        }
                };
        }

        protected Link<Object> newStatelessPagingNavigationLink(final String id, final String pageableId, final int currentPage,
                        final int increment) {
                // PageParameters p1 = new PageParameters();
                // p1.putAll(pp);
                pp.put(pageableId, String.valueOf(currentPage + increment));
                return new BookmarkablePageLink<Object>(id, clazz, pp) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        public boolean isVisible() {
                                return (currentPage + increment) < page_count && (currentPage + increment) >= 0;
                        }
                };
        }

        @Override
        protected Link<Object> newPagingNavigationLink(final String id, final IPageable pageable, final int pageNumber) {
                Component c = (Component) pageable;
                // PageParameters p1 = new PageParameters();
                // p1.putAll(pp);
                pp.put(c.getId(), String.valueOf(pageNumber));
                return new BookmarkablePageLink(id, clazz, pp);
        }

}

Which inherits from

package com.base.components;

import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.navigation.paging.IPageable;
import org.apache.wicket.markup.html.navigation.paging.IPagingLabelProvider;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigation;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigator;

public class SimplePagingNavigator extends PagingNavigator {
        private static final long serialVersionUID = 1L;

        private int viewsize = 0;
        private boolean anchorSelf = false;

        public SimplePagingNavigator(final String id, final IPageable pageable, final int viewsize) {
                this(id, pageable, viewsize, false);
        }

        public SimplePagingNavigator(final String id, final IPageable pageable, final int viewsize, final boolean anchorSelf) {
                super(id, pageable);
                setOutputMarkupId(true);
                this.setViewsize(viewsize);
                this.setAnchorSelf(anchorSelf);
        }

        @Override
        protected void onBeforeRender() {
                if (get("navigation") != null) {
                        remove("navigation");
                }
                if (get("prev") != null) {
                        remove("prev");
                }
                if (get("next") != null) {
                        remove("next");
                }
                super.onBeforeRender();
                if (get("first") != null) {
                        remove("first");
                }
                if (get("last") != null) {
                        remove("last");
                }
                if (getViewsize() != 0) {
                        getPagingNavigation().setViewSize(getViewsize());
                }
        }

        @Override
        protected PagingNavigation newNavigation(final IPageable pageable, final IPagingLabelProvider labelProvider) {
                PagingNavigation pg = new PagingNavigation("navigation", pageable, labelProvider) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        protected Link newPagingNavigationLink(final String id, final IPageable pageable, final int pageIndex) {
                                Link lnk = (Link) super.newPagingNavigationLink(id, pageable, pageIndex);
                                if (isAnchorSelf()) {
                                        lnk.setAnchor(SimplePagingNavigator.this);
                                }
                                return lnk;
                        }
                };
                pg.setViewSize(getViewsize());
                return pg;
        }

        @Override
        public boolean isVisible() {
                if (getPageable() != null) {
                        return (getPageable().getPageCount() > 1);
                }
                return true;
        }

        public void setAnchorSelf(final boolean anchorSelf) {
                this.anchorSelf = anchorSelf;
        }

        public boolean isAnchorSelf() {
                return anchorSelf;
        }

        public void setViewsize(final int viewsize) {
                this.viewsize = viewsize;
        }

        public int getViewsize() {
                return viewsize;
        }

}

<wicket:panel>
        <a wicket:id="prev">Previous</a>
    <span wicket:id="navigation">
                  <a wicket:id="pageLink" href="#"><span wicket:id="pageNumber">5</span></a>
    </span>
    <a wicket:id="next">Next</a>
  </wicket:panel>
Tagged with ,

I am about to run out the house for the Labor Day weekend, quickie post on my new coding url strategy.

MixedParamHybridUrlCodingStrategy lets you keep stateful multi-pagemap URLs clean while using mixed parameters.

Example…

mount(new MixedParamHybridUrlCodingStrategy("/questions",QuestionsPage.class,false,new String[]{"cat"}));

This will mount “/questions/stupid-category” and convert it to…

new PageParameters("cat","stupid-category");
package com.base.target.coding;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.IRequestTarget;
import org.apache.wicket.PageParameters;
import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
import org.apache.wicket.request.target.coding.HybridUrlCodingStrategy;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.value.ValueMap;

public class MixedParamHybridUrlCodingStrategy extends HybridUrlCodingStrategy {
        private final String[] parameterNames;

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass) {
                super(mountPath, pageClass);
                this.parameterNames = new String[] {};
        }

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass,final boolean refresh) {
                super(mountPath,pageClass,refresh);
                this.parameterNames = new String[] {};
        }

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass,final String[] params) {
                super(mountPath,pageClass,false);
                this.parameterNames = params;
        }

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass,final boolean refresh,final String[] params) {
                super(mountPath,pageClass,refresh);
                this.parameterNames = params;
        }

        @Override
        protected void appendParameters(final AppendingStringBuffer url, final Map parameters)
        {

                Set parameterNamesToAdd = new HashSet(parameters.keySet());
                // Find index of last specified parameter
                boolean foundParameter = false;
                int lastSpecifiedParameter = parameterNames.length;
                while (lastSpecifiedParameter != 0 && !foundParameter)
                {
                        foundParameter = parameters.containsKey(parameterNames[lastSpecifiedParameter]);
                }

                if (foundParameter)
                {
                        for (int i = 0; i <= lastSpecifiedParameter; i++)
                        {
                                String parameterName = parameterNames[i];
                                final Object param = parameters.get(parameterName);
                                String value = param instanceof String[] ? ((String[])param)[0] : (String)param;
                                if (value == null)
                                {
                                        value = "";
                                }
                                if (!url.endsWith("/"))
                                {
                                        url.append("/");
                                }
                                url.append(urlEncodePathComponent(value));
                                parameterNamesToAdd.remove(parameterName);
                        }
                }

                if (!parameterNamesToAdd.isEmpty())
                {
                        boolean first = true;
                        final Iterator iterator = parameterNamesToAdd.iterator();
                        while (iterator.hasNext())
                        {
                                url.append(first ? ‘?’ : ‘&’);
                                String parameterName = (String)iterator.next();
                                final Object param = parameters.get(parameterName);
                                String value = param instanceof String[] ? ((String[])param)[0] : (String)param;
                                url.append(urlEncodeQueryComponent(parameterName)).append("=").append(
                                                urlEncodeQueryComponent(value));
                                first = false;
                        }
                }

                String pageMap = (String)parameters.get(WebRequestCodingStrategy.PAGEMAP);
                if (pageMap != null)
                {
                        pageMap = WebRequestCodingStrategy.encodePageMapName(pageMap);
                        if (!url.endsWith("/"))
                        {
                                url.append("/");
                        }
                        url.append(WebRequestCodingStrategy.PAGEMAP).append("/").append(
                                        urlEncodePathComponent(pageMap));
                }
        }

        @Override
        public CharSequence encode(final IRequestTarget requestTarget) {
                String url = String.valueOf(super.encode(requestTarget));
                if(url.contains(".wicket")) {
                        // Rewrite URL from..  /foo?bar=5.wicket-xxx to /foo.wicket-xxx?bar=5
                        return url.replaceAll("(.*)\\?(.*)\\.(wicket-[0-9]+)$", "$1.$3?$2");
                }
                return url;
        }

        @Override
        protected ValueMap decodeParameters(final String urlFragment, final Map urlParameters)
        {

                PageParameters params = new PageParameters();
                if(urlFragment == null) {
                        return params;
                }

                // Add all url parameters and normalize
                for(Iterator<Map.Entry> itr = urlParameters.entrySet().iterator();itr.hasNext();) {
                        Map.Entry<String,String[]> e = itr.next();
                        if(e.getValue() != null) {
                                String val = (e.getValue())[0];
                                if(val != null) {
                                        if(val.contains(".wicket")) {
                                                val = val.substring(0,val.indexOf(".wicket"));
                                                urlParameters.put(e.getKey(), val);
                                        }
                                }
                        }
                }
                params.putAll(urlParameters);

                String urlPath = urlFragment;
                if (urlPath.startsWith("/"))
                {
                        urlPath = urlPath.substring(1);
                }

                if (urlPath.length() > 0)
                {
                        String[] pathParts = urlPath.split("/");
                        if (pathParts.length > parameterNames.length)
                        {
                                throw new IllegalArgumentException(
                                "Too many path parts, please provide sufficient number of path parameter names");
                        }

                        for (int i = 0; i < pathParts.length; i++)
                        {
                                if(pathParts[i].contains(".wicket")) {
                                        pathParts[i].substring(0,pathParts[i].indexOf(".wicket"));
                                }
                                if (WebRequestCodingStrategy.PAGEMAP.equals(pathParts[i]))
                                {
                                        params.put(WebRequestCodingStrategy.PAGEMAP,
                                                        WebRequestCodingStrategy.decodePageMapName(urlDecodePathComponent(pathParts[i])));
                                }
                                params.put(parameterNames[i], urlDecodePathComponent(pathParts[i]));
                        }

                }
                return params;
        }

}

Tagged with ,

Wicket is a wonderful framework for building complex web applications. However, it is all too easy to hang your self with session use in Wicket. Wicket manages the session on your behalf so you don’t have to worry about UI state. Since every page is serialized to the session, you need to be extra careful with your entity graph so it does not get serialized with the page. Here are some anti patterns that I have personally stumbled upon.

Anti-Pattern #1.

public FooPanel(String id,IModel<User> userMdl) {
        super(id,userMdl);
        final User user = getModelObject();
        IModel<List<User>> model = new LoadableDetachableModel<List<User>>() {
                private static final long serialVersionUID = 1L;

                @Override
                protected List<User> load() {
                        // A Stale User entity is now serialized with each page view
                        // This can consume lots of session space if the user entity is large.
                        return getWebSession().getUserService().getUserFriends(user);
                }
        };
}

Here is the correct way to do this

//IModel needs to be a LoadableDetachableModel!
public FooPanel(String id,IModel<User> userMdl) {
        super(id,userMdl);
        final User user = getModelObject();
        IModel<List<User>> model = new LoadableDetachableModel<List<User>>() {
                private static final long serialVersionUID = 1L;
                @Override
                protected List<User> load() {
                        User u = FooPanel.this.getModelObject();
                        return getWebSession().getUserService().getUserFriends(u);
                }
        };
}

The entity is fetched each page view, and NOT serialized with the page. The only thing that gets serialized is the fetching model.

Anti-Pattern #2.

public FooPanel(String id,IModel<User> userMdl) {
        super(id.userMdl);
        User user = getModelObject();
        // Doh! user is not serialized with each page view, the PropertyModel holds a reference!
        add(new Label("name",new PropertyModel(user,"screenName")));
}

Here is the correct way to do this

public FooPanel(String id,IModel<User> userMdl) {
        super(id.userMdl);
        add(new Label("name",new PropertyModel(userMdl,"screenName")));
}

The PropertyModel holds a reference to the Model which fetches the User on each page view. That way the User object is always fresh and is not serialized with the page.

Anti-Pattern #3.

public FooPanel(String id,User user) {
        // The stale User object will be in your session
        // for the life span of the stateful page.
        super(id.new Model(user));
}

Here is the correct way to do this, though not very elegant.

public FooPanel(String id,User user) {
        super(id);
        final int id = user.getId();
        setModel(new LoadableDetachableModel<List<User>>() {
                @Override
                protected List<User> load() {
                        return getUserDao().findById(id);
                }
        });

I hope these Wicket anti-patterns help you avoid the common pitfalls I have run into while developing
Fabulously40

Tagged with