Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In search of a GWT validation example... where art thou?

As a follow-up to Why are there no decent examples of CompositeCell in use within a CellTable?

I am trying to add-on JSR-303 validation support. I have followed Koma's config advice here: How to install gwt-validation with gwt-2.4.0 (Note: I'm using GWT 2.4's built-in validation, not GWT-Validation).

Again, in order to get some re-use I crafted a pair of classes, ValidatableInputCell and AbstractValidatableColumn. I got inspiration for them from:

  • http://code.google.com/p/google-web-toolkit/source/browse/trunk/samples/validation/src/main/java/com/google/gwt/sample/validation/client/ValidationView.java?r=10642
  • http://gwt.google.com/samples/Showcase/Showcase.html#!CwCellValidation

Let's have a look at 'em...

public class ValidatableInputCell extends AbstractInputCell<String, ValidatableInputCell.ValidationData> {

interface Template extends SafeHtmlTemplates {
    @Template("<input type=\"text\" value=\"{0}\" size=\"{1}\" style=\"{2}\" tabindex=\"-1\"></input>")
    SafeHtml input(String value, String width, SafeStyles color);
}

private static Template template;

/**
 * The error message to be displayed as a pop-up near the field
 */
private String errorMessage;

private static final int DEFAULT_INPUT_SIZE = 15;

/**
 * Specifies the width, in characters, of the &lt;input&gt; element contained within this cell
 */
private int inputSize = DEFAULT_INPUT_SIZE;

public ValidatableInputCell() {
    super("change", "keyup");
    if (template == null) {
        template = GWT.create(Template.class);
    }
}

public void setInputSize(int inputSize) {
    this.inputSize = inputSize;
}

public void setErrorMessage(String errorMessage) {
    this.errorMessage = SafeHtmlUtils.htmlEscape(errorMessage);
}

@Override
public void onBrowserEvent(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    super.onBrowserEvent(context, parent, value, event, valueUpdater);

    // Ignore events that don't target the input.
    final InputElement input = (InputElement) getInputElement(parent);
    final Element target = event.getEventTarget().cast();
    if (!input.isOrHasChild(target)) {
        return;
    }

    final Object key = context.getKey();
    final String eventType = event.getType();

    if ("change".equals(eventType)) {
        finishEditing(parent, value, key, valueUpdater);
    } else if ("keyup".equals(eventType)) {
        // Mark cell as containing a pending change
        input.getStyle().setColor("blue");

        ValidationData viewData = getViewData(key);
        // Save the new value in the view data.
        if (viewData == null) {
            viewData = new ValidationData();
            setViewData(key, viewData);
        }
        final String newValue = input.getValue();
        viewData.setValue(newValue);
        finishEditing(parent, newValue, key, valueUpdater);

        // Update the value updater, which updates the field updater.
        if (valueUpdater != null) {
            valueUpdater.update(newValue);
        }
    }
}

@Override
public void render(Context context, String value, SafeHtmlBuilder sb) {
    // Get the view data.
    final Object key = context.getKey();
    ValidationData viewData = getViewData(key);
    if (viewData != null && viewData.getValue().equals(value)) {
        // Clear the view data if the value is the same as the current value.
        clearViewData(key);
        viewData = null;
    }

    /*
     * If viewData is null, just paint the contents black. If it is non-null,
     * show the pending value and paint the contents red if they are known to
     * be invalid.
     */
    final String pendingValue = viewData == null ? null : viewData.getValue();
    final boolean invalid = viewData == null ? false : viewData.isInvalid();

    final String color = pendingValue != null ? invalid ? "red" : "blue" : "black";
    final SafeStyles safeColor = SafeStylesUtils.fromTrustedString("color: " + color + ";");
    sb.append(template.input(pendingValue != null ? pendingValue : value, String.valueOf(inputSize), safeColor));
}

@Override
protected void onEnterKeyDown(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    final Element target = event.getEventTarget().cast();
    if (getInputElement(parent).isOrHasChild(target)) {
        finishEditing(parent, value, context.getKey(), valueUpdater);
    } else {
        super.onEnterKeyDown(context, parent, value, event, valueUpdater);
    }
}

@Override
protected void finishEditing(Element parent, String value, Object key,
        ValueUpdater<String> valueUpdater) {
    final ValidationData viewData = getViewData(key);

    final String pendingValue = viewData == null ? null : viewData.getValue();
    final boolean invalid = viewData == null ? false : viewData.isInvalid();

    if (invalid) {
        final DecoratedPopupPanel errorMessagePopup = new DecoratedPopupPanel(true);
        final VerticalPanel messageContainer = new VerticalPanel();
        messageContainer.setWidth("200px");
        final Label messageTxt = new Label(errorMessage, true);
        messageTxt.setStyleName(UiResources.INSTANCE.style().error());
        messageContainer.add(messageTxt);
        errorMessagePopup.setWidget(messageContainer);

        // Reposition the popup relative to input field
        final int left = parent.getAbsoluteRight() + 25;
        final int top = parent.getAbsoluteTop();

        errorMessagePopup.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
            @Override
            public void setPosition(int offsetWidth, int offsetHeight) {
                errorMessagePopup.setPopupPosition(left, top);
            }
        });
    }
    // XXX let user continue or force focus until value is valid? for now the former is implemented
    super.finishEditing(parent, pendingValue, key, valueUpdater);
}

/**
 * The ViewData used by {@link ValidatableInputCell}.
 */
static class ValidationData {
    private boolean invalid;
    private String value;

    public String getValue() {
        return value;
    }

    public boolean isInvalid() {
        return invalid;
    }

    public void setInvalid(boolean invalid) {
        this.invalid = invalid;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

}

and

public abstract class AbstractValidatableColumn<T> implements HasCell<T, String> {

private ValidatableInputCell cell = new ValidatableInputCell();
private CellTable<T> table;

public AbstractValidatableColumn(int inputSize, CellTable<T> table) {
    cell.setInputSize(inputSize);
    this.table = table;
}

@Override
public Cell<String> getCell() {
    return cell;
}

@Override
public FieldUpdater<T, String> getFieldUpdater() {
    return new FieldUpdater<T, String>() {
        @Override
        public void update(int index, T dto, String value) {
            final Set<ConstraintViolation<T>> violations = validate(dto);
            final ValidationData viewData = cell.getViewData(dto);
            if (!violations.isEmpty()) {  // invalid
                final StringBuffer errorMessage = new StringBuffer();
                for (final ConstraintViolation<T> constraintViolation : violations) {
                    errorMessage.append(constraintViolation.getMessage());
                }
                viewData.setInvalid(true);
                cell.setErrorMessage(errorMessage.toString());
                table.redraw();
            } else {  // valid
                viewData.setInvalid(false);
                cell.setErrorMessage(null);
                doUpdate(index, dto, value);
            }
        }
    };
}

protected abstract void doUpdate(int index, T dto, String value);

protected Set<ConstraintViolation<T>> validate(T dto) {
    final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    final Set<ConstraintViolation<T>> violations = validator.validate(dto);
    return violations;
}

}

I use the AbstractValidatableColumn like so...

protected HasCell<ReserveOfferDTO, String> generatePriceColumn(DisplayMode currentDisplayMode) {
    HasCell<ReserveOfferDTO, String> priceColumn;
    if (isInEditMode(currentDisplayMode)) {
        priceColumn = new AbstractValidatableColumn<ReserveOfferDTO>(5, this) {

            @Override
            public String getValue(ReserveOfferDTO reserveOffer) {
                return obtainPriceValue(reserveOffer);
            }

            @Override
            protected void doUpdate(int index, ReserveOfferDTO reserveOffer, String value) {
                // number format exceptions should be caught and handled by event bus's handle method
                final double valueAsDouble = NumberFormat.getDecimalFormat().parse(value);
                final BigDecimal price = BigDecimal.valueOf(valueAsDouble);
                reserveOffer.setPrice(price);
            }

        };
    } else {
        priceColumn = new Column<ReserveOfferDTO, String>(new TextCell()) {

            @Override
            public String getValue(ReserveOfferDTO reserveOffer) {
                return obtainPriceValue(reserveOffer);
            }
        };
    }
    return priceColumn;
}

Oh! And here's the DTO with JSR-303 annotations...

public class ReserveOfferDTO extends DateComparable implements Serializable {

private static final long serialVersionUID = 1L;

@NotNull @Digits(integer=6, fraction=2)
private BigDecimal price;
@NotNull @Digits(integer=6, fraction=2)
private BigDecimal fixedMW;

private String dispatchStatus;
private String resourceName;
private String dateTime;
private String marketType;
private String productType;

...

}

Dropping a breakpoint in onBrowserEvent I would expect to have the validation trigger on each key stroke and/or after cell loses focus. It never gets invoked. I can enter whatever I like in the cell. Any clues as to an approach to fix?

My early thoughts... a) AbstractValidatableColumn#getFieldUpdater is never getting invoked and b) the logic in either ValidatableInputCell#onBrowserEvent or ValidatableInputCell#render needs an overhaul.

Ultimately, I'd like to see a popup appearing next to each cell that violates a constraint, and of course see that the appropriate coloring is applied.

like image 796
Chris Phillipson Avatar asked Feb 03 '12 02:02

Chris Phillipson


2 Answers

Coming up for air here. I finally figured out a solution! I opted to employ the GWT Validation library, see http://code.google.com/p/gwt-validation/wiki/GWT_Validation_2_0 (the code below is known to work with the 2.1 SNAPSHOT).

The trick when performing validation for a cell is to call validateValue rather than validate (the latter triggers validation for all entity's fields). As well, all input cell values are String, and converted to the respective entity field type before being validated. (Even works for nested entity fields).

Here's the revised impls for both AbstractValidatableColumn (AVC) and ValidatableInputCell.

/**
 * A {@link Column} implementation that encapsulates a {@link ValidatableInputCell}.
 * Performs JSR-303 validation on a field (or nested field) of the type.
 * @author cphillipson
 *
 * @param <T> the type
 * @param <O> the owning type of the field to be validated; in many cases T may have only primitive or wrapper types, therefore O will be the same type as T
 */
public abstract class AbstractValidatableColumn<T, O> extends Column<T, String> {

/**
 * Preferred constructor.
 * Allows for definition of tabIndex but uses a default for the input cell size.
 * @param tabIndex the <code>tabindex</code> attribute's value for the input cell
 * @param table the grid instance
 */
public AbstractValidatableColumn(int tabIndex, final AbstractHasData<T> table) {
    this(App.INSTANCE.defaultValidatableInputCellSize(), tabIndex, table);
}

/**
 * Overloaded constructor.
 * Allows for definition of tabIndex and allows for an override to the default for the input cell size.
 * @param inputSize the <code>size</code> attribute's value for the input cell
 * @param tabIndex the <code>tabindex</code> attribute's value for the input cell
 * @param table the grid instance
 */
public AbstractValidatableColumn(int inputSize, int tabIndex, final AbstractHasData<T> table) {
    super(new ValidatableInputCell());
    getCell().setInputSize(inputSize);
    getCell().setTabIndex(tabIndex);
    init(table);
}

// meat and potatoes
private void init(final AbstractHasData<T> table) {
    setFieldUpdater(new FieldUpdater<T, String>() {
        @Override
        public void update(int index, T dto, String newValue) {
            final ConversionResult cr = attemptValueConversion(newValue);
            final ValidationData viewData = getCell().getViewData(dto);
            if (cr.wasConvertedSuccessfully()) {
                final Set<ConstraintViolation<O>> violations = validate(cr.getValue());
                if (!violations.isEmpty()) {  // invalid
                    final StringBuffer errorMessage = new StringBuffer();
                    for (final ConstraintViolation<O> constraintViolation : violations) {
                        errorMessage.append(constraintViolation.getMessage());
                    }
                    viewData.setInvalid(true);
                    getCell().setErrorMessage(errorMessage.toString());
                } else {  // valid
                    viewData.setInvalid(false);
                    getCell().setErrorMessage("");
                    doUpdate(index, dto, newValue);
                }
            } else { // conversion exception
                viewData.setInvalid(true);
                getCell().setErrorMessage(UiMessages.INSTANCE.improper_input_format());
            }
        }
    });
}


/**
 * Attempts conversion of a String value into another type
 * Instances are responsible for the conversion logic as it may vary from type to type
 * @param value a String value to be converted into an owning class's property type
 * @return a ConversionResult
 */
protected abstract ConversionResult attemptValueConversion(String value);

@Override
public ValidatableInputCell getCell() {
    return (ValidatableInputCell) super.getCell();
}

/**
 * Template method for updating a field (or nested field) value within a DTO
 * @param index the row index for the instance of the DTO within the grid
 * @param dto the object whose field we wish to update
 * @param value the new value that will be set on a field (or nested field) of the DTO
 */
protected abstract void doUpdate(int index, T dto, String value);

/**
 * Template method for specifying the property name of an owning class
 * @return the field name of the owning class whose value is to be updated
 */
protected abstract String getPropertyName();

/**
 * Template method for specifying the owning class
 * @return the owning class of the field whose value is to be updated
 */
protected abstract Class<O> getPropertyOwner();

/**
 * Validates a value against a set of constraints (i.e., JSR-303 annotations on a field)
 * @param newValue the value to be validated
 * @return the set of constraint violations induced by an inappropriate value
 */
protected Set<ConstraintViolation<O>> validate(Object newValue) {
    final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    final Set<ConstraintViolation<O>> violations = validator.validateValue(getPropertyOwner(), getPropertyName(), newValue);
    return violations;
}

}

/**
 * <p>A cell that will update its styling and provide feedback upon a validation constraint violation.</p>
 * <p>Implementation based upon GWT Showcase's <a href="http://gwt.google.com/samples/Showcase/Showcase.html#!CwCellValidation">Cell Validation</a> example.</p>
 * @author cphillipson
 *
 */
 public class ValidatableInputCell extends AbstractInputCell<String, ValidatableInputCell.ValidationData> {

interface Template extends SafeHtmlTemplates {
    @Template("<input type=\"text\" value=\"{0}\" size=\"{1}\" style=\"{2}\" tabindex=\"{3}\"></input>")
    SafeHtml input(String value, String width, SafeStyles color, String tabIndex);
}

private static Template template;

/**
 * The error message to be displayed as a pop-up near the field
 */
private String errorMessage;

private static final int DEFAULT_INPUT_SIZE = App.INSTANCE.defaultValidatableInputCellSize();

/**
 * Specifies the width, in characters, of the &lt;input&gt; element contained within this cell
 */
private int inputSize = DEFAULT_INPUT_SIZE;

/**
 * Specifies the tab index for this cell
 */
private int tabIndex = -1;

public ValidatableInputCell() {
    // since onBrowserEvent method is overridden, we must register all events that handled in overridden method impl
    super("change", "keyup", "focus", "blur", "keydown");
    if (template == null) {
        template = GWT.create(Template.class);
    }
}

public void setInputSize(int inputSize) {
    this.inputSize = inputSize;
}

public void setTabIndex(int index) {
    tabIndex = index;
}

public void setErrorMessage(String errorMessage) {
    this.errorMessage = SafeHtmlUtils.fromSafeConstant(errorMessage).asString();
}

@Override
public void onBrowserEvent(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    super.onBrowserEvent(context, parent, value, event, valueUpdater);

    final InputElement input = (InputElement) getInputElement(parent);
    final Object key = context.getKey();
    final String eventType = event.getType();

    if ("keyup".equals(eventType)) {

        ValidationData viewData = getViewData(key);
        // Save the new value in the view data.
        if (viewData == null) {
            viewData = new ValidationData();
            setViewData(key, viewData);
        }
        final String newValue = input.getValue();
        viewData.setValue(newValue);

        finishEditing(parent, newValue, key, valueUpdater);
    }
}

@Override
public void render(Context context, String value, SafeHtmlBuilder sb) {
    // Get the view data.
    final Object key = context.getKey();
    ValidationData viewData = getViewData(key);
    if (viewData != null && viewData.getValue().equals(value)) {
        // Clear the view data if the value is the same as the current value.
        clearViewData(key);
        viewData = null;
    }

    /*
     * If viewData is null, just paint the contents black. If it is non-null,
     * show the pending value and paint the contents red if they are known to
     * be invalid.
     */
    final String pendingValue = viewData == null ? null : viewData.getValue();
    final boolean invalid = viewData == null ? false : viewData.isInvalid();

    final String color = pendingValue != null ? invalid ? App.INSTANCE.invalidCellInputTextColor() : App.INSTANCE.pendingCellInputTextColor() : App.INSTANCE.defaultCellInputTextColor();
    final String backgroundColor = pendingValue != null ? invalid ? App.INSTANCE.invalidCellInputTextBackgroundColor() : App.INSTANCE.pendingCellInputTextBackgroundColor() : App.INSTANCE.defaultCellInputTextBackgroundColor();
    final SafeStyles style = SafeStylesUtils.fromTrustedString("color: " + color + "; background-color: " + backgroundColor + ";");
    sb.append(template.input(pendingValue != null ? pendingValue : value, String.valueOf(inputSize), style, String.valueOf(tabIndex)));
}

/*
@Override
protected void onEnterKeyDown(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    final Element target = event.getEventTarget().cast();
    if (getInputElement(parent).isOrHasChild(target)) {
        finishEditing(parent, value, context.getKey(), valueUpdater);
    } else {
        super.onEnterKeyDown(context, parent, value, event, valueUpdater);
    }
}
 */

@Override
protected void onEnterKeyDown(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    // do nothing
}

@Override
protected void finishEditing(Element parent, String value, Object key,
        ValueUpdater<String> valueUpdater) {

    // Update the value updater, which updates the field updater.
    if (valueUpdater != null) {
        valueUpdater.update(value);
    }

    final InputElement input = (InputElement) getInputElement(parent);
    final ValidationData viewData = getViewData(key);

    /*
     * If viewData is null, just paint the contents black. If it is non-null,
     * show the pending value and paint the contents red if they are known to
     * be invalid.
     */
    final String pendingValue = viewData == null ? null : viewData.getValue();
    final boolean invalid = viewData == null ? false : viewData.isInvalid();

    final String color = pendingValue != null ? invalid ? App.INSTANCE.invalidCellInputTextColor() : App.INSTANCE.pendingCellInputTextColor() : App.INSTANCE.defaultCellInputTextColor();
    final String backgroundColor = pendingValue != null ? invalid ? App.INSTANCE.invalidCellInputTextBackgroundColor() : App.INSTANCE.pendingCellInputTextBackgroundColor() : App.INSTANCE.defaultCellInputTextBackgroundColor();
    input.getStyle().setColor(color);
    input.getStyle().setBackgroundColor(backgroundColor);

    if (invalid) {
        final DecoratedPopupPanel errorMessagePopup = new DecoratedPopupPanel(true);
        final FlowPanel messageContainer = new FlowPanel();
        messageContainer.setWidth(App.INSTANCE.errorMessagePopupWidth());
        final Label messageTxt = new Label(errorMessage, true);
        messageTxt.setStyleName(UiResources.INSTANCE.style().error());
        messageContainer.add(messageTxt);
        errorMessagePopup.setWidget(messageContainer);

        // Reposition the popup relative to input field
        final int left = parent.getAbsoluteRight() +5;
        final int top = parent.getAbsoluteTop() - 5;

        errorMessagePopup.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
            @Override
            public void setPosition(int offsetWidth, int offsetHeight) {
                errorMessagePopup.setPopupPosition(left, top);
            }
        });
    }

}

/**
 * The ViewData used by {@link ValidatableInputCell}.
 */
static class ValidationData {
    private boolean invalid;
    private String value;

    public String getValue() {
        return value;
    }

    public boolean isInvalid() {
        return invalid;
    }

    public void setInvalid(boolean invalid) {
        this.invalid = invalid;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

}

Variants of AVC might look like...

/**
 * A variant of {@link AbstractValidatableColumn} that works with {@link BigDecimal} field types.
 * @author cphillipson
 *
 * @param <T> the type
 * @param <O> the owning type of the field to be validated; in many cases T may have only primitive or wrapper types, therefore O will be the same type as T
 */
public abstract class BigDecimalValidatableColumn<T, O> extends AbstractValidatableColumn<T, O> {

public BigDecimalValidatableColumn(int tabIndex, AbstractHasData table) {
    super(tabIndex, table);
}

public BigDecimalValidatableColumn(int inputSize, int tabIndex, final AbstractHasData<T> table) {
    super(inputSize, tabIndex, table);
}

@Override
protected ConversionResult attemptValueConversion(String value) {
    return doConversion(value);
}

public static ConversionResult doConversion(String value) {
    ConversionResult result = null;
    try {
        final Double dblValue = Double.valueOf(value);
        final BigDecimal convertedValue = BigDecimal.valueOf(dblValue);
        result = ConversionResult.converted(convertedValue);
    } catch (final NumberFormatException nfe) {
        result = ConversionResult.not_converted();
    }
    return result;
}
}

A ConversionResult is consulted by a Column's fieldUpdater. Here's what it looks like...

/**
 * An attempted conversion result.
 * Returns both the converted value (from <code>String</code>) and whether or not the conversion was successful.
 * E.g., if you tried to convert from a <code>String</code> to a <code>Number</code>, in the failure case this would result in a <code>NumberFormatException</code>.
 * On failure, the boolean would be false and the value would be null.
 * On success, the boolean would be true and the value would be of the type needed to continue validation against a set of constraints
 * @author cphillipson
 *
 */
public  class ConversionResult {
private Object value;
private boolean convertedSuccessfully;

private ConversionResult () {}

/**
 * Use this method when a successful conversion is made to return a result
 * @param value the convertedValue
 * @return the result of the conversion containing the converted value and a success flag
 */
public static ConversionResult converted(Object value) {
    final ConversionResult result = new ConversionResult();
    result.setConvertedSuccessfully(true);
    result.setValue(value);
    return result;
}

/**
 * Use this method when an attempt to convert a String value failed
 * @return the result of a failed conversion
 */
public static ConversionResult not_converted() {
    return new ConversionResult();
}

private void setValue(Object value) {
    this.value = value;
}

public Object getValue() {
    return value;
}

private void setConvertedSuccessfully(boolean flag) {
    convertedSuccessfully = flag;
}

public boolean wasConvertedSuccessfully() {
    return convertedSuccessfully;
}
}

Finally, here's how you might spec a column in a grid

new BigDecimalValidatableColumn<EnergyOfferDTO, OfferPriceMwPairDTO>(nextTabIndex(), getGrid()) {

            @Override
            public String getValue(EnergyOfferDTO energyOffer) {
                return obtainPriceValue(colIndex, energyOffer, false);
            }

            @Override
            public void doUpdate(int index, EnergyOfferDTO energyOffer, String value) {
                if (value != null && !value.isEmpty()) {
                    // number format exceptions should be caught and handled by event bus's handle method
                    final double valueAsDouble = NumberFormat.getDecimalFormat().parse(value);

                    final BigDecimal price = BigDecimal.valueOf(valueAsDouble);
                    final List<OfferPriceMwPairDTO> offerPriceCurve = energyOffer.getCurve();
                    final OfferPriceMwPairDTO offerPriceMwPairDTO = offerPriceCurve.get(colIndex);
                    if (offerPriceMwPairDTO == null) {  // we have a new price value
                        newOfferPriceMwPair.setPrice(price);
                        offerPriceCurve.add(newOfferPriceMwPair);
                    } else {
                        offerPriceMwPairDTO.setPrice(price);
                    }

                }
            }

            @Override
            protected String getPropertyName() {
                return "price";
            }

            @Override
            protected Class<OfferPriceMwPairDTO> getPropertyOwner() {
                return OfferPriceMwPairDTO.class;
            }

        };

Note the DTO's in the example above have their fields JSR-303 constraint annotated (e.g., with @Digits, @NotNull).

The above took some doing, and it may just be the most comprehensive solution out on the net at the moment. Enjoy!

like image 99
Chris Phillipson Avatar answered Sep 22 '22 07:09

Chris Phillipson


It isn't clear to me why a HasCell is being returned from generatePriceColumn, since that can't be consumed by practically anything, except CompositeCell - maybe you are trying to wrap up all of this in a bigger cell. Before asking, you might consider in the future breaking down your example further, the problem might become clear.

I changed the 'column' creating code so it actually returned a Column - this meant changing AbstractValidatableColumn to extend Column. Along the way, I noticed that you were overriding getFieldUpdater, without modifying the underlying field, which will prevent other pieces of Column's internals from working, as they look for that field. As a result of this, my initial experiments were getting to ValidatableInputCell.onBrowserEvent's keyup case correctly, but there was no ValueUpdater instance to work with, since the FieldUpdater was null in Column.

At that point, the validation logic, which I didn't wire up, is being invoked - As of GWT 2.4.0, this is still tagged in every class as "EXPERIMENTAL", and as not for use in production code, so I've given it a pass until 2.5.0 or so, when the rough edges have been rounded off. If I were to continue though (and if you have issues), I'd start with the project at http://code.google.com/p/google-web-toolkit/source/browse/trunk/samples/validation/ - get that to work, and then steal details until mine worked as well.

A few other observations:

Don't extend classes to add functionality, except where you expect/allow any consumers of that class to use it as they would the subclass. Hard to tell in this case, but generatePriceColumn appears to be on a CellTable subclass, which

  1. Lets any code that uses it change how the rest of the celltable is set up,
  2. Doesn't really act like a CellTable method - other column-focused methods actually add the column instead of returning it
  3. Might lock you in to always use CellTable (since that is what you subclass) while that method will work perfectly well otherwise with AbstractCellTable subclasses like DataTable, a newer CellTable

In this case, I'd either change the method to be addPriceColumn(...), and have it use a Column and add it to the list, or keep it out, either as a subclassed column, or entirely on its own as a utility method. My final AbstractValidationColumn ended up not having much reason to be a subclass at all, effectively just a convenience constructor for Column:

public abstract class AbstractValidatableColumn<T> extends Column<T, String> {

  public AbstractValidatableColumn(int inputSize, final AbstractCellTable<T> table) {
    super(new ValidatableInputCell());
    ((ValidatableInputCell) getCell()).setInputSize(inputSize);

    setFieldUpdater(new FieldUpdater<T, String>() {
      public void update(int index, T dto, String value) {
        final Set<ConstraintViolation<T>> violations = validate(dto);
        final ValidationData viewData = getCell().getViewData(dto);
        if (!violations.isEmpty()) {  // invalid
          final StringBuffer errorMessage = new StringBuffer();
          for (final ConstraintViolation<T> constraintViolation : violations) {
            errorMessage.append(constraintViolation.getMessage());
          }
          viewData.setInvalid(true);
          getCell().setErrorMessage(errorMessage.toString());
          table.redraw();
        } else {  // valid
          viewData.setInvalid(false);
          getCell().setErrorMessage(null);
          doUpdate(index, dto, value);
        }
      }
    });
  }

  @Override
  public ValidatableInputCell getCell() {
    return (ValidatableInputCell)super.getCell();
  }

  protected abstract void doUpdate(int index, T dto, String value);

  protected Set<ConstraintViolation<T>> validate(T dto) {
    final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    final Set<ConstraintViolation<T>> violations = validator.validate(dto);
    return violations;
  }
}

The FieldUpdater is the interesting part here, that is what should be focused on, and leave as many other pieces to be reused as possible. This will allow any cell to run its own ValueUpdater when it is ready - perhaps not as frequently as you like, but it'll generally make things easier to use more quickly. Make a FieldUpdater impl that wraps another FieldUpdater, which can be specific to whatever field is being changed in that case.

I think another bug is lurking here, and might show up if you test the column/fieldupdater on its own - the new value isn't applied to the bean of type T until after the validation has run, so the bean is being validated with the old valid value. doUpdate needs to be called sooner.

And finally, I'd encourage you to keep your example simpler as you go - some brain-dead 'is it null' check for validation, and a simple straightforward CellTable setup would let you see that the column itself only has the validation stuff working if the Column.fieldUpdater field is non null. Build up from a simpler configuration that works, so only one thing can go wrong at each stage.

like image 1
Colin Alworth Avatar answered Sep 21 '22 07:09

Colin Alworth