We use wicket ( http://wicket.apache.org/) at my current employers. And now that I have written quite some amount of code in wicket, I want to share my code.
No I am not about to quit; my boss allows me to do so... :)
I just hope to get opinions on improvement.
Also I hope it gets to any of someone's use.
Here goes the story::
(If you don't code please forget about reading further or that this blog even exists in this world. )
I start using wicket. Initially everything is cool. With so much code for a developer already coming in the examples and pre-built components. Also I go and read all things wicket(planet wicket).
And as there is so much activity here, similar is on the mailing list.
My first encounter with any difficulty happens when I want to use choice components in certain scenarios.
The first one is the Country,State,City trio. I want them to be in rhythm on all my pages. I also want this to happen using Ajax.
Here are my three wicket guys for the work::
DropDownChoice countryDrpDwn ;//--from core wicket
DropDownChoice stateDrpDwn;
AutoCompleteTextField cityTxtFld;//--from extensions
Then I need to create them so that I have ids used by them internally for , but descriptions shown to the outer world.
And I found IChoiceRenderer does this job.
Now I need models N subject them to the renderer. The use case looks simple enough for a key-value based model, with a simple renderer.
I could provide my countryDrpDwn a list of the key-value models.
But my stateDrpDwn looks dependent on the country value selected. If I state it well, my stateDrpDwn's list model is dependent on the country value.
And further to complicate things enough cityTxtFld's list model is dependent on the stateDrpDwn's selected value.
I realise(and my realisation could be wrong)..that I needed custom model for wicket. Which could fit these components well.
I end up writing custom models, writing code more than what I needed minimally(happens with a lot of people i know ;))..........(imagine of a fast forward in the movie now....I just provide the results here)
I have a set of following :::
1. ChoiceHelper's which help me work with my choices model outside the Choice component.
2.ChoiceModel's which are the end product which can be used with components
Apologies for the very few comments. I put a lot of them here only now when I write all this, consider them part of my talking.(1)
/**
* Has(Provides) a strict set of choices with no defaults etc.
* @see ChoicesProvider-- factory+defaults for an encapsulated ChoicesHelper
* @see AbstractChoicesHelper--
A default choices helper--provides some basic functionality + 'nested
helper' functionality so that child classes
* don't have to worry about it.
* @author dontEatTooMuch
*
*/
public interface ChoicesHelper extends IClusterable
{
public List list();
public IChoiceRenderer renderer();
/**
* To support any nested helpers..
* e.g. States for a country_key
* If there's no nested list...throw an exception on this call
* @param key
* @return
*/
public ChoicesHelper helper(Object idvalue);
/**
* Calling the helper(String id) will definitely throw an exception if there's no nested list of choices for the key passed..
* This method allows to check if there is a nested helper to avoid calling the above method
* @param key
* @return
*/
public boolean containsNestedHelper(Object idvalue);
/**
* Returns display values of all choices.
* @return
*/
public String[] displayValues();
public String displayValueOf(String id, Component component);
public Object ObjectOf(String id);
}
(2)
public abstract class AbstractChoicesHelper implements ChoicesHelper
{
public Object ObjectOf(String id)
{
if (id == null)
return null;
final List choices = list();
final IChoiceRenderer renderer = renderer();
for (int index = 0; index < choices.size(); index++)
{
final Object choice = choices.get(index);
if (renderer.getIdValue(choice, index).equals(id))
{
return choice;
}
}
return null;
}
public String displayValueOf(String id, Component component)
{
if (id == null)
return null;
final List choices = list();
final IChoiceRenderer renderer = renderer();
for (int index = 0; index < choices.size(); index++)
{
final Object choice = choices.get(index);
if (renderer.getIdValue(choice, index).equals(id))
{
Object objectValue = renderer.getDisplayValue(choice);
Class objectClass = objectValue == null ? null : objectValue.getClass();
final String displayValue = component == null
? String.valueOf(objectValue)
: component.getConverter(objectClass).convertToString(objectValue,
component.getLocale());
return displayValue;
}
}
return null;
}
public String[] displayValues()
{
final List choices = list();
final ArrayList dispVals = new ArrayList();
final IChoiceRenderer renderer = renderer();
for (int index = 0; index < choices.size(); index++)
{
final Object choice = choices.get(index);
dispVals.add(String.valueOf(renderer.getDisplayValue(choice)));
}
return dispVals.toArray(new String[dispVals.size()]);
}
public IChoiceRenderer renderer()
{
return new ChoiceRenderer();
}
}
(3)
/**
* all its subclasses provide similar functionality, the difference is supposed to be just the underlying storage
*
* @param
* key in doAdd(K key,V value) implementation in subclass
* @param
* value in doAdd(K key,V value) implementation in subclass
*/
public abstract class AbstractKeyValChoicesHelper extends AbstractChoicesHelper
{
private HashMap nestedHelpers = new HashMap();
public AbstractKeyValChoicesHelper()
{
super();
}
/**
* Add key-value pairs. Also implements a shortcut where the java.lang.Class
* which extends ChoicesHelper can be passed as key, its getSimpleName()
* name will be used as key, also an instance will be stored of this class
* as nested helper. e.g.add(in.class, "India") where in.class is a
* ChoicesHelper providing list of indian states.
*
* @param _key--
* key for storage and reference to this particular choice node
* @param value--
* display value, which if a non-string, will be converted to
* string,based on the locale
* @return-- AbstractChoicesHelper to call the add as you call append in
* StringBuffer
*/
public final AbstractKeyValChoicesHelper add(Object key, Object value)
{
if (key == null || value == null)
{
throw new IllegalArgumentException("key can't be null");
}
if (key instanceof Class)
{
if ((ChoicesHelper.class.isAssignableFrom((Class)key)))
{
String classSimpleName = ((Class)key).getSimpleName();
try
{
nestedHelpers.put(classSimpleName, ((Class)key).newInstance());
}
catch (Exception e)
{
e.printStackTrace();
}
key = classSimpleName;
}
else
{
throw new IllegalArgumentException(key
+ " is not of type ChoicesHelper, which is the only class supported as key");
}
}
doAdd((K)key, (V)value);
return this;
}
/**
* An instance of nestedhelperclass will be stored as nested helper for
* getting children list of this choice
*
* @param _key--
* key for storage and reference to this particular choice node
* @param value--
* display value, which if a non-string, will be converted to
* string,based on the locale
* @param nestedhelperclass--nested
* helper class name
* @return-- AbstractChoicesHelper to call the add as you call append in
* StringBuffer
*/
public final AbstractKeyValChoicesHelper add(K key, V value, Class nestedhelperclass)
{
String strkey;
if (key == null)
{
throw new IllegalArgumentException("key can't be null");
}
if (key instanceof Class)
{
throw new UnsupportedOperationException("please use the other add method");
}
doAdd(key, value);
if (nestedhelperclass != null)
{
try
{
nestedHelpers.put(key, ((Class)nestedhelperclass).newInstance());
}
catch (Exception e)
{
e.printStackTrace();
}
}
return this;
}
/**
* An instance of nestedhelper will be stored as nested helper for getting
* children list of this choice
*
* @param _key--
* key for storage and reference to this particular choice node
* @param value--
* display value, which if a non-string, will be converted to
* string,based on the locale
* @param nestedhelper--nested
* helper instance
* @return-- AbstractChoicesHelper to call the add as you call append in
* StringBuffer
*/
public final AbstractKeyValChoicesHelper add(K key, V value, ChoicesHelper nestedhelper)
{
String strkey;
if (key == null)
{
throw new IllegalArgumentException("key can't be null");
}
if (key instanceof Class)
{
throw new UnsupportedOperationException("please use the other add method");
}
doAdd(key, value);
if (nestedhelper != null)
{
try
{
nestedHelpers.put(String.valueOf(key), nestedhelper);
}
catch (Exception e)
{
e.printStackTrace();
}
}
return this;
}
public ChoicesHelper helper(Object id)
{
if (!nestedHelpers.containsKey(id))
{
throw new IllegalArgumentException(
"There are no nested choices which you are trying to fetch for key" + id);
}
return ((ChoicesHelper)nestedHelpers.get(id));
}
public boolean containsNestedHelper(Object id)
{
return nestedHelpers.containsKey(id);
}
/**
* don't call this method directly--call add(Object _key,Object value)
*
* @param _key
* @param value
*/
protected abstract void doAdd(K _key, V value);
public String displayValueOf(IModel idmodel, Component component)
{
return displayValueOf(String.valueOf(idmodel.getObject()), component);
}
}
(4)
public abstract class MapChoicesHelper<K, V> extends AbstractKeyValChoicesHelper<Object, Object>
{
private IChoiceRenderer renderer = new IChoiceRenderer()
{
public Object getDisplayValue(Object object)
{
return map().get(object);
}
public String getIdValue(Object object, int index)
{
return object == null ? null : object.toString();
}
};
public IChoiceRenderer renderer()
{
return renderer;
}
/**
* Note that this list should not be modified directly
*/
public List list()
{
return new ArrayList(map().keySet());
}
public void doAdd(Object key, Object value)
{
map().put((K)key, (V)value);
}
public Object ObjectOf(String id)
{
return map().get(id);
}
protected abstract Map<K, V> map();
}
(5)
public class SortedMapChoicesHelper extends MapChoicesHelper{
private SortedMap choicesKeyValMap= new TreeMap();
@Override
protected Map map() {
return choicesKeyValMap;
}
}
(6)
public class EnumMapChoicesHelper<T> extends MapChoicesHelper{
private EnumMap choicesKeyValueMap;
public EnumMapChoicesHelper(Class enumclass) {
super();
choicesKeyValueMap= new EnumMap(enumclass);
}
@Override
protected Map<Enum,Object> map() {
return choicesKeyValueMap;
}
}
(7)
public abstract class ListChoicesHelper extends AbstractChoicesHelper{
private HashMap<Object, ChoicesHelper> nestedHelpers = new HashMap<Object, ChoicesHelper>();
private int indx=0;
private ArrayList choiceLst = new ArrayList();
public final ListChoicesHelper add(Object obj){
add(obj,null);
return this;
}
public final ListChoicesHelper add(Object obj,ChoicesHelper nestedhelper){
String strkey;
list().add(obj);
strkey=renderer().getIdValue(obj, list().indexOf(obj));
if(nestedhelper!=null){
try {
nestedHelpers.put(strkey,nestedhelper);
} catch (Exception e) {
e.printStackTrace();
}
}
return this;
}
public boolean containsNestedHelper(Object id) {
return nestedHelpers.containsKey(id);
}
public ChoicesHelper helper(Object id) {
if(!nestedHelpers.containsKey(id)){
throw new IllegalArgumentException("There are no nested choices which you are trying to fetch for key"+id);
}
return ((ChoicesHelper)nestedHelpers.get(id));
}
/**
* Should return the same list instance always
*/
public List list(){
return choiceLst;
};
}
(8)
/**
* acts as a factory to build ChoicesHelper & reuses its instance
*/
public abstract class ChoicesProvider implements ChoicesHelper
{
protected abstract ChoicesHelper buildhelper();
private ChoicesHelper helper = null;
public List list()
{
return helper.list();
}
public String[] displayValues()
{
return helper.displayValues();
}
public IChoiceRenderer renderer()
{
return helper.renderer();
}
public ChoicesProvider helper(final Object id)
{
return new ChoicesProvider()
{
@Override
protected ChoicesHelper buildhelper()
{
ChoicesHelper helperForNested;
if (!helper.containsNestedHelper(id))
{
helperForNested = defaultHelper();
}
else
{
helperForNested = helper.helper(id);
}
// Still null!!
if (helperForNested == null)
{
helperForNested = defaultHelper();
}
return helperForNested;
}
};
}
public ChoicesProvider()
{
super();
if (helper == null)
{
helper = buildhelper();
}
if (helper == null)
{
helper = defaultHelper();
}
}
/**
* Method used to return the defaultHelper()
*
* @return
*/
public final ChoicesHelper defaultHelper()
{
KeyValueChoicesHelper defHelper = new KeyValueChoicesHelper();
return defHelper;
}
public boolean containsNestedHelper(Object id)
{
return helper.containsNestedHelper(id);
}
public Object ObjectOf(String id)
{
return helper.ObjectOf(id);
}
public String displayValueOf(String id, Component component)
{
return helper.displayValueOf(id, component);
}
}
(9)
public abstract class ChoicesModel extends LoadableDetachableModel{
protected static Logger log = LoggerFactory.getLogger(ChoicesModel.class);
public IChoiceRenderer renderer(){
return provider().renderer();
}
@Override
protected Object load() {
return provider().list();
}
/**
* Just to add flexibility of doing stuff before returning an object which is a list here,
* this method makes a call to beforeGetObject() which can be overriden
*/
@Override
public Object getObject() {
beforeGetObject();
return super.getObject();
}
public abstract ChoicesHelper provider();
/**
* Override and do any initialization of other things before returning the nested list object
*/
protected void beforeGetObject(){
}
}
(10)
/**
* For nested choices which are dependent on parents choice
* Intelligent dependent model which provides its listof values based on current value of parent
* @author dontEatToomuch
*
*/
public abstract class DependentChoicesModel extends ChoicesModel
{
public DependentChoicesModel()
{
super();
}
/**
* The key with which this list of choices is linked to something else
* For example you wanted to work with all states of india, you will return "in" for
* getParentDependencyValue() (in the parent list of countries india is represented by "in")
* @return
*/
protected abstract Object getParentDependencyValue();
@Override
public final ChoicesHelper provider()
{
return parentprovider().helper(getParentDependencyValue());
}
protected abstract ChoicesHelper parentprovider();
}
(11)
//start countries-----------------------------------------------------------------------------------------------
public class Countries extends ChoicesProvider{
private static ChoicesHelper locationshelper;
static{
new KeyValueChoicesHelper()
.add(in.class, "India")
.add(usa.class,"United States Of America");
}
@Override
protected ChoicesHelper buildhelper() {
return locationshelper;
}
}
//end countries-------------------------------------------------------------------------------------------------
public static class usa extends ChoicesProvider{
@Override
protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("al", "Alabama")
.add("den", "Denver")
.add("was", "Washington");
}
}
//start india----------------------------------------------------------------------------------------------------
public static class in extends ChoicesProvider{
@Override
protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add(ass.class, "Assam")
.add(ap.class, "Andhra Pradesh")
.add(kar.class, "Karnataka");
}
//start states-------------------------------------------------------------------------------------------------------
public static class ass extends ChoicesProvider{
@Override
protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("dib", "Dibrugarh")
.add("guw", "Guwahati");
}
}
public static class ap extends ChoicesProvider{
@Override
protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("hyd", "Hyderabad");
}
}
public static class kar extends ChoicesProvider{
@Override
protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("ban", "Bangalore");
}
}
//end states------------------------------------------------------------------------------------------------------
}
//end
india----------------------------------------------------------------------------------------------------
(12)
public class Months {
private static List<Integer> monthslst = new ArrayList<Integer>();
private static IChoiceRenderer renderer = null;
static {
for (int i = Calendar.JANUARY; i <= Calendar.DECEMBER; i++) {
monthslst.add(new Integer(i));
}
renderer = new IChoiceRenderer() {
private static final long serialVersionUID = -8125518552105537844L;
public Object getDisplayValue(Object object) {
String month;
final int value = ((Integer) object).intValue();
switch (value) {
case Calendar.JANUARY:
month = "Jan";
break;
case Calendar.FEBRUARY:
month = "Feb";
break;
case Calendar.MARCH:
month = "Mar";
break;
case Calendar.APRIL:
month = "Apr";
break;
case Calendar.MAY:
month = "May";
break;
case Calendar.JUNE:
month = "Jun";
break;
case Calendar.JULY:
month = "Jul";
break;
case Calendar.AUGUST:
month = "Aug";
break;
case Calendar.SEPTEMBER:
month = "Sep";
break;
case Calendar.OCTOBER:
month = "Oct";
break;
case Calendar.NOVEMBER:
month = "Nov";
break;
case Calendar.DECEMBER:
month = "Dec";
break;
default:
throw new IllegalStateException(value + " is not mapped!");
}
return month;
}
public String getIdValue(Object object, int index) {
return object.toString();
}
};
}
public static List<Integer> list() {
return monthslst;
}
public static IChoiceRenderer renderer() {
return renderer;
}
}
(13)
/**
* example::
* Numbers fromChoices = new Numbers(1, MAX_VAL);
* DropDownChoice fromChoice = new DropDownChoice("from", new
PropertyModel(getModel(),"from"), fromChoices, fromChoices.renderer()));
*
* Numbers toChoices = new DependentChoicesModel()
{
@Override
protected Object getParentDependencyValue()
{
return fromChoice.getModelObject();
}
@Override
protected ChoicesHelper parentprovider()
{
return fromChoices.provider();
}
};
* DropDownChoice toChoice = (FormComponent)new DropDownChoice("to", new PropertyModel(
getModel(), "to"), toChoices, toChoices.renderer())
{
@Override
public boolean isEnabled()
{
return ((Integer)fromChoice.getModelObject() > 0);
}
}.setOutputMarkupId(true);
*
*/
public class Numbers extends ChoicesModel
{
private int from;
private int to;
private ChoicesHelper provider = new AbstractChoicesHelper()
{
public boolean containsNestedHelper(Object id)
{
return true;
}
public ChoicesHelper helper(final Object id)
{
return new AbstractChoicesHelper()
{
public boolean containsNestedHelper(Object id)
{
return false;
}
public ChoicesHelper helper(Object id)
{
throw new IllegalArgumentException();
}
public List list()
{
int fromval = from;
if (id != null)
{
fromval = (Integer)id;
}
ArrayList lst = new ArrayList();
for (int i = fromval + 1; i <= to; i++)
{
lst.add(i);
}
return lst;
}
};
}
public List list()
{
ArrayList lst = new ArrayList();
for (int i = from; i <= to; i++)
{
lst.add(i);
}
return lst;
}
};
public Numbers(int from, int to)
{
super();
this.from = from;
this.to = to;
}
@Override
public ChoicesHelper provider()
{
return provider;
}
}
===============================================
As it is obvious implimenting ajax functionality is a breeze...
There is nothing much to blog about it.
=============================================
Note:::
1. I had learnt by forgetting that i often forget this
(needed for updating the components via ajax):: setOutputMarkupId(true);
2. I can use all my utility code for my radio choices as well
3. One should look at these::