Senin, 27 Januari 2014

Objects in h:selectOneMenu



WARNING - OUTDATED CONTENT!


This article is targeted on JSF 1.2. For JSF 2.0, using objects in UISelectOne and UISelectMany components can be approached much more elegantly with help of OmniFaces SelectItemsConverter without the need to write a custom converter.








Introduction


A HTML response is in Object Oriented perspective nothing less or more than one big String value (technically: a long character array). Non-String-typed Java Objects are forced to their String representation using Object#toString() when they are about to be written to the HTML response. That's why using custom Object types in a UISelectOne or UISelectMany component, for example h:selectOneMenu, can drive unaware JSF developers nuts. If you gently have attached a to the component for error messages, or added the tag to the page for debugging purposes, then you will likely get the following error message:




Conversion Error setting value 'mypackage.Foo@1234567' for 'null Converter'.



That roughly means that JSF cannot convert the given String value "mypackage.Foo@1234567" (which is just the String representation of the Object, obtained by Object#toString()) to the Object type which is expected by the valuebinding of the component, e.g. mypackage.Foo. The message also indicates that JSF couldn't find a suitable converter for it, pointing to the null ID in the 'null Converter' message part. Generally this will only occur if those objects are not of a String type and JSF does not have a built-in converter for it as well. That's why, next to plain String values, for example the types of the Number superclass just works in the h:selectOneMenu. At least in the newer builds of JSF, the 1.1_02 and 1.2_02 or later, due to some coerce bugs with Number types in SelectItem in the older builds.


There are two general solutions for this conversion problem: 1) implement javax.faces.convert.Converter and write logic which converts between String (or Number) and the desired Object. 2) maintain a backing map with an unique String (or Number) representation as key and the appropriate Object as value. Both approaches will be descibed here in detail.


Back to top


Using a Converter


Using a Converter is fairly simple. You just need to write some logic how to Convert between String and the desired Object type. Instead of String you can also use any Number type, for example Long, because JSF already has built-in converters for it.


Let's start with a relevant part of the JSF page:




value="#{myBean.selectedItem}">
value="#{myBean.selectItems}" />
converterId="fooConverter" />

value="submit" action="#{myBean.action}" />



Note the f:converter facet. The value of its converterId attribute must match with one of the converters as definied in the faces-config.xml. The faces-config.xml example will be shown later in this paragraph.


And here is the appropriate backing bean MyBean:



package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

// Init ---------------------------------------------------------------------------------------

private static FooDAO fooDAO = new FooDAO();
private List selectItems;
private Foo selectedItem;

{
fillSelectItems();
}

// Actions ------------------------------------------------------------------------------------

public void action() {
System.out.println("Selected Foo item: " + selectedItem);
}

// Getters ------------------------------------------------------------------------------------

public List getSelectItems() {
return selectItems;
}

public Foo getSelectedItem() {
return selectedItem;
}

// Setters ------------------------------------------------------------------------------------

public void setSelectedItem(Foo selectedItem) {
this.selectedItem = selectedItem;
}

// Helpers ------------------------------------------------------------------------------------

private void fillSelectItems() {
selectItems = new ArrayList();
for (Foo foo : fooDAO.list()) {
selectItems.add(new SelectItem(foo, foo.getValue()));
}
}

}


This is how the Foo object type look like. It is actually a random Data Transfer Object (DTO). Please note the Object#equals() implementation. This is very important for JSF. After conversion, it will compare the selected item against the items in the list. As the Object#equals() also require Object#hashCode(), this is implemented as well.



package mypackage;

public class Foo {

// Init ---------------------------------------------------------------------------------------

private String key; // You can also use any Number type, e.g. Long.
private String value;

// Constructors -------------------------------------------------------------------------------

public Foo() {
// Default constructor, keep alive.
}

public Foo(String key, String value) {
this.key = key;
this.value = value;
}

// Getters ------------------------------------------------------------------------------------

public String getKey() {
return key;
}

public String getValue() {
return value;
}

// Setters ------------------------------------------------------------------------------------

public void setKey(String key) {
this.key = key;
}

public void setValue(String value) {
this.value = value;
}

// Helpers ------------------------------------------------------------------------------------

// This must return true for another Foo object with same key/id.
public boolean equals(Object other) {
return other instanceof Foo && (key != null) ? key.equals(((Foo) other).key) : (other == this);
}

// This must return the same hashcode for every Foo object with the same key.
public int hashCode() {
return key != null ? this.getClass().hashCode() + key.hashCode() : super.hashCode();
}

// Override Object#toString() so that it returns a human readable String representation.
// It is not required by the Converter or so, it just pleases the reading in the logs.
public String toString() {
return "Foo[" + key + "," + value + "]";
}

}


And here is a fake DAO which maintains the Foo DTO's. Take note that the database is simulated by a static backing map, this doesn't need to occur in real life. It should actually be mapped to a database or maybe even configuration files or so. For more information about the DAO pattern, check this article: DAO tutorial - the data layer.



package mypackage;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class FooDAO {

// Init ---------------------------------------------------------------------------------------

private static Map fooMap;

static {
loadFooMap(); // Preload the fake database.
}

// Actions ------------------------------------------------------------------------------------

public Foo find(String key) {
return fooMap.get(key);
}

public List list() {
return new ArrayList(fooMap.values());
}

public Map map() {
return fooMap;
}

// Helpers ------------------------------------------------------------------------------------

private static void loadFooMap() {
// This is just a fake database. We're using LinkedHashMap as it maintains the ordering.
fooMap = new LinkedHashMap();
fooMap.put("fooKey1", new Foo("fooKey1", "fooValue1"));
fooMap.put("fooKey2", new Foo("fooKey2", "fooValue2"));
fooMap.put("fooKey3", new Foo("fooKey3", "fooValue3"));
}

}


Finally here is the long-awaiting FooConverter which can be used in JSF components. It converts from Foo to String and vice versa. You can attach it to almost any HTML input or output component using the converter attribute or using the f:converter facet.



package mypackage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public class FooConverter implements Converter {

// Init ---------------------------------------------------------------------------------------

private static FooDAO fooDAO = new FooDAO();

// Actions ------------------------------------------------------------------------------------

public Object getAsObject(FacesContext context, UIComponent component, String value) {
// Convert the unique String representation of Foo to the actual Foo object.
return fooDAO.find(value);
}

public String getAsString(FacesContext context, UIComponent component, Object value) {
// Convert the Foo object to its unique String representation.
return ((Foo) value).getKey();
}

}

Take care with runtime exceptions as NullPointerException, ClassCastException and maybe some exceptions which can be thrown by your DAO. Those have to be checked and/or caught and should be thrown as a new ConverterException with the appropriate message which would be shown in the attached h:message(s) tag.



The MyBean and the FooConverter are definied in the faces-config.xml as follows:




fooConverter
mypackage.FooConverter


myBean
mypackage.MyBean
request


You can however also use converter-for-class instead of converter-id so that JSF will always use the specified converter class for the given class. This way you don't need to specify the f:converter in your JSF code.




mypackage.Foo
mypackage.FooConverter


That's all, folks!


Back to top


Using a backing Map


You can also decide to use a so-called backing Map to maintain String-Object pairs and convert against it in the backing bean logic. The String keys can be used in the view layer (JSF pages) and the Object values can be used in the business and data layers (backing beans and DAO's). You can also use Number-Object pairs however.


The relevant part of the JSF page is almost the same as in the case of using a converter, only the f:converter tag is removed:




value="#{myBean.selectedItem}">
value="#{myBean.selectItems}" />

value="submit" action="#{myBean.action}" />



And here is the appropriate backing bean MyBean, it has a static Map of String-Foo pairs. It's your choice how you would load/maintain it, this is just a basic example. Take note that the selectedItem is now a String instead of Foo.



package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

// Init ---------------------------------------------------------------------------------------

private static Map fooMap = new FooDAO().map();
private List selectItems;
private String selectedItem; // You can also use any Number type, e.g. Long.

{
fillSelectItems();
}

// Actions ------------------------------------------------------------------------------------

public void action() {
System.out.println("Selected Foo item: " + fooMap.get(selectedItem));
}

// Getters ------------------------------------------------------------------------------------

public List getSelectItems() {
return selectItems;
}

public String getSelectedItem() {
return selectedItem;
}

// Setters ------------------------------------------------------------------------------------

public void setSelectedItem(String selectedItem) {
this.selectedItem = selectedItem;
}

// Helpers ------------------------------------------------------------------------------------

private void fillSelectItems() {
selectItems = new ArrayList();
for (Foo foo : fooMap.values()) {
selectItems.add(new SelectItem(foo.getKey(), foo.getValue()));
}
}

}


That's it! You can just reuse the Foo DTO, the FooDAO DAO and the faces-config.xml snippet from the Using a Converter paragraph here above. No FooConverter is needed in here and you can also safely remove it from the faces-config.xml.


Back to top


Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.


(C) September 2007, BalusC



Source:http://balusc.blogspot.com/2007/09/objects-in-hselectonemenu.html

Tidak ada komentar:

Posting Komentar