I have problems understanding how to use selection in JSF 2 with POJO/entity effectively. For example, I'm trying to select a Warehouse
entity via the below dropdown:
<h:selectOneMenu value="#{bean.selectedWarehouse}">
<f:selectItem itemLabel="Choose one .." itemValue="#{null}" />
<f:selectItems value="#{bean.availableWarehouses}" />
</h:selectOneMenu>
And the below managed bean:
@Named
@ViewScoped
public class Bean {
private Warehouse selectedWarehouse;
private List<SelectItem> availableWarehouses;
// ...
@PostConstruct
public void init() {
// ...
availableWarehouses = new ArrayList<>();
for (Warehouse warehouse : warehouseService.listAll()) {
availableWarehouses.add(new SelectItem(warehouse, warehouse.getName()));
}
}
// ...
}
Notice that I use the whole Warehouse
entity as the value of SelectItem
.
When I submit the form, this fails with the following faces message:
Conversion Error setting value 'com.example.Warehouse@cafebabe' for 'null Converter'.
I was hoping that JSF could just set the correct Warehouse
object to my managed bean when I wrap it in a SelectItem
. Wrapping my entity inside the SelectItem
was meant to skip creating a Converter
for my entity.
Do I really have to use a Converter
whenever I want to make use of entities in my <h:selectOneMenu>
? It should for JSF be possible to just extract the selected item from the list of available items. If I really have to use a converter, what is the practical way of doing it? So far I came up to this:
Converter
implementation for the entity.getAsString()
. I think I don't need this since the label property of the SelectItem
will be used to display the dropdown option label.getAsObject()
. I think this will be used to return the correct SelectItem
or entity depending on the type of the selected field defined in the managed bean. The getAsObject()
confuses me. What is the efficient way to do this? Having the string value, how do I get the associated entity object? Should I query the entity object from the service object based on the string value and return the entity? Or perhaps somehow I can access the list of the entities that form the selection items, loop them to find the correct entity, and return the entity?
What is the normal approach of this?
JSF generates HTML. HTML is in Java terms basically one large String
. To represent Java objects in HTML, they have to be converted to String
. Also, when a HTML form is submitted, the submitted values are treated as String
in the HTTP request parameters. Under the covers, JSF extracts them from the HttpServletRequest#getParameter()
which returns String
.
To convert between a non-standard Java object (i.e. not a String
, Number
or Boolean
for which EL has builtin conversions, or Date
for which JSF provides builtin <f:convertDateTime>
tag), you really have to supply a custom Converter
. The SelectItem
has no special purpose at all. It's just a leftover from JSF 1.x when it wasn't possible to supply e.g. List<Warehouse>
directly to <f:selectItems>
. It has also no special treatment as to labels and conversion.
You need to implement getAsString()
method in such way that the desired Java object is been represented in an unique String
representation which can be used as HTTP request parameter. Normally, the technical ID (the database primary key) is used here.
public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
if (modelValue == null) {
return "";
}
if (modelValue instanceof Warehouse) {
return String.valueOf(((Warehouse) modelValue).getId());
} else {
throw new ConverterException(new FacesMessage(modelValue + " is not a valid Warehouse"));
}
}
Note that returning an empty string in case of a null/empty model value is significant and required by the javadoc. See also Using a "Please select" f:selectItem with null/empty value inside a p:selectOneMenu.
You need to implement getAsObject()
in such way that exactly that String
representation as returned by getAsString()
can be converted back to exactly the same Java object specified as modelValue
in getAsString()
.
public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
if (submittedValue == null || submittedValue.isEmpty()) {
return null;
}
try {
return warehouseService.find(Long.valueOf(submittedValue));
} catch (NumberFormatException e) {
throw new ConverterException(new FacesMessage(submittedValue + " is not a valid Warehouse ID"), e);
}
}
In other words, you must be technically able to pass back the returned object as modelValue
argument of getAsString()
and then pass back the obtained string as submittedValue
argument of getAsObject()
in an infinite loop.
Finally just annotate the Converter
with @FacesConverter
to hook on the object type in question, JSF will then automatically take care of conversion when Warehouse
type ever comes into the picture:
@FacesConverter(forClass=Warehouse.class)
That was the "canonical" JSF approach. It's after all not very effective as it could indeed also just have grabbed the item from the <f:selectItems>
. But the most important point of a Converter
is that it returns an unique String
representation, so that the Java object could be identified by a simple String
suitable for passing around in HTTP and HTML.
JSF utility library OmniFaces has a SelectItemsConverter
which works based on toString()
outcome of the entity. This way you do not need to fiddle with getAsObject()
and expensive business/database operations anymore. For some concrete use examples, see also the showcase.
To use it, just register it as below:
<h:selectOneMenu ... converter="omnifaces.SelectItemsConverter">
And make sure that the toString()
of your Warehouse
entity returns an unique representation of the entity. You could for instance directly return the ID:
@Override
public String toString() {
return String.valueOf(id);
}
Or something more readable/reusable:
@Override
public String toString() {
return "Warehouse[id=" + id + "]";
}
Unrelated to the problem, since JSF 2.0 it's not explicitly required anymore to have a List<SelectItem>
as <f:selectItem>
value. Just a List<Warehouse>
would also suffice.
<h:selectOneMenu value="#{bean.selectedWarehouse}">
<f:selectItem itemLabel="Choose one .." itemValue="#{null}" />
<f:selectItems value="#{bean.availableWarehouses}" var="warehouse"
itemLabel="#{warehouse.name}" itemValue="#{warehouse}" />
</h:selectOneMenu>
private Warehouse selectedWarehouse;
private List<Warehouse> availableWarehouses;