/**
   * The constants used in this Content Widget.
   */
  public static interface CwConstants extends Constants {
    String cwCellValidationColumnAddress();

    String cwCellValidationColumnName();

    String cwCellValidationDescription();

    String cwCellValidationError();

    String cwCellValidationName();
  }

  /**
   * An input cell that changes color based on the validation status.
   */
  private static class ValidatableInputCell extends
      AbstractInputCell<String, ValidationData> {

    private SafeHtml errorMessage;

    public ValidatableInputCell(String errorMessage) {
      super("change");
      if (template == null) {
        template = GWT.create(Template.class);
      }
      this.errorMessage = SimpleHtmlSanitizer.sanitizeHtml(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.
      Element target = event.getEventTarget().cast();
      if (!parent.getFirstChildElement().isOrHasChild(target)) {
        return;
      }

      Object key = context.getKey();
      ValidationData viewData = getViewData(key);
      String eventType = event.getType();
      if ("change".equals(eventType)) {
        InputElement input = parent.getFirstChild().cast();

        // Mark cell as containing a pending change
        input.getStyle().setColor("blue");

        // Save the new value in the view data.
        if (viewData == null) {
          viewData = new ValidationData();
          setViewData(key, viewData);
        }
        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.
      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.
       */
      String pendingValue = (viewData == null) ? null : viewData.getValue();
      boolean invalid = (viewData == null) ? false : viewData.isInvalid();

      sb.append(template.input(pendingValue != null ? pendingValue : value,
          pendingValue != null ? (invalid ? "red" : "blue") : ("black")));

      if (invalid) {
        sb.appendHtmlConstant(" <span style='color:red;'>");
        sb.append(errorMessage);
        sb.appendHtmlConstant("</span>");
      }
    }

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

  /**
   * The ViewData used by {@link ValidatableInputCell}.
   */
  private 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;
    }
  }

  /**
   * Checks if an address is valid. A valid address consists of a number
   * followed by a street name, which may be composed of multiple words.
   *
   * @param address the address
   * @return true if valid, false if invalid
   */
  public static boolean isAddressValid(String address) {
    // Cannot be null.
    if (address == null) {
      return false;
    }

    // Must have two or more parts.
    String[] parts = address.split(" ");
    if (parts.length < 2) {
      return false;
    }

    // First part is a number.
    try {
      Integer.parseInt(parts[0]);
    } catch (NumberFormatException e) {
      return false;
    }

    // The remaining parts form the street name.
    return true;
  }

  /**
   * An instance of the constants.
   */
  private final CwConstants constants;

  /**
   * Initialize this example.
   */
  @Override
  public Widget onInitialize() {
    // Create a table.
    final CellTable<ContactInfo> table = new CellTable<ContactInfo>(10,
        ContactInfo.KEY_PROVIDER);

    // Add the Name column.
    table.addColumn(new Column<ContactInfo, String>(new TextCell()) {
      @Override
      public String getValue(ContactInfo object) {
        return object.getFullName();
      }
    }, constants.cwCellValidationColumnName());

    // Add an editable address column.
    final ValidatableInputCell addressCell = new ValidatableInputCell(
        constants.cwCellValidationError());
    Column<ContactInfo, String> addressColumn = new Column<ContactInfo, String>(
        addressCell) {
      @Override
      public String getValue(ContactInfo object) {
        return object.getAddress();
      }
    };
    table.addColumn(addressColumn, constants.cwCellValidationColumnAddress());
    addressColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
      public void update(int index, final ContactInfo object, final String value) {
        // Perform validation after 2 seconds to simulate network delay.
        new Timer() {
          @Override
          public void run() {
            if (isAddressValid(value)) {
              // The cell will clear the view data when it sees the updated
              // value.
              object.setAddress(value);

              // Push the change to the views.
              ContactDatabase.get().refreshDisplays();
            } else {
              // Update the view data to mark the pending value as invalid.
              ValidationData viewData = addressCell.getViewData(ContactInfo.KEY_PROVIDER.getKey(object));
              viewData.setInvalid(true);

              // We only modified the cell, so do a local redraw.
              table.redraw();
            }
          }
        }.schedule(1000);
      }
    });

    // Add the table to the database.
    ContactDatabase.get().addDataDisplay(table);

    return table;
  }