();
unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds());
unboundPropertyIds.removeAll(propertyIdToField.keySet());
return unboundPropertyIds;
}
/**
* Commits all changes done to the bound fields.
*
* Calls all {@link CommitHandler}s before and after committing the field
* changes to the item data source. The whole commit is aborted and state is
* restored to what it was before commit was called if any
* {@link CommitHandler} throws a CommitException or there is a problem
* committing the fields
*
* @throws CommitException
* If the commit was aborted
*/
public void commit() throws CommitException {
if (!isBuffered()) {
// Not using buffered mode, nothing to do
return;
}
for (Field> f : fieldToPropertyId.keySet()) {
((Property.Transactional>) f.getPropertyDataSource())
.startTransaction();
}
try {
firePreCommitEvent();
// Commit the field values to the properties
for (Field> f : fieldToPropertyId.keySet()) {
f.commit();
}
firePostCommitEvent();
// Commit the properties
for (Field> f : fieldToPropertyId.keySet()) {
((Property.Transactional>) f.getPropertyDataSource())
.commit();
}
} catch (Exception e) {
for (Field> f : fieldToPropertyId.keySet()) {
try {
((Property.Transactional>) f.getPropertyDataSource())
.rollback();
} catch (Exception rollbackException) {
// FIXME: What to do ?
}
}
throw new CommitException("Commit failed", e);
}
}
/**
* Sends a preCommit event to all registered commit handlers
*
* @throws CommitException
* If the commit should be aborted
*/
private void firePreCommitEvent() throws CommitException {
CommitHandler[] handlers = commitHandlers
.toArray(new CommitHandler[commitHandlers.size()]);
for (CommitHandler handler : handlers) {
handler.preCommit(new CommitEvent(this));
}
}
/**
* Sends a postCommit event to all registered commit handlers
*
* @throws CommitException
* If the commit should be aborted
*/
private void firePostCommitEvent() throws CommitException {
CommitHandler[] handlers = commitHandlers
.toArray(new CommitHandler[commitHandlers.size()]);
for (CommitHandler handler : handlers) {
handler.postCommit(new CommitEvent(this));
}
}
/**
* Discards all changes done to the bound fields.
*
* Only has effect if buffered mode is used.
*
*/
public void discard() {
for (Field> f : fieldToPropertyId.keySet()) {
try {
f.discard();
} catch (Exception e) {
// TODO: handle exception
// What can we do if discard fails other than try to discard all
// other fields?
}
}
}
/**
* Returns the field that is bound to the given property id
*
* @param propertyId
* The property id to use to lookup the field
* @return The field that is bound to the property id or null if no field is
* bound to that property id
*/
public Field> getField(Object propertyId) {
return propertyIdToField.get(propertyId);
}
/**
* Returns the property id that is bound to the given field
*
* @param field
* The field to use to lookup the property id
* @return The property id that is bound to the field or null if the field
* is not bound to any property id by this FieldBinder
*/
public Object getPropertyId(Field> field) {
return fieldToPropertyId.get(field);
}
/**
* Adds a commit handler.
*
* The commit handler is called before the field values are committed to the
* item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item
* has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a
* {@link CommitHandler} throws a CommitException the whole commit is
* aborted and the fields retain their old values.
*
* @param commitHandler
* The commit handler to add
*/
public void addCommitHandler(CommitHandler commitHandler) {
commitHandlers.add(commitHandler);
}
/**
* Removes the given commit handler.
*
* @see #addCommitHandler(CommitHandler)
*
* @param commitHandler
* The commit handler to remove
*/
public void removeCommitHandler(CommitHandler commitHandler) {
commitHandlers.remove(commitHandler);
}
/**
* Returns a list of all commit handlers for this {@link FieldGroup}.
*
* Use {@link #addCommitHandler(CommitHandler)} and
* {@link #removeCommitHandler(CommitHandler)} to register or unregister a
* commit handler.
*
* @return A collection of commit handlers
*/
protected Collection getCommitHandlers() {
return Collections.unmodifiableCollection(commitHandlers);
}
/**
* CommitHandlers are used by {@link FieldGroup#commit()} as part of the
* commit transactions. CommitHandlers can perform custom operations as part
* of the commit and cause the commit to be aborted by throwing a
* {@link CommitException}.
*/
public interface CommitHandler extends Serializable {
/**
* Called before changes are committed to the field and the item is
* updated.
*
* Throw a {@link CommitException} to abort the commit.
*
* @param commitEvent
* An event containing information regarding the commit
* @throws CommitException
* if the commit should be aborted
*/
public void preCommit(CommitEvent commitEvent) throws CommitException;
/**
* Called after changes are committed to the fields and the item is
* updated..
*
* Throw a {@link CommitException} to abort the commit.
*
* @param commitEvent
* An event containing information regarding the commit
* @throws CommitException
* if the commit should be aborted
*/
public void postCommit(CommitEvent commitEvent) throws CommitException;
}
/**
* FIXME javadoc
*
*/
public static class CommitEvent implements Serializable {
private FieldGroup fieldBinder;
private CommitEvent(FieldGroup fieldBinder) {
this.fieldBinder = fieldBinder;
}
/**
* Returns the field binder that this commit relates to
*
* @return The FieldBinder that is being committed.
*/
public FieldGroup getFieldBinder() {
return fieldBinder;
}
}
/**
* Checks the validity of the bound fields.
*
* Call the {@link Field#validate()} for the fields to get the individual
* error messages.
*
* @return true if all bound fields are valid, false otherwise.
*/
public boolean isValid() {
try {
for (Field> field : getFields()) {
field.validate();
}
return true;
} catch (InvalidValueException e) {
return false;
}
}
/**
* Checks if any bound field has been modified.
*
* @return true if at least one field has been modified, false otherwise
*/
public boolean isModified() {
for (Field> field : getFields()) {
if (field.isModified()) {
return true;
}
}
return false;
}
/**
* Gets the field factory for the {@link FieldGroup}. The field factory is
* only used when {@link FieldGroup} creates a new field.
*
* @return The field factory in use
*
*/
public FieldGroupFieldFactory getFieldFactory() {
return fieldFactory;
}
/**
* Sets the field factory for the {@link FieldGroup}. The field factory is
* only used when {@link FieldGroup} creates a new field.
*
* @param fieldFactory
* The field factory to use
*/
public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
this.fieldFactory = fieldFactory;
}
/**
* Binds member fields found in the given object.
*
* This method processes all (Java) member fields whose type extends
* {@link Field} and that can be mapped to a property id. Property id
* mapping is done based on the field name or on a @{@link PropertyId}
* annotation on the field. All non-null fields for which a property id can
* be determined are bound to the property id.
*
*
* For example:
*
*
* public class MyForm extends VerticalLayout {
* private TextField firstName = new TextField("First name");
* @PropertyId("last")
* private TextField lastName = new TextField("Last name");
* private TextField age = new TextField("Age"); ... }
*
* MyForm myForm = new MyForm();
* ...
* fieldGroup.bindMemberFields(myForm);
*
*
*
* This binds the firstName TextField to a "firstName" property in the item,
* lastName TextField to a "last" property and the age TextField to a "age"
* property.
*
* @param objectWithMemberFields
* The object that contains (Java) member fields to bind
* @throws BindException
* If there is a problem binding a field
*/
public void bindMemberFields(Object objectWithMemberFields)
throws BindException {
buildAndBindMemberFields(objectWithMemberFields, false);
}
/**
* Binds member fields found in the given object and builds member fields
* that have not been initialized.
*
* This method processes all (Java) member fields whose type extends
* {@link Field} and that can be mapped to a property id. Property ids are
* searched in the following order: @{@link PropertyId} annotations, exact
* field name matches and the case-insensitive matching that ignores
* underscores. Fields that are not initialized (null) are built using the
* field factory. All non-null fields for which a property id can be
* determined are bound to the property id.
*
*
* For example:
*
*
* public class MyForm extends VerticalLayout {
* private TextField firstName = new TextField("First name");
* @PropertyId("last")
* private TextField lastName = new TextField("Last name");
* private TextField age;
*
* MyForm myForm = new MyForm();
* ...
* fieldGroup.buildAndBindMemberFields(myForm);
*
*
*
*
* This binds the firstName TextField to a "firstName" property in the item,
* lastName TextField to a "last" property and builds an age TextField using
* the field factory and then binds it to the "age" property.
*
*
* @param objectWithMemberFields
* The object that contains (Java) member fields to build and
* bind
* @throws BindException
* If there is a problem binding or building a field
*/
public void buildAndBindMemberFields(Object objectWithMemberFields)
throws BindException {
buildAndBindMemberFields(objectWithMemberFields, true);
}
/**
* Binds member fields found in the given object and optionally builds
* member fields that have not been initialized.
*
* This method processes all (Java) member fields whose type extends
* {@link Field} and that can be mapped to a property id. Property ids are
* searched in the following order: @{@link PropertyId} annotations, exact
* field name matches and the case-insensitive matching that ignores
* underscores. Fields that are not initialized (null) are built using the
* field factory is buildFields is true. All non-null fields for which a
* property id can be determined are bound to the property id.
*
*
* @param objectWithMemberFields
* The object that contains (Java) member fields to build and
* bind
* @throws BindException
* If there is a problem binding or building a field
*/
protected void buildAndBindMemberFields(Object objectWithMemberFields,
boolean buildFields) throws BindException {
Class> objectClass = objectWithMemberFields.getClass();
for (java.lang.reflect.Field memberField : getFieldsInDeclareOrder(objectClass)) {
if (!Field.class.isAssignableFrom(memberField.getType())) {
// Process next field
continue;
}
PropertyId propertyIdAnnotation = memberField
.getAnnotation(PropertyId.class);
Class extends Field> fieldType = (Class extends Field>) memberField
.getType();
Object propertyId = null;
if (propertyIdAnnotation != null) {
// @PropertyId(propertyId) always overrides property id
propertyId = propertyIdAnnotation.value();
} else {
try {
propertyId = findPropertyId(memberField);
} catch (SearchException e) {
// Property id was not found, skip this field
continue;
}
if (propertyId == null) {
// Property id was not found, skip this field
continue;
}
}
// Ensure that the property id exists
Class> propertyType;
try {
propertyType = getPropertyType(propertyId);
} catch (BindException e) {
// Property id was not found, skip this field
continue;
}
Field> field;
try {
// Get the field from the object
field = (Field>) ReflectTools.getJavaFieldValue(
objectWithMemberFields, memberField, Field.class);
} catch (Exception e) {
// If we cannot determine the value, just skip the field and try
// the next one
continue;
}
if (field == null && buildFields) {
Caption captionAnnotation = memberField
.getAnnotation(Caption.class);
String caption;
if (captionAnnotation != null) {
caption = captionAnnotation.value();
} else {
caption = DefaultFieldFactory
.createCaptionByPropertyId(propertyId);
}
// Create the component (Field)
field = build(caption, propertyType, fieldType);
// Store it in the field
try {
ReflectTools.setJavaFieldValue(objectWithMemberFields,
memberField, field);
} catch (IllegalArgumentException e) {
throw new BindException("Could not assign value to field '"
+ memberField.getName() + "'", e);
} catch (IllegalAccessException e) {
throw new BindException("Could not assign value to field '"
+ memberField.getName() + "'", e);
} catch (InvocationTargetException e) {
throw new BindException("Could not assign value to field '"
+ memberField.getName() + "'", e);
}
}
if (field != null) {
// Bind it to the property id
bind(field, propertyId);
}
}
}
/**
* Searches for a property id from the current itemDataSource that matches
* the given memberField.
*
* If perfect match is not found, uses a case insensitive search that also
* ignores underscores. Returns null if no match is found. Throws a
* SearchException if no item data source has been set.
*
*
* The propertyId search logic used by
* {@link #buildAndBindMemberFields(Object, boolean)
* buildAndBindMemberFields} can easily be customized by overriding this
* method. No other changes are needed.
*
*
* @param memberField
* The field an object id is searched for
* @return
*/
protected Object findPropertyId(java.lang.reflect.Field memberField) {
String fieldName = memberField.getName();
if (getItemDataSource() == null) {
throw new SearchException(
"Property id type for field '"
+ fieldName
+ "' could not be determined. No item data source has been set.");
}
Item dataSource = getItemDataSource();
if (dataSource.getItemProperty(fieldName) != null) {
return fieldName;
} else {
String minifiedFieldName = minifyFieldName(fieldName);
for (Object itemPropertyId : dataSource.getItemPropertyIds()) {
if (itemPropertyId instanceof String) {
String itemPropertyName = (String) itemPropertyId;
if (minifiedFieldName
.equals(minifyFieldName(itemPropertyName))) {
return itemPropertyName;
}
}
}
}
return null;
}
protected static String minifyFieldName(String fieldName) {
return fieldName.toLowerCase().replace("_", "");
}
public static class CommitException extends Exception {
public CommitException() {
super();
// TODO Auto-generated constructor stub
}
public CommitException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public CommitException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public CommitException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
public static class BindException extends RuntimeException {
public BindException(String message) {
super(message);
}
public BindException(String message, Throwable t) {
super(message, t);
}
}
public static class SearchException extends RuntimeException {
public SearchException(String message) {
super(message);
}
public SearchException(String message, Throwable t) {
super(message, t);
}
}
/**
* Builds a field and binds it to the given property id using the field
* binder.
*
* @param propertyId
* The property id to bind to. Must be present in the field
* finder.
* @throws BindException
* If there is a problem while building or binding
* @return The created and bound field
*/
public Field> buildAndBind(Object propertyId) throws BindException {
String caption = DefaultFieldFactory
.createCaptionByPropertyId(propertyId);
return buildAndBind(caption, propertyId);
}
/**
* Builds a field using the given caption and binds it to the given property
* id using the field binder.
*
* @param caption
* The caption for the field
* @param propertyId
* The property id to bind to. Must be present in the field
* finder.
* @throws BindException
* If there is a problem while building or binding
* @return The created and bound field. Can be any type of {@link Field}.
*/
public Field> buildAndBind(String caption, Object propertyId)
throws BindException {
Class> type = getPropertyType(propertyId);
return buildAndBind(caption, propertyId, Field.class);
}
/**
* Builds a field using the given caption and binds it to the given property
* id using the field binder. Ensures the new field is of the given type.
*
* @param caption
* The caption for the field
* @param propertyId
* The property id to bind to. Must be present in the field
* finder.
* @throws BindException
* If the field could not be created
* @return The created and bound field. Can be any type of {@link Field}.
*/
public T buildAndBind(String caption, Object propertyId,
Class fieldType) throws BindException {
Class> type = getPropertyType(propertyId);
T field = build(caption, type, fieldType);
bind(field, propertyId);
return field;
}
/**
* Creates a field based on the given data type.
*
* The data type is the type that we want to edit using the field. The field
* type is the type of field we want to create, can be {@link Field} if any
* Field is good.
*
*
* @param caption
* The caption for the new field
* @param dataType
* The data model type that we want to edit using the field
* @param fieldType
* The type of field that we want to create
* @return A Field capable of editing the given type
* @throws BindException
* If the field could not be created
*/
protected T build(String caption, Class> dataType,
Class fieldType) throws BindException {
T field = getFieldFactory().createField(dataType, fieldType);
if (field == null) {
throw new BindException("Unable to build a field of type "
+ fieldType.getName() + " for editing "
+ dataType.getName());
}
field.setCaption(caption);
return field;
}
/**
* Returns an array containing Field objects reflecting all the fields of
* the class or interface represented by this Class object. The elements in
* the array returned are sorted in declare order from sub class to super
* class.
*
* @param searchClass
* @return
*/
protected static List getFieldsInDeclareOrder(
Class searchClass) {
ArrayList memberFieldInOrder = new ArrayList();
while (searchClass != null) {
for (java.lang.reflect.Field memberField : searchClass
.getDeclaredFields()) {
memberFieldInOrder.add(memberField);
}
searchClass = searchClass.getSuperclass();
}
return memberFieldInOrder;
}
}