Datatable dinamiche con PrimeFaces

In questo articolo ci occuperemo di come utilizzare la Reflection per estrarre dei modelli PrimeFaces dai nostri POJO. Nel caso specifico andremo a creare una dataTable dinamica utilizzando il componente p:columns.

Setup

Lavorare con la Reflection può rendere il codice abbastanza verboso e di difficile lettura. Fortunatamente esiste una libreria che ci aiuta tantissimo quando dobbiamo utilizzare questa tecnica. Il primo passo è quindi aggiungere la libreria Apache Commons BeanUtils al nostro pom.xml.

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.2</version>
</dependency>

Model

Andremo ad utilizzare il seguente POJO per il nostro primo esempio, una semplice (quanto inutile) implementazione del pattern Active Record.

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 Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String name) {
		this.firstName = name;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

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

In pratica il nostro scopo è quello di creare dinamicamente le colonne id,firstName e lastName. Per ottenere questo risultato sfrutteremo la seguente classe: ColumnModel.

public class ColumnModel implements Serializable {

	private static final long serialVersionUID = 1L;

	private String property;
	private String header;
	private Class<?> type;

	public ColumnModel() {}

	public String getProperty() {
		return property;
	}
	public void setProperty(String property) {
		this.property = property;
	}
	public String getHeader() {
		return header;
	}
	public void setHeader(String header) {
		this.header = header;
	}
	public Class<?> getType() {
		return type;
	}
	public void setType(Class<?> type) {
		this.type = type;
	}
}

Come possiamo notare la classe ColumnModel è un POJO a sua volta, il cui scopo è contenere le informazioni riguardo la proprietà da stampare (nome e tipo) e il titolo da visualizzare. Dobbiamo generare un’istanza di questa classe per ogni proprietà del nostro model User. Per fare ciò utilizzeremo il ReflectionColumnModelBuilder, il cui codice è il seguente:

public class ReflectionColumnModelBuilder {

	private Class modelClass;
	private Comparator<PropertyDescriptor> propertySortComparator;
	private Predicate propertyFilterPredicate;
	private Set<String> excludedProperties;
	private static Set<String> defaultExcludedProperties = new HashSet<String>(0);

	static{
		defaultExcludedProperties.add("class");
	}

	public ReflectionColumnModelBuilder(Class modelClass) {
		this.modelClass = modelClass;
		this.propertyFilterPredicate = PredicateUtils.truePredicate();
		this.propertySortComparator = new Comparator<PropertyDescriptor>() {
			public int compare(PropertyDescriptor o1, PropertyDescriptor o2) {
				return o1.getName().compareTo(o2.getName());
			}
		};
		this.excludedProperties = new HashSet<String>(0);
	}

	public ReflectionColumnModelBuilder setPropertyFilterPredicate(Predicate p){
		this.propertyFilterPredicate = p;
		return this;
	}

	public ReflectionColumnModelBuilder setPropertySortComparator(Comparator<PropertyDescriptor> c){
		this.propertySortComparator = c;
		return this;
	}

	public ReflectionColumnModelBuilder setExcludedProperties(Set<String> p){
		this.excludedProperties = p;
		return this;
	}

	public ReflectionColumnModelBuilder setExcludedProperties(String...p){
		this.excludedProperties = new HashSet<String>(0);
		for (String excludedProperty : p) {
			this.excludedProperties.add(excludedProperty);
		}
		return this;
	}

	public List<ColumnModel> build(){
		List<ColumnModel> columns = new ArrayList<ColumnModel>(0);

		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 &&
						!defaultExcludedProperties.contains(propertyDescriptor.getName()) &&
						!excludedProperties.contains(propertyDescriptor.getName());
			}
		}));

		Collections.sort(propertyDescriptors, propertySortComparator);

		for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
			ColumnModel columnDescriptor = new ColumnModel();

			columnDescriptor.setProperty(propertyDescriptor.getName());
			columnDescriptor.setHeader(StringUtils.capitalize(StringUtils.join(StringUtils.splitByCharacterTypeCamelCase(propertyDescriptor.getName())," ")));
			columnDescriptor.setType(propertyDescriptor.getPropertyType());

			columns.add(columnDescriptor);
		}

		return columns;
	}
}

Concentriamoci sul metodo build. Utilizziamo l’istruzione PropertyUtils.getPropertyDescriptors(modelClass) per ottenere un elenco di tutti i PropertyDescriptor della classe passata come parametro. Il Javadoc della classe PropertyDescriptor afferma che questo oggetto “describes one property that a Java Bean exports via a pair of accessor methods”, in pratica ci fornisce informazioni su tutto quello che viene esposto tramite getter o setter. Filtriamo e ordiniamo i PropertyDescriptor e wrappiamo il tutto in un lista di ColumnModel.

Un primo esempio

Mettiamo subito alla prova la nostra tecnica con un primo semplice esempio:

Pagina XHTML
<h:body>
	<p:dataTable var="user" value="#{basicExampleBean.users}">
		<p:columns
			value="#{basicExampleBean.columns}"
			var="column"
			headerText="#{column.header}">
			<h:outputText value="#{user[column.property]}" />
		</p:columns>
	</p:dataTable>
</h:body>
Managed Bean
@ManagedBean
@ViewScoped
public class BasicExampleBean implements Serializable{

	private List<ColumnModel> columns = new ArrayList<ColumnModel>(0);

	@PostConstruct
	public void init() {
		columns = new ReflectionColumnModelBuilder(User.class).
					setExcludedProperties("id").
					build();
	}

	public List<User> getUsers(){
		return User.getAll();
	}

	public List<ColumnModel> getColumns() {
		return columns;
	}
}

Come potete notare dal codice appena letto, l’unica cosa che dobbiamo fare per ottenere la nostra datatable dinamica è stata passare come parametro al nostro builder la classe User. Notiamo inoltre che abbiamo escluso la colonna id dal nostro output. Il risultato di questo primo esempio è il seguente:

Primo Esempio

Primo Esempio

AJAX

Nel prossimo esempio vedremo come sia possibile modificare la classe da visualizzare tramite un evento AJAX. Come prima cosa creiamo una seconda classe che farà da model: questa volta gestiremo un elenco di auto.

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 boolean used;

	static{
		cars.add(new Car(0, "Honda","Yellow",1995,false));
		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) {
		super();
		this.id = id;
		this.brand = brand;
		this.color = color;
		this.year = year;
		this.used = used;
	}

	public Car() {}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getColor() {
		return color;
	}

	public void setColor(String color) {
		this.color = color;
	}

	public String getBrand() {
		return brand;
	}

	public void setBrand(String brand) {
		this.brand = brand;
	}

	public Integer getYear() {
		return year;
	}

	public void setYear(Integer year) {
		this.year = year;
	}

	public boolean isUsed() {
		return used;
	}

	public void setUsed(boolean used) {
		this.used = used;
	}

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

Notiamo che questa classe ha un proprietà di tipo boolean, andremo a sfruttare la proprietà type del ColumnModel per utilizzare in questo caso un p:selectBooleanCheckbox al posto del classico h:outputText. Sceglieremo infine la classe da gestire al momento tramite un radio button.

Pagina XHTML
<h:body>
	<h:form>
		<h:panelGrid columns="2" style="margin-bottom:10px" cellpadding="5">
	        <p:outputLabel for="currentClass" value="Class:" />
	        <p:selectOneRadio id="currentClass" value="#{dynamicExampleBean.currentClass}">
	            <f:selectItem itemLabel="User" itemValue="it.strazz.primefaces.model.User" />
	            <f:selectItem itemLabel="Car" itemValue="it.strazz.primefaces.model.Car" />
	            <p:ajax listener="#{dynamicExampleBean.onChangeClass}" update="@form"/>
	        </p:selectOneRadio>
	    </h:panelGrid>
		<p:dataTable var="obj" value="#{dynamicExampleBean.data}">
			<p:columns
				value="#{dynamicExampleBean.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:form>
</h:body>
Managed Bean
@ManagedBean
@ViewScoped
public class DynamicExampleBean implements Serializable{

	private List<ColumnModel> columns = new ArrayList<ColumnModel>(0);
	private String currentClass = User.class.getName();

	@PostConstruct
	public void init() {
		onChangeClass();
	}

	public void onChangeClass(){
		try {
			columns = new ReflectionColumnModelBuilder(Class.forName(currentClass)).
					setExcludedProperties("id").
					build();
		} catch (ClassNotFoundException e) {
			//Will not happen
			columns = null;
		}
	}

	public List<Object> getData(){
		try {
			return (List<Object>) MethodUtils.invokeExactStaticMethod(Class.forName(currentClass), "getAll", null);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public List<ColumnModel> getColumns() {
		return columns;
	}
	public String getCurrentClass() {
		return currentClass;
	}

	public void setCurrentClass(String currentClass) {
		this.currentClass = currentClass;
	}
}

L’esempio è molto simile al precedente, l’unica cosa degna di nota è l’utilizzo del metodo MethodUtils.invokeExactStaticMethod per invocare il metodo getAll della classe scelta al momento dall’utente. Il risultato di questo secondo esempio è il seguente:

Secondo Esempio

Secondo Esempio

Utilizzare i ViewParam

In questo ultimo esempio andremo infine a selezionare i dati da visualizzare tramite un f:viewParam. L’esempio è in tutto e per tutto simile al precedente, con l’unica differenza che i dati vengono generati durante l’evento preRenderView.

Pagina XHTML
<f:metadata>
	<f:viewParam name="class" value="#{viewParamExampleBean.currentClass}"></f:viewParam>
	<f:event listener="#{viewParamExampleBean.onPreRender}" type="preRenderView"></f:event>
</f:metadata>
<h:body>
	<p:dataTable var="obj" value="#{viewParamExampleBean.data}">
		<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>
Managed Bean
@ManagedBean
@ViewScoped
public class ViewParamExampleBean implements Serializable{

	private List<ColumnModel> columns = new ArrayList<ColumnModel>(0);
	private String currentClass = User.class.getName();

	public void onPreRender() {
		try {
			columns = new ReflectionColumnModelBuilder(Class.forName(currentClass)).
					setExcludedProperties("id").
					build();
		} catch (ClassNotFoundException e) {
			//Will not happen
			columns = null;
		}
	}

	public List<Object> getData(){
		try {
			return (List<Object>) MethodUtils.invokeExactStaticMethod(Class.forName(currentClass), "getAll", null);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public List<ColumnModel> getColumns() {
		return columns;
	}

	public String getCurrentClass() {
		return currentClass;
	}

	public void setCurrentClass(String currentClass) {
		this.currentClass = currentClass;
	}
}

Ora possiamo utilizzare un’unica pagina JSF per visualizzare qualsiasi modello della nostra applicazione, e per passare da un modello all’altro ci basta un semplice menù come il seguente.

<h:form>
   <p:menubar>
        <p:menuitem value="View Param (User)" action="param?faces-redirect=true&amp;class=it.strazz.primefaces.model.User"/>
        <p:menuitem value="View Param (Car)" action="param?faces-redirect=true&amp;class=it.strazz.primefaces.model.Car"/>
   </p:menubar>
</h:form>

Per creare nuove “pagine” l’unica cosa che dobbiamo fare è quindi creare il POJO che farà da model come negli esempi precedenti. Il resto verrà gestito dal ColumnModel e dal rispettivo builder. Ovviamente in un’applicazione reale avrete bisogno di un vero layer di persistenza magari gestito con Hibernate, al posto del nostro “finto” Active Record.

Conclusioni

Durante una delle mie prime lezioni all’università il mio professore disse: “La Reflection è il male!”. Un po’ melodrammatico ma vero. Non bisogna però sottovalutare il potere di questo tipo di “hack” in qualche situazione. Questo che avete appena visto sarà la base di un piccolo framework di prototipazione rapida che sfrutta PrimeFaces come strato di View. Non sarà uno scaffolder  come il CRUD generator di NetBeans, ma quasi tutto quello che è standard verrà generato a runtime come negli esempi precedenti. Se volete contribuire potete inviarmi una mail oppure forkare il repository GitHub.

In uno dei miei prossimi articoli utilizzerò la stessa tecnica per creare pagine di dettaglio per i nostri model. 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.