summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java22
-rw-r--r--server/src/com/vaadin/data/fieldgroup/FieldGroup.java107
-rw-r--r--server/src/com/vaadin/ui/Grid.java198
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java94
4 files changed, 396 insertions, 25 deletions
diff --git a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
index 8e32585a47..b6bf97e68e 100644
--- a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
+++ b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
@@ -129,10 +129,6 @@ public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
return (T) field;
}
- private boolean anyField(Class<?> fieldType) {
- return fieldType == Field.class || fieldType == AbstractField.class;
- }
-
protected AbstractSelect createCompatibleSelect(
Class<? extends AbstractSelect> fieldType) {
AbstractSelect select;
@@ -157,7 +153,23 @@ public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
return select;
}
- private boolean anySelect(Class<? extends Field> fieldType) {
+ /**
+ * @since 7.4
+ * @param fieldType
+ * the type of the field
+ * @return true if any AbstractField can be assigned to the field
+ */
+ protected boolean anyField(Class<?> fieldType) {
+ return fieldType == Field.class || fieldType == AbstractField.class;
+ }
+
+ /**
+ * @since 7.4
+ * @param fieldType
+ * the type of the field
+ * @return true if any AbstractSelect can be assigned to the field
+ */
+ protected boolean anySelect(Class<? extends Field> fieldType) {
return anyField(fieldType) || fieldType == AbstractSelect.class;
}
diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
index 069cb2e153..8f1bf8b40a 100644
--- a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
+++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -23,6 +23,7 @@ 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;
@@ -465,46 +466,74 @@ public class FieldGroup implements Serializable {
try {
firePreCommitEvent();
- List<InvalidValueException> invalidValueExceptions = commitFields();
+ Map<Field<?>, InvalidValueException> invalidValueExceptions = commitFields();
if (invalidValueExceptions.isEmpty()) {
firePostCommitEvent();
commitTransactions();
} else {
- throwInvalidValueException(invalidValueExceptions);
+ throw new FieldGroupInvalidValueException(
+ invalidValueExceptions);
}
} catch (Exception e) {
rollbackTransactions();
-
- throw new CommitException("Commit failed", e);
+ throw new CommitException("Commit failed", this, e);
}
}
- private List<InvalidValueException> commitFields() {
- List<InvalidValueException> invalidValueExceptions = new ArrayList<InvalidValueException>();
+ /**
+ * 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.add(e);
+ invalidValueExceptions.put(f, e);
}
}
return invalidValueExceptions;
}
- private void throwInvalidValueException(
- List<InvalidValueException> invalidValueExceptions) {
- if (invalidValueExceptions.size() == 1) {
- throw invalidValueExceptions.get(0);
- } else {
- InvalidValueException[] causes = invalidValueExceptions
- .toArray(new InvalidValueException[invalidValueExceptions
- .size()]);
+ /**
+ * Exception which wraps InvalidValueExceptions from all invalid fields in a
+ * FieldGroup
+ *
+ * @since 7.4
+ */
+ public static class FieldGroupInvalidValueException extends
+ InvalidValueException {
+ private Map<Field<?>, InvalidValueException> invalidValueExceptions;
- throw new InvalidValueException(null, causes);
+ /**
+ * 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;
}
}
@@ -1006,26 +1035,64 @@ public class FieldGroup implements Serializable {
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();
- // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(String message, FieldGroup fieldGroup,
+ Throwable cause) {
+ super(message, cause);
+ this.fieldGroup = fieldGroup;
}
public CommitException(String message, Throwable cause) {
super(message, cause);
- // TODO Auto-generated constructor stub
}
public CommitException(String message) {
super(message);
- // TODO Auto-generated constructor stub
}
public CommitException(Throwable cause) {
super(cause);
- // TODO Auto-generated constructor stub
+ }
+
+ /**
+ * 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;
}
}
diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java
index 8f35001447..458522f58e 100644
--- a/server/src/com/vaadin/ui/Grid.java
+++ b/server/src/com/vaadin/ui/Grid.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -47,6 +48,8 @@ import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.RpcDataProviderExtension;
import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
@@ -87,6 +90,7 @@ import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
import com.vaadin.shared.ui.grid.HeightMode;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Notification.Type;
import com.vaadin.ui.renderer.ObjectRenderer;
import com.vaadin.ui.renderer.Renderer;
import com.vaadin.util.ReflectTools;
@@ -166,6 +170,11 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
* been bound.
*/
private final class CustomFieldGroup extends FieldGroup {
+
+ public CustomFieldGroup() {
+ setFieldFactory(EditorFieldFactory.get());
+ }
+
@Override
protected Class<?> getPropertyType(Object propertyId)
throws BindException {
@@ -178,6 +187,155 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * Field factory used by default in the editor.
+ *
+ * Aims to fields of suitable type and with suitable size for use in the
+ * editor row.
+ */
+ public static class EditorFieldFactory extends
+ DefaultFieldGroupFieldFactory {
+ private static final EditorFieldFactory INSTANCE = new EditorFieldFactory();
+
+ protected EditorFieldFactory() {
+ }
+
+ /**
+ * Returns the singleton instance
+ *
+ * @return the singleton instance
+ */
+ public static EditorFieldFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public <T extends Field> T createField(Class<?> type, Class<T> fieldType) {
+ T f = super.createField(type, fieldType);
+ if (f != null) {
+ f.setWidth("100%");
+ }
+ return f;
+ }
+
+ @Override
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ if (anySelect(fieldType)) {
+ return super.createCompatibleSelect(ComboBox.class);
+ }
+ return super.createCompatibleSelect(fieldType);
+ }
+
+ @Override
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ // Use enums directly and the EnumToStringConverter to be consistent
+ // with what is shown in the Grid
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ select.addItem(r);
+ }
+ }
+ }
+
+ /**
+ * Error handler for the editor
+ */
+ public interface EditorErrorHandler extends Serializable {
+
+ /**
+ * Called when an exception occurs while the editor row is being saved
+ *
+ * @param event
+ * An event providing more information about the error
+ */
+ void commitError(CommitErrorEvent event);
+ }
+
+ /**
+ * An event which is fired when saving the editor fails
+ */
+ public static class CommitErrorEvent extends Component.Event {
+
+ private CommitException cause;
+
+ public CommitErrorEvent(Grid grid, CommitException cause) {
+ super(grid);
+ this.cause = cause;
+ }
+
+ /**
+ * Retrieves the cause of the failure
+ *
+ * @return the cause of the failure
+ */
+ public CommitException getCause() {
+ return cause;
+ }
+
+ @Override
+ public Grid getComponent() {
+ return (Grid) super.getComponent();
+ }
+
+ /**
+ * Checks if validation exceptions caused this error
+ *
+ * @return true if the problem was caused by a validation error
+ */
+ public boolean isValidationFailure() {
+ return cause.getCause() instanceof InvalidValueException;
+ }
+
+ }
+
+ /**
+ * Default error handler for the editor
+ *
+ */
+ public class DefaultEditorErrorHandler implements EditorErrorHandler {
+
+ @Override
+ public void commitError(CommitErrorEvent event) {
+ Map<Field<?>, InvalidValueException> invalidFields = event
+ .getCause().getInvalidFields();
+
+ if (!invalidFields.isEmpty()) {
+ // Validation error, show first failure as
+ // "<Column header>: <message>"
+ FieldGroup fieldGroup = event.getCause().getFieldGroup();
+ Object propertyId = getFirstPropertyId(fieldGroup,
+ invalidFields.keySet());
+ Field<?> field = fieldGroup.getField(propertyId);
+ String caption = getColumn(propertyId).getHeaderCaption();
+ // TODO This should be shown in the editor component once
+ // there is a place for that. Optionally, all errors should be
+ // shown
+ Notification.show(caption + ": "
+ + invalidFields.get(field).getLocalizedMessage(),
+ Type.ERROR_MESSAGE);
+
+ } else {
+ com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).error(
+ new ConnectorErrorEvent(Grid.this, event.getCause()));
+ }
+ }
+
+ private Object getFirstPropertyId(FieldGroup fieldGroup,
+ Set<Field<?>> keySet) {
+ for (Column c : getColumns()) {
+ Object propertyId = c.getPropertyId();
+ Field<?> f = fieldGroup.getField(propertyId);
+ if (keySet.contains(f)) {
+ return propertyId;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
* Selection modes representing built-in {@link SelectionModel
* SelectionModels} that come bundled with {@link Grid}.
* <p>
@@ -2561,6 +2719,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
*/
private boolean defaultContainer = true;
+ private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select", SelectionEvent.class);
@@ -2800,6 +2960,15 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
try {
saveEditor();
success = true;
+ } catch (CommitException e) {
+ try {
+ getEditorErrorHandler().commitError(
+ new CommitErrorEvent(Grid.this, e));
+ } catch (Exception ee) {
+ // A badly written error handler can throw an exception,
+ // which would lock up the Grid
+ handleError(ee);
+ }
} catch (Exception e) {
handleError(e);
}
@@ -4653,6 +4822,35 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * Sets the error handler for the editor.
+ *
+ * The error handler is called whenever there is an exception in the editor.
+ *
+ * @param editorErrorHandler
+ * The editor error handler to use
+ * @throws IllegalArgumentException
+ * if the error handler is null
+ */
+ public void setEditorErrorHandler(EditorErrorHandler editorErrorHandler)
+ throws IllegalArgumentException {
+ if (editorErrorHandler == null) {
+ throw new IllegalArgumentException(
+ "The error handler cannot be null");
+ }
+ this.editorErrorHandler = editorErrorHandler;
+ }
+
+ /**
+ * Gets the error handler used for the editor
+ *
+ * @see #setErrorHandler(com.vaadin.server.ErrorHandler)
+ * @return the editor error handler, never null
+ */
+ public EditorErrorHandler getEditorErrorHandler() {
+ return editorErrorHandler;
+ }
+
+ /**
* Gets the field factory for the {@link FieldGroup}. The field factory is
* only used when {@link FieldGroup} creates a new field.
* <p>
diff --git a/server/tests/src/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java b/server/tests/src/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java
index da86c83771..d77a2e190b 100644
--- a/server/tests/src/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java
+++ b/server/tests/src/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java
@@ -15,10 +15,23 @@
*/
package com.vaadin.tests.server.component.fieldgroup;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
import org.junit.Assert;
import org.junit.Test;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.util.AbstractProperty;
+import com.vaadin.ui.Field;
import com.vaadin.ui.TextField;
/**
@@ -52,4 +65,85 @@ public class FieldGroupTest {
Assert.assertFalse("Field is not writable", field.isReadOnly());
}
+
+ @Test
+ public void commit_validationFailed_allValidationFailuresAvailable()
+ throws CommitException {
+ FieldGroup fieldGroup = new FieldGroup();
+
+ fieldGroup.setItemDataSource(new TestItem());
+
+ TextField field1 = new TextField();
+ field1.setRequired(true);
+ fieldGroup.bind(field1, "prop1");
+
+ TextField field2 = new TextField();
+ field2.setRequired(true);
+ fieldGroup.bind(field2, "prop2");
+
+ Set<TextField> set = new HashSet<TextField>(Arrays.asList(field1,
+ field2));
+
+ try {
+ fieldGroup.commit();
+ Assert.fail("No commit exception is thrown");
+ } catch (CommitException exception) {
+ Map<Field<?>, ? extends InvalidValueException> invalidFields = exception
+ .getInvalidFields();
+ for (Entry<Field<?>, ? extends InvalidValueException> entry : invalidFields
+ .entrySet()) {
+ set.remove(entry.getKey());
+ }
+ Assert.assertEquals(
+ "Some fields are not found in the invalid fields map", 0,
+ set.size());
+ Assert.assertEquals(
+ "Invalid value exception should be thrown for each field",
+ 2, invalidFields.size());
+ }
+ }
+
+ private static class TestItem implements Item {
+
+ @Override
+ public Property<String> getItemProperty(Object id) {
+ return new StringProperty();
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Arrays.asList("prop1", "prop2");
+ }
+
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ return false;
+ }
+
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ return false;
+ }
+
+ }
+
+ private static class StringProperty extends AbstractProperty<String> {
+
+ @Override
+ public String getValue() {
+ return null;
+ }
+
+ @Override
+ public void setValue(String newValue) throws Property.ReadOnlyException {
+ }
+
+ @Override
+ public Class<? extends String> getType() {
+ return String.class;
+ }
+ }
+
}