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;
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;
}
}
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;
}
}
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.BindException;
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;
}
}
+ /**
+ * 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}.
*/
private boolean defaultContainer = true;
+ private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select", SelectionEvent.class);
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);
}
editorFieldGroup.setFieldFactory(fieldFactory);
}
+ /**
+ * 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;
+ }
+
@Override
public void addItemClickListener(ItemClickListener listener) {
addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
*/
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;
/**
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;
+ }
+ }
+
}
intField.clear();
intField.sendKeys("banana phone");
editor.save();
- assertTrue(
- "No exception on invalid value.",
- logContainsText("Exception occured, com.vaadin.data.fieldgroup.FieldGroup$CommitException: Commit failed"));
+ WebElement n = $(NotificationElement.class).first();
+ assertEquals("Column 7: Could not convert value to Integer",
+ n.getText());
+ n.click();
editor.cancel();
selectMenuPath(EDIT_ITEM_100);
package com.vaadin.tests.fieldgroup;
import java.util.Iterator;
+import java.util.Map;
import com.vaadin.annotations.Theme;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.fieldgroup.BeanFieldGroup;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.fieldgroup.PropertyId;
import com.vaadin.data.util.BeanItem;
import com.vaadin.data.util.BeanItemContainer;
+import com.vaadin.data.validator.IntegerRangeValidator;
import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.tests.components.AbstractTestUIWithLog;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Component;
import com.vaadin.ui.Field;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.Notification;
+import com.vaadin.ui.Notification.Type;
import com.vaadin.ui.TextField;
+import com.vaadin.ui.themes.ValoTheme;
@Theme("valo")
public abstract class AbstractBasicCrud extends AbstractTestUIWithLog {
private TextField birthDate = new TextField("Birth date");
private TextField age = new TextField("Age");
private CheckBox alive = new CheckBox("Alive");
+ private Label errorLabel = new Label((String) null, ContentMode.HTML);
@PropertyId("address.streetAddress")
private TextField address_streetAddress = new TextField(
address_country.setNullRepresentation("");
birthDate.setNullRepresentation("");
+ age.addValidator(new IntegerRangeValidator(
+ "Must be between 0 and 100", 0, 100));
+
setDefaultComponentAlignment(Alignment.MIDDLE_LEFT);
addComponents(firstName, lastName, gender, birthDate, age, alive,
address_streetAddress, address_postalCode, address_city,
address_country);
+ errorLabel.addStyleName(ValoTheme.LABEL_COLORED);
+ setRows(3);
+ addComponent(errorLabel, 0, 2, getColumns() - 1, 2);
+
HorizontalLayout hl = new HorizontalLayout(save, cancel);
hl.setSpacing(true);
addComponent(hl);
}
+ @Override
+ protected void handleCommitException(CommitException e) {
+ String message = "";
+ // Produce error message in the order in which the fields are in the
+ // layout
+ for (Component c : this) {
+ if (!(c instanceof Field)) {
+ continue;
+ }
+ Field<?> f = (Field<?>) c;
+ Map<Field<?>, InvalidValueException> exceptions = e
+ .getInvalidFields();
+ if (exceptions.containsKey(f)) {
+ message += f.getCaption() + ": "
+ + exceptions.get(f).getLocalizedMessage()
+ + "<br/>\n";
+ }
+ }
+
+ errorLabel.setValue(message);
+ }
+
+ @Override
+ protected void discard() {
+ super.discard();
+ errorLabel.setValue(null);
+ }
}
protected abstract void deselectAll();
fieldGroup.commit();
log("Saved " + fieldGroup.getItemDataSource());
} catch (CommitException e) {
+ handleCommitException(e);
log("Commit failed: " + e.getMessage());
}
}
@Override
public void buttonClick(ClickEvent event) {
log("Discarded " + fieldGroup.getItemDataSource());
- deselectAll();
+ discard();
}
});
}
+ protected void discard() {
+ deselectAll();
+ }
+
+ protected void handleCommitException(CommitException e) {
+ String message = "";
+ for (Object propertyId : e.getInvalidFields().keySet()) {
+ Field<?> f = e.getFieldGroup().getField(propertyId);
+ message += f.getCaption() + ": "
+ + e.getInvalidFields().get(propertyId);
+ }
+
+ if (!message.isEmpty()) {
+ Notification.show(message, Type.ERROR_MESSAGE);
+ }
+ }
+
public void edit(BeanItem<ComplexPerson> item) {
fieldGroup.setItemDataSource(item);
}
*/
package com.vaadin.tests.fieldgroup;
+import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
+import com.vaadin.testbench.elements.DateFieldElement;
import com.vaadin.testbench.elements.GridElement;
import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.GridElement.GridEditorElement;
+import com.vaadin.tests.annotations.TestCategory;
import com.vaadin.tests.tb3.MultiBrowserTest;
+@TestCategory("grid")
public class BasicCrudGridEditorRowTest extends MultiBrowserTest {
+ private GridElement grid;
+
+ @Before
+ public void openTest() {
+ openTestURL();
+ grid = $(GridElement.class).first();
+
+ }
@Test
public void lookAndFeel() throws Exception {
- openTestURL();
- GridElement grid = $(GridElement.class).first();
GridCellElement ritaBirthdate = grid.getCell(2, 3);
compareScreen("grid");
// Open editor row
new Actions(getDriver()).doubleClick(ritaBirthdate).perform();
compareScreen("editorrow");
+ }
+
+ @Test
+ public void editorRowOneInvalidValue() throws Exception {
+ GridCellElement ritaBirthdate = grid.getCell(2, 3);
+ // Open editor row
+ new Actions(getDriver()).doubleClick(ritaBirthdate).perform();
+ GridEditorElement editor = grid.getEditor();
+ DateFieldElement dateField = editor.$(DateFieldElement.class).first();
+ WebElement input = dateField.findElement(By.xpath("input"));
+ // input.click();
+ input.sendKeys("Invalid", Keys.TAB);
+ editor.save();
+
+ Assert.assertTrue("Editor wasn't displayed.", editor.isDisplayed());
+ Assert.assertTrue("DateField wasn't displayed.",
+ dateField.isDisplayed());
+
+ Assert.assertTrue("DateField didn't have 'v-invalid' css class.",
+ hasCssClass(dateField, "v-datefield-error"));
}
+
}