selectOneListbox - JSF - Java Server Faces

26.02.2013

JSF - <h:selectOneListbox/>-Beispiel

In dem folgenden Beispiel wird erklärt, wie eine selectOneListbox mit einer Liste von Datenbank-Entities verbunden wird. Genauer gesagt, soll innerhalb einer JSF-Seite eine Auswahlbox angezeigt werden, die ihre SelectItem-Elemente über eine Service-Schicht (Controller) direkt aus einer Datenbank bezieht. Zwischen der JSF-Oberfläche und dem Service wird eine Liste mit Pojos/Beans/Entites übergeben (z.B. List<Customer>). Da die Customer-Elemente nicht so einfach übertragen werden können, kommt ein javax.faces.convert.Converter (@FacesConverter) zum Einsatz.

Als Beispiel wird eine JSF-Seite erstellt, die eine Liste mit Customer-Objekten anzeigt. Aus der Liste soll ein Customer ausgewählt und mit einem commandButton (Select) bestätigt werden. Daraufhin wird über ein outputLabel der ausgewählte Customer unterhalb der selectOneListbox angezeigt.

Prinzipiell ist das Verbinden der SelectBox mit dem Datenbank-Entity kein Problem. Eine Sache, die allerdings beachtet werden muss ist, dass die Methoden equals und hashCode der Entity-Klasse überschrieben (@Overwrite) werden müssen. Der Grund dafür ist, dass in der generierten HTML-Seite nur Strings angezeigt werden. D.h. diese Strings werden nach dem Klick auf den Select-Button auch wieder zurück an den Server geschickt und müssen dort natürlich zugeordnet werden können. Dafür werden die Methoden equals und hashCode benötigt.

Die fertige Beispielanwendung ist auf folgenden Screenshot zu sehen:

Es wird also eine <h:SelectOneListbox/> erstellt, über die ein selectItem ausgewählt werden kann. Durch ein Klick auf den Button Select wird der Text hinter Select item: angepasst.

Bei ersten Start wird allerdings eine Seite mit einem Button Create Customers angezeigt. Durch einen Klick auf den Button werden einige Entity-Elemente vom Type Customer in der Datenbank angelegt.

Als erstes wird ein Entity von Type Customer angelegt:

package org.hameister.selectbox;

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

@NamedQueries ({
@NamedQuery(name = "findAllCustomers", query = "SELECT c FROM Cust c"),
@NamedQuery(name = "findCustomerByName", query = "SELECT c FROM Cust c WHERE c.customerName = :customerName")
})
@Entity(name="Cust")
public class Customer implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String customerName;

    public Customer() {
    }

    public Customer(String customerName) {
        this.customerName = customerName;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    /**
     * Needed for the binding with the h:selectOneListbox.
     */
    @Override
    public boolean equals(Object other) {
        return (other instanceof Customer) && (customerName != null)
                ? customerName.equals(((Customer) other).customerName)
                : (other == this);
    }

    /**
     * Needed for the binding with the h:selectOneListbox.
     */
    @Override
    public int hashCode() {
        return (customerName != null)
                ? (this.getClass().hashCode() + customerName.hashCode())
                : super.hashCode();
    }
}

Der Customer besitzt nur das Attribute customerName. Zu beachten sind die NamedQueries am Anfang der Klasse. Diese werden von der Service-Schicht (CustomerService) verwendet. Wichtig sind die Methoden equals und hashCode. Sie müssen überschrieben werden, damit das Binding zwischen JSF-Seite, Konverter und Entity funktioniert.

Als nächstes wird ein @FacesConverter für die Klasse Customer angelegt. In dem Konverter muss das Interface javax.faces.convert.Converter mit seinen beiden Methoden implementiert werden.

package org.hameister.selectbox;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.FacesConverter;
import javax.inject.Inject;
import javax.inject.Named;

@FacesConverter(forClass=Customer.class)
@Named
public class CustomerConverter implements Converter {

    @Inject 
    CustomerService customerService;
    
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String customerName) {
        return customerService.findCustomerByName(customerName);
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object customer) {
        return ((Customer)customer).getCustomerName();
    }  
}

Zum Registrieren des Konverters wird die Annotation @FacesConverter verwendet. Damit der Konverter in der JSF-Seite erreichbar ist, wird die Annotation @Named benötigt.

Die Methode getAsObject wandelt den String in ein Object von Type Customer um. Dazu wird der customerName verwendet, um den customerService nach dem Customer-Objekt zu fragen.

Der customerService wird per Dependency-Injection (CDI) in den Converter injiziert.

Die zweite Methode getAsString konvertiert das Objekt customer in einen String. Dazu wird einfach die Methode getCustomerName() aufgerufen.

Zum Schluss wird noch eine Stateless Session Bean benötigt, die als Controller zwischen der JSF-View und JPA-Model dient.

package org.hameister.selectbox;

import java.io.Serializable;
import java.util.List;
import javax.ejb.Stateless;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Stateless
@Named
public class CustomerService implements Serializable {

    private Customer customer;
    
    @PersistenceContext
    EntityManager em;

    public List<Customer> getCustomers() {
        return em.createNamedQuery("findAllCustomers").getResultList();
    }

    public Customer findCustomerByName(String name) {
        List<Customer> customers = em.createNamedQuery("findCustomerByName").setParameter("customerName", name).getResultList();
        if (customers != null && customers.size() >= 1) {
            return customers.get(0);
        }
        return null;
    }

    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }

    public void store() {
        System.out.println("======> Store customer:" + customer.getCustomerName());
    }

    public void createTestCustomers() {
        em.persist(new Customer("Customer 1"));
        em.persist(new Customer("Customer 2"));
        em.persist(new Customer("Customer 3"));
        em.persist(new Customer("Customer 4"));
        em.persist(new Customer("Customer 5"));        
    }
}

In der Variable customer wird der in der SelectOneListbox ausgewählte Customer abgespeichert. Für diese Variable wird ein getter und ein setter benötigt, damit die JSF-Seite darauf zugreifen kann. Für den Zugriff auf die Datenbank wird mit der Annotation @PersistenceContext der EntityManager mittels CDI injiziert. Damit die JSF-Seite auf die Customer-Objekte zugreifen kann, werden diese in der Methode getCustomers() mit einer NamedQuery ausgelesen. Die Methode findCustomerByName wird, wie oben schon beschrieben, vom Konverter aufgerufen. Die Funktion store() wird vom select-Button aufgerufen, wenn er angeklickt wird. Mit der Methode createTestCustomer() werden fünf Customer-Objekte erstellt und in der Datenbank abgespeichert. Diese Methode wird von dem Create Customer-Button aufgerufen, der nur angezeigt wird, wenn noch keine Daten in der Datenbank sind.

Was jetzt noch fehlt ist die JSF-Seite, d.h. die View mit der selectOneListBox.

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jsp/jstl/core">
    <h:head>
        <title>Item Selection with selectOneListbox</title>
    </h:head>
    <h:body>
        <h:form>
            <c:choose>
                <c:when test="${empty customerService.customers}">
                    <h:commandButton action="#{customerService.createTestCustomers()}" value="Create Customers"/>
                </c:when>
            </c:choose>
            <br/>

            <c:choose>
                <c:when test="${not empty customerService.customers}">
                    <p>
                        <strong><h:outputLabel value="Please select an item: "/></strong>
                        <h:selectOneListbox size="1" value="#{customerService.customer}" converter="#{customerConverter}">
                            <f:selectItems value="#{customerService.customers}" var="c" itemLabel="#{c.customerName}" itemValue="#{c}"/>
                        </h:selectOneListbox>
                        <h:commandButton action="#{customerService.store()}" value="Select"/>
                    </p>
                    <p>
                        <strong><h:outputLabel value="Selected item: "/></strong>
                        <h:outputLabel value="#{customerService.customer.customerName}"/>
                    </p>
                </c:when>
            </c:choose>
        </h:form>
    </h:body>
</html>

Im oberen choose-Block wird der Button Create Customers nur eingeblendet, wenn sich noch keine Customer-Objekte in der Datenbank befinden.

Falls sich schon Objekte in der Datenbank befinden, wird ein outputLabel, die selectOneListbox und ein commandButton in der ersten Zeile angezeigt. In der zweiten Zeile werden zwei outputLabels benutzt, um den selektierten Customer anzuzeigen.

Bei der selectOneListBox sind folgende Attribute zu beachten:

  • selectOneListbox:
    • value="#{customerService.customer}": Das ist das Binding an die Variable Customer in dem CustomerServer, welches die getter und setter aufruft.
    • converter="#{customerConverter}": Damit wird der Konverter verbunden.
  • selectItems:
    • value="#{customerService.customers}": Aufruf von getCustomers() im CustomerService, d.h. das Befüllen der Combobox.
    • var="c": Das "selektierte" Customer-Objekt, welches mit dem setter an den CustomerService übergeben wird.
    • itemLabel="#{c.customerName}": Auslesen des customerName zum Anzeigen in der Combobox.
    • itemValue="#{c}": Der Wert, d.h. das Customer-Objekt

Die Datei persistenc.xml für Eclipselink sieht folgendermaßen aus. Allerdings funktioniert das Beispiel nach kleinen Anpassungen auch mit Hibernate.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="SelectBoxExamplePU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>jdbc/sample</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>