com.expressui.core.view.field.FormField.java Source code

Java tutorial

Introduction

Here is the source code for com.expressui.core.view.field.FormField.java

Source

/*
 * Copyright (c) 2012 Brown Bag Consulting.
 * This file is part of the ExpressUI project.
 * Author: Juan Osuna
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License Version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * Brown Bag Consulting, Brown Bag Consulting DISCLAIMS THE WARRANTY OF
 * NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the ExpressUI software without
 * disclosing the source code of your own applications. These activities
 * include: offering paid services to customers as an ASP, providing
 * services from a web application, shipping ExpressUI with a closed
 * source product.
 *
 * For more information, please contact Brown Bag Consulting at this
 * address: juan@brownbagconsulting.com.
 */

package com.expressui.core.view.field;

import com.expressui.core.MainApplication;
import com.expressui.core.dao.EntityDao;
import com.expressui.core.dao.ReferenceEntityDao;
import com.expressui.core.entity.ReferenceEntity;
import com.expressui.core.util.*;
import com.expressui.core.util.assertion.Assert;
import com.expressui.core.validation.NumberConversionValidator;
import com.expressui.core.view.field.format.EmptyPropertyFormatter;
import com.expressui.core.view.form.EntityForm;
import com.expressui.core.view.form.FormFieldSet;
import com.vaadin.addon.beanvalidation.BeanValidationValidator;
import com.vaadin.data.Property;
import com.vaadin.data.Validator;
import com.vaadin.data.util.BeanItemContainer;
import com.vaadin.data.util.PropertyFormatter;
import com.vaadin.terminal.CompositeErrorMessage;
import com.vaadin.terminal.ErrorMessage;
import com.vaadin.terminal.Sizeable;
import com.vaadin.ui.*;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.beans.BeanUtils;

import javax.annotation.Resource;
import javax.persistence.Lob;
import javax.validation.constraints.NotNull;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.*;

/**
 * A field in a form that wraps a Vaadin field component, while providing other features and integration with ExpressUI:
 * <ul>
 * <li>Automatically and intelligently generates Vaadin field component based on data type of property this field
 * is bound to</li>
 * <li>Automatically and intelligently configures each Vaadin field with default settings</li>
 * <li>Automatically sets fields as required (with *) if bound property is @NotNull or @NotEmpty</li>
 * <li>Automatically adjusts width of fields based on property values/data</li>
 * <li>Keeps track of row and column positions in the form grid layout. A field can span multiple rows and columns.</li>
 * </ul>
 */
public class FormField extends DisplayField {

    private String tabName;
    private Field field;
    private Integer columnStart;
    private Integer rowStart;
    private Integer columnEnd;
    private Integer rowEnd;
    private boolean isRequired;
    private boolean isReadOnly;
    private com.vaadin.ui.Label label;
    private boolean isVisible;
    private AutoAdjustWidthMode autoAdjustWidthMode = AutoAdjustWidthMode.PARTIAL;
    private Integer defaultWidth;
    private boolean hasConversionError;

    @Resource
    private ReferenceEntityDao referenceEntityDao;

    /**
     * Constructs with reference to fieldSet this field belongs to and the property name this field is bound to, often
     * an entity object.
     *
     * @param formFieldSet fieldSet that contains this field
     * @param propertyId   name of the property this field is bound to
     */
    public FormField(FormFieldSet formFieldSet, String propertyId) {
        super(formFieldSet, propertyId);

        SpringApplicationContext.autowire(this);
    }

    /**
     * Gets the label used for this field. Generates one automatically, if not already set.
     * Generated one can be derived from the property name in the code, @Label annotation on the bound property
     * or looked up from resource bundle properties file, using the property name as the key.
     * <p/>
     * If I18n is required, then define labels in resource bundle properties files domainMessages/.
     * These messages have priority over annotations. Generating a label from the property name in code is done as a
     * last resort.
     *
     * @return display label
     */
    public com.vaadin.ui.Label getFieldLabel() {
        getField(); // make sure field is initialized before label
        if (label == null) {
            String labelText = generateLabelText();
            if (isOriginallyRequired()) {
                labelText = "<span class=\"e-required-field-indicator\">*</span>" + labelText;
            }
            label = new com.vaadin.ui.Label(labelText, com.vaadin.ui.Label.CONTENT_XHTML);
            label.setSizeUndefined();

            setToolTip(generateTooltip());
        }

        return label;
    }

    @Override
    protected String getLabelSectionDisplayName() {
        if (tabName.isEmpty()) {
            return getFieldSet().uiMessageSource.getMessage("formField.defaultLabelSectionDisplayName");
        } else {
            return tabName;
        }
    }

    /**
     * Sets the field label, thus overriding default generated label.
     *
     * @param labelText display label
     */
    public void setFieldLabel(String labelText) {
        getFieldLabel().setValue(labelText);
    }

    /**
     * Gets the name of the form tab this field resides in.
     *
     * @return name of form tab that contains this field
     */
    public String getTabName() {
        return tabName;
    }

    /**
     * Sets the name of the form tab this field resides in.
     *
     * @param tabName name of form tab that contains this field
     */
    public void setTabName(String tabName) {
        Assert.PROGRAMMING.isTrue(!(tabName.isEmpty() && getFormFieldSet().hasTabs()),
                "tabName arg must not be empty" + " if named tabs already exist for property "
                        + getTypeAndPropertyId());

        Set<String> tabNames = getFormFieldSet().getTabNames();
        for (String name : tabNames) {
            Assert.PROGRAMMING.isTrue(tabName.isEmpty() || !name.isEmpty(), "tabName arg must be empty"
                    + " if empty tabNames already exist for property " + getTypeAndPropertyId());
        }

        this.tabName = tabName;
    }

    /**
     * Gets the column start coordinate of this field, starting with 1 not 0.
     *
     * @return column start coordinate
     */
    public Integer getColumnStart() {
        return columnStart;
    }

    /**
     * Sets the column start coordinate of this field, starting with 1 not 0.
     *
     * @param columnStart column start coordinate
     */
    public void setColumnStart(Integer columnStart) {
        this.columnStart = columnStart;
    }

    /**
     * Gets the row start coordinate of this field, starting with 1 not 0.
     *
     * @return row start coordinate
     */
    public Integer getRowStart() {
        return rowStart;
    }

    /**
     * Sets the row start coordinate of this field, starting with 1 not 0.
     *
     * @param rowStart row start coordinate
     */
    public void setRowStart(Integer rowStart) {
        this.rowStart = rowStart;
    }

    /**
     * Gets the column end coordinate of this field.
     *
     * @return column end coordinate
     */
    public Integer getColumnEnd() {
        return columnEnd;
    }

    /**
     * Sets the column end coordinate of this field.
     *
     * @param columnEnd column end coordinate
     */
    public void setColumnEnd(Integer columnEnd) {
        this.columnEnd = columnEnd;
    }

    /**
     * Gets the row end coordinate of this field.
     *
     * @return row end coordinate
     */
    public Integer getRowEnd() {
        return rowEnd;
    }

    /**
     * Sets the row end coordinate of this field.
     *
     * @param rowEnd row end coordinate
     */
    public void setRowEnd(Integer rowEnd) {
        this.rowEnd = rowEnd;
    }

    /**
     * Asserts that column start and row start are not null.
     */
    public void assertValid() {
        Assert.PROGRAMMING.notNull(columnStart,
                "columnStart must not be null for property " + getTypeAndPropertyId());
        Assert.PROGRAMMING.notNull(rowStart, "rowStart must not be null for property " + getTypeAndPropertyId());
    }

    /**
     * Gets the underlying Vaadin field. The field is intelligently and automatically generated based on the property
     * type.
     * <p/>
     * In most cases, applications will not need to access Vaadin APIs directly. However,
     * the Vaadin field is exposed in case Vaadin features are needed that are not exposed by ExpressUI.
     *
     * @return Vaadin field
     */
    public Field getField() {
        if (field == null) {
            field = generateField();
            initializeFieldDefaults();
        }

        return field;
    }

    /**
     * Sets the underlying Vaadin field. The field is intelligently and automatically generated based on the property type.
     * <p/>
     * In most cases, applications will not need to access Vaadin APIs directly. However,
     * the Vaadin field is exposed in case Vaadin features are needed that are not exposed by ExpressUI.
     */
    public void setField(Field field) {
        setField(field, true);
    }

    /**
     * Sets the underlying Vaadin field, overriding the automatically generated one.
     *
     * @param field              Vaadin field
     * @param initializeDefaults allow ExpressUI to initialize the default settings for Vaadin field
     */
    public void setField(Field field, boolean initializeDefaults) {
        this.field = field;
        if (initializeDefaults) {
            initializeFieldDefaults();
        }
    }

    private void initWidthAndMaxLengthDefaults(AbstractTextField abstractTextField) {
        Integer defaultTextWidth = MainApplication.getInstance().applicationProperties.getDefaultTextFieldWidth();
        defaultWidth = MathUtil.maxIgnoreNull(defaultTextWidth, getBeanPropertyType().getMinimumLength());
        abstractTextField.setWidth(defaultWidth, Sizeable.UNITS_EM);

        Integer maxWidth = getBeanPropertyType().getMaximumLength();
        if (maxWidth != null) {
            abstractTextField.setMaxLength(maxWidth);
        }
    }

    /**
     * Gets auto-adjust-width mode.
     *
     * @return auto-adjust-width mode
     */
    public AutoAdjustWidthMode getAutoAdjustWidthMode() {
        return autoAdjustWidthMode;
    }

    /**
     * Sets auto-adjust-width mode.
     *
     * @param autoAdjustWidthMode auto-adjust-width mode
     */
    public void setAutoAdjustWidthMode(AutoAdjustWidthMode autoAdjustWidthMode) {
        this.autoAdjustWidthMode = autoAdjustWidthMode;
    }

    /**
     * Intelligently adjusts the width of fields to accommodate currently populated data.
     */
    public void autoAdjustTextFieldWidth() {
        Assert.PROGRAMMING.instanceOf(getField(), AbstractTextField.class,
                "FormField.autoAdjustWidth can only be called on text fields for property "
                        + getTypeAndPropertyId());

        if (autoAdjustWidthMode == AutoAdjustWidthMode.NONE)
            return;

        Object value = getField().getPropertyDataSource().getValue();
        if (value != null) {
            AbstractTextField textField = (AbstractTextField) getField();
            int approximateWidth = StringUtil.approximateEmWidth(value.toString());
            if (autoAdjustWidthMode == AutoAdjustWidthMode.FULL) {
                textField.setWidth(approximateWidth, Sizeable.UNITS_EM);
            } else if (autoAdjustWidthMode == AutoAdjustWidthMode.PARTIAL) {
                textField.setWidth(MathUtil.maxIgnoreNull(approximateWidth, defaultWidth), Sizeable.UNITS_EM);
            }
        }
    }

    /**
     * Gets width of the field.
     *
     * @return width of the field
     */
    public float getWidth() {
        return getField().getWidth();
    }

    /**
     * Manually sets width of the field and turns off auto width adjustment.
     *
     * @param width size of width
     * @param unit  unit of measurement defined in Sizeable
     * @see Sizeable
     */
    public void setWidth(float width, int unit) {
        setAutoAdjustWidthMode(FormField.AutoAdjustWidthMode.NONE);
        getField().setWidth(width, unit);
    }

    /**
     * Sets height of the field.
     *
     * @param height size of width
     * @param unit   unit of measurement defined in Sizeable
     * @see Sizeable
     */
    public void setHeight(float height, int unit) {
        getField().setHeight(height, unit);
    }

    /**
     * Intelligently adjusts the width of select fields to accommodate currently populated data.
     */
    public void autoAdjustSelectWidth() {
        Assert.PROGRAMMING.instanceOf(getField(), AbstractSelect.class,
                "FormField.autoAdjustSelectWidth can only be called on select fields for property "
                        + getTypeAndPropertyId());

        if (autoAdjustWidthMode == AutoAdjustWidthMode.NONE)
            return;

        AbstractSelect selectField = (AbstractSelect) getField();
        Collection itemsIds = selectField.getItemIds();

        int maxWidth = 0;
        for (Object itemsId : itemsIds) {
            String caption = selectField.getItemCaption(itemsId);
            int approximateWidth = StringUtil.approximateEmWidth(caption);
            maxWidth = Math.max(maxWidth, approximateWidth);
        }

        if (autoAdjustWidthMode == AutoAdjustWidthMode.FULL) {
            selectField.setWidth(maxWidth, Sizeable.UNITS_EM);
        } else if (autoAdjustWidthMode == AutoAdjustWidthMode.PARTIAL) {
            Integer defaultSelectWidth = MainApplication.getInstance().applicationProperties
                    .getDefaultSelectFieldWidth();
            selectField.setWidth(MathUtil.maxIgnoreNull(maxWidth, defaultSelectWidth), Sizeable.UNITS_EM);
        }
    }

    /**
     * Sets the menu options in a select.
     *
     * @param items list of items
     *              see com.expressui.core.entity.ReferenceEntity.DISPLAY_PROPERTY
     */
    public void setSelectItems(List items) {
        // could be either collection or single item
        Object selectedItems = getSelectedItems();

        Field field = getField();
        Assert.PROGRAMMING.instanceOf(field, AbstractSelect.class,
                "property " + getTypeAndPropertyId() + " is not a AbstractSelect field");

        boolean isReadOnly = false;
        AbstractSelect selectField = (AbstractSelect) field;
        try {
            if (selectField.isReadOnly()) {
                isReadOnly = true;
                selectField.setReadOnly(false);
            }

            if (selectField.getContainerDataSource() == null
                    || !(selectField.getContainerDataSource() instanceof BeanItemContainer)) {
                BeanItemContainer container;
                if (getBeanPropertyType().isCollectionType()) {
                    container = new BeanItemContainer(getBeanPropertyType().getCollectionValueType(), items);
                } else {
                    container = new BeanItemContainer(getPropertyType(), items);
                }

                selectField.setContainerDataSource(container);
            } else {
                BeanItemContainer container = (BeanItemContainer) selectField.getContainerDataSource();
                container.removeAllItems();
                container.addAll(items);

                if (!getBeanPropertyType().isCollectionType() && !container.containsId(selectedItems)) {
                    selectField.select(selectField.getNullSelectionItemId());
                }
            }
        } finally {
            if (isReadOnly) {
                selectField.setReadOnly(true);
            }
        }

        autoAdjustSelectWidth();
    }

    /**
     * Sets menu options in a select.
     *
     * @param items map of items where key is bound to entity and value is the display caption
     */
    public void setSelectItems(Map<Object, String> items) {
        String nullCaption = getFieldSet().uiMessageSource.getMessage("formFieldSet.select.nullCaption");
        setSelectItems(items, nullCaption);
    }

    /**
     * Sets menu options in a select.
     *
     * @param items       map of items where key is bound to entity and value is the display caption
     * @param nullCaption caption displayed to represent null or no selection
     */
    public void setSelectItems(Map<Object, String> items, String nullCaption) {
        Field field = getField();
        Assert.PROGRAMMING.instanceOf(field, AbstractSelect.class,
                "property " + getTypeAndPropertyId() + " is not a AbstractSelect field");

        boolean isReadOnly = false;
        AbstractSelect selectField = (AbstractSelect) field;
        try {
            if (selectField.isReadOnly()) {
                isReadOnly = true;
                selectField.setReadOnly(false);
            }

            Object previouslySelectedValue = selectField.getValue();

            selectField.setItemCaptionMode(Select.ITEM_CAPTION_MODE_EXPLICIT);
            selectField.removeAllItems();

            if (nullCaption != null) {
                selectField.addItem(nullCaption);
                selectField.setItemCaption(nullCaption, nullCaption);
                selectField.setNullSelectionItemId(nullCaption);
            }

            for (Object item : items.keySet()) {
                String caption = items.get(item);
                selectField.addItem(item);
                selectField.setItemCaption(item, caption);
                if (previouslySelectedValue != null && previouslySelectedValue.equals(item)) {
                    selectField.setValue(item);
                }
            }
        } finally {
            if (isReadOnly) {
                selectField.setReadOnly(true);
            }
        }

        autoAdjustSelectWidth();
    }

    /**
     * Gets selected items, which could be a single item or collection.
     *
     * @return single item or collection
     */
    public Object getSelectedItems() {
        Field field = getField();
        Assert.PROGRAMMING.instanceOf(field, AbstractSelect.class,
                "property " + getTypeAndPropertyId() + " is not a AbstractSelect field");
        AbstractSelect selectField = (AbstractSelect) field;
        return selectField.getValue();
    }

    /**
     * Sets the dimensions of a multi-select menu
     *
     * @param rows    height
     * @param columns width
     */
    public void setMultiSelectDimensions(int rows, int columns) {
        Field field = getField();
        Assert.PROGRAMMING.instanceOf(field, ListSelect.class,
                "property " + getTypeAndPropertyId() + " is not a AbstractSelect field");
        ListSelect selectField = (ListSelect) field;
        selectField.setRows(rows);
        selectField.setColumns(columns);
    }

    /**
     * Sets the property Id to be used as display caption in select menu. This property id must be defined
     * in the type of object that is bound to this select field.
     *
     * @param displayCaptionPropertyId bean property name
     */
    public void setDisplayCaptionPropertyId(String displayCaptionPropertyId) {
        Assert.PROGRAMMING.instanceOf(field, AbstractSelect.class,
                "property " + getTypeAndPropertyId() + " is not a Select field");

        ((AbstractSelect) field).setItemCaptionPropertyId(displayCaptionPropertyId);
    }

    /**
     * Adds listener for changes in this field's value.
     *
     * @param target     target object to invoke
     * @param methodName name of method to invoke
     */
    public void addValueChangeListener(Object target, String methodName) {
        AbstractComponent component = (AbstractComponent) getField();
        component.addListener(Property.ValueChangeEvent.class, target, methodName);
    }

    /**
     * Gets the FormFieldSet that contains this field.
     *
     * @return FormFieldSet that contains this field
     */
    public FormFieldSet getFormFieldSet() {
        return (FormFieldSet) getFieldSet();
    }

    /**
     * Sets the visibility of this field and label
     *
     * @param isVisible true if visible
     */
    public void setVisible(boolean isVisible) {
        this.isVisible = isVisible;
        getField().setVisible(isVisible);
        getFieldLabel().setVisible(isVisible);
    }

    /**
     * Allows the field to be visible from a security permissions standpoint, if it is configured to be visible
     */
    public void allowView() {
        getField().setVisible(isVisible);
        getFieldLabel().setVisible(isVisible);
    }

    /**
     * Prevents the field from being visible from a security permissions standpoint.
     */
    public void denyView() {
        getField().setVisible(false);
        getFieldLabel().setVisible(false);
    }

    /**
     * Asks if this field is originally required, which is automatically set based on the bound bean's validation
     * annotations, or can be set programmatically.
     *
     * @return true if required
     */
    public boolean isOriginallyRequired() {
        return isRequired;
    }

    /**
     * Sets whether or not field is required. This value may also be set programmatically.
     *
     * @param isRequired true if read-only
     */
    public void setOriginallyRequired(boolean isRequired) {
        setDynamicallyRequired(isRequired);
        this.isRequired = isRequired;
    }

    /**
     * Asks if this field is dynamically required. Note that this may be false while {@link #isOriginallyRequired()}
     * is true.
     * This may happen in the case where this field is bound to nested property id where the leaf is required but
     * one of its ancestors is not required and is currently null. For example, street may be required in Address but
     * contact.mailingAddress is not required and currently null. In this scenario, the street field may be currently
     * not required, since it's ancestor (contact.mailingAddress) is null. If contact.mailingAddress is set,
     * then the street field becomes required.
     *
     * @return true if required
     */
    public boolean isDynamicallyRequired() {
        return getField().isRequired();
    }

    /**
     * Sets whether or not this field is dynamically required. Note that this may be false while
     * {@link #isOriginallyRequired()} is true.
     * This may happen in the case where this field is bound to nested property id where the leaf is required but
     * one of its ancestors is not required and is dynamically null. For example, street may be required in Address but
     * contact.mailingAddress is not required and dynamically null. In this scenario, the street field may be currently
     * not required, since it's ancestor (contact.mailingAddress) is null. If contact.mailingAddress is set,
     * then the street field becomes required.
     *
     * @param isRequired true if required
     */
    public void setDynamicallyRequired(boolean isRequired) {
        getField().setRequired(isRequired);
    }

    /**
     * Restore is-required setting to originally configured value.
     */
    public void restoreIsRequired() {
        getField().setRequired(isRequired);
    }

    private String generateTooltip(Object... args) {
        String toolTipText = getToolTipTextFromMessageSource(false, args);
        if (toolTipText == null) {
            toolTipText = getToolTipTextFromAnnotation();
        }

        return toolTipText;
    }

    private String getToolTipTextFromMessageSource(boolean useDefaultLocale, Object... args) {
        List<BeanPropertyType> ancestors = getBeanPropertyType().getAncestors();
        String label = null;
        for (int i = 0; i < ancestors.size(); i++) {
            BeanPropertyType ancestor = ancestors.get(i);
            Class currentType = ancestor.getContainerType();

            String currentPropertyId = ancestor.getId();
            for (int y = i + 1; y < ancestors.size(); y++) {
                BeanPropertyType bpt = ancestors.get(y);
                currentPropertyId += "." + bpt.getId();
            }

            while (label == null && currentType != null) {
                label = getToolTipTextFromMessageSource(useDefaultLocale, currentType, currentPropertyId, args);
                currentType = currentType.getSuperclass();
            }

            if (label != null)
                break;

            Class[] interfaces = ancestor.getContainerType().getInterfaces();
            for (Class anInterface : interfaces) {
                Class currentInterface = anInterface;
                while (label == null && currentInterface != null) {
                    label = getToolTipTextFromMessageSource(useDefaultLocale, currentInterface, currentPropertyId,
                            args);
                    currentInterface = currentInterface.getSuperclass();
                }
                if (label != null)
                    break;
            }

            if (label != null)
                break;
        }

        if (label == null) {
            label = getToolTipTextFromMessageSource(useDefaultLocale, getBeanPropertyType().getType(), null, args);
        }

        if (label != null && label.contains("{0}") && args.length == 0) {
            return null;
        } else {
            if (label == null && !useDefaultLocale) {
                return getToolTipTextFromMessageSource(true, args);
            } else {
                return label;
            }
        }
    }

    private String getToolTipTextFromMessageSource(boolean useDefaultLocale, Class type, String propertyId,
            Object... args) {
        String fullPropertyPath = type.getName() + (propertyId == null ? "" : "." + propertyId) + ".toolTip";
        if (useDefaultLocale) {
            return getFieldSet().domainMessageSourceNoFallback.getOptionalToolTipFromDefaultLocale(fullPropertyPath,
                    args);
        } else {
            return getFieldSet().domainMessageSourceNoFallback.getOptionalToolTip(fullPropertyPath, args);
        }
    }

    private String getToolTipTextFromAnnotation() {
        Class propertyContainerType = getBeanPropertyType().getContainerType();
        String propertyIdRelativeToContainerType = getBeanPropertyType().getId();
        PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(propertyContainerType,
                propertyIdRelativeToContainerType);
        Method method = descriptor.getReadMethod();
        ToolTip toolTipAnnotation = method.getAnnotation(ToolTip.class);
        if (toolTipAnnotation == null) {
            return null;
        } else {
            return toolTipAnnotation.value();
        }
    }

    /**
     * Gets the description displayed during mouse-over/hovering.
     *
     * @return description displayed to user
     */
    public String getToolTip() {
        return getField().getDescription();
    }

    /**
     * Sets the description displayed during mouse-over/hovering.
     *
     * @param toolTip description displayed to user
     */
    public void setToolTip(String toolTip) {
        if (toolTip != null) {
            getField().setDescription(toolTip);
        }
    }

    /**
     * Generates or re-generates tooltip, passing in arguments for interpolation using standard {0}, {1}, {2}
     * notation. This feature only works with resource bundle messages defined in domainMessages/.
     *
     * @param args
     */
    public void setToolTipArgs(Object... args) {
        setToolTip(generateTooltip(args));
    }

    /**
     * Sets whether or not field is enabled.
     *
     * @param isEnabled true if enabled
     */
    public void setEnabled(boolean isEnabled) {
        getField().setEnabled(isEnabled);
    }

    /**
     * Asks if this field is originally read-only, irrespective of view-mode or security permissions.
     *
     * @return true if this field is read-only
     */
    public boolean isOriginallyReadOnly() {
        return isReadOnly;
    }

    /**
     * Sets whether or not field is read-only, irrespective of view-mode or security permissions.
     *
     * @param isReadOnly true if read-only
     */
    public void setOriginallyReadOnly(boolean isReadOnly) {
        setDynamicallyReadOnly(isReadOnly);
        this.isReadOnly = isReadOnly;
    }

    /**
     * Sets whether or not field is dynamically read-only, based on view-mode or security permissions.
     *
     * @param isReadOnly true if read-only
     */
    public void setDynamicallyReadOnly(boolean isReadOnly) {
        if (getField() instanceof SelectField) {
            ((SelectField) getField()).setButtonVisible(!isReadOnly);
        }

        getField().setReadOnly(isReadOnly);
    }

    /**
     * Restores read-only setting to original value, if they were temporarily changed for view-only mode
     * or security permissions.
     */
    public void restoreIsReadOnly() {
        if (getField() instanceof SelectField) {
            ((SelectField) getField()).setButtonVisible(!isOriginallyReadOnly());
        }
        getField().setReadOnly(isOriginallyReadOnly());
    }

    /**
     * Sets the value of the field.
     *
     * @param value value of field
     */
    public void setValue(Object value) {
        getField().setValue(value);
    }

    /**
     * Asks if field dynamically has an error.
     *
     * @return true if field dynamically has an error
     */
    public boolean hasError() {
        if (hasConversionError) {
            return true;
        } else if (getField() instanceof AbstractComponent) {
            AbstractComponent abstractComponent = (AbstractComponent) getField();
            return abstractComponent.getComponentError() != null || hasIsRequiredError();
        } else {
            return false;
        }
    }

    /**
     * Asks if field currently has error because field is empty but is required.
     *
     * @return true if field currently has error because field is empty but is required
     */
    public boolean hasIsRequiredError() {
        return getField().isRequired() && StringUtil.isEmpty(getField().getValue());
    }

    /**
     * Clears any errors on this field.
     *
     * @param clearConversionError true to clear data-type conversion error as well
     */
    public void clearError(boolean clearConversionError) {
        if (clearConversionError) {
            hasConversionError = false;
        }
        if (getField() instanceof AbstractComponent) {
            AbstractComponent abstractComponent = (AbstractComponent) getField();
            abstractComponent.setComponentError(null);
        }
    }

    /**
     * Asks if this field has a data-type conversion error.
     *
     * @return true if this field as a data-type conversion error
     */
    public boolean hasConversionError() {
        return hasConversionError;
    }

    /**
     * Sets whether or not this field has a data-type conversion error.
     *
     * @param hasConversionError true if this field as a data-type conversion error
     */
    public void setHasConversionError(boolean hasConversionError) {
        this.hasConversionError = hasConversionError;
    }

    /**
     * Add error message to this field.
     *
     * @param errorMessage error message, builds Vaadin composite error message
     */
    public void addError(ErrorMessage errorMessage) {
        Assert.PROGRAMMING.instanceOf(getField(), AbstractComponent.class,
                "Error message cannot be added to field that is not an AbstractComponent for property "
                        + getTypeAndPropertyId());

        AbstractComponent abstractComponent = (AbstractComponent) getField();
        ErrorMessage existingErrorMessage = abstractComponent.getComponentError();
        if (existingErrorMessage == null) {
            abstractComponent.setComponentError(errorMessage);
        } else if (existingErrorMessage instanceof CompositeErrorMessage) {
            CompositeErrorMessage existingCompositeErrorMessage = (CompositeErrorMessage) existingErrorMessage;
            Iterator<ErrorMessage> iterator = existingCompositeErrorMessage.iterator();
            Set<ErrorMessage> newErrorMessages = new LinkedHashSet<ErrorMessage>();
            while (iterator.hasNext()) {
                ErrorMessage next = iterator.next();
                newErrorMessages.add(next);
            }
            newErrorMessages.add(errorMessage);
            CompositeErrorMessage newCompositeErrorMessage = new CompositeErrorMessage(newErrorMessages);
            abstractComponent.setComponentError(newCompositeErrorMessage);
        } else {
            Set<ErrorMessage> newErrorMessages = new LinkedHashSet<ErrorMessage>();
            newErrorMessages.add(existingErrorMessage);
            newErrorMessages.add(errorMessage);
            CompositeErrorMessage newCompositeErrorMessage = new CompositeErrorMessage(newErrorMessages);
            abstractComponent.setComponentError(newCompositeErrorMessage);
        }
    }

    @Override
    public PropertyFormatter getPropertyFormatter() {
        if (getField() instanceof AbstractTextField) {
            return super.getPropertyFormatter();
        } else {
            return new EmptyPropertyFormatter();
        }
    }

    private Field generateField() {
        Class propertyType = getPropertyType();

        if (propertyType == null) {
            return null;
        }

        if (Date.class.isAssignableFrom(propertyType)) {
            return new DateField();
        }

        if (boolean.class.isAssignableFrom(propertyType) || Boolean.class.isAssignableFrom(propertyType)) {
            return new CheckBox();
        }

        if (ReferenceEntity.class.isAssignableFrom(propertyType)) {
            return new Select();
        }

        if (Currency.class.isAssignableFrom(propertyType)) {
            return new Select();
        }

        if (propertyType.isEnum()) {
            return new Select();
        }

        if (Collection.class.isAssignableFrom(propertyType)) {
            return new ListSelect();
        }

        if (getBeanPropertyType().hasAnnotation(Lob.class)) {
            return new RichTextArea();
        }

        return new TextField();
    }

    private void initializeFieldDefaults() {
        if (field == null) {
            return;
        }

        field.setInvalidAllowed(true);

        if (field instanceof AbstractField) {
            initAbstractFieldDefaults((AbstractField) field);
        }

        if (field instanceof AbstractTextField) {
            initTextFieldDefaults((AbstractTextField) field);
            initWidthAndMaxLengthDefaults((AbstractTextField) field);
        }

        if (field instanceof RichTextArea) {
            initRichTextFieldDefaults((RichTextArea) field);
        }

        if (field instanceof DateField) {
            initDateFieldDefaults((DateField) field);
        }

        if (field instanceof AbstractSelect) {
            initAbstractSelectDefaults((AbstractSelect) field);

            if (field instanceof Select) {
                initSelectDefaults((Select) field);
            }

            if (field instanceof ListSelect) {
                initListSelectDefaults((ListSelect) field);
            }

            Class valueType = getPropertyType();
            if (getBeanPropertyType().isCollectionType()) {
                valueType = getBeanPropertyType().getCollectionValueType();
            }

            List referenceEntities = null;
            if (Currency.class.isAssignableFrom(valueType)) {
                referenceEntities = CurrencyUtil.getAvailableCurrencies();
                ((AbstractSelect) field).setItemCaptionPropertyId("currencyCode");
            } else if (valueType.isEnum()) {
                Object[] enumConstants = valueType.getEnumConstants();
                referenceEntities = Arrays.asList(enumConstants);
            } else if (ReferenceEntity.class.isAssignableFrom(valueType)) {
                EntityDao propertyDao = SpringApplicationContext
                        .getBeanByTypeAndGenericArgumentType(EntityDao.class, valueType);
                if (propertyDao != null) {
                    referenceEntities = propertyDao.findAll();
                } else {
                    referenceEntities = referenceEntityDao.findAll(valueType);
                }
            }

            if (referenceEntities != null) {
                setSelectItems(referenceEntities);
            }
        }

        if (getFormFieldSet().isEntityForm()) {
            if (getBeanPropertyType().isValidatable()) {
                initializeIsRequired();
                initializeValidators();
            }

            // Change listener causes erratic behavior for RichTextArea
            if (!(field instanceof RichTextArea)) {
                field.addListener(new FieldValueChangeListener());
            }
        }

        setOriginallyReadOnly(field.isReadOnly());
        isVisible = field.isVisible();
    }

    private void initializeValidators() {
        if (field instanceof AbstractTextField) {
            if (getBeanPropertyType().getBusinessType() != null
                    && getBeanPropertyType().getBusinessType().equals(BeanPropertyType.BusinessType.NUMBER)) {
                addValidator(new NumberConversionValidator(this));
            }
        }
    }

    /**
     * Adds Vaadin validator to this field.
     *
     * @param validator Vaadin validator
     */
    public void addValidator(Validator validator) {
        getField().addValidator(validator);
    }

    private void initializeIsRequired() {
        BeanValidationValidator validator = new BeanValidationValidator(getBeanPropertyType().getContainerType(),
                getBeanPropertyType().getId());
        if (validator.isRequired()) {
            field.setRequired(true);
            field.setRequiredError(MainApplication.getInstance().validationMessageSource
                    .getMessage("com.expressui.core.view.field.FormField.required.message"));
        }

        setOriginallyRequired(field.isRequired());
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public static void initAbstractFieldDefaults(AbstractField field) {
        field.setRequiredError(MainApplication.getInstance().validationMessageSource
                .getMessage("com.expressui.core.view.field.FormField.required.message"));
        field.setImmediate(true);
        field.setInvalidCommitted(false);
        field.setWriteThrough(true);
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public static void initTextFieldDefaults(AbstractTextField field) {
        Integer defaultTextWidth = MainApplication.getInstance().applicationProperties.getDefaultTextFieldWidth();
        field.setWidth(defaultTextWidth, Sizeable.UNITS_EM);
        field.setNullRepresentation("");
        field.setNullSettingAllowed(true);
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public static void initRichTextFieldDefaults(RichTextArea field) {
        field.setNullRepresentation("");
        field.setNullSettingAllowed(false);
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public static void initDateFieldDefaults(DateField field) {
        field.setResolution(DateField.RESOLUTION_DAY);
        field.setParseErrorMessage(MainApplication.getInstance().validationMessageSource
                .getMessage("com.expressui.core.view.field.FormField.dateParseError.message"));
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public void initAbstractSelectDefaults(AbstractSelect field) {
        Integer defaultSelectWidth = MainApplication.getInstance().applicationProperties
                .getDefaultSelectFieldWidth();
        field.setWidth(defaultSelectWidth, Sizeable.UNITS_EM);
        field.setItemCaptionMode(Select.ITEM_CAPTION_MODE_PROPERTY);
        if (getBeanPropertyType().hasAnnotation(NotNull.class)
                || getBeanPropertyType().hasAnnotation(NotEmpty.class)
                || getBeanPropertyType().hasAnnotation(NotBlank.class)) {
            field.setNullSelectionAllowed(false);
        } else {
            field.setNullSelectionAllowed(true);
        }

        field.setItemCaptionPropertyId(ReferenceEntity.DISPLAY_PROPERTY);
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public static void initSelectDefaults(Select field) {
        field.setFilteringMode(Select.FILTERINGMODE_CONTAINS);
    }

    /**
     * Initializes field to default settings.
     *
     * @param field Vaadin field to initialize
     */
    public static void initListSelectDefaults(ListSelect field) {
        field.setMultiSelect(true);
    }

    private class FieldValueChangeListener implements Property.ValueChangeListener {
        @Override
        public void valueChange(Property.ValueChangeEvent event) {
            EntityForm entityForm = (EntityForm) getFormFieldSet().getForm();

            if (entityForm.isValidationEnabled()) {
                entityForm.validate(false);
            }
        }
    }

    /**
     * Mode for automatically adjusting field widths.
     */
    public enum AutoAdjustWidthMode {
        /**
         * Fully automatic
         */
        FULL,
        /**
         * Automatic but with minimum width specified in application.properties
         */
        PARTIAL,
        /**
         * Turn off automatic width adjustment
         */
        NONE
    }
}