aboutsummaryrefslogtreecommitdiffstats
path: root/compatibility-server/src/main/java/com/vaadin/data
diff options
context:
space:
mode:
Diffstat (limited to 'compatibility-server/src/main/java/com/vaadin/data')
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java272
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java27
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java254
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java1258
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java43
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java60
6 files changed, 1914 insertions, 0 deletions
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java
new file mode 100644
index 0000000000..96e4621761
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.beans.IntrospectionException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.util.BeanUtil;
+import com.vaadin.v7.data.validator.LegacyBeanValidator;
+import com.vaadin.v7.ui.LegacyField;
+
+public class BeanFieldGroup<T> extends FieldGroup {
+
+ private final Class<T> beanType;
+
+ private static Boolean beanValidationImplementationAvailable = null;
+ private final Map<LegacyField<?>, LegacyBeanValidator> defaultValidators;
+
+ public BeanFieldGroup(Class<T> beanType) {
+ this.beanType = beanType;
+ this.defaultValidators = new HashMap<LegacyField<?>, LegacyBeanValidator>();
+ }
+
+ @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.
+ */
+ try {
+ Class<?> type = BeanUtil.getPropertyType(beanType,
+ propertyId.toString());
+ if (type == null) {
+ throw new BindException(
+ "Cannot determine type of propertyId '" + propertyId
+ + "'. The propertyId was not found in "
+ + beanType.getName());
+ }
+ return type;
+ } catch (IntrospectionException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'. Unable to introspect " + beanType,
+ e);
+ }
+ }
+ }
+
+ @Override
+ protected Object findPropertyId(java.lang.reflect.Field memberField) {
+ String fieldName = memberField.getName();
+ Item dataSource = getItemDataSource();
+ if (dataSource != null
+ && dataSource.getItemProperty(fieldName) != null) {
+ return fieldName;
+ } else {
+ String minifiedFieldName = minifyFieldName(fieldName);
+ try {
+ return getFieldName(beanType, minifiedFieldName);
+ } catch (SecurityException e) {
+ } catch (NoSuchFieldException e) {
+ }
+ }
+ return null;
+ }
+
+ private static String getFieldName(Class<?> cls, String propertyId)
+ throws SecurityException, NoSuchFieldException {
+ for (java.lang.reflect.Field field1 : cls.getDeclaredFields()) {
+ if (propertyId.equals(minifyFieldName(field1.getName()))) {
+ return field1.getName();
+ }
+ }
+ // Try super classes until we reach Object
+ Class<?> superClass = cls.getSuperclass();
+ if (superClass != null && superClass != Object.class) {
+ return getFieldName(superClass, propertyId);
+ } else {
+ throw new NoSuchFieldException();
+ }
+ }
+
+ /**
+ * 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)}.
+ * <p>
+ * For null values, a null item is passed to
+ * {@link #setItemDataSource(Item)} to be properly clear fields.
+ *
+ * @param bean
+ * The bean to use as data source.
+ */
+ public void setItemDataSource(T bean) {
+ if (bean == null) {
+ setItemDataSource((Item) null);
+ } else {
+ setItemDataSource(new BeanItem<T>(bean, beanType));
+ }
+ }
+
+ @Override
+ public void setItemDataSource(Item item) {
+ if (item == null || (item instanceof BeanItem)) {
+ super.setItemDataSource(item);
+ } else {
+ throw new RuntimeException(getClass().getSimpleName()
+ + " only supports BeanItems as item data source");
+ }
+ }
+
+ @Override
+ public BeanItem<T> getItemDataSource() {
+ return (BeanItem<T>) super.getItemDataSource();
+ }
+
+ private void ensureNestedPropertyAdded(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);
+ }
+ }
+ }
+
+ @Override
+ public void bind(LegacyField field, Object propertyId) {
+ ensureNestedPropertyAdded(propertyId);
+ super.bind(field, propertyId);
+ }
+
+ @Override
+ public <T extends LegacyField> T buildAndBind(String caption,
+ Object propertyId, Class<T> fieldType) throws BindException {
+ ensureNestedPropertyAdded(propertyId);
+ return super.buildAndBind(caption, propertyId, fieldType);
+ }
+
+ @Override
+ public void unbind(LegacyField<?> field) throws BindException {
+ super.unbind(field);
+
+ LegacyBeanValidator removed = defaultValidators.remove(field);
+ if (removed != null) {
+ field.removeValidator(removed);
+ }
+ }
+
+ @Override
+ protected void configureField(LegacyField<?> field) {
+ super.configureField(field);
+ // Add Bean validators if there are annotations
+ if (isBeanValidationImplementationAvailable()
+ && !defaultValidators.containsKey(field)) {
+ LegacyBeanValidator validator = new LegacyBeanValidator(beanType,
+ getPropertyId(field).toString());
+ field.addValidator(validator);
+ if (field.getLocale() != null) {
+ validator.setLocale(field.getLocale());
+ }
+ defaultValidators.put(field, validator);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Convenience method to bind Fields from a given "field container" to a
+ * given bean with buffering disabled.
+ * <p>
+ * The returned {@link BeanFieldGroup} can be used for further
+ * configuration.
+ *
+ * @see #bindFieldsBuffered(Object, Object)
+ * @see #bindMemberFields(Object)
+ * @since 7.2
+ * @param bean
+ * the bean to be bound
+ * @param objectWithMemberFields
+ * the class that contains {@link LegacyField}s for bean
+ * properties
+ * @return the bean field group used to make binding
+ */
+ public static <T> BeanFieldGroup<T> bindFieldsUnbuffered(T bean,
+ Object objectWithMemberFields) {
+ return createAndBindFields(bean, objectWithMemberFields, false);
+ }
+
+ /**
+ * Convenience method to bind Fields from a given "field container" to a
+ * given bean with buffering enabled.
+ * <p>
+ * The returned {@link BeanFieldGroup} can be used for further
+ * configuration.
+ *
+ * @see #bindFieldsUnbuffered(Object, Object)
+ * @see #bindMemberFields(Object)
+ * @since 7.2
+ * @param bean
+ * the bean to be bound
+ * @param objectWithMemberFields
+ * the class that contains {@link LegacyField}s for bean
+ * properties
+ * @return the bean field group used to make binding
+ */
+ public static <T> BeanFieldGroup<T> bindFieldsBuffered(T bean,
+ Object objectWithMemberFields) {
+ return createAndBindFields(bean, objectWithMemberFields, true);
+ }
+
+ private static <T> BeanFieldGroup<T> createAndBindFields(T bean,
+ Object objectWithMemberFields, boolean buffered) {
+ @SuppressWarnings("unchecked")
+ BeanFieldGroup<T> beanFieldGroup = new BeanFieldGroup<T>(
+ (Class<T>) bean.getClass());
+ beanFieldGroup.setItemDataSource(bean);
+ beanFieldGroup.setBuffered(buffered);
+ beanFieldGroup.bindMemberFields(objectWithMemberFields);
+ return beanFieldGroup;
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java
new file mode 100644
index 0000000000..d752aa78d2
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+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/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
new file mode 100644
index 0000000000..24c97eedc5
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.util.Date;
+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.ComboBox;
+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.v7.ui.LegacyAbstractField;
+import com.vaadin.v7.ui.LegacyAbstractTextField;
+import com.vaadin.v7.ui.LegacyCheckBox;
+import com.vaadin.v7.ui.LegacyDateField;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyInlineDateField;
+import com.vaadin.v7.ui.LegacyPopupDateField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+/**
+ * This class contains a basic implementation for {@link FieldGroupFieldFactory}
+ * .The class is singleton, use {@link #get()} method to get reference to the
+ * instance.
+ *
+ * @author Vaadin Ltd
+ */
+public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
+
+ private static final DefaultFieldGroupFieldFactory INSTANCE = new DefaultFieldGroupFieldFactory();
+
+ public static final Object CAPTION_PROPERTY_ID = "Caption";
+
+ protected DefaultFieldGroupFieldFactory() {
+ }
+
+ /**
+ * Gets the singleton instance.
+ *
+ * @since 7.4
+ *
+ * @return the singleton instance
+ */
+ public static DefaultFieldGroupFieldFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public <T extends LegacyField> T createField(Class<?> type,
+ Class<T> fieldType) {
+ if (Enum.class.isAssignableFrom(type)) {
+ return createEnumField(type, fieldType);
+ } else if (Date.class.isAssignableFrom(type)) {
+ return createDateField(type, fieldType);
+ } else if (Boolean.class.isAssignableFrom(type)
+ || boolean.class.isAssignableFrom(type)) {
+ return createBooleanField(fieldType);
+ }
+ if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ return fieldType.cast(createAbstractTextField(
+ fieldType.asSubclass(LegacyAbstractTextField.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 extends LegacyField> T createEnumField(Class<?> type,
+ Class<T> fieldType) {
+ // Determine first if we should (or can) create a select for the enum
+ Class<AbstractSelect> selectClass = null;
+ if (AbstractSelect.class.isAssignableFrom(fieldType)) {
+ selectClass = (Class<AbstractSelect>) fieldType;
+ } else if (anySelect(fieldType)) {
+ selectClass = AbstractSelect.class;
+ }
+
+ if (selectClass != null) {
+ AbstractSelect s = createCompatibleSelect(selectClass);
+ populateWithEnumData(s, (Class<? extends Enum>) type);
+ return (T) s;
+ } else if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField(
+ (Class<? extends LegacyAbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ private <T extends LegacyField> T createDateField(Class<?> type,
+ Class<T> fieldType) {
+ LegacyAbstractField field;
+
+ if (LegacyInlineDateField.class.isAssignableFrom(fieldType)) {
+ field = new LegacyInlineDateField();
+ } else if (anyField(fieldType)
+ || LegacyDateField.class.isAssignableFrom(fieldType)) {
+ field = new LegacyPopupDateField();
+ } else if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ field = createAbstractTextField(
+ (Class<? extends LegacyAbstractTextField>) fieldType);
+ } else {
+ return null;
+ }
+
+ field.setImmediate(true);
+ return (T) field;
+ }
+
+ 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;
+ }
+
+ /**
+ * @since 7.4
+ * @param fieldType
+ * the type of the field
+ * @return true if any LegacyAbstractField can be assigned to the field
+ */
+ protected boolean anyField(Class<?> fieldType) {
+ return fieldType == LegacyField.class
+ || fieldType == LegacyAbstractField.class;
+ }
+
+ /**
+ * @since 7.4
+ * @param fieldType
+ * the type of the field
+ * @return true if any AbstractSelect can be assigned to the field
+ */
+ protected boolean anySelect(Class<? extends LegacyField> fieldType) {
+ return anyField(fieldType) || fieldType == AbstractSelect.class;
+ }
+
+ protected <T extends LegacyField> T createBooleanField(Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(LegacyCheckBox.class)) {
+ LegacyCheckBox cb = new LegacyCheckBox(null);
+ cb.setImmediate(true);
+ return (T) cb;
+ } else if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField(
+ (Class<? extends LegacyAbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ protected <T extends LegacyAbstractTextField> T createAbstractTextField(
+ Class<T> fieldType) {
+ if (fieldType == LegacyAbstractTextField.class) {
+ fieldType = (Class<T>) LegacyTextField.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 <T>
+ * 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 extends LegacyField> T createDefaultField(Class<?> type,
+ Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(LegacyTextField.class)) {
+ return fieldType
+ .cast(createAbstractTextField(LegacyTextField.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/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java
new file mode 100644
index 0000000000..1254009cfc
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -0,0 +1,1258 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+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.Map;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.TransactionalPropertyWrapper;
+import com.vaadin.ui.DefaultFieldFactory;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.ui.LegacyAbstractField;
+import com.vaadin.v7.ui.LegacyField;
+
+/**
+ * FieldGroup provides an easy way of binding fields to data and handling
+ * commits of these fields.
+ * <p>
+ * The typical use case is to create a layout outside the FieldGroup and then
+ * use FieldGroup to bind the fields to a data source.
+ * </p>
+ * <p>
+ * {@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.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public class FieldGroup implements Serializable {
+
+ private Item itemDataSource;
+ private boolean buffered = true;
+
+ private boolean enabled = true;
+ private boolean readOnly = false;
+
+ private HashMap<Object, LegacyField<?>> propertyIdToField = new HashMap<Object, LegacyField<?>>();
+ private LinkedHashMap<LegacyField<?>, Object> fieldToPropertyId = new LinkedHashMap<LegacyField<?>, Object>();
+ private List<CommitHandler> commitHandlers = new ArrayList<CommitHandler>();
+
+ /**
+ * The field factory used by builder methods.
+ */
+ private FieldGroupFieldFactory fieldFactory = DefaultFieldGroupFieldFactory
+ .get();
+
+ /**
+ * 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 (LegacyField<?> 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.
+ * <p>
+ *
+ * @see #setBuffered(boolean) for more details on buffered mode
+ *
+ * @see LegacyField#isBuffered()
+ * @return true if buffered mode is on, false otherwise
+ *
+ */
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ /**
+ * Sets the buffered mode for the bound fields.
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * The default is to use buffered mode.
+ * </p>
+ *
+ * @see LegacyField#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 (LegacyField<?> field : getFields()) {
+ field.setBuffered(buffered);
+ }
+ }
+
+ /**
+ * Returns the enabled status for the fields.
+ * <p>
+ * 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 (LegacyField<?> field : getFields()) {
+ field.setEnabled(fieldsEnabled);
+ }
+ }
+
+ /**
+ * Returns the read only status that is used by default with all fields that
+ * have a writable data source.
+ * <p>
+ * 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;
+ }
+
+ /**
+ * Sets the read only state to the given value for all fields with writable
+ * data source. Fields with read only data source will always be set to read
+ * only.
+ *
+ * @param fieldsReadOnly
+ * true to set the fields with writable data source to read only,
+ * false to set them to read write
+ */
+ public void setReadOnly(boolean fieldsReadOnly) {
+ readOnly = fieldsReadOnly;
+ for (LegacyField<?> field : getFields()) {
+ if (field.getPropertyDataSource() == null
+ || !field.getPropertyDataSource().isReadOnly()) {
+ field.setReadOnly(fieldsReadOnly);
+ } else {
+ field.setReadOnly(true);
+ }
+ }
+ }
+
+ /**
+ * Returns a collection of all fields that have been bound.
+ * <p>
+ * The fields are not returned in any specific order.
+ * </p>
+ *
+ * @return A collection with all bound Fields
+ */
+ public Collection<LegacyField<?>> 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)}.
+ * <p>
+ * This method also adds validators when applicable.
+ * </p>
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind to the field
+ * @throws BindException
+ * If the field is null or the property id is already bound to
+ * another field by this field binder
+ */
+ public void bind(LegacyField<?> field, Object propertyId)
+ throws BindException {
+ throwIfFieldIsNull(field, propertyId);
+ throwIfPropertyIdAlreadyBound(field, propertyId);
+
+ fieldToPropertyId.put(field, propertyId);
+ propertyIdToField.put(propertyId, field);
+ if (itemDataSource == null) {
+ // Clear any possible existing binding to clear the field
+ field.setPropertyDataSource(null);
+ boolean fieldReadOnly = field.isReadOnly();
+ if (!fieldReadOnly) {
+ field.clear();
+ } else {
+ // Temporarily make the field read-write so we can clear the
+ // value. Needed because setPropertyDataSource(null) does not
+ // currently clear the field
+ // (https://dev.vaadin.com/ticket/14733)
+ field.setReadOnly(false);
+ field.clear();
+ field.setReadOnly(true);
+ }
+
+ // Will be bound when data source is set
+ return;
+ }
+
+ field.setPropertyDataSource(
+ wrapInTransactionalProperty(getItemProperty(propertyId)));
+ configureField(field);
+ }
+
+ /**
+ * Wrap property to transactional property.
+ */
+ protected <T> Property.Transactional<T> wrapInTransactionalProperty(
+ Property<T> itemProperty) {
+ return new TransactionalPropertyWrapper<T>(itemProperty);
+ }
+
+ private void throwIfFieldIsNull(LegacyField<?> field, Object propertyId) {
+ if (field == null) {
+ throw new BindException(String.format(
+ "Cannot bind property id '%s' to a null field.",
+ propertyId));
+ }
+ }
+
+ private void throwIfPropertyIdAlreadyBound(LegacyField<?> field,
+ Object propertyId) {
+ if (propertyIdToField.containsKey(propertyId)
+ && propertyIdToField.get(propertyId) != field) {
+ throw new BindException("Property id " + propertyId
+ + " is already bound to another field");
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * 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(LegacyField<?> field) throws BindException {
+ Object propertyId = fieldToPropertyId.get(field);
+ if (propertyId == null) {
+ throw new BindException(
+ "The given field is not part of this FieldBinder");
+ }
+
+ TransactionalPropertyWrapper<?> wrapper = null;
+ Property fieldDataSource = field.getPropertyDataSource();
+ if (fieldDataSource instanceof TransactionalPropertyWrapper) {
+ wrapper = (TransactionalPropertyWrapper<?>) fieldDataSource;
+ fieldDataSource = ((TransactionalPropertyWrapper<?>) fieldDataSource)
+ .getWrappedProperty();
+
+ }
+ if (getItemDataSource() != null
+ && fieldDataSource == getItemProperty(propertyId)) {
+ if (null != wrapper) {
+ wrapper.detachFromProperty();
+ }
+ field.setPropertyDataSource(null);
+ }
+ fieldToPropertyId.remove(field);
+ propertyIdToField.remove(propertyId);
+ }
+
+ /**
+ * Configures a field with the settings set for this FieldBinder.
+ * <p>
+ * By default this updates the buffered, read only and enabled state of the
+ * field. Also adds validators when applicable. Fields with read only data
+ * source are always configured as read only.
+ *
+ * @param field
+ * The field to update
+ */
+ protected void configureField(LegacyField<?> field) {
+ field.setBuffered(isBuffered());
+
+ field.setEnabled(isEnabled());
+
+ if (field.getPropertyDataSource().isReadOnly()) {
+ field.setReadOnly(true);
+ } else {
+ 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.
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of bound property ids
+ */
+ public Collection<Object> 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.
+ * <p>
+ * Will always return an empty collection before an item has been set using
+ * {@link #setItemDataSource(Item)}.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of property ids that have not been bound to fields
+ */
+ public Collection<Object> getUnboundPropertyIds() {
+ if (getItemDataSource() == null) {
+ return new ArrayList<Object>();
+ }
+ List<Object> unboundPropertyIds = new ArrayList<Object>();
+ unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds());
+ unboundPropertyIds.removeAll(propertyIdToField.keySet());
+ return unboundPropertyIds;
+ }
+
+ /**
+ * Commits all changes done to the bound fields.
+ * <p>
+ * 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;
+ }
+
+ startTransactions();
+
+ try {
+ firePreCommitEvent();
+
+ Map<LegacyField<?>, InvalidValueException> invalidValueExceptions = commitFields();
+
+ if (invalidValueExceptions.isEmpty()) {
+ firePostCommitEvent();
+ commitTransactions();
+ } else {
+ throw new FieldGroupInvalidValueException(
+ invalidValueExceptions);
+ }
+ } catch (Exception e) {
+ rollbackTransactions();
+ throw new CommitException("Commit failed", this, e);
+ }
+
+ }
+
+ /**
+ * Tries to commit all bound fields one by one and gathers any validation
+ * exceptions in a map, which is returned to the caller
+ *
+ * @return a propertyId to validation exception map which is empty if all
+ * commits succeeded
+ */
+ private Map<LegacyField<?>, InvalidValueException> commitFields() {
+ Map<LegacyField<?>, InvalidValueException> invalidValueExceptions = new HashMap<LegacyField<?>, InvalidValueException>();
+
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ try {
+ f.commit();
+ } catch (InvalidValueException e) {
+ invalidValueExceptions.put(f, e);
+ }
+ }
+
+ return invalidValueExceptions;
+ }
+
+ /**
+ * Exception which wraps InvalidValueExceptions from all invalid fields in a
+ * FieldGroup
+ *
+ * @since 7.4
+ */
+ public static class FieldGroupInvalidValueException
+ extends InvalidValueException {
+ private Map<LegacyField<?>, InvalidValueException> invalidValueExceptions;
+
+ /**
+ * Constructs a new exception with the specified validation exceptions.
+ *
+ * @param invalidValueExceptions
+ * a property id to exception map
+ */
+ public FieldGroupInvalidValueException(
+ Map<LegacyField<?>, InvalidValueException> invalidValueExceptions) {
+ super(null, invalidValueExceptions.values().toArray(
+ new InvalidValueException[invalidValueExceptions.size()]));
+ this.invalidValueExceptions = invalidValueExceptions;
+ }
+
+ /**
+ * Returns a map containing fields which failed validation and the
+ * exceptions the corresponding validators threw.
+ *
+ * @return a map with all the invalid value exceptions
+ */
+ public Map<LegacyField<?>, InvalidValueException> getInvalidFields() {
+ return invalidValueExceptions;
+ }
+ }
+
+ private void startTransactions() throws CommitException {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ Property.Transactional<?> property = (Property.Transactional<?>) f
+ .getPropertyDataSource();
+ if (property == null) {
+ throw new CommitException(
+ "Property \"" + fieldToPropertyId.get(f)
+ + "\" not bound to datasource.");
+ }
+ property.startTransaction();
+ }
+ }
+
+ private void commitTransactions() {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ ((Property.Transactional<?>) f.getPropertyDataSource()).commit();
+ }
+ }
+
+ private void rollbackTransactions() {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ try {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .rollback();
+ } catch (Exception rollbackException) {
+ // FIXME: What to do ?
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * Only has effect if buffered mode is used.
+ *
+ */
+ public void discard() {
+ for (LegacyField<?> 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 LegacyField<?> 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(LegacyField<?> field) {
+ return fieldToPropertyId.get(field);
+ }
+
+ /**
+ * Adds a commit handler.
+ * <p>
+ * 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}.
+ * <p>
+ * Use {@link #addCommitHandler(CommitHandler)} and
+ * {@link #removeCommitHandler(CommitHandler)} to register or unregister a
+ * commit handler.
+ *
+ * @return A collection of commit handlers
+ */
+ protected Collection<CommitHandler> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * Call the {@link LegacyField#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 (LegacyField<?> 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 (LegacyField<?> 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.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link LegacyField} 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.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * &#64;PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age = new TextField("Age"); ... }
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.bindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * 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.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link LegacyField} 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.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * &#64;PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age;
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.buildAndBindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link LegacyField} 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.
+ * </p>
+ *
+ * @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 (!LegacyField.class.isAssignableFrom(memberField.getType())) {
+ // Process next field
+ continue;
+ }
+
+ PropertyId propertyIdAnnotation = memberField
+ .getAnnotation(PropertyId.class);
+
+ Class<? extends LegacyField> fieldType = (Class<? extends LegacyField>) 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;
+ }
+
+ LegacyField<?> field;
+ try {
+ // Get the field from the object
+ field = (LegacyField<?>) ReflectTools.getJavaFieldValue(
+ objectWithMemberFields, memberField, LegacyField.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 (LegacyField)
+ 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.
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * The propertyId search logic used by
+ * {@link #buildAndBindMemberFields(Object, boolean)
+ * buildAndBindMemberFields} can easily be customized by overriding this
+ * method. No other changes are needed.
+ * </p>
+ *
+ * @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("_", "");
+ }
+
+ /**
+ * Exception thrown by a FieldGroup when the commit operation fails.
+ *
+ * Provides information about validation errors through
+ * {@link #getInvalidFields()} if the cause of the failure is that all bound
+ * fields did not pass validation
+ *
+ */
+ public static class CommitException extends Exception {
+
+ private FieldGroup fieldGroup;
+
+ public CommitException() {
+ super();
+ }
+
+ public CommitException(String message, FieldGroup fieldGroup,
+ Throwable cause) {
+ super(message, cause);
+ this.fieldGroup = fieldGroup;
+ }
+
+ public CommitException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public CommitException(String message) {
+ super(message);
+ }
+
+ public CommitException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Returns a map containing the fields which failed validation and the
+ * exceptions the corresponding validators threw.
+ *
+ * @since 7.4
+ * @return a map with all the invalid value exceptions. Can be empty but
+ * not null
+ */
+ public Map<LegacyField<?>, InvalidValueException> getInvalidFields() {
+ if (getCause() instanceof FieldGroupInvalidValueException) {
+ return ((FieldGroupInvalidValueException) getCause())
+ .getInvalidFields();
+ }
+ return new HashMap<LegacyField<?>, InvalidValueException>();
+ }
+
+ /**
+ * Returns the field group where the exception occurred
+ *
+ * @since 7.4
+ * @return the field group
+ */
+ public FieldGroup getFieldGroup() {
+ return fieldGroup;
+ }
+
+ }
+
+ 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 LegacyField<?> 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 LegacyField}.
+ */
+ public LegacyField<?> buildAndBind(String caption, Object propertyId)
+ throws BindException {
+ return buildAndBind(caption, propertyId, LegacyField.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 LegacyField}.
+ */
+
+ public <T extends LegacyField> T buildAndBind(String caption,
+ Object propertyId, Class<T> 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.
+ * <p>
+ * 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 LegacyField}
+ * if any LegacyField is good.
+ * </p>
+ *
+ * @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 LegacyField capable of editing the given type
+ * @throws BindException
+ * If the field could not be created
+ */
+ protected <T extends LegacyField> T build(String caption, Class<?> dataType,
+ Class<T> 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 LegacyField 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<java.lang.reflect.Field> getFieldsInDeclareOrder(
+ Class searchClass) {
+ ArrayList<java.lang.reflect.Field> memberFieldInOrder = new ArrayList<java.lang.reflect.Field>();
+
+ while (searchClass != null) {
+ for (java.lang.reflect.Field memberField : searchClass
+ .getDeclaredFields()) {
+ memberFieldInOrder.add(memberField);
+ }
+ searchClass = searchClass.getSuperclass();
+ }
+ return memberFieldInOrder;
+ }
+
+ /**
+ * Clears the value of all fields.
+ *
+ * @since 7.4
+ */
+ public void clear() {
+ for (LegacyField<?> f : getFields()) {
+ if (f instanceof LegacyAbstractField) {
+ ((LegacyAbstractField) f).clear();
+ }
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
new file mode 100644
index 0000000000..a80d51c6df
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+
+import com.vaadin.v7.ui.LegacyField;
+
+/**
+ * Factory interface for creating new LegacyField-instances based on the data
+ * type that should be edited.
+ *
+ * @author Vaadin Ltd.
+ * @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 LegacyField} 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 extends LegacyField> T createField(Class<?> dataType,
+ Class<T> fieldType);
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java
new file mode 100644
index 0000000000..a2a8aa27af
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+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;
+
+/**
+ * Defines the custom property name to be bound to a {@link LegacyField} using
+ * {@link FieldGroup} or {@link BeanFieldGroup}.
+ * <p>
+ * The automatic data binding in FieldGroup and BeanFieldGroup relies on a
+ * naming convention by default: properties of an item are bound to similarly
+ * named field components in given a editor object. If you want to map a
+ * property with a different name (ID) to a
+ * {@link com.vaadin.client.ui.LegacyField}, you can use this annotation for the
+ * member fields, with the name (ID) of the desired property as the parameter.
+ * <p>
+ * In following usage example, the text field would be bound to property "foo"
+ * in the Entity class. <code>
+ * <pre>
+ * class Editor extends FormLayout {
+ &#64;PropertyId("foo")
+ TextField myField = new TextField();
+ }
+
+ class Entity {
+ String foo;
+ }
+
+ {
+ Editor e = new Editor();
+ BeanFieldGroup.bindFieldsUnbuffered(new Entity(), e);
+ }
+ </pre>
+ * </code>
+ *
+ * @since 7.0
+ * @author Vaadin Ltd
+ */
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PropertyId {
+ String value();
+}