All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.data.fieldgroup.FieldGroup Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Vaadin Framework 7
 *
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * 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.Validator.InvalidValueException;
import com.vaadin.data.util.TransactionalPropertyWrapper;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.DefaultFieldFactory;
import com.vaadin.ui.Field;
import com.vaadin.ui.Form;
import com.vaadin.util.ReflectTools;

/**
 * FieldGroup provides an easy way of binding fields to data and handling
 * commits of these fields.
 * 

* The functionality of FieldGroup is similar to {@link Form} but * {@link FieldGroup} does not handle layouts in any way. The typical use case * is to create a layout outside the FieldGroup and then use FieldGroup to bind * the fields to a data source. *

*

* {@link FieldGroup} is not a UI component so it cannot be added to a layout. * Using the buildAndBind methods {@link FieldGroup} can create fields for you * using a FieldGroupFieldFactory but you still have to add them to the correct * position in your layout. *

* * @author Vaadin Ltd * @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> propertyIdToField = new HashMap>(); private LinkedHashMap, Object> fieldToPropertyId = new LinkedHashMap, Object>(); private List commitHandlers = new ArrayList(); /** * The field factory used by builder methods. */ private FieldGroupFieldFactory fieldFactory = 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. *

* * @see #setBuffered(boolean) for more details on buffered mode * * @see Field#isBuffered() * @return true if buffered mode is on, false otherwise * */ public boolean isBuffered() { return buffered; } /** * Sets the buffered mode for the bound fields. *

* When buffered mode is on the item will not be updated until * {@link #commit()} is called. If buffered mode is off the item will be * updated once the fields are updated. *

*

* The default is to use buffered mode. *

* * @see Field#setBuffered(boolean) * @param buffered * true to turn on buffered mode, false otherwise */ public void setBuffered(boolean buffered) { if (buffered == this.buffered) { return; } this.buffered = buffered; for (Field field : getFields()) { field.setBuffered(buffered); } } /** * Returns the enabled status for the fields. *

* Note that this will not accurately represent the enabled status of all * fields if you change the enabled status of the fields through some other * method than {@link #setEnabled(boolean)}. * * @return true if the fields are enabled, false otherwise */ public boolean isEnabled() { return enabled; } /** * Updates the enabled state of all bound fields. * * @param fieldsEnabled * true to enable all bound fields, false to disable them */ public void setEnabled(boolean fieldsEnabled) { enabled = fieldsEnabled; for (Field field : getFields()) { field.setEnabled(fieldsEnabled); } } /** * Returns the read only status that is used by default with all fields that * have a writable data source. *

* 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. *

* The fields are not returned in any specific order. *

* * @return A collection with all bound Fields */ public Collection> getFields() { return fieldToPropertyId.keySet(); } /** * Binds the field with the given propertyId from the current item. If an * item has not been set then the binding is postponed until the item is set * using {@link #setItemDataSource(Item)}. *

* This method also adds validators when applicable. *

* * @param field * The field to bind * @param propertyId * The propertyId to bind to the field * @throws BindException * If the 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 Property.Transactional wrapInTransactionalProperty( Property itemProperty) { return new TransactionalPropertyWrapper(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. *

* 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. *

* 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. *

* Note that this will return property ids even before the item has been * set. In that case it returns the property ids that will be bound once the * item is set. *

*

* No guarantee is given for the order of the property ids *

* * @return A collection of bound property ids */ public Collection getBoundPropertyIds() { return Collections.unmodifiableCollection(propertyIdToField.keySet()); } /** * Returns a collection of all property ids that exist in the item set using * {@link #setItemDataSource(Item)} but have not been bound to fields. *

* Will always return an empty collection before an item has been set using * {@link #setItemDataSource(Item)}. *

*

* No guarantee is given for the order of the property ids *

* * @return A collection of property ids that have not been bound to fields */ public Collection getUnboundPropertyIds() { if (getItemDataSource() == null) { return new ArrayList(); } List unboundPropertyIds = new ArrayList(); unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds()); unboundPropertyIds.removeAll(propertyIdToField.keySet()); return unboundPropertyIds; } /** * Commits all changes done to the bound fields. *

* Calls all {@link CommitHandler}s before and after committing the field * changes to the item data source. The whole commit is aborted and state is * restored to what it was before commit was called if any * {@link CommitHandler} throws a CommitException or there is a problem * committing the fields * * @throws CommitException * If the commit was aborted */ public void commit() throws CommitException { if (!isBuffered()) { // Not using buffered mode, nothing to do return; } startTransactions(); try { firePreCommitEvent(); Map, 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, InvalidValueException> commitFields() { Map, InvalidValueException> invalidValueExceptions = new HashMap, 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 */ public static class FieldGroupInvalidValueException extends InvalidValueException { private Map, InvalidValueException> invalidValueExceptions; /** * Constructs a new exception with the specified validation exceptions. * * @param invalidValueExceptions * a property id to exception map */ public FieldGroupInvalidValueException( Map, 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, 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. *

* Only has effect if buffered mode is used. * */ public void discard() { for (Field f : fieldToPropertyId.keySet()) { try { f.discard(); } catch (Exception e) { // TODO: handle exception // What can we do if discard fails other than try to discard all // other fields? } } } /** * Returns the field that is bound to the given property id * * @param propertyId * The property id to use to lookup the field * @return The field that is bound to the property id or null if no field is * bound to that property id */ public Field getField(Object propertyId) { return propertyIdToField.get(propertyId); } /** * Returns the property id that is bound to the given field * * @param field * The field to use to lookup the property id * @return The property id that is bound to the field or null if the field * is not bound to any property id by this FieldBinder */ public Object getPropertyId(Field field) { return fieldToPropertyId.get(field); } /** * Adds a commit handler. *

* The commit handler is called before the field values are committed to the * item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item * has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a * {@link CommitHandler} throws a CommitException the whole commit is * aborted and the fields retain their old values. * * @param commitHandler * The commit handler to add */ public void addCommitHandler(CommitHandler commitHandler) { commitHandlers.add(commitHandler); } /** * Removes the given commit handler. * * @see #addCommitHandler(CommitHandler) * * @param commitHandler * The commit handler to remove */ public void removeCommitHandler(CommitHandler commitHandler) { commitHandlers.remove(commitHandler); } /** * Returns a list of all commit handlers for this {@link FieldGroup}. *

* Use {@link #addCommitHandler(CommitHandler)} and * {@link #removeCommitHandler(CommitHandler)} to register or unregister a * commit handler. * * @return A collection of commit handlers */ protected Collection getCommitHandlers() { return Collections.unmodifiableCollection(commitHandlers); } /** * CommitHandlers are used by {@link FieldGroup#commit()} as part of the * commit transactions. CommitHandlers can perform custom operations as part * of the commit and cause the commit to be aborted by throwing a * {@link CommitException}. */ public interface CommitHandler extends Serializable { /** * Called before changes are committed to the field and the item is * updated. *

* Throw a {@link CommitException} to abort the commit. * * @param commitEvent * An event containing information regarding the commit * @throws CommitException * if the commit should be aborted */ public void preCommit(CommitEvent commitEvent) throws CommitException; /** * Called after changes are committed to the fields and the item is * updated. *

* Throw a {@link CommitException} to abort the commit. * * @param commitEvent * An event containing information regarding the commit * @throws CommitException * if the commit should be aborted */ public void postCommit(CommitEvent commitEvent) throws CommitException; } /** * FIXME javadoc * */ public static class CommitEvent implements Serializable { private FieldGroup fieldBinder; private CommitEvent(FieldGroup fieldBinder) { this.fieldBinder = fieldBinder; } /** * Returns the field binder that this commit relates to * * @return The FieldBinder that is being committed. */ public FieldGroup getFieldBinder() { return fieldBinder; } } /** * Checks the validity of the bound fields. *

* Call the {@link Field#validate()} for the fields to get the individual * error messages. * * @return true if all bound fields are valid, false otherwise. */ public boolean isValid() { try { for (Field field : getFields()) { field.validate(); } return true; } catch (InvalidValueException e) { return false; } } /** * Checks if any bound field has been modified. * * @return true if at least one field has been modified, false otherwise */ public boolean isModified() { for (Field field : getFields()) { if (field.isModified()) { return true; } } return false; } /** * Gets the field factory for the {@link FieldGroup}. The field factory is * only used when {@link FieldGroup} creates a new field. * * @return The field factory in use * */ public FieldGroupFieldFactory getFieldFactory() { return fieldFactory; } /** * Sets the field factory for the {@link FieldGroup}. The field factory is * only used when {@link FieldGroup} creates a new field. * * @param fieldFactory * The field factory to use */ public void setFieldFactory(FieldGroupFieldFactory fieldFactory) { this.fieldFactory = fieldFactory; } /** * Binds member fields found in the given object. *

* This method processes all (Java) member fields whose type extends * {@link Field} and that can be mapped to a property id. Property id * mapping is done based on the field name or on a @{@link PropertyId} * annotation on the field. All non-null fields for which a property id can * be determined are bound to the property id. *

*

* For example: * *

     * public class MyForm extends VerticalLayout {
     * private TextField firstName = new TextField("First name");
     * @PropertyId("last")
     * private TextField lastName = new TextField("Last name");
     * private TextField age = new TextField("Age"); ... }
     *
     * MyForm myForm = new MyForm();
     * ...
     * fieldGroup.bindMemberFields(myForm);
     * 
* *

* This binds the firstName TextField to a "firstName" property in the item, * lastName TextField to a "last" property and the age TextField to a "age" * property. * * @param objectWithMemberFields * The object that contains (Java) member fields to bind * @throws BindException * If there is a problem binding a field */ public void bindMemberFields(Object objectWithMemberFields) throws BindException { buildAndBindMemberFields(objectWithMemberFields, false); } /** * Binds member fields found in the given object and builds member fields * that have not been initialized. *

* This method processes all (Java) member fields whose type extends * {@link Field} and that can be mapped to a property id. Property ids are * searched in the following order: @{@link PropertyId} annotations, exact * field name matches and the case-insensitive matching that ignores * underscores. Fields that are not initialized (null) are built using the * field factory. All non-null fields for which a property id can be * determined are bound to the property id. *

*

* For example: * *

     * public class MyForm extends VerticalLayout {
     * private TextField firstName = new TextField("First name");
     * @PropertyId("last")
     * private TextField lastName = new TextField("Last name");
     * private TextField age;
     *
     * MyForm myForm = new MyForm();
     * ...
     * fieldGroup.buildAndBindMemberFields(myForm);
     * 
* *

*

* This binds the firstName TextField to a "firstName" property in the item, * lastName TextField to a "last" property and builds an age TextField using * the field factory and then binds it to the "age" property. *

* * @param objectWithMemberFields * The object that contains (Java) member fields to build and * bind * @throws BindException * If there is a problem binding or building a field */ public void buildAndBindMemberFields(Object objectWithMemberFields) throws BindException { buildAndBindMemberFields(objectWithMemberFields, true); } /** * Binds member fields found in the given object and optionally builds * member fields that have not been initialized. *

* This method processes all (Java) member fields whose type extends * {@link Field} and that can be mapped to a property id. Property ids are * searched in the following order: @{@link PropertyId} annotations, exact * field name matches and the case-insensitive matching that ignores * underscores. Fields that are not initialized (null) are built using the * field factory is buildFields is true. All non-null fields for which a * property id can be determined are bound to the property id. *

* * @param objectWithMemberFields * The object that contains (Java) member fields to build and * bind * @throws BindException * If there is a problem binding or building a field */ protected void buildAndBindMemberFields(Object objectWithMemberFields, boolean buildFields) throws BindException { Class objectClass = objectWithMemberFields.getClass(); for (java.lang.reflect.Field memberField : getFieldsInDeclareOrder( objectClass)) { if (!Field.class.isAssignableFrom(memberField.getType())) { // Process next field continue; } PropertyId propertyIdAnnotation = memberField .getAnnotation(PropertyId.class); Class fieldType = (Class) memberField .getType(); Object propertyId = null; if (propertyIdAnnotation != null) { // @PropertyId(propertyId) always overrides property id propertyId = propertyIdAnnotation.value(); } else { try { propertyId = findPropertyId(memberField); } catch (SearchException e) { // Property id was not found, skip this field continue; } if (propertyId == null) { // Property id was not found, skip this field continue; } } // Ensure that the property id exists Class propertyType; try { propertyType = getPropertyType(propertyId); } catch (BindException e) { // Property id was not found, skip this field continue; } Field field; try { // Get the field from the object field = (Field) ReflectTools.getJavaFieldValue( objectWithMemberFields, memberField, Field.class); } catch (Exception e) { // If we cannot determine the value, just skip the field and try // the next one continue; } if (field == null && buildFields) { Caption captionAnnotation = memberField .getAnnotation(Caption.class); String caption; if (captionAnnotation != null) { caption = captionAnnotation.value(); } else { caption = DefaultFieldFactory .createCaptionByPropertyId(propertyId); } // Create the component (Field) field = build(caption, propertyType, fieldType); // Store it in the field try { ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField, field); } catch (IllegalArgumentException e) { throw new BindException("Could not assign value to field '" + memberField.getName() + "'", e); } catch (IllegalAccessException e) { throw new BindException("Could not assign value to field '" + memberField.getName() + "'", e); } catch (InvocationTargetException e) { throw new BindException("Could not assign value to field '" + memberField.getName() + "'", e); } } if (field != null) { // Bind it to the property id bind(field, propertyId); } } } /** * Searches for a property id from the current itemDataSource that matches * the given memberField. *

* If perfect match is not found, uses a case insensitive search that also * ignores underscores. Returns null if no match is found. Throws a * SearchException if no item data source has been set. *

*

* The propertyId search logic used by * {@link #buildAndBindMemberFields(Object, boolean) * buildAndBindMemberFields} can easily be customized by overriding this * method. No other changes are needed. *

* * @param memberField * The field an object id is searched for * @return */ protected Object findPropertyId(java.lang.reflect.Field memberField) { String fieldName = memberField.getName(); if (getItemDataSource() == null) { throw new SearchException("Property id type for field '" + fieldName + "' could not be determined. No item data source has been set."); } Item dataSource = getItemDataSource(); if (dataSource.getItemProperty(fieldName) != null) { return fieldName; } else { String minifiedFieldName = minifyFieldName(fieldName); for (Object itemPropertyId : dataSource.getItemPropertyIds()) { if (itemPropertyId instanceof String) { String itemPropertyName = (String) itemPropertyId; if (minifiedFieldName .equals(minifyFieldName(itemPropertyName))) { return itemPropertyName; } } } } return null; } protected static String minifyFieldName(String fieldName) { return fieldName.toLowerCase().replace("_", ""); } /** * 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, InvalidValueException> getInvalidFields() { if (getCause() instanceof FieldGroupInvalidValueException) { return ((FieldGroupInvalidValueException) getCause()) .getInvalidFields(); } return new HashMap, 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 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 buildAndBind(String caption, Object propertyId, Class fieldType) throws BindException { Class type = getPropertyType(propertyId); T field = build(caption, type, fieldType); bind(field, propertyId); return field; } /** * Creates a field based on the given data type. *

* The data type is the type that we want to edit using the field. The field * type is the type of field we want to create, can be {@link Field} if any * Field is good. *

* * @param caption * The caption for the new field * @param dataType * The data model type that we want to edit using the field * @param fieldType * The type of field that we want to create * @return A Field capable of editing the given type * @throws BindException * If the field could not be created */ protected T build(String caption, Class dataType, Class fieldType) throws BindException { T field = getFieldFactory().createField(dataType, fieldType); if (field == null) { throw new BindException( "Unable to build a field of type " + fieldType.getName() + " for editing " + dataType.getName()); } field.setCaption(caption); return field; } /** * Returns an array containing Field objects reflecting all the fields of * the class or interface represented by this Class object. The elements in * the array returned are sorted in declare order from sub class to super * class. * * @param searchClass * @return */ protected static List getFieldsInDeclareOrder( Class searchClass) { ArrayList memberFieldInOrder = new ArrayList(); while (searchClass != null) { for (java.lang.reflect.Field memberField : searchClass .getDeclaredFields()) { memberFieldInOrder.add(memberField); } searchClass = searchClass.getSuperclass(); } return memberFieldInOrder; } /** * Clears the value of all fields. * * @since 7.4 */ public void clear() { for (Field f : getFields()) { if (f instanceof AbstractField) { ((AbstractField) f).clear(); } } } }