adam bien's blog

Delving into JSF Scaffolding with JPA on Netbeans 6.1 📎

In Netbeans 6.1 the JSF-Crud scaffolding wizard is available again (it disappeared in 6.0). It allows the creation of CRUD-JSF applications from any tables you like. Even the relations will be considered in the generation. The scaffolding is a three-step process:

  1. You will need Netbeans 6.1 beta. After the installation create a WebApplication (Strg+Shift+N -> folder Web -> Webapplication)
  2. Right click on the project and choose: "Entity Classes From Database". This wizard creates JPA-Entities from a JDBC-source. You have just to choose an existing JDBC-source. The persistence.xml will be created for you. Noteworthy: on the second wizard page, the checkbox: "Generate Named Query Annotations For Persistent Fields" can be unchecked. Then all the NamedQueries wouldn't be generated. This shortens the startup and deployment time.
  3. Right click on the project again and choose: "JSF Pages From Entity Class". This will generate JSF pages for existing JPA entities. Then just "Run" the project, and you should be able to browse in the database.

The generation of the entities is clean. Netbeans 6.1 uses field-based injection (what I prefer):

@Entity
@Table(name = "CUSTOMER")
@NamedQueries({

@NamedQuery(name = "Customer.findByCustomerId", query = "SELECT c FROM Customer c WHERE c.customerId = :customerId"),
@NamedQuery(name = "Customer.findByZip", query = "SELECT c FROM Customer c WHERE c.zip = :zip"),
@NamedQuery(name = "Customer.findByName", query = "SELECT c FROM Customer c WHERE c.name = :name"),
@NamedQuery(name = "Customer.findByAddressline1", query = "SELECT c FROM Customer c WHERE c.addressline1 = :addressline1"),
@NamedQuery(name = "Customer.findByFax", query = "SELECT c FROM Customer c WHERE c.fax = :fax"),

(... and remaining queries)})
public class Customer implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Column(name = "CUSTOMER_ID", nullable = false)
    private Integer customerId;
    @Column(name = "ZIP", nullable = false)
    private String zip;
    @Column(name = "NAME")
    private String name;
    @Column(name = "ADDRESSLINE1")
    private String addressline1;

    @JoinColumn(name = "DISCOUNT_CODE", referencedColumnName = "DISCOUNT_CODE")
    @ManyToOne
    private DiscountCode discountCode;
 

//additional attributes

    public Customer() {    }

    public Customer(Integer customerId) {
        this.customerId = customerId;
    }

    public Customer(Integer customerId, String zip) {
        this.customerId = customerId;
        this.zip = zip;
    }

    public Integer getCustomerId() {
        return customerId;
    }

//remaining Getters / Setters
}

 
The persistent Entities are absolutely usable in real-world projects, however some things like inheritance, unidirectional 1:n relations, sometimes just cannot be derived from the schema.

Netbeans generates the glue between the JPA-entities and the JSF-view as well. It generates a controller (the CustomerController):

public class CustomerController {
    private Customer customer = null;
    private List<Customer> customers = null;
    @Resource
    private UserTransaction utx = null;
    @PersistenceUnit(unitName = "JSFScaffoldingPU")
    private EntityManagerFactory emf = null;

    public EntityManager getEntityManager() {
        return emf.createEntityManager();
    }
    public int batchSize = 5;
    private int firstItem = 0;
    private int itemCount = -1;

    public SelectItem[] getCustomersAvailableSelectMany() {
        return getCustomersAvailable(false);
    }

    public SelectItem[] getCustomersAvailableSelectOne() {
        return getCustomersAvailable(true);
    }

    private SelectItem[] getCustomersAvailable(boolean one) {
        List<Customer> allCustomers = getCustomers(true);
        int size = one ? allCustomers.size() + 1 : allCustomers.size();
        SelectItem[] items = new SelectItem[size];
        int i = 0;
        if (one) {
            items[0] = new SelectItem("", "---");
            i++;
        }
        for (Customer x : allCustomers) {
            items[i++] = new SelectItem(x, x.toString());
        }
        return items;
    }

    public Customer getCustomer() {
        if (customer == null) {
            customer = getCustomerFromRequest();
        }
        if (customer == null) {
            customer = new Customer();
        }
        return customer;
    }

    public String listSetup() {
        reset(true);
        return "customer_list";
    }

    public String createSetup() {
        reset(false);
        customer = new Customer();
        return "customer_create";
    }

    public String create() {
        EntityManager em = getEntityManager();
        try {
            utx.begin();
            em.persist(customer);
            DiscountCode discountCode = customer.getDiscountCode();
            if (discountCode != null) {
                discountCode.getCustomerCollection().add(customer);
                discountCode = em.merge(discountCode);
            }
            utx.commit();
            addSuccessMessage("Customer was successfully created.");
        } catch (Exception ex) {
            try {
                ensureAddErrorMessage(ex, "A persistence error occurred.");
                utx.rollback();
            } catch (Exception e) {
                ensureAddErrorMessage(e, "An error occurred attempting to roll back the transaction.");
            }
            return null;
        } finally {
            em.close();
        }
        return listSetup();
    }

    public String detailSetup() {
        return scalarSetup("customer_detail");
    }

    public String editSetup() {
        return scalarSetup("customer_edit");
    }

    private String scalarSetup(String destination) {
        reset(false);
        customer = getCustomerFromRequest();
        if (customer == null) {
            String requestCustomerString = getRequestParameter("jsfcrud.currentCustomer");
            addErrorMessage("The customer with id " + requestCustomerString + " no longer exists.");
            String relatedControllerOutcome = relatedControllerOutcome();
            if (relatedControllerOutcome != null) {
                return relatedControllerOutcome;
            }
            return listSetup();
        }
        return destination;
    }

    public String edit() {
        CustomerConverter converter = new CustomerConverter();
        String customerString = converter.getAsString(FacesContext.getCurrentInstance(), null, customer);
        String currentCustomerString = getRequestParameter("jsfcrud.currentCustomer");
        if (customerString == null || customerString.length() == 0 || !customerString.equals(currentCustomerString)) {
            String outcome = editSetup();
            if ("customer_edit".equals(outcome)) {
                addErrorMessage("Could not edit customer. Try again.");
            }
            return outcome;
        }
        EntityManager em = getEntityManager();
        try {
            utx.begin();
            DiscountCode discountCodeOld = em.find(com.abien.scaffolding.Customer.class, customer.getCustomerId()).getDiscountCode();
            customer = em.merge(customer);
            DiscountCode discountCodeNew = customer.getDiscountCode();
            if (discountCodeOld != null && !discountCodeOld.equals(discountCodeNew)) {
                discountCodeOld.getCustomerCollection().remove(customer);
                discountCodeOld = em.merge(discountCodeOld);
            }
            if (discountCodeNew != null && !discountCodeNew.equals(discountCodeOld)) {
                discountCodeNew.getCustomerCollection().add(customer);
                discountCodeNew = em.merge(discountCodeNew);
            }
            utx.commit();
            addSuccessMessage("Customer was successfully updated.");
        } catch (Exception ex) {
            try {
                String msg = ex.getLocalizedMessage();
                if (msg != null && msg.length() > 0) {
                    addErrorMessage(msg);
                } else if (getCustomerFromRequest() == null) {
                    addErrorMessage("The customer with id " + currentCustomerString + " no longer exists.");
                    utx.rollback();
                    return listSetup();
                } else {
                    addErrorMessage("A persistence error occurred.");
                }
                utx.rollback();
            } catch (Exception e) {
                ensureAddErrorMessage(e, "An error occurred attempting to roll back the transaction.");
            }
            return null;
        } finally {
            em.close();
        }
        return detailSetup();
    }

    public String destroy() {
        customer = getCustomerFromRequest();
        if (customer == null) {
            String currentCustomerString = getRequestParameter("jsfcrud.currentCustomer");
            addErrorMessage("The customer with id " + currentCustomerString + " no longer exists.");
            String relatedControllerOutcome = relatedControllerOutcome();
            if (relatedControllerOutcome != null) {
                return relatedControllerOutcome;
            }
            return listSetup();
        }
        EntityManager em = getEntityManager();
        try {
            utx.begin();
            customer = em.merge(customer);
            DiscountCode discountCode = customer.getDiscountCode();
            if (discountCode != null) {
                discountCode.getCustomerCollection().remove(customer);
                discountCode = em.merge(discountCode);
            }
            em.remove(customer);
            utx.commit();
            addSuccessMessage("Customer was successfully deleted.");
        } catch (Exception ex) {
            try {
                ensureAddErrorMessage(ex, "A persistence error occurred.");
                utx.rollback();
            } catch (Exception e) {
                ensureAddErrorMessage(e, "An error occurred attempting to roll back the transaction.");
            }
            return null;
        } finally {
            em.close();
        }
        String relatedControllerOutcome = relatedControllerOutcome();
        if (relatedControllerOutcome != null) {
            return relatedControllerOutcome;
        }
        return listSetup();
    }

    private Customer getCustomerFromRequest() {
        String theId = getRequestParameter("jsfcrud.currentCustomer");
        return (Customer) new CustomerConverter().getAsObject(FacesContext.getCurrentInstance(), null, theId);
    }

    private String getRequestParameter(String key) {
        return FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get(key);
    }

    public List<Customer> getCustomers() {
        if (customers == null) {
            customers = getCustomers(false);
        }
        return customers;
    }

    public List<Customer> getCustomers(boolean all) {
        EntityManager em = getEntityManager();
        try {
            Query q = em.createQuery("select object(o) from Customer as o");
            if (!all) {
                q.setMaxResults(batchSize);
                q.setFirstResult(getFirstItem());
            }
            return q.getResultList();
        } finally {
            em.close();
        }
    }

    private void ensureAddErrorMessage(Exception ex, String defaultMsg) {
        String msg = ex.getLocalizedMessage();
        if (msg != null && msg.length() > 0) {
            addErrorMessage(msg);
        } else {
            addErrorMessage(defaultMsg);
        }
    }

    public static void addErrorMessage(String msg) {
        FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg);
        FacesContext.getCurrentInstance().addMessage(null, facesMsg);
    }

    public static void addSuccessMessage(String msg) {
        FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg);
        FacesContext.getCurrentInstance().addMessage("successInfo", facesMsg);
    }

    public Customer findCustomer(Integer id) {
        EntityManager em = getEntityManager();
        try {
            Customer o = (Customer) em.find(Customer.class, id);
            return o;
        } finally {
            em.close();
        }
    }

    public int getItemCount() {
        if (itemCount == -1) {
            EntityManager em = getEntityManager();
            try {
                itemCount = ((Long) em.createQuery("select count(o) from Customer as o").getSingleResult()).intValue();
            } finally {
                em.close();
            }
        }
        return itemCount;
    }

    public int getFirstItem() {
        getItemCount();
        if (firstItem >= itemCount) {
            if (itemCount == 0) {
                firstItem = 0;
            } else {
                int zeroBasedItemCount = itemCount - 1;
                double pageDouble = zeroBasedItemCount / batchSize;
                int page = (int) Math.floor(pageDouble);
                firstItem = page * batchSize;
            }
        }
        return firstItem;
    }

    public int getLastItem() {
        getFirstItem();
        return firstItem + batchSize > itemCount ? itemCount : firstItem + batchSize;
    }

    public int getBatchSize() {
        return batchSize;
    }

    public String next() {
        reset(false);
        getFirstItem();
        if (firstItem + batchSize < itemCount) {
            firstItem += batchSize;
        }
        return "customer_list";
    }

    public String prev() {
        reset(false);
        getFirstItem();
        firstItem -= batchSize;
        if (firstItem < 0) {
            firstItem = 0;
        }
        return "customer_list";
    }

    private String relatedControllerOutcome() {
        String relatedControllerString = getRequestParameter("jsfcrud.relatedController");
        String relatedControllerTypeString = getRequestParameter("jsfcrud.relatedControllerType");
        if (relatedControllerString != null && relatedControllerTypeString != null) {
            FacesContext context = FacesContext.getCurrentInstance();
            Object relatedController = context.getApplication().getELResolver().getValue(context.getELContext(), null, relatedControllerString);
            try {
                Class<?> relatedControllerType = Class.forName(relatedControllerTypeString);
                Method detailSetupMethod = relatedControllerType.getMethod("detailSetup");
                return (String) detailSetupMethod.invoke(relatedController);
            } catch (ClassNotFoundException e) {
                throw new FacesException(e);
            } catch (NoSuchMethodException e) {
                throw new FacesException(e);
            } catch (IllegalAccessException e) {
                throw new FacesException(e);
            } catch (InvocationTargetException e) {
                throw new FacesException(e);
            }
        }
        return null;
    }

    private void reset(boolean resetFirstItem) {
        customer = null;
        customers = null;
        itemCount = -1;
        if (resetFirstItem) {
            firstItem = 0;
        }
    }
    private Map<Customer, String> asString = null;

    public Map<Customer, String> getAsString() {
        if (asString == null) {
            asString = new HashMap<Customer, String>() {

                @Override
                public String get(Object key) {
                    return new CustomerConverter().getAsString(FacesContext.getCurrentInstance(), null, (Customer) key);
                }
            };
        }
        return asString;
    }
    private Validator entityCreationValidator = null;

    public Validator getEntityCreationValidator() {
        if (entityCreationValidator == null) {
            entityCreationValidator = new Validator() {

                public void validate(FacesContext facesContext, UIComponent component, Object value) {
                    CustomerConverter converter = new CustomerConverter();
                    String newCustomerString = converter.getAsString(FacesContext.getCurrentInstance(), null, new Customer());
                    String customerString = converter.getAsString(FacesContext.getCurrentInstance(), null, customer);
                    if (!newCustomerString.equals(customerString)) {
                        createSetup();
                        throw new ValidatorException(new FacesMessage("Could not create customer. Try again."));
                    }
                }
            };
        }
        return entityCreationValidator;
    }

}

 Registers it as a managed-bean in the faces-config.xml:

<faces-config version="1.2"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
    <managed-bean>
        <managed-bean-name>customer</managed-bean-name>
        <managed-bean-class>com.abien.scaffolding.CustomerController</managed-bean-class>
        <managed-bean-scope>session</managed-bean-scope>
    </managed-bean>

</faces-config>

 The CustomerController is a POJO. So the transaction management, resource management are implemented inside. The CustomerController is the mediator between the view and the persistence. However it implements the presentation and some business logic as well. For every entity a converter class is created:

public class CustomerConverter implements Converter {

    public Object getAsObject(FacesContext facesContext, UIComponent component, String string) {
        if (string == null || string.length() == 0) {
            return null;
        }
        Integer id = new Integer(string);
        com.abien.scaffolding.CustomerController controller = (com.abien.scaffolding.CustomerController) facesContext.getApplication().getELResolver().getValue(facesContext.getELContext(), null, "customer");

        return controller.findCustomer(id);
    }

    public String getAsString(FacesContext facesContext, UIComponent component, Object object) {
        if (object == null) {
            return null;
        }
        if (object instanceof Customer) {
            Customer o = (Customer) object;
            return o.getCustomerId() == null ? "" : o.getCustomerId().toString();
        } else {
            throw new IllegalArgumentException("object:" + object + " of type:" + object.getClass().getName() + "; expected type: com.abien.scaffolding.Customer");
        }
    }

The CustomerConverter is a javax.faces.convert.Converter, his responsibility is the conversion of domain objects and flat string. The javadoc says: "Converter is an interface describing a Java class that can perform Object-to-String and String-to-Object conversions between model data objects and a String representation of those objects that is suitable for rendering"

Netbeans 6.1 cares about the registration of the converter in the faces-config.xml:

    <converter>
        <converter-for-class>com.abien.scaffolding.Customer</converter-for-class>
        <converter-class>com.abien.scaffolding.CustomerConverter</converter-class>
    </converter>

Netbeans 6.1 scaffolding is an interesting way to start with JSF programming to learn basic JSF and JPA stuff. Some thoughts:

 

  1. Also the JSF editor is really useful (syntax highligting, code HTML and Java code completion), I wasn't able to edit them visually afterwards. This can be a serious limitation.
  2. The controller is a POJO. So it has to care about the transactions and resource management. The usage of EJB 3 would result in much less code. However it isn't possible now to register an EJB 3 in the faces-config.xml. WebBeans and Seam will be able to provide this, but now it isn't just possible with standard means... However the Controller could act as a mediator and delegate the invocations to an Stateless Session Bean (I'm thinking seriously about hacking/extending the wizard)
  3. The controller mixes the presentation and business logic - so it isn't a best practice - but good enough for hacking a smaller project.
  4. I had some trouble with imports. In the Controllers/Converters the package statement was incorrect. (scaffolding was swallowed. com.abien -> com.abien.scaffolding). I had to change it manually. This is a new problem - in 5.5.1 it worked just well.
Btw. This functionality comes with Netbeans 6.1 EE (the left choice) - no additional plugins are required :-).