com.vaadin.v7.data.fieldgroup.FieldGroup.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.v7.data.fieldgroup.FieldGroup.java

Source

/*
 * Copyright 2000-2018 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.v7.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.Locale;
import java.util.Map;

import com.vaadin.annotations.PropertyId;
import com.vaadin.data.Binder;
import com.vaadin.util.ReflectTools;
import com.vaadin.v7.data.Item;
import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Validator.InvalidValueException;
import com.vaadin.v7.data.util.TransactionalPropertyWrapper;
import com.vaadin.v7.ui.AbstractField;
import com.vaadin.v7.ui.DefaultFieldFactory;
import com.vaadin.v7.ui.Field;

/**
 * FieldGroup provides an easy way of binding fields to data and handling
 * commits of these fields.
 * <p>
 * The functionality of FieldGroup is similar to {@link com.vaadin.v7.ui.Form
 * 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.
 * </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
 * @deprecated As of 8.0, replaced by {@link Binder}
 */
@Deprecated
public class FieldGroup implements Serializable {

    private Item itemDataSource;
    private boolean buffered = true;

    private boolean enabled = true;
    private boolean readOnly = false;

    private Map<Object, Field<?>> propertyIdToField = new HashMap<Object, Field<?>>();
    private LinkedHashMap<Field<?>, Object> fieldToPropertyId = new LinkedHashMap<Field<?>, 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;

        bindFields();
    }

    /**
     * Binds all fields to the properties in the item in use.
     *
     * @since 7.7.5
     */
    protected void bindFields() {
        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.
     * <p>
     *
     * @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.
     * <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 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.
     * <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 (Field<?> 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 (Field<?> 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<Field<?>> 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(Field<?> field, Object propertyId) throws BindException {
        throwIfFieldIsNull(field, propertyId);
        throwIfPropertyIdAlreadyBound(field, propertyId);

        fieldToPropertyId.put(field, propertyId);
        propertyIdToField.put(propertyId, field);
        if (itemDataSource == null) {
            clearField(field);

            // Will be bound when data source is set
            return;
        }

        field.setPropertyDataSource(wrapInTransactionalProperty(getItemProperty(propertyId)));
        configureField(field);
    }

    /**
     * Clears field and any possible existing binding.
     *
     * @param field
     *            The field to be cleared
     * @since 7.7.5
     */
    protected void clearField(Field<?> field) {
        // 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);
        }
    }

    /**
     * Wrap property to transactional property.
     */
    protected <T> Property.Transactional<T> wrapInTransactionalProperty(Property<T> itemProperty) {
        return new TransactionalPropertyWrapper<T>(itemProperty);
    }

    private void throwIfFieldIsNull(Field<?> field, Object propertyId) {
        if (field == null) {
            throw new BindException(String.format("Cannot bind property id '%s' to a null field.", propertyId));
        }
    }

    private void throwIfPropertyIdAlreadyBound(Field<?> 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(Field<?> 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(Field<?> 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<Field<?>, 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<Field<?>, InvalidValueException> commitFields() {
        Map<Field<?>, InvalidValueException> invalidValueExceptions = new HashMap<Field<?>, InvalidValueException>();

        for (Field<?> 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
     */
    @Deprecated
    public static class FieldGroupInvalidValueException extends InvalidValueException {
        private Map<Field<?>, InvalidValueException> invalidValueExceptions;

        /**
         * Constructs a new exception with the specified validation exceptions.
         *
         * @param invalidValueExceptions
         *            a property id to exception map
         */
        public FieldGroupInvalidValueException(Map<Field<?>, 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<Field<?>, InvalidValueException> getInvalidFields() {
            return invalidValueExceptions;
        }
    }

    private void startTransactions() throws CommitException {
        for (Field<?> 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 (Field<?> f : fieldToPropertyId.keySet()) {
            ((Property.Transactional<?>) f.getPropertyDataSource()).commit();
        }
    }

    private void rollbackTransactions() {
        for (Field<?> 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 (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.
     * <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}.
     */
    @Deprecated
    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;
    }

    /**
     * The commit event.
     *
     */
    @Deprecated
    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 Field#validate()} for the fields to get the individual
     * error messages.
     *
     * @return true if all bound fields are valid, false otherwise.
     */
    public boolean isValid() {
        try {
            for (Field<?> field : getFields()) {
                field.validate();
            }
            return true;
        } catch (InvalidValueException e) {
            return false;
        }
    }

    /**
     * Checks if any bound field has been modified.
     *
     * @return true if at least one field has been modified, false otherwise
     */
    public boolean isModified() {
        for (Field<?> field : getFields()) {
            if (field.isModified()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets the field factory for the {@link FieldGroup}. The field factory is
     * only used when {@link FieldGroup} creates a new field.
     *
     * @return The field factory in use
     *
     */
    public FieldGroupFieldFactory getFieldFactory() {
        return fieldFactory;
    }

    /**
     * Sets the field factory for the {@link FieldGroup}. The field factory is
     * only used when {@link FieldGroup} creates a new field.
     *
     * @param fieldFactory
     *            The field factory to use
     */
    public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
        this.fieldFactory = fieldFactory;
    }

    /**
     * Binds member fields found in the given object.
     * <p>
     * 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.
     * </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 Field} and that can be mapped to a property id. Property ids are
     * searched in the following order: @{@link PropertyId} annotations, exact
     * field name matches and the case-insensitive matching that ignores
     * underscores. Fields that are not initialized (null) are built using the
     * field factory. All non-null fields for which a property id can be
     * determined are bound to the property id.
     * </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 Field} and that can be mapped to a property id. Property ids are
     * searched in the following order: @{@link PropertyId} annotations, exact
     * field name matches and the case-insensitive matching that ignores
     * underscores. Fields that are not initialized (null) are built using the
     * field factory is buildFields is true. All non-null fields for which a
     * property id can be determined are bound to the property id.
     * </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 (!Field.class.isAssignableFrom(memberField.getType())) {
                // Process next field
                continue;
            }

            PropertyId propertyIdAnnotation = memberField.getAnnotation(PropertyId.class);

            Class<? extends Field> fieldType = (Class<? extends Field>) memberField.getType();

            Object propertyId = null;
            if (propertyIdAnnotation != null) {
                // @PropertyId(propertyId) always overrides property id
                propertyId = propertyIdAnnotation.value();
            } else {
                try {
                    propertyId = findPropertyId(memberField);
                } catch (SearchException e) {
                    // Property id was not found, skip this field
                    continue;
                }
                if (propertyId == null) {
                    // Property id was not found, skip this field
                    continue;
                }
            }

            // Ensure that the property id exists
            Class<?> propertyType;

            try {
                propertyType = getPropertyType(propertyId);
            } catch (BindException e) {
                // Property id was not found, skip this field
                continue;
            }

            Field<?> field;
            try {
                // Get the field from the object
                field = (Field<?>) ReflectTools.getJavaFieldValue(objectWithMemberFields, memberField, Field.class);
            } catch (Exception e) {
                // If we cannot determine the value, just skip the field and try
                // the next one
                continue;
            }

            if (field == null && buildFields) {
                Caption captionAnnotation = memberField.getAnnotation(Caption.class);
                String caption;
                if (captionAnnotation != null) {
                    caption = captionAnnotation.value();
                } else {
                    caption = DefaultFieldFactory.createCaptionByPropertyId(propertyId);
                }

                // Create the component (Field)
                field = build(caption, propertyType, fieldType);

                // Store it in the field
                try {
                    ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField, field);
                } catch (IllegalArgumentException e) {
                    throw new BindException("Could not assign value to field '" + memberField.getName() + "'", e);
                } catch (IllegalAccessException e) {
                    throw new BindException("Could not assign value to field '" + memberField.getName() + "'", e);
                } catch (InvocationTargetException e) {
                    throw new BindException("Could not assign value to field '" + memberField.getName() + "'", e);
                }
            }

            if (field != null) {
                // Bind it to the property id
                bind(field, propertyId);
            }
        }
    }

    /**
     * Searches for a property id from the current itemDataSource that matches
     * the given memberField.
     * <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(Locale.ROOT).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
     *
     */
    @Deprecated
    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<Field<?>, InvalidValueException> getInvalidFields() {
            if (getCause() instanceof FieldGroupInvalidValueException) {
                return ((FieldGroupInvalidValueException) getCause()).getInvalidFields();
            }
            return new HashMap<Field<?>, InvalidValueException>();
        }

        /**
         * Returns the field group where the exception occurred.
         *
         * @since 7.4
         * @return the field group
         */
        public FieldGroup getFieldGroup() {
            return fieldGroup;
        }

    }

    @Deprecated
    public static class BindException extends RuntimeException {

        public BindException(String message) {
            super(message);
        }

        public BindException(String message, Throwable t) {
            super(message, t);
        }

    }

    @Deprecated
    public static class SearchException extends RuntimeException {

        public SearchException(String message) {
            super(message);
        }

        public SearchException(String message, Throwable t) {
            super(message, t);
        }

    }

    /**
     * Builds a field and binds it to the given property id using the field
     * binder.
     *
     * @param propertyId
     *            The property id to bind to. Must be present in the field
     *            finder.
     * @throws BindException
     *             If there is a problem while building or binding
     * @return The created and bound field
     */
    public Field<?> buildAndBind(Object propertyId) throws BindException {
        String caption = DefaultFieldFactory.createCaptionByPropertyId(propertyId);
        return buildAndBind(caption, propertyId);
    }

    /**
     * Builds a field using the given caption and binds it to the given property
     * id using the field binder.
     *
     * @param caption
     *            The caption for the field
     * @param propertyId
     *            The property id to bind to. Must be present in the field
     *            finder.
     * @throws BindException
     *             If there is a problem while building or binding
     * @return The created and bound field. Can be any type of {@link Field}.
     */
    public Field<?> buildAndBind(String caption, Object propertyId) throws BindException {
        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 extends Field> 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 Field} if any
     * Field 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 Field capable of editing the given type
     * @throws BindException
     *             If the field could not be created
     */
    protected <T extends Field> 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 Field objects reflecting all the fields of
     * the class or interface represented by this Class object. The elements in
     * the array returned are sorted in declare order from sub class to super
     * class.
     *
     * @param searchClass
     * @return
     */
    protected static List<java.lang.reflect.Field> getFieldsInDeclareOrder(Class searchClass) {
        List<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 (Field<?> f : getFields()) {
            if (f instanceof AbstractField) {
                ((AbstractField) f).clear();
            }
        }

    }

}