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