Dynamic PrimeFaces detail forms

In my last post – Dynamic PrimeFaces Datatables –     I have explained how to use Java Reflection to dynamically generate PrimeFaces DataTables. In this article we are going to use the same technique to create dynamic detail forms.

In our following examples we will use the DynaForm component of the PrimeFacex Extensions project. As you can see from the showcase example, this component it€™s useful when we have a dynamically described form.

We€™re are going to create detail forms for the following POJO classes:

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

}

And here is the Car POJO Class:

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 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 String getNotes() {
		return notes;
	}

	public void setNotes(String notes) {
		this.notes = notes;
	}

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

To use a DynaForm we need to create a DynaFormModel. This object will contain an instance of DynaFormRow for every accessible property of our POJO. We will automatically generate the rows using the property descriptors of the class, just like in the last post. In order to achieve this goal we will use this 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;
	}
}

As you can see, for every readable property we generate a DynaFormRow, a DynaFormLabel and, most importantly, a DynaFormControl. We also generate a DynaPropertyModel, this object will contain all the metadata of the property, for now it will simply contains the name of the property itself.

public class DynaPropertyModel implements Serializable{
    
	private static final long serialVersionUID = 1L;
	
	private String name;
    
    public DynaPropertyModel() {}
    
    public DynaPropertyModel(String name) {
		super();
		this.name = name;
	}
    
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

Basic Example

Using this builder is quite simple, as we can see in the next example:

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);
		}
	}
	
	public String getCurrentClassName() {
		return currentClassName;
	}

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

	public Integer getId() {
		return id;
	}

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

	public Boolean getDisabled() {
		return disabled;
	}

	public void setDisabled(Boolean disabled) {
		this.disabled = disabled;
	}

	public Object getModel() {
		return model;
	}

	public DynaFormModel getFormModel() {
		return formModel;
	}
}

And the XHTML Page

<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:dynaForm>
	    
	    <p:commandButton
	    	process="@form"
	    	value="Save" 
	    	disabled="#{basicDetailExampleBean.disabled}"
	    	action="#{basicDetailExampleBean.save}"/>
    </h:form>
</h:body>
</html>

What we just obtained it€™s a dynamic form, we just need to change the €˜class€™ and €˜id€™ view param to change class and model to edit/view.

primefaces detail forms

primefaces form detail

Datatable Integration

All the parameters in the detail form are passed via view paramters. So it€™s extremely simple to integrate our new detail form with the dynamic datatable from previous post.

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

As you can see with just two managed beans we can manage every POJO class of a simple project.

primefaces detail form

Advanced Usage

In the Car class, we have a €˜notes€™ field. If we want a better user experience we need to use a p:inputTextArea in place of the standard p:inputText. And propably the €˜notes€™ field should be the last input in the form. We can force the behaviour of our ReflectionDynaFormModelBuilder, changing the DynaFormRow for a specific field or changing the order or the fields.

@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 this new managed bean we changed the type of the notes filed from €˜string€™ to €˜text€™ (to show a textArea). The default behaviour is to use the simple name of the class of the property for choosing the input component. We can also add a Dependency Injection framework like Spring or Google Guice in our application, to avoid the creation of the second bean.

primefaces forms tutorial

Conclusions

Thanks to the reflection, with just a bunch of classes we can manage a full CRUD application. Obviously the code that you just analyzed it’s not ready for production. In the next releases of this small  Prototyping Framework I will try to obtain the field attributes (like required,size,maxlength) from the javax.validation annotations. Another possible feature could be some kind of COC implementation: for example every €˜notes€™ field should be the last field with a textArea component.

If you want to contribute just fork the repository of the project! Enjoy!