Form dinamiche con PrimeFaces

Nell’ultimo post ho cercato di spiegare come ottenere delle datatable dinamiche con PrimeFaces e la Reflection. In questo nuovo articolo utilizzeremo la stessa tecnica per creare delle form di dettaglio dinamiche. Per fare ciò utilizzeremo il componente DynaForm del progetto PrimeFaces Extensions. Come potete vedere nei vari esempi presenti nello showcase, la DynaForm è utile quando abbiamo una form descritta dinamicamente come ad esempio da database o da un file di configurazione. Cerchermo con lo stesso codice di gestire due POJO differenti: User e Car.

[code lang=”java”]
public class User implements Serializable{
private static final long serialVersionUID = 1L;
private static List<User> users = new ArrayList<User>();
private Integer id;
private String lastName;
private String firstName;
static{
users.add(new User(0, "Solid","Snake"));
users.add(new User(1, "Vulcan","Raven"));
users.add(new User(2, "Meryl","Silverburgh"));
users.add(new User(3, "Hal","Emmerich"));
users.add(new User(4, "Frank","Jaeger"));
}
public User(Integer id, String firstName, String lastName) {
super();
this.id = id;
this.lastName = lastName;
this.firstName = firstName;
}
public User() {}
public static List<User> getAll(){
return users;
}
public static User get(final Integer id){
return (User) CollectionUtils.find(users, new Predicate() {
public boolean evaluate(Object object) {
return ((User) object).getId().equals(id);
}
});
}
public static User store(User p){
if(p.getId() == null){
User maxUserId = Collections.max(users, new Comparator<User>() {
public int compare(User o1, User o2) {
return o1.getId().compareTo(o2.getId());
}
});
p.setId(maxUserId.getId()+1);
users.add(p);
}else{
users.set(p.getId(), p);
}
return p;
}
public static void delete(User p){
users.remove(p);
}
//getter e setters
}
[/code]
[code lang=”java”]
public class Car implements Serializable{
private static final long serialVersionUID = 1L;
private static List<Car> cars = new ArrayList<Car>();
private Integer id;
private String brand;
private String color;
private Integer year;
private String notes;
private boolean used;
static{
cars.add(new Car(0, "Honda","Yellow",1995,false,"Broken Brakes"));
cars.add(new Car(1, "Volvo","Black",1973,true));
cars.add(new Car(1, "Audi","Silver",1987,false));
cars.add(new Car(1, "Renault","White",1963,true));
cars.add(new Car(1, "Volkswagen","Black",1985,true));
}
public Car(Integer id, String brand, String color, Integer year, boolean used,String notes) {
super();
this.id = id;
this.brand = brand;
this.color = color;
this.year = year;
this.used = used;
this.notes = notes;
}
public Car(Integer id, String brand, String color, Integer year, boolean used) {
this(id,brand,color,year,used,"");
}
public Car() {}
public static List<Car> getAll(){
return cars;
}
public static Car get(final Integer id){
return (Car) CollectionUtils.find(cars, new Predicate() {
public boolean evaluate(Object object) {
return ((Car) object).getId().equals(id);
}
});
}
public static Car store(Car p){
if(p.getId() == null){
Car maxUserId = Collections.max(cars, new Comparator<Car>() {
public int compare(Car o1, Car o2) {
return o1.getId().compareTo(o2.getId());
}
});
p.setId(maxUserId.getId()+1);
cars.add(p);
}else{
cars.set(p.getId(), p);
}
return p;
}
public static void delete(Car p){
cars.remove(p);
}
//getter e setters
}
[/code]

Per sfruttare la DynaForm dobbiamo creare un’instanza di DynaFormModel. Questo oggetto è composto da una serie di DynaFormRow. Il nostro scopo è quindi quello di creare una DynaFormRow per ogni proprietà accessibile dei nostri modelli, in maniera del tutto analoga a quanto analizzato nel precedente post. Per farlo utilizzeremo il seguente Builder.

[code lang=”java”]
public class ReflectionDynaFormModelBuilder {
private Class modelClass;
private Comparator<PropertyDescriptor> propertySortComparator;
private Predicate propertyFilterPredicate;
private Set<String> excludedProperties;
private static Set<String> defaultExcludedProperties = new HashSet<String>(0);
private Map<String,FormControlBuilder> customBuilders = new HashMap<String, FormControlBuilder>();
public static Comparator<PropertyDescriptor> DEFAULT_PROPERTY_COMPARATOR = new Comparator<PropertyDescriptor>() {
public int compare(PropertyDescriptor o1, PropertyDescriptor o2) {
return o1.getName().compareTo(o2.getName());
}
};
static{
defaultExcludedProperties.add("class");
}
public ReflectionDynaFormModelBuilder(Class modelClass) {
this.modelClass = modelClass;
this.propertyFilterPredicate = PredicateUtils.truePredicate();
this.propertySortComparator = DEFAULT_PROPERTY_COMPARATOR;
this.excludedProperties = new HashSet<String>(0);
}
public ReflectionDynaFormModelBuilder setPropertyFilterPredicate(Predicate p){
this.propertyFilterPredicate = p;
return this;
}
public ReflectionDynaFormModelBuilder setPropertySortComparator(Comparator<PropertyDescriptor> c){
this.propertySortComparator = c;
return this;
}
public ReflectionDynaFormModelBuilder setExcludedProperties(Set<String> p){
this.excludedProperties = p;
return this;
}
public ReflectionDynaFormModelBuilder putCustomBuilder(String name,FormControlBuilder builder){
this.customBuilders.put(name, builder);
return this;
}
public ReflectionDynaFormModelBuilder putCustomBuilders(Map<String,FormControlBuilder> builders){
this.customBuilders.putAll(builders);
return this;
}
public ReflectionDynaFormModelBuilder setExcludedProperties(String…p){
this.excludedProperties = new HashSet<String>(0);
for (String excludedProperty : p) {
this.excludedProperties.add(excludedProperty);
}
return this;
}
public DynaFormModel build(){
DynaFormModel formModel = new DynaFormModel();
List<PropertyDescriptor> propertyDescriptors = new ArrayList<PropertyDescriptor>(Arrays.asList(PropertyUtils.getPropertyDescriptors(modelClass)));
CollectionUtils.filter(propertyDescriptors, PredicateUtils.andPredicate(propertyFilterPredicate, new Predicate() {
public boolean evaluate(Object object) {
PropertyDescriptor propertyDescriptor = (PropertyDescriptor) object;
return
propertyDescriptor.getReadMethod() != null &&
propertyDescriptor.getWriteMethod() != null &&
!defaultExcludedProperties.contains(propertyDescriptor.getName()) &&
!excludedProperties.contains(propertyDescriptor.getName());
}
}));
Collections.sort(propertyDescriptors, propertySortComparator);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
DynaFormRow row = formModel.createRegularRow();
if(customBuilders.containsKey(propertyDescriptor.getName())){
customBuilders.get(propertyDescriptor.getName()).populateRow(row);
}else{
//Default Row
DynaFormLabel label = row.addLabel(propertyDescriptor.getName());
DynaFormControl input = row.addControl(new DynaPropertyModel(propertyDescriptor.getName()), propertyDescriptor.getPropertyType().getSimpleName().toLowerCase());
label.setForControl(input);
}
}
return formModel;
}
}
[/code]

Come potete vedere, per ogni proprietà abbiamo generato un DynaFormRow, una DynaFormLabel ma soprattutto un DynaFormControl. Abbiamo anche creato un’istanza di DynaPropertyModel: questa classe gestisce tutti i metadati della proprietà corrente. In questa prima versione contiene semplicemente il nome della proprietà stessa.

[code lang=”java”]
public class DynaPropertyModel implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
public DynaPropertyModel() {}
public DynaPropertyModel(String name) {
super();
this.name = name;
}
//getter e setters
}
[/code]

Come utilizzare il Builder

Utilizzare il ReflectionDynaFormModelBuilder è estremamente semplice, come possiamo vedere nel seguente managed bean.

[code lang=”java”]
@ManagedBean
@ViewScoped
public class BasicDetailExampleBean implements Serializable{
private static final long serialVersionUID = 1L;
private Object model;
private Class currentClass;
private String currentClassName;
private Integer id;
private Boolean disabled = false;
private DynaFormModel formModel;
public final void onPreRender(){
try {
currentClass = Class.forName(currentClassName);
this.formModel = new ReflectionDynaFormModelBuilder(currentClass)
.setExcludedProperties("id")
.setPropertySortComparator(getPropertyComparator())
.putCustomBuilders(getCustomBuilders())
.build();
if(id != null){
this.model = MethodUtils.invokeExactStaticMethod(currentClass, "get", new Object[]{id});
}else{
this.model = currentClass.newInstance();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected Comparator<PropertyDescriptor> getPropertyComparator() {
return ReflectionDynaFormModelBuilder.DEFAULT_PROPERTY_COMPARATOR;
}
protected Map<String, FormControlBuilder> getCustomBuilders() {
//No Custom
return new HashMap<String, FormControlBuilder>(0);
}
public final String save(){
try {
MethodUtils.invokeExactStaticMethod(currentClass, "store", new Object[]{this.model});
return "param.xhtml?class=" + currentClassName + "&faces-redirect=true";
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//getter e setters
}
[/code]

Nella pagina XHTML, basta poi collegare il model appena creato alla DynaForm.

[code lang=”html”]
<f:metadata>
<f:viewParam name="class" value="#{basicDetailExampleBean.currentClassName}"></f:viewParam>
<f:viewParam name="id" value="#{basicDetailExampleBean.id}" converter="javax.faces.Integer"></f:viewParam>
<f:viewParam name="disabled" value="#{basicDetailExampleBean.disabled}" converter="javax.faces.Boolean"></f:viewParam>
<f:event listener="#{basicDetailExampleBean.onPreRender}" type="preRenderView"></f:event>
</f:metadata>
<h:body>
<h:form>
<p:messages id="messages" showSummary="true"/>
<pe:dynaForm id="dynaForm" value="#{basicDetailExampleBean.formModel}" var="data">
<pe:dynaFormControl type="string" for="txt">
<p:inputText
id="txt"
value="#{basicDetailExampleBean.model[data.name]}"
disabled="#{basicDetailExampleBean.disabled}"/>
</pe:dynaFormControl>
<pe:dynaFormControl type="boolean" for="boolean">
<p:selectBooleanCheckbox
id="boolean"
value="#{basicDetailExampleBean.model[data.name]}"
disabled="#{basicDetailExampleBean.disabled}"/>
</pe:dynaFormControl>
<pe:dynaFormControl type="integer" for="integer">
<p:inputText
id="integer"
value="#{basicDetailExampleBean.model[data.name]}"
converter="javax.faces.Integer"
disabled="#{basicDetailExampleBean.disabled}"/>
</pe:dynaFormControl>
<pe:dynaFormControl type="text" for="text">
<p:inputTextarea
id="text"
value="#{basicDetailExampleBean.model[data.name]}"
disabled="#{basicDetailExampleBean.disabled}"/>
</pe:dynaFormControl>
</pe:dynaForm>
<p:commandButton
process="@form"
value="Save"
disabled="#{basicDetailExampleBean.disabled}"
action="#{basicDetailExampleBean.save}"/>
</h:form>
</h:body>
[/code]

Quella che otteniamo è una form dinamica a tutti gli effetti, basta cambiare i parametri class e id per cambiare forma e valori della form stessa.

Form Dinamica: User
Esempio di form dinamica: classe User

Form Dinamica: Car
Esempio di form dinamica: classe Car

Integrazione con una Datatable

Dato che tutti i parametri della form dinamica sono gestiti tramite i view params, la sua integrazione con una datatable dinamica a sua volta, è immediato.

[code lang=”html”]
<f:metadata>
<f:viewParam name="class" value="#{viewParamExampleBean.currentClass}"></f:viewParam>
<f:event listener="#{viewParamExampleBean.onPreRender}" type="preRenderView"></f:event>
</f:metadata>
<h:body>
<h:outputLink
value="basicDetail.xhtml?faces-redirect=true">
Create
<f:param name="class" value="#{viewParamExampleBean.currentClass}"/>
</h:outputLink>
<p:dataTable var="obj" value="#{viewParamExampleBean.data}">
<p:column>
<h:outputLink
value="basicDetail.xhtml?faces-redirect=true">
Edit
<f:param name="id" value="#{obj.id}"/>
<f:param name="class" value="#{viewParamExampleBean.currentClass}"/>
</h:outputLink>
</p:column>
<p:column>
<h:outputLink
value="basicDetail.xhtml?faces-redirect=true">
View
<f:param name="id" value="#{obj.id}"/>
<f:param name="disabled" value="true"/>
<f:param name="class" value="#{viewParamExampleBean.currentClass}"/>
</h:outputLink>
</p:column>
<p:columns
value="#{viewParamExampleBean.columns}"
var="column"
headerText="#{column.header}">
<p:selectBooleanCheckbox
value="#{obj[column.property]}"
disabled="true"
rendered="#{column.type.toString() == ‘boolean’}"/>
<h:outputText value="#{obj[column.property]}" rendered="#{column.type.toString() != ‘boolean’}" />
</p:columns>
</p:dataTable>
</h:body>
[/code]

Come potete vedere, con un solo managed bean riusciamo a gestire le operazioni CRUD per ogni classe di un piccolo progetto.

Come gestire le personalizzazioni

Questo micro framework accetta anche delle piccole personalizzazioni. Ad esempio per questioni di usabilità il campo notes della classe Car dovrebbe utilizzare una <p:inputTextArea> al posto della <p:inputTextArea>. Inoltre potrebbe essere comodo posizionare il campo in ultima posizione.
Possiamo forzare il comportamento del ReflectionDynaFormModelBuilder per qualche specifica proprietà dei nostri model in questo modo:

[code lang=”java”]
@ManagedBean
@ViewScoped
public class AdvancedDetailExampleBean extends BasicDetailExampleBean{
private static final String NOTES_FIELD = "notes";
private static final long serialVersionUID = 1L;
@Override
protected Comparator<PropertyDescriptor> getPropertyComparator() {
return new Comparator<PropertyDescriptor>() {
public int compare(PropertyDescriptor first, PropertyDescriptor second) {
if(NOTES_FIELD.equals(first.getName())){
return 1;
}else if(NOTES_FIELD.equals(second.getName())){
return -1;
}else{
return ReflectionDynaFormModelBuilder.DEFAULT_PROPERTY_COMPARATOR.compare(first, second);
}
}
};
}
@Override
protected Map<String, FormControlBuilder> getCustomBuilders() {
Map<String, FormControlBuilder> toReturn = new HashMap<String, FormControlBuilder>(0);
toReturn.put(NOTES_FIELD, new FormControlBuilder() {
public void populateRow(DynaFormRow row) {
//We will show notes in a textArea
DynaFormLabel label = row.addLabel(NOTES_FIELD);
DynaFormControl input = row.addControl(new DynaPropertyModel(NOTES_FIELD), "text");
label.setForControl(input);
}
});
return toReturn;
}
}
[/code]

In questo esempio abbiamo utilizzato una piccola interfaccia (FormControlBuilder) agganciata tramite una Map alla proprietà notes, per generare un DynaFormControl di tipo ‘text’, al posto del più canonico ‘string’. Il comportamento di default del builder è quello di utilizzare come tipo campo, il valore del metodo getSimpleName() della classe della proprietà stessa. Inoltre abbiamo utilizzato un Comparator per fare in modo che notes sia posizionato in fondo.

Potremmo anche aggiungere al progetto un framework per Dependecy Injection come Spring o Google Guice. In questo modo le customizzazioni potrebbero essere iniettate ed eviteremmo la ‘bruttura’ di dover creare un bean apposito.

Form custom per classe Car
Form custom per classe Car

Conclusioni

Con questo secondo post si conclude la prima versione di questo piccolo framework, che ci ha permesso con solo una manciata di classi di creare un’applicazione CRUD funzionante. Ovviamente questo codice non è pronto per entrare in produzione, come avrete notato il salvataggio dati è completamente ‘dummy’ e andrebbe sostituito con uno strato di persistenza vero. Nelle prossime release mi piacerebbe ampliare la classe DynaPropertyModel per contenere maggiori informazioni sulla proprietà da gestire come required, size e maxlength. In questo caso potremmo sfruttare le annotation della Bean Validation per popolare questi metadati a run time. Un’altra possibile implementazione potrebbe essere quella di gestire delle convezioni sui nomi dei campi: ad esempio un campo notes potrebbe sempre essere gestito con una textarea e posizionato per ultimo.

Ovviamente qualsiasi consiglio/miglioria è ben accetta e potete contribuire al progetto collegandovi al mio repository GitHub. Alla prossima!