Form dinamiche con PrimeFaces

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.

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

}
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

}

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.

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;
	}
}

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.

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

}

Come utilizzare il Builder

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

@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

}

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

<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>

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.

<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>

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:

@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;
	}
}

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!

Francesco Strazzullo

Faccio il Front-end developer per e-xtrategy dove mi occupo di applicazioni AngularJS e mobile. In passato ho lavorato principalmente con applicazioni con stack Spring+Hibernate+JSF 2.X+Primefaces. Sono tra i collaboratori del progetto Primefaces Extensions: suite di componenti aggiuntivi ufficialmente riconosciuta da Primefaces. Sono anche uno dei fondatori del progetto MaterialPrime: una libreria JSF che segue le direttive del Material Design di Google.