Friday, December 28, 2007

Playing with Wicket((The component oriented web application framework)) Components: Part 1 :Choices

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::

  1. systemmobile link--basics on dropdown
  2. wicket wiki link
  3. on wicket website
  4. system mobile link--chaining choices- country,state example



4 comments:

Mayank said...

Just want to make one comment(request) here..improving the formatting
and readability would help a lot
..For me it worked OK as i have been through some warming up in my wicket based project

...But dude, this one is a very useful post.
I sure found a lot of code I can reuse. Let me take some time and i will definitely come
with some areas of improvement in your classes..

Alex said...

I would be nice if your idea would be included in the wicket core, or at least in extension. This is because, working with DropDownChoice component is the most confusing for newbies.

DontEatTooMuch said...

I have made the code more readable.
Also added ListChoicesHelper and few more like Months,Numbers,..which show example usage.
Now for me all these classes cater for 90% of the use case..
I have no idea of the wicket-standards or the kind of code worth going in to core/extensions etc..probably a commiter can say on that.

Unknown said...

you have been thoofed