Entity Form Support

An entity can have one or more properties whose values are single entity or a list of entities. For example, an Employee has one home Address, an ExpenseClaim has a list of ExpenseClaimItems. If the values of the properties are shown as BackingBean(s) inside their enclosing bean, then these properties are called Form Bean Properties.

Take the Example HR module for instance. An Employee has a home address and a mailing address. The O/R mapping between Employee and Address is (unrelevant code omitted):


@Entity(name="Employee")
@Table(name="Employee")
public class Employee extends PersistenceEntityBase implements NormativeId {
	private Address homeAddress;
	private Address mailingAddress;
  
	@OneToOne
	@JoinColumn(name="homeAddressId")
	public Address getHomeAddress() {
    	return homeAddress;
	}

	public void setHomeAddress(Address homeAddress) {
		this.homeAddress = homeAddress;
	}

	@OneToOne
	@JoinColumn(name="mailingAddressId")
	public Address getMailingAddress() {
		return mailingAddress;
	}

	public void setMailingAddress(Address mailingAddress) {
		this.mailingAddress = mailingAddress;
	}
}

@Entity(name="Address")
@Table(name="Address")
public class Address extends PersistenceEntityBase implements EntityChoiceSupport, ActiveAware {

	private Employee employee;
	private String choice;

	@Override
	public void copyFromSameAsEntity(EntityChoiceSupport entity) {
		...
	}

	@Transient
	@Override
	public String getChoice() {
		return choice;
	}

	public void setChoice(String choice) {
		this.choice = choice;
	}
	
	@ManyToOne
	@JoinColumn(name="employeeId")
	public Employee getEmployee() {
		return employee;
	}

	public void setEmployee(Employee employee) {
		this.employee = employee;
	}

}
Address implements EntityChoiceSupport. In addtion to being created in place inside Employee bean, home and mailing addresses can be selected from the employee's existing addresses. When creating an employee, no addresses are available for selection for the employee. But mailing address can be set as the same as home address.

EntityFormSupportBean

EntityFormSupportBean supports one or more entity form bean properties, and takes care of creating and merging form entities in persistence. For example, EmployeeBean:

public class EmployeeBean extends EntityFormSupportBean<Employee> {

}

Form Bean Properties

The values of form bean properties can be an entity or entity list, and they must be annotated or created dynamically. For example, home and mail addresses are entity form bean properties.

	// list home address before mailing address since mailing address
	// can be the same as home address
    @Property(name=Employee.PROPERTY_HOME_ADDRESS, view={ViewType.ENTITY}, 
     		entityPropertyType=EntityFormBeanProperty.class),  
     		
    @Property(name=Employee.PROPERTY_MAILING_ADDRESS, view={ViewType.ENTITY}, 
     		entityPropertyType=EntityFormBeanProperty.class)

Same As Another Property

If the value of an entity form bean property can be the same as another property, tell the framework about it so that a choice will be provided for this purpose. For example, mailing address can be same as home address for an employee.

	@Override
	protected String canFormEntityBeSameAs(Employee entity, String entityFormProperty) {
		if (entityFormProperty.equals(Employee.PROPERTY_MAILING_ADDRESS))
			return Employee.PROPERTY_HOME_ADDRESS;
		
		return null;
	}

Entity Choice Select Query

If a form entity type implements EntityChoiceSupport, then its value can be selected from existing entities. A query criteria is needed to build query criteria for entity selections. For example, Employee home and mailing address can be selected from the employee's existing addresses if the employee has been persisted.

	@Override
	protected List<CriteriaElement> getChoicePropertyQueryElements(Employee entity, 
  		EntityFormBeanProperty<Employee> property) throws SystemException {
		
		String propertyName = property.getName();
		if (propertyName.equals(Employee.PROPERTY_HOME_ADDRESS) ||
			propertyName.equals(Employee.PROPERTY_MAILING_ADDRESS)) {
			if (isCreating()) // employee not created, no choice
				return null;

			// employee addresses for selection
			List<CriteriaElement> propertyQueryElements = new ArrayList<CriteriaElement>();	  	
			propertyQueryElements.add(DetachedCriteria.eq(Address.PROPERTY_EMPLOYEE, entity));
			propertyQueryElements.add(DetachedCriteria.desc(Address.PROPERTY_ID));

			return propertyQueryElements;
		}
		
		return super.getChoicePropertyQueryElements(entity, property);
	}
The list of CriteriaElement(s) returned will be used to build query criteria. Return null if entity selection is not available.

Form Entity Persistence

During creating or merging the enclosing entity in persistence, the EntityFormSupportBean will call prepareFormEntityBeforeCommit(...) twice for each entity form bean property: one in preCreate or preApplyChange phase, the other in postCreate or postApplyChange phase.

Based on the O/R mapping between Employee and Address, an Employee's home and mailing addresses are nullable, but an Address' employee is not nullable. During preCreate or preApplyChange, prepare the address for persistence by setting employee and active properties. Return false to tell the framework to create or merge the Address entity before creating or merging the Employee entity. The framework will take care of the circular reference between the Employee and Address entity. During postCreate or postApplyChange, doing nothing. It does not matter to return true or false since the framework will not create or merge the Address again if it has already created or merged in persistence.


	@Override
	protected <M extends PersistenceEntity> boolean prepareFormEntityBeforeCommit(
			Employee entity, String entityFormProperty, M formEntity, boolean preCreateOrApplyChage,
			PersistenceEntityManager peManager, QueryDescriptor qd) throws SystemException {
		boolean createdOrMerged = super.prepareFormEntityBeforeCommit(entity, entityFormProperty, formEntity, 
				preCreateOrApplyChage, peManager, qd);
		
		if (!(entityFormProperty.equals(Employee.PROPERTY_HOME_ADDRESS) ||
				entityFormProperty.equals(Employee.PROPERTY_MAILING_ADDRESS)))
			return createdOrMerged;
		
		if (preCreateOrApplyChage) { // avoid setting twice
			Address addr = (Address) formEntity;
			if (addr.getEmployee() == null)
				addr.setEmployee(entity);
			
			if (addr.getActive() == null)
				addr.setActive(true);
			
			return false;
		}
		
		return createdOrMerged;
	}

What if an Employee's home and mailing addresses are not nullable, but an Address' employee is nullable. In this case, both Address entities must be created before creating the Employee entity. If the Employee is not persisted, any Address' reference to the employee must be removed during preCreate phase, and restored during postCreate phase. An entity can not reference to any transient entities when the entity is created or merged in persistence.

	@Override
	protected <M extends PersistenceEntity> boolean prepareFormEntityBeforeCommit(
			Employee entity, String entityFormProperty, M formEntity, boolean preCreateOrApplyChage,
			PersistenceEntityManager peManager, QueryDescriptor qd) throws SystemException {
		boolean createdOrMerged = super.prepareFormEntityBeforeCommit(entity, entityFormProperty, formEntity, 
				preCreateOrApplyChage, peManager, qd);
		
		if (!(entityFormProperty.equals(Employee.PROPERTY_HOME_ADDRESS) ||
				entityFormProperty.equals(Employee.PROPERTY_MAILING_ADDRESS)))
			return createdOrMerged;
		
		if (preCreateOrApplyChage) {
			// break the reference to employee if it is transient(not persisted)
			Address addr = (Address) formEntity;
			Employee employee = addr.getEmployee();
			if (employee != null && !employee.isPersisted())
				addr.setEmployee(null);
				
			if (addr.getActive() == null)
				addr.setActive(true);
			
			return false;  // tell framework to create the address
		}
		else {
			// the employee and address have been created/merged
			Address addr = (Address) formEntity;
			if (addr.getEmployee() == null) {
				addr.setEmployee(entity);
				addr.setChanged(true); // set change flag
				return false; // tell framework to merge the address
			}
		}
		
		return createdOrMerged;
	}