From: Artur Signell
Date: Thu, 22 Dec 2011 11:40:14 +0000 (+0200)
Subject: com.vaadin.data.fieldbinder -> com.vaadin.data.fieldgroup
X-Git-Tag: 7.0.0.alpha1~18
X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=38ee0b491e019c371e547ecb9166c315f351a23e;p=vaadin-framework.git
com.vaadin.data.fieldbinder -> com.vaadin.data.fieldgroup
---
diff --git a/src/com/vaadin/data/fieldbinder/BeanFieldGroup.java b/src/com/vaadin/data/fieldbinder/BeanFieldGroup.java
deleted file mode 100644
index 3cb295bb2a..0000000000
--- a/src/com/vaadin/data/fieldbinder/BeanFieldGroup.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-@VaadinApache2LicenseForJavaFiles@
- */
-package com.vaadin.data.fieldbinder;
-
-import java.lang.reflect.Method;
-
-import com.vaadin.data.Item;
-import com.vaadin.data.util.BeanItem;
-import com.vaadin.data.validator.BeanValidator;
-import com.vaadin.ui.Field;
-
-public class BeanFieldGroup extends FieldGroup {
-
- private Class beanType;
-
- private static Boolean beanValidationImplementationAvailable = null;
-
- public BeanFieldGroup(Class beanType) {
- this.beanType = beanType;
- }
-
- @Override
- protected Class> getPropertyType(Object propertyId) {
- if (getItemDataSource() != null) {
- return super.getPropertyType(propertyId);
- } else {
- // Data source not set so we need to figure out the type manually
- /*
- * toString should never really be needed as propertyId should be of
- * form "fieldName" or "fieldName.subField[.subField2]" but the
- * method declaration comes from parent.
- */
- java.lang.reflect.Field f;
- try {
- f = getField(beanType, propertyId.toString());
- return f.getType();
- } catch (SecurityException e) {
- throw new BindException("Cannot determine type of propertyId '"
- + propertyId + "'.", e);
- } catch (NoSuchFieldException e) {
- throw new BindException("Cannot determine type of propertyId '"
- + propertyId + "'. The propertyId was not found in "
- + beanType.getName(), e);
- }
- }
- }
-
- private static java.lang.reflect.Field getField(Class> cls,
- String propertyId) throws SecurityException, NoSuchFieldException {
- if (propertyId.contains(".")) {
- String[] parts = propertyId.split("\\.", 2);
- // Get the type of the field in the "cls" class
- java.lang.reflect.Field field1 = getField(cls, parts[0]);
- // Find the rest from the sub type
- return getField(field1.getType(), parts[1]);
- } else {
- try {
- // Try to find the field directly in the given class
- java.lang.reflect.Field field1 = cls
- .getDeclaredField(propertyId);
- return field1;
- } catch (NoSuchFieldError e) {
- // Try super classes until we reach Object
- Class> superClass = cls.getSuperclass();
- if (superClass != Object.class) {
- return getField(superClass, propertyId);
- } else {
- throw e;
- }
- }
- }
- }
-
- /**
- * Helper method for setting the data source directly using a bean. This
- * method wraps the bean in a {@link BeanItem} and calls
- * {@link #setItemDataSource(Item)}.
- *
- * @param bean
- * The bean to use as data source.
- */
- public void setItemDataSource(T bean) {
- setItemDataSource(new BeanItem(bean));
- }
-
- @Override
- public void setItemDataSource(Item item) {
- if (!(item instanceof BeanItem)) {
- throw new RuntimeException(getClass().getSimpleName()
- + " only supports BeanItems as item data source");
- }
- super.setItemDataSource(item);
- }
-
- @Override
- public BeanItem getItemDataSource() {
- return (BeanItem) super.getItemDataSource();
- }
-
- @Override
- public void bind(Field field, Object propertyId) {
- if (getItemDataSource() != null) {
- // The data source is set so the property must be found in the item.
- // If it is not we try to add it.
- try {
- getItemProperty(propertyId);
- } catch (BindException e) {
- // Not found, try to add a nested property;
- // BeanItem property ids are always strings so this is safe
- getItemDataSource().addNestedProperty((String) propertyId);
- }
- }
-
- super.bind(field, propertyId);
- }
-
- @Override
- protected void configureField(Field> field) {
- super.configureField(field);
- // Add Bean validators if there are annotations
- if (isBeanValidationImplementationAvailable()) {
- BeanValidator validator = new BeanValidator(
- beanType, getPropertyId(field).toString());
- field.addValidator(validator);
- if (field.getLocale() != null) {
- validator.setLocale(field.getLocale());
- }
- }
- }
-
- /**
- * Checks whether a bean validation implementation (e.g. Hibernate Validator
- * or Apache Bean Validation) is available.
- *
- * TODO move this method to some more generic location
- *
- * @return true if a JSR-303 bean validation implementation is available
- */
- protected static boolean isBeanValidationImplementationAvailable() {
- if (beanValidationImplementationAvailable != null) {
- return beanValidationImplementationAvailable;
- }
- try {
- Class> validationClass = Class
- .forName("javax.validation.Validation");
- Method buildFactoryMethod = validationClass
- .getMethod("buildDefaultValidatorFactory");
- Object factory = buildFactoryMethod.invoke(null);
- beanValidationImplementationAvailable = (factory != null);
- } catch (Exception e) {
- // no bean validation implementation available
- beanValidationImplementationAvailable = false;
- }
- return beanValidationImplementationAvailable;
- }
-}
\ No newline at end of file
diff --git a/src/com/vaadin/data/fieldbinder/Caption.java b/src/com/vaadin/data/fieldbinder/Caption.java
deleted file mode 100644
index 7f45f64f18..0000000000
--- a/src/com/vaadin/data/fieldbinder/Caption.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-@VaadinApache2LicenseForJavaFiles@
- */
-package com.vaadin.data.fieldbinder;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target({ ElementType.FIELD })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface Caption {
- String value();
-}
diff --git a/src/com/vaadin/data/fieldbinder/DefaultFieldGroupFieldFactory.java b/src/com/vaadin/data/fieldbinder/DefaultFieldGroupFieldFactory.java
deleted file mode 100644
index 28eff13617..0000000000
--- a/src/com/vaadin/data/fieldbinder/DefaultFieldGroupFieldFactory.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
-@VaadinApache2LicenseForJavaFiles@
- */
-package com.vaadin.data.fieldbinder;
-
-import java.util.EnumSet;
-
-import com.vaadin.data.Item;
-import com.vaadin.data.fieldbinder.FieldGroup.BindException;
-import com.vaadin.ui.AbstractSelect;
-import com.vaadin.ui.AbstractTextField;
-import com.vaadin.ui.CheckBox;
-import com.vaadin.ui.ComboBox;
-import com.vaadin.ui.Field;
-import com.vaadin.ui.ListSelect;
-import com.vaadin.ui.NativeSelect;
-import com.vaadin.ui.OptionGroup;
-import com.vaadin.ui.RichTextArea;
-import com.vaadin.ui.Table;
-import com.vaadin.ui.TextField;
-
-public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
-
- public static final Object CAPTION_PROPERTY_ID = "Caption";
-
- public T createField(Class> type, Class fieldType) {
- if (Enum.class.isAssignableFrom(type)) {
- return createEnumField(type, fieldType);
- } else if (Boolean.class.isAssignableFrom(type)
- || boolean.class.isAssignableFrom(type)) {
- return createBooleanField(fieldType);
- }
- if (AbstractTextField.class.isAssignableFrom(fieldType)) {
- return fieldType.cast(createAbstractTextField(fieldType
- .asSubclass(AbstractTextField.class)));
- } else if (fieldType == RichTextArea.class) {
- return fieldType.cast(createRichTextArea());
- }
- return createDefaultField(type, fieldType);
- }
-
- protected RichTextArea createRichTextArea() {
- RichTextArea rta = new RichTextArea();
- rta.setImmediate(true);
-
- return rta;
- }
-
- private T createEnumField(Class> type,
- Class fieldType) {
- if (AbstractSelect.class.isAssignableFrom(fieldType)) {
- AbstractSelect s = createCompatibleSelect((Class extends AbstractSelect>) fieldType);
- populateWithEnumData(s, (Class extends Enum>) type);
- return (T) s;
- }
-
- return null;
- }
-
- protected AbstractSelect createCompatibleSelect(
- Class extends AbstractSelect> fieldType) {
- AbstractSelect select;
- if (fieldType.isAssignableFrom(ListSelect.class)) {
- select = new ListSelect();
- select.setMultiSelect(false);
- } else if (fieldType.isAssignableFrom(NativeSelect.class)) {
- select = new NativeSelect();
- } else if (fieldType.isAssignableFrom(OptionGroup.class)) {
- select = new OptionGroup();
- select.setMultiSelect(false);
- } else if (fieldType.isAssignableFrom(Table.class)) {
- Table t = new Table();
- t.setSelectable(true);
- select = t;
- } else {
- select = new ComboBox(null);
- }
- select.setImmediate(true);
- select.setNullSelectionAllowed(false);
-
- return select;
- }
-
- protected T createBooleanField(Class fieldType) {
- if (fieldType.isAssignableFrom(CheckBox.class)) {
- CheckBox cb = new CheckBox(null);
- cb.setImmediate(true);
- return (T) cb;
- } else if (AbstractTextField.class.isAssignableFrom(fieldType)) {
- return (T) createAbstractTextField((Class extends AbstractTextField>) fieldType);
- }
-
- return null;
- }
-
- protected T createAbstractTextField(
- Class fieldType) {
- if (fieldType == AbstractTextField.class) {
- fieldType = (Class) TextField.class;
- }
- try {
- T field = fieldType.newInstance();
- field.setImmediate(true);
- return field;
- } catch (Exception e) {
- throw new BindException("Could not create a field of type "
- + fieldType, e);
- }
- }
-
- /**
- * Fallback when no specific field has been created. Typically returns a
- * TextField.
- *
- * @param
- * The type of field to create
- * @param type
- * The type of data that should be edited
- * @param fieldType
- * The type of field to create
- * @return A field capable of editing the data or null if no field could be
- * created
- */
- protected T createDefaultField(Class> type,
- Class fieldType) {
- if (fieldType.isAssignableFrom(TextField.class)) {
- return fieldType.cast(createAbstractTextField(TextField.class));
- }
- return null;
- }
-
- /**
- * Populates the given select with all the enums in the given {@link Enum}
- * class. Uses {@link Enum}.toString() for caption.
- *
- * @param select
- * The select to populate
- * @param enumClass
- * The Enum class to use
- */
- protected void populateWithEnumData(AbstractSelect select,
- Class extends Enum> enumClass) {
- select.removeAllItems();
- for (Object p : select.getContainerPropertyIds()) {
- select.removeContainerProperty(p);
- }
- select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, "");
- select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID);
- @SuppressWarnings("unchecked")
- EnumSet> enumSet = EnumSet.allOf(enumClass);
- for (Object r : enumSet) {
- Item newItem = select.addItem(r);
- newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(r.toString());
- }
- }
-}
diff --git a/src/com/vaadin/data/fieldbinder/FieldGroup.java b/src/com/vaadin/data/fieldbinder/FieldGroup.java
deleted file mode 100644
index 8f5a507470..0000000000
--- a/src/com/vaadin/data/fieldbinder/FieldGroup.java
+++ /dev/null
@@ -1,978 +0,0 @@
-/*
-@VaadinApache2LicenseForJavaFiles@
- */
-package com.vaadin.data.fieldbinder;
-
-import java.io.Serializable;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.logging.Logger;
-
-import com.vaadin.data.Item;
-import com.vaadin.data.Property;
-import com.vaadin.data.TransactionalProperty;
-import com.vaadin.data.Validator.InvalidValueException;
-import com.vaadin.data.util.TransactionalPropertyWrapper;
-import com.vaadin.tools.ReflectTools;
-import com.vaadin.ui.DefaultFieldFactory;
-import com.vaadin.ui.Field;
-import com.vaadin.ui.Form;
-
-/**
- * FieldGroup provides an easy way of binding fields to data and handling
- * commits of these fields.
- *
- * The functionality of FieldGroup is similar to {@link Form} but
- * {@link FieldGroup} does not handle layouts in any way. The typical use case
- * is to create a layout outside the FieldGroup and then use FieldGroup to bind
- * the fields to a data source.
- *
- *
- * {@link FieldGroup} is not a UI component so it cannot be added to a layout.
- * Using the buildAndBind methods {@link FieldGroup} can create fields for you
- * using a FieldGroupFieldFactory but you still have to add them to the correct
- * position in your layout.
- *
- * 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 id
- * mapping is done based on the field name or on a @{@link PropertyId}
- * annotation on the field. 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 : objectClass
- .getDeclaredFields()) {
-
- 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 {
- propertyId = memberField.getName();
- }
-
- // 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);
- } 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);
- }
- }
- }
-
- 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);
- }
-
- }
-
- /**
- * 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;
- }
-}
\ No newline at end of file
diff --git a/src/com/vaadin/data/fieldbinder/FieldGroupFieldFactory.java b/src/com/vaadin/data/fieldbinder/FieldGroupFieldFactory.java
deleted file mode 100644
index 8a8c2b9d46..0000000000
--- a/src/com/vaadin/data/fieldbinder/FieldGroupFieldFactory.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-@VaadinApache2LicenseForJavaFiles@
- */
-package com.vaadin.data.fieldbinder;
-
-import java.io.Serializable;
-
-import com.vaadin.ui.Field;
-
-/**
- * Factory interface for creating new Field-instances based on the data type
- * that should be edited.
- *
- * @author Vaadin Ltd.
- * @version @version@
- * @since 7.0
- */
-public interface FieldGroupFieldFactory extends Serializable {
- /**
- * Creates a field based on the data type that we want to edit
- *
- * @param dataType
- * The type that we want to edit using the field
- * @param fieldType
- * The type of field we want to create. If set to {@link Field}
- * then any type of field is accepted
- * @return A field that can be assigned to the given fieldType and that is
- * capable of editing the given type of data
- */
- T createField(Class> dataType, Class fieldType);
-}
diff --git a/src/com/vaadin/data/fieldbinder/PropertyId.java b/src/com/vaadin/data/fieldbinder/PropertyId.java
deleted file mode 100644
index b9d4e808f3..0000000000
--- a/src/com/vaadin/data/fieldbinder/PropertyId.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-@VaadinApache2LicenseForJavaFiles@
- */
-package com.vaadin.data.fieldbinder;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target({ ElementType.FIELD })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface PropertyId {
- String value();
-}
diff --git a/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java
new file mode 100644
index 0000000000..8ca6c95069
--- /dev/null
+++ b/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.validator.BeanValidator;
+import com.vaadin.ui.Field;
+
+public class BeanFieldGroup extends FieldGroup {
+
+ private Class beanType;
+
+ private static Boolean beanValidationImplementationAvailable = null;
+
+ public BeanFieldGroup(Class beanType) {
+ this.beanType = beanType;
+ }
+
+ @Override
+ protected Class> getPropertyType(Object propertyId) {
+ if (getItemDataSource() != null) {
+ return super.getPropertyType(propertyId);
+ } else {
+ // Data source not set so we need to figure out the type manually
+ /*
+ * toString should never really be needed as propertyId should be of
+ * form "fieldName" or "fieldName.subField[.subField2]" but the
+ * method declaration comes from parent.
+ */
+ java.lang.reflect.Field f;
+ try {
+ f = getField(beanType, propertyId.toString());
+ return f.getType();
+ } catch (SecurityException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'.", e);
+ } catch (NoSuchFieldException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'. The propertyId was not found in "
+ + beanType.getName(), e);
+ }
+ }
+ }
+
+ private static java.lang.reflect.Field getField(Class> cls,
+ String propertyId) throws SecurityException, NoSuchFieldException {
+ if (propertyId.contains(".")) {
+ String[] parts = propertyId.split("\\.", 2);
+ // Get the type of the field in the "cls" class
+ java.lang.reflect.Field field1 = getField(cls, parts[0]);
+ // Find the rest from the sub type
+ return getField(field1.getType(), parts[1]);
+ } else {
+ try {
+ // Try to find the field directly in the given class
+ java.lang.reflect.Field field1 = cls
+ .getDeclaredField(propertyId);
+ return field1;
+ } catch (NoSuchFieldError e) {
+ // Try super classes until we reach Object
+ Class> superClass = cls.getSuperclass();
+ if (superClass != Object.class) {
+ return getField(superClass, propertyId);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method for setting the data source directly using a bean. This
+ * method wraps the bean in a {@link BeanItem} and calls
+ * {@link #setItemDataSource(Item)}.
+ *
+ * @param bean
+ * The bean to use as data source.
+ */
+ public void setItemDataSource(T bean) {
+ setItemDataSource(new BeanItem(bean));
+ }
+
+ @Override
+ public void setItemDataSource(Item item) {
+ if (!(item instanceof BeanItem)) {
+ throw new RuntimeException(getClass().getSimpleName()
+ + " only supports BeanItems as item data source");
+ }
+ super.setItemDataSource(item);
+ }
+
+ @Override
+ public BeanItem getItemDataSource() {
+ return (BeanItem) super.getItemDataSource();
+ }
+
+ @Override
+ public void bind(Field field, Object propertyId) {
+ if (getItemDataSource() != null) {
+ // The data source is set so the property must be found in the item.
+ // If it is not we try to add it.
+ try {
+ getItemProperty(propertyId);
+ } catch (BindException e) {
+ // Not found, try to add a nested property;
+ // BeanItem property ids are always strings so this is safe
+ getItemDataSource().addNestedProperty((String) propertyId);
+ }
+ }
+
+ super.bind(field, propertyId);
+ }
+
+ @Override
+ protected void configureField(Field> field) {
+ super.configureField(field);
+ // Add Bean validators if there are annotations
+ if (isBeanValidationImplementationAvailable()) {
+ BeanValidator validator = new BeanValidator(
+ beanType, getPropertyId(field).toString());
+ field.addValidator(validator);
+ if (field.getLocale() != null) {
+ validator.setLocale(field.getLocale());
+ }
+ }
+ }
+
+ /**
+ * Checks whether a bean validation implementation (e.g. Hibernate Validator
+ * or Apache Bean Validation) is available.
+ *
+ * TODO move this method to some more generic location
+ *
+ * @return true if a JSR-303 bean validation implementation is available
+ */
+ protected static boolean isBeanValidationImplementationAvailable() {
+ if (beanValidationImplementationAvailable != null) {
+ return beanValidationImplementationAvailable;
+ }
+ try {
+ Class> validationClass = Class
+ .forName("javax.validation.Validation");
+ Method buildFactoryMethod = validationClass
+ .getMethod("buildDefaultValidatorFactory");
+ Object factory = buildFactoryMethod.invoke(null);
+ beanValidationImplementationAvailable = (factory != null);
+ } catch (Exception e) {
+ // no bean validation implementation available
+ beanValidationImplementationAvailable = false;
+ }
+ return beanValidationImplementationAvailable;
+ }
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/fieldgroup/Caption.java b/src/com/vaadin/data/fieldgroup/Caption.java
new file mode 100644
index 0000000000..e9ae01a2d2
--- /dev/null
+++ b/src/com/vaadin/data/fieldgroup/Caption.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Caption {
+ String value();
+}
diff --git a/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
new file mode 100644
index 0000000000..2fc7bc6b7e
--- /dev/null
+++ b/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
@@ -0,0 +1,156 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.util.EnumSet;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.fieldgroup.FieldGroup.BindException;
+import com.vaadin.ui.AbstractSelect;
+import com.vaadin.ui.AbstractTextField;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.ListSelect;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.OptionGroup;
+import com.vaadin.ui.RichTextArea;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.TextField;
+
+public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
+
+ public static final Object CAPTION_PROPERTY_ID = "Caption";
+
+ public T createField(Class> type, Class fieldType) {
+ if (Enum.class.isAssignableFrom(type)) {
+ return createEnumField(type, fieldType);
+ } else if (Boolean.class.isAssignableFrom(type)
+ || boolean.class.isAssignableFrom(type)) {
+ return createBooleanField(fieldType);
+ }
+ if (AbstractTextField.class.isAssignableFrom(fieldType)) {
+ return fieldType.cast(createAbstractTextField(fieldType
+ .asSubclass(AbstractTextField.class)));
+ } else if (fieldType == RichTextArea.class) {
+ return fieldType.cast(createRichTextArea());
+ }
+ return createDefaultField(type, fieldType);
+ }
+
+ protected RichTextArea createRichTextArea() {
+ RichTextArea rta = new RichTextArea();
+ rta.setImmediate(true);
+
+ return rta;
+ }
+
+ private T createEnumField(Class> type,
+ Class fieldType) {
+ if (AbstractSelect.class.isAssignableFrom(fieldType)) {
+ AbstractSelect s = createCompatibleSelect((Class extends AbstractSelect>) fieldType);
+ populateWithEnumData(s, (Class extends Enum>) type);
+ return (T) s;
+ }
+
+ return null;
+ }
+
+ protected AbstractSelect createCompatibleSelect(
+ Class extends AbstractSelect> fieldType) {
+ AbstractSelect select;
+ if (fieldType.isAssignableFrom(ListSelect.class)) {
+ select = new ListSelect();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(NativeSelect.class)) {
+ select = new NativeSelect();
+ } else if (fieldType.isAssignableFrom(OptionGroup.class)) {
+ select = new OptionGroup();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(Table.class)) {
+ Table t = new Table();
+ t.setSelectable(true);
+ select = t;
+ } else {
+ select = new ComboBox(null);
+ }
+ select.setImmediate(true);
+ select.setNullSelectionAllowed(false);
+
+ return select;
+ }
+
+ protected T createBooleanField(Class fieldType) {
+ if (fieldType.isAssignableFrom(CheckBox.class)) {
+ CheckBox cb = new CheckBox(null);
+ cb.setImmediate(true);
+ return (T) cb;
+ } else if (AbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField((Class extends AbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ protected T createAbstractTextField(
+ Class fieldType) {
+ if (fieldType == AbstractTextField.class) {
+ fieldType = (Class) TextField.class;
+ }
+ try {
+ T field = fieldType.newInstance();
+ field.setImmediate(true);
+ return field;
+ } catch (Exception e) {
+ throw new BindException("Could not create a field of type "
+ + fieldType, e);
+ }
+ }
+
+ /**
+ * Fallback when no specific field has been created. Typically returns a
+ * TextField.
+ *
+ * @param
+ * The type of field to create
+ * @param type
+ * The type of data that should be edited
+ * @param fieldType
+ * The type of field to create
+ * @return A field capable of editing the data or null if no field could be
+ * created
+ */
+ protected T createDefaultField(Class> type,
+ Class fieldType) {
+ if (fieldType.isAssignableFrom(TextField.class)) {
+ return fieldType.cast(createAbstractTextField(TextField.class));
+ }
+ return null;
+ }
+
+ /**
+ * Populates the given select with all the enums in the given {@link Enum}
+ * class. Uses {@link Enum}.toString() for caption.
+ *
+ * @param select
+ * The select to populate
+ * @param enumClass
+ * The Enum class to use
+ */
+ protected void populateWithEnumData(AbstractSelect select,
+ Class extends Enum> enumClass) {
+ select.removeAllItems();
+ for (Object p : select.getContainerPropertyIds()) {
+ select.removeContainerProperty(p);
+ }
+ select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, "");
+ select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID);
+ @SuppressWarnings("unchecked")
+ EnumSet> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ Item newItem = select.addItem(r);
+ newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(r.toString());
+ }
+ }
+}
diff --git a/src/com/vaadin/data/fieldgroup/FieldGroup.java b/src/com/vaadin/data/fieldgroup/FieldGroup.java
new file mode 100644
index 0000000000..a5d676a5e9
--- /dev/null
+++ b/src/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -0,0 +1,978 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.TransactionalProperty;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.util.TransactionalPropertyWrapper;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.DefaultFieldFactory;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.Form;
+
+/**
+ * FieldGroup provides an easy way of binding fields to data and handling
+ * commits of these fields.
+ *
+ * The functionality of FieldGroup is similar to {@link Form} but
+ * {@link FieldGroup} does not handle layouts in any way. The typical use case
+ * is to create a layout outside the FieldGroup and then use FieldGroup to bind
+ * the fields to a data source.
+ *
+ *
+ * {@link FieldGroup} is not a UI component so it cannot be added to a layout.
+ * Using the buildAndBind methods {@link FieldGroup} can create fields for you
+ * using a FieldGroupFieldFactory but you still have to add them to the correct
+ * position in your layout.
+ *
+ *
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ */
+public class FieldGroup implements Serializable {
+
+ private static final Logger logger = Logger.getLogger(FieldGroup.class
+ .getName());
+
+ private Item itemDataSource;
+ private boolean buffered = true;
+
+ private boolean enabled = true;
+ private boolean readOnly = false;
+
+ private HashMap> propertyIdToField = new HashMap>();
+ private LinkedHashMap, Object> fieldToPropertyId = new LinkedHashMap, Object>();
+ private List commitHandlers = new ArrayList();
+
+ /**
+ * The field factory used by builder methods.
+ */
+ private FieldGroupFieldFactory fieldFactory;
+
+ /**
+ * Constructs a field binder. Use {@link #setItemDataSource(Item)} to set a
+ * data source for the field binder.
+ *
+ */
+ public FieldGroup() {
+
+ }
+
+ /**
+ * Constructs a field binder that uses the given data source.
+ *
+ * @param itemDataSource
+ * The data source to bind the fields to
+ */
+ public FieldGroup(Item itemDataSource) {
+ setItemDataSource(itemDataSource);
+ }
+
+ /**
+ * Updates the item that is used by this FieldBinder. Rebinds all fields to
+ * the properties in the new item.
+ *
+ * @param itemDataSource
+ * The new item to use
+ */
+ public void setItemDataSource(Item itemDataSource) {
+ this.itemDataSource = itemDataSource;
+
+ for (Field> f : fieldToPropertyId.keySet()) {
+ bind(f, fieldToPropertyId.get(f));
+ }
+ }
+
+ /**
+ * Gets the item used by this FieldBinder. Note that you must call
+ * {@link #commit()} for the item to be updated unless buffered mode has
+ * been switched off.
+ *
+ * @see #setBuffered(boolean)
+ * @see #commit()
+ *
+ * @return The item used by this FieldBinder
+ */
+ public Item getItemDataSource() {
+ return itemDataSource;
+ }
+
+ /**
+ * Checks the buffered mode for the bound fields.
+ *
+ *
+ * @see #setBuffered(boolean) for more details on buffered mode
+ *
+ * @see Field#isBuffered()
+ * @return true if buffered mode is on, false otherwise
+ *
+ */
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ /**
+ * Sets the buffered mode for the bound fields.
+ *
+ * When buffered mode is on the item will not be updated until
+ * {@link #commit()} is called. If buffered mode is off the item will be
+ * updated once the fields are updated.
+ *
+ *
+ * The default is to use buffered mode.
+ *
+ *
+ * @see Field#setBuffered(boolean)
+ * @param buffered
+ * true to turn on buffered mode, false otherwise
+ */
+ public void setBuffered(boolean buffered) {
+ if (buffered == this.buffered) {
+ return;
+ }
+
+ this.buffered = buffered;
+ for (Field> field : getFields()) {
+ field.setBuffered(buffered);
+ }
+ }
+
+ /**
+ * Returns the enabled status for the fields.
+ *
+ * Note that this will not accurately represent the enabled status of all
+ * fields if you change the enabled status of the fields through some other
+ * method than {@link #setEnabled(boolean)}.
+ *
+ * @return true if the fields are enabled, false otherwise
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Updates the enabled state of all bound fields.
+ *
+ * @param fieldsEnabled
+ * true to enable all bound fields, false to disable them
+ */
+ public void setEnabled(boolean fieldsEnabled) {
+ enabled = fieldsEnabled;
+ for (Field> field : getFields()) {
+ field.setEnabled(fieldsEnabled);
+ }
+ }
+
+ /**
+ * Returns the read only status for the fields.
+ *
+ * Note that this will not accurately represent the read only status of all
+ * fields if you change the read only status of the fields through some
+ * other method than {@link #setReadOnly(boolean)}.
+ *
+ * @return true if the fields are set to read only, false otherwise
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Updates the read only state of all bound fields.
+ *
+ * @param fieldsReadOnly
+ * true to set all bound fields to read only, false to set them
+ * to read write
+ */
+ public void setReadOnly(boolean fieldsReadOnly) {
+ readOnly = fieldsReadOnly;
+ }
+
+ /**
+ * Returns a collection of all fields that have been bound.
+ *
+ * The fields are not returned in any specific order.
+ *
+ *
+ * @return A collection with all bound Fields
+ */
+ public Collection> getFields() {
+ return fieldToPropertyId.keySet();
+ }
+
+ /**
+ * Binds the field with the given propertyId from the current item. If an
+ * item has not been set then the binding is postponed until the item is set
+ * using {@link #setItemDataSource(Item)}.
+ *
+ * This method also adds validators when applicable.
+ *
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind to the field
+ * @throws BindException
+ * If the property id is already bound to another field by this
+ * field binder
+ */
+ public void bind(Field> field, Object propertyId) throws BindException {
+ if (propertyIdToField.containsKey(propertyId)
+ && propertyIdToField.get(propertyId) != field) {
+ throw new BindException("Property id " + propertyId
+ + " is already bound to another field");
+ }
+ fieldToPropertyId.put(field, propertyId);
+ propertyIdToField.put(propertyId, field);
+ if (itemDataSource == null) {
+ // Will be bound when data source is set
+ return;
+ }
+
+ field.setPropertyDataSource(wrapInTransactionalProperty(getItemProperty(propertyId)));
+ configureField(field);
+ }
+
+ private Property.Transactional wrapInTransactionalProperty(
+ Property itemProperty) {
+ return new TransactionalPropertyWrapper(itemProperty);
+ }
+
+ /**
+ * Gets the property with the given property id from the item.
+ *
+ * @param propertyId
+ * The id if the property to find
+ * @return The property with the given id from the item
+ * @throws BindException
+ * If the property was not found in the item or no item has been
+ * set
+ */
+ protected Property> getItemProperty(Object propertyId)
+ throws BindException {
+ Item item = getItemDataSource();
+ if (item == null) {
+ throw new BindException("Could not lookup property with id "
+ + propertyId + " as no item has been set");
+ }
+ Property> p = item.getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException("A property with id " + propertyId
+ + " was not found in the item");
+ }
+ return p;
+ }
+
+ /**
+ * Detaches the field from its property id and removes it from this
+ * FieldBinder.
+ *
+ * Note that the field is not detached from its property data source if it
+ * is no longer connected to the same property id it was bound to using this
+ * FieldBinder.
+ *
+ * @param field
+ * The field to detach
+ * @throws BindException
+ * If the field is not bound by this field binder or not bound
+ * to the correct property id
+ */
+ public void unbind(Field> field) throws BindException {
+ Object propertyId = fieldToPropertyId.get(field);
+ if (propertyId == null) {
+ throw new BindException(
+ "The given field is not part of this FieldBinder");
+ }
+
+ Property fieldDataSource = field.getPropertyDataSource();
+ if (fieldDataSource instanceof TransactionalPropertyWrapper) {
+ fieldDataSource = ((TransactionalPropertyWrapper) fieldDataSource)
+ .getWrappedProperty();
+ }
+ if (fieldDataSource == getItemProperty(propertyId)) {
+ field.setPropertyDataSource(null);
+ }
+ fieldToPropertyId.remove(field);
+ propertyIdToField.remove(propertyId);
+ }
+
+ /**
+ * Configures a field with the settings set for this FieldBinder.
+ *
+ * By default this updates the buffered, read only and enabled state of the
+ * field. Also adds validators when applicable.
+ *
+ * @param field
+ * The field to update
+ */
+ protected void configureField(Field> field) {
+ field.setBuffered(isBuffered());
+
+ field.setEnabled(isEnabled());
+ field.setReadOnly(isReadOnly());
+ }
+
+ /**
+ * Gets the type of the property with the given property id.
+ *
+ * @param propertyId
+ * The propertyId. Must be find
+ * @return The type of the property
+ */
+ protected Class> getPropertyType(Object propertyId) throws BindException {
+ if (getItemDataSource() == null) {
+ throw new BindException(
+ "Property type for '"
+ + propertyId
+ + "' could not be determined. No item data source has been set.");
+ }
+ Property> p = getItemDataSource().getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException(
+ "Property type for '"
+ + propertyId
+ + "' could not be determined. No property with that id was found.");
+ }
+
+ return p.getType();
+ }
+
+ /**
+ * Returns a collection of all property ids that have been bound to fields.
+ *
+ * Note that this will return property ids even before the item has been
+ * set. In that case it returns the property ids that will be bound once the
+ * item is set.
+ *
+ *
+ * No guarantee is given for the order of the property ids
+ *
+ *
+ * @return A collection of bound property ids
+ */
+ public Collection getBoundPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIdToField.keySet());
+ }
+
+ /**
+ * Returns a collection of all property ids that exist in the item set using
+ * {@link #setItemDataSource(Item)} but have not been bound to fields.
+ *
+ * Will always return an empty collection before an item has been set using
+ * {@link #setItemDataSource(Item)}.
+ *
+ *
+ * No guarantee is given for the order of the property ids
+ *
+ *
+ * @return A collection of property ids that have not been bound to fields
+ */
+ public Collection getUnboundPropertyIds() {
+ if (getItemDataSource() == null) {
+ return new ArrayList();
+ }
+ List unboundPropertyIds = new ArrayList();
+ 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()) {
+ ((TransactionalProperty>) 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()) {
+ ((TransactionalProperty>) f.getPropertyDataSource()).commit();
+ }
+
+ } catch (Exception e) {
+ for (Field> f : fieldToPropertyId.keySet()) {
+ try {
+ ((TransactionalProperty>) 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 on 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 id
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. 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.
+ *
+ * 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 id
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. 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 : objectClass
+ .getDeclaredFields()) {
+
+ 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 {
+ propertyId = memberField.getName();
+ }
+
+ // 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);
+ } 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);
+ }
+ }
+ }
+
+ 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);
+ }
+
+ }
+
+ /**
+ * 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;
+ }
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java b/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
new file mode 100644
index 0000000000..6945a492bd
--- /dev/null
+++ b/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Field;
+
+/**
+ * Factory interface for creating new Field-instances based on the data type
+ * that should be edited.
+ *
+ * @author Vaadin Ltd.
+ * @version @version@
+ * @since 7.0
+ */
+public interface FieldGroupFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the data type that we want to edit
+ *
+ * @param dataType
+ * The type that we want to edit using the field
+ * @param fieldType
+ * The type of field we want to create. If set to {@link Field}
+ * then any type of field is accepted
+ * @return A field that can be assigned to the given fieldType and that is
+ * capable of editing the given type of data
+ */
+ T createField(Class> dataType, Class fieldType);
+}
diff --git a/src/com/vaadin/data/fieldgroup/PropertyId.java b/src/com/vaadin/data/fieldgroup/PropertyId.java
new file mode 100644
index 0000000000..588fdc3020
--- /dev/null
+++ b/src/com/vaadin/data/fieldgroup/PropertyId.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PropertyId {
+ String value();
+}
diff --git a/tests/testbench/com/vaadin/tests/fieldbinder/AbstractBeanFieldBinderTest.java b/tests/testbench/com/vaadin/tests/fieldbinder/AbstractBeanFieldBinderTest.java
index 8b4dac2d6f..e2ed2ad696 100644
--- a/tests/testbench/com/vaadin/tests/fieldbinder/AbstractBeanFieldBinderTest.java
+++ b/tests/testbench/com/vaadin/tests/fieldbinder/AbstractBeanFieldBinderTest.java
@@ -1,7 +1,7 @@
package com.vaadin.tests.fieldbinder;
-import com.vaadin.data.fieldbinder.BeanFieldGroup;
-import com.vaadin.data.fieldbinder.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.tests.components.TestBase;
import com.vaadin.tests.util.Log;
import com.vaadin.ui.Button;
diff --git a/tests/testbench/com/vaadin/tests/fieldbinder/BasicPersonForm.java b/tests/testbench/com/vaadin/tests/fieldbinder/BasicPersonForm.java
index dd3f201f49..1ba9426fce 100644
--- a/tests/testbench/com/vaadin/tests/fieldbinder/BasicPersonForm.java
+++ b/tests/testbench/com/vaadin/tests/fieldbinder/BasicPersonForm.java
@@ -1,10 +1,10 @@
package com.vaadin.tests.fieldbinder;
-import com.vaadin.data.fieldbinder.BeanFieldGroup;
-import com.vaadin.data.fieldbinder.FieldGroup;
-import com.vaadin.data.fieldbinder.FieldGroup.CommitEvent;
-import com.vaadin.data.fieldbinder.FieldGroup.CommitException;
-import com.vaadin.data.fieldbinder.FieldGroup.CommitHandler;
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitEvent;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitHandler;
import com.vaadin.data.util.BeanItem;
import com.vaadin.data.util.converter.StringToBooleanConverter;
import com.vaadin.data.validator.EmailValidator;
diff --git a/tests/testbench/com/vaadin/tests/fieldbinder/FieldBinderWithBeanValidation.java b/tests/testbench/com/vaadin/tests/fieldbinder/FieldBinderWithBeanValidation.java
index f6c585d5db..e7b581907e 100644
--- a/tests/testbench/com/vaadin/tests/fieldbinder/FieldBinderWithBeanValidation.java
+++ b/tests/testbench/com/vaadin/tests/fieldbinder/FieldBinderWithBeanValidation.java
@@ -1,8 +1,8 @@
package com.vaadin.tests.fieldbinder;
-import com.vaadin.data.fieldbinder.BeanFieldGroup;
-import com.vaadin.data.fieldbinder.FieldGroup;
-import com.vaadin.data.fieldbinder.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.util.BeanItem;
import com.vaadin.tests.components.TestBase;
import com.vaadin.tests.data.bean.Address;
diff --git a/tests/testbench/com/vaadin/tests/fieldbinder/FormBuilderWithNestedProperties.java b/tests/testbench/com/vaadin/tests/fieldbinder/FormBuilderWithNestedProperties.java
index da6a666590..e786dae279 100644
--- a/tests/testbench/com/vaadin/tests/fieldbinder/FormBuilderWithNestedProperties.java
+++ b/tests/testbench/com/vaadin/tests/fieldbinder/FormBuilderWithNestedProperties.java
@@ -1,8 +1,8 @@
package com.vaadin.tests.fieldbinder;
-import com.vaadin.data.fieldbinder.BeanFieldGroup;
-import com.vaadin.data.fieldbinder.FieldGroup;
-import com.vaadin.data.fieldbinder.PropertyId;
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.PropertyId;
import com.vaadin.data.util.BeanItem;
import com.vaadin.tests.components.TestBase;
import com.vaadin.tests.data.bean.Address;
diff --git a/tests/testbench/com/vaadin/tests/fieldbinder/FormWithNestedProperties.java b/tests/testbench/com/vaadin/tests/fieldbinder/FormWithNestedProperties.java
index 9886bc61f5..7dbf1e7f13 100644
--- a/tests/testbench/com/vaadin/tests/fieldbinder/FormWithNestedProperties.java
+++ b/tests/testbench/com/vaadin/tests/fieldbinder/FormWithNestedProperties.java
@@ -1,7 +1,7 @@
package com.vaadin.tests.fieldbinder;
-import com.vaadin.data.fieldbinder.BeanFieldGroup;
-import com.vaadin.data.fieldbinder.PropertyId;
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.data.fieldgroup.PropertyId;
import com.vaadin.tests.data.bean.Address;
import com.vaadin.tests.data.bean.Country;
import com.vaadin.tests.data.bean.Person;