Package com.tll.client.ui.field

Source Code of com.tll.client.ui.field.FieldGroup

/**
* The Logic Lab
* @author jpk
*/
package com.tll.client.ui.field;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.Widget;
import com.tll.client.ui.IWidgetRef;
import com.tll.client.validate.CompositeValidator;
import com.tll.client.validate.Error;
import com.tll.client.validate.ErrorClassifier;
import com.tll.client.validate.ErrorDisplay;
import com.tll.client.validate.IErrorHandler;
import com.tll.client.validate.IValidator;
import com.tll.client.validate.ValidationException;
import com.tll.model.schema.IPropertyMetadataProvider;
import com.tll.util.PropertyPath;
import com.tll.util.StringUtil;

/**
* FieldGroup - A group of {@link IField}s which may in turn be nested
* {@link FieldGroup}s. Thus a FieldGroup is a hierarchical collection of
* {@link IField}s.
* <p>
* Non-FieldGroup children of {@link FieldGroup}s are:
* <ol>
* <li>Assumed to be {@link IFieldWidget} instances
* <li>Expected to have a unique <em>property</em> name (in addition to a unique
* name)
* </ol>
* Unique property names enable the resolution of {@link IFieldWidget}s.
* <p>
* A {@link FieldGroup} is a grouping of {@link IField}s for UI purposes
* <em>does not necessarily represent model hierarchy boundaries</em>
* <p>
* <b>IMPT: </b> {@link FieldGroup}s do <em>NOT</em> (as yet) support handling
* of circular references! As such, do not add a field to a field group that is
* a child of another field group where both are children of a common ancestor
* field group.
* @author jpk
*/
public final class FieldGroup implements IField, Iterable<IField> {

  /**
   * Recursively searches the given field group for a field whose name matches
   * that given. The first found field is returned.
   * @param name The name to search for. If <code>null</code> is specified,
   *        <code>null</code> is returned.
   * @param group The group to search in
   * @return The found IField or <code>null</code> if no matching field found
   */
  private static IField findByName(final String name, FieldGroup group) {
    if(name == null) return null;
    if(name.equals(group.name)) return group;

    // first go through the non-group child fields
    for(final IField fld : group) {
      if(fld instanceof FieldGroup == false) {
        if(name.equals(fld.getName())) {
          return fld;
        }
      }
    }

    IField rfld;
    for(final IField fld : group) {
      if(fld instanceof FieldGroup) {
        rfld = findByName(name, (FieldGroup) fld);
        if(rfld != null) return rfld;
      }
    }

    return null;
  }

  /**
   * Recursively searches the given field group for a nested field whose
   * property name matches that given. The first found field is returned.
   * @param propertyName The property name to search for
   * @param group The group to search in
   * @return The found IField or <code>null</code> if no matching field found
   */
  private static IFieldWidget<?> findByPropertyName(final String propertyName, FieldGroup group) {

    // first go through the non-group child fields
    for(final IField fld : group) {
      if(fld instanceof IFieldWidget<?>) {
        if(((IFieldWidget<?>) fld).getPropertyName().equals(propertyName)) {
          return (IFieldWidget<?>) fld;
        }
      }
    }

    IFieldWidget<?> rfld;
    for(final IField fld : group) {
      if(fld instanceof FieldGroup) {
        rfld = findByPropertyName(propertyName, (FieldGroup) fld);
        if(rfld != null) return rfld;
      }
    }

    return null;
  }

  /**
   * Recursively extracts all {@link IFieldWidget}s whose property name matches
   * by regular expression the given property path. The found fields are added
   * to the given set.
   * @param propPath The regex token of the property path. If <code>null</code>,
   *        all encountered {@link IFieldWidget}s are included
   * @param group The field group to search
   * @param set The set of found fields
   */
  private static void findFieldWidgets(final String propPath, FieldGroup group, Set<IFieldWidget<?>> set) {
    Set<FieldGroup> gset = null;
    for(final IField fld : group) {
      if(fld instanceof IFieldWidget<?>) {
        if(propPath == null || ((IFieldWidget<?>) fld).getPropertyName().matches(propPath)) {
          set.add((IFieldWidget<?>) fld);
        }
      }
      else {
        if(gset == null) gset = new HashSet<FieldGroup>();
        gset.add((FieldGroup) fld);
      }
    }
    if(gset != null) {
      for(final FieldGroup fg : gset) {
        findFieldWidgets(propPath, fg, set);
      }
    }
  }

  /**
   * Recursively pre-pends the given parent property path to all applicable
   * child fields' property names.
   * @param field
   * @param parentPropPath The parent property path to pre-pend
   */
  private static void setParentPropertyPath(IField field, final String parentPropPath) {
    if(field instanceof FieldGroup) {
      for(final IField f : (FieldGroup) field) {
        setParentPropertyPath(f, parentPropPath);
      }
    }
    else {
      assert field instanceof IFieldWidget<?>;
      ((IFieldWidget<?>) field).setPropertyName(PropertyPath.getPropertyPath(parentPropPath, ((IFieldWidget<?>) field)
          .getPropertyName()));
    }
  }

  /**
   * Recursively replaces the parent property path with the given replacement
   * parent property path.
   * @param field the current field
   * @param old the existing parent property path
   * @param repl the replacement parent property path
   */
  private static void replaceParentPropertyPath(IField field, final String old, final String repl) {
    if(field instanceof FieldGroup) {
      for(final IField f : (FieldGroup) field) {
        replaceParentPropertyPath(f, old, repl);
      }
    }
    else {
      assert field instanceof IFieldWidget<?>;
      final IFieldWidget<?> fw = ((IFieldWidget<?>) field);
      final String pname = fw.getPropertyName();
      final PropertyPath p = new PropertyPath(pname);
      p.replace(old, repl);
      fw.setPropertyName(p.toString());
    }
  }

  /**
   * Verifies a field's worthiness to add to this group based on:
   * <ol>
   * <li>Name uniqueness for <em>all</em> existing fields in this group.
   * <li>Property name uniqueness <em>if</em> the given field is an
   * {@link IFieldWidget} instance.
   * </ol>
   * @param f the field to verify
   * @param group
   * @throws IllegalArgumentException When the verification fails.
   */
  private static void verifyAddField(final IField f, FieldGroup group) throws IllegalArgumentException {
    assert group != null;
    if(f == null) throw new IllegalArgumentException("No field specified.");
    final boolean isWidget = (f instanceof IFieldWidget<?>);
    for(final IField ef : group) {
      if(f.getName().equals(ef.getName())) {
        throw new IllegalArgumentException("Field name: '" + f.getName() + "' already exists.");
      }
      if(isWidget && (ef instanceof IFieldWidget<?>)) {
        if(((IFieldWidget<?>) f).getPropertyName().equals(((IFieldWidget<?>) ef).getPropertyName())) {
          throw new IllegalArgumentException("Field property name: '" + ((IFieldWidget<?>) f).getPropertyName()
              + "' already exists.");
        }
      }
      if(ef instanceof FieldGroup) {
        verifyAddField(f, (FieldGroup) ef);
      }
    }
  }

  /**
   * Creates a nested path token.
   * @param parents
   * @param field
   * @param includeFirstParent include the first parent in the path?
   * @return the created nested path
   */
  private static String nestedPath(List<IField> parents, IField field, boolean includeFirstParent) {
    final StringBuilder sb = new StringBuilder();
    for(int i = (includeFirstParent ? 0 : 1); i < parents.size(); i++) {
      sb.append(parents.get(i).descriptor());
      sb.append(" - ");
    }
    sb.append(field.descriptor());
    return sb.toString();
  }

  /**
   * Recursive validation routine that populates a list or {@link Error}s.
   * instance and tracks the nesting. Nesting is tracked to assemble a "fully
   * qualified" field better validation feedback.
   * @param errors the sole constant instance
   * @param group the field group
   * @param parents the field group parents
   */
  private static void validate(final ArrayList<Error> errors, FieldGroup group, List<FieldGroup> parents) {
    for(final IField field : group) {
      if(field instanceof FieldGroup) {
        final ArrayList<FieldGroup> list = new ArrayList<FieldGroup>(parents.size() + 1);
        list.addAll(parents);
        list.add(group);
        validate(errors, ((FieldGroup) field), list);
      }
      else {
        try {
          field.validate();
        }
        catch(final ValidationException e) {
          final ArrayList<IField> list = new ArrayList<IField>(parents.size() + 1);
          list.addAll(parents);
          list.add(group);
          for(final Error error : e.getErrors()) {
            error.setTarget(new IWidgetRef() {

              @Override
              public Widget getWidget() {
                return field.getWidget();
              }

              @SuppressWarnings("synthetic-access")
              @Override
              public String descriptor() {
                return nestedPath(list, field, false);
              }
            });
          }
          errors.addAll(e.getErrors());
        }
      }
    }

    if(group.validator != null) {
      try {
        group.validator.validate(null);
      }
      catch(final ValidationException e) {
        for(final Error error : e.getErrors()) {
          error.setTarget(group);
        }
        errors.addAll(e.getErrors());
      }
    }
  }

  /**
   * The required presentation worthy and unique name to ascribe to this group.
   */
  private String name;

  /**
   * The collection of child fields with order preservation.
   */
  private final LinkedHashSet<IField> fields = new LinkedHashSet<IField>();

  /**
   * On ordered collection of commands to be executed before validation happens.
   */
  private ArrayList<Command> preValidationActions;

  /**
   * The field group validator(s).
   */
  private CompositeValidator validator;

  /**
   * The Widget that is used to convey validation feedback.
   */
  private Widget feedbackWidget;

  /**
   * The error handler to employ for validation.
   */
  private IErrorHandler errorHandler;

  /**
   * Constructor
   * @param name The required unique name for this field group.
   */
  public FieldGroup(String name) {
    super();
    setName(name);
  }

  public void addPreValidationAction(Command action) {
    if(preValidationActions == null) {
      preValidationActions = new ArrayList<Command>();
    }
    preValidationActions.add(action);
  }

  public void removePreValidationAction(Command action) {
    if(action != null && preValidationActions != null) {
      preValidationActions.remove(action);
    }
  }

  public void clearPreValidationActions() {
    if(preValidationActions != null) preValidationActions.clear();
  }

  @Override
  public String descriptor() {
    return getName();
  }

  public Iterator<IField> iterator() {
    return fields.iterator();
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    if(StringUtil.isEmpty(name)) {
      throw new IllegalArgumentException("A field group must have a name.");
    }
    this.name = name;
  }

  /**
   * @return The designated Widget to receive validation messages.
   */
  public Widget getWidget() {
    return feedbackWidget;
  }

  /**
   * Sets the Widget to for which validation messages are bound.
   * @param feedbackWidget A Widget designated to be the validation feeback
   *        hook.
   */
  public void setWidget(Widget feedbackWidget) {
    this.feedbackWidget = feedbackWidget;
  }

  /**
   * Recursively searches for a field having the given name.
   * @param nme
   * @return The found field or <code>null</code> if no field exists with the
   *         given name.
   */
  public IField getFieldByName(String nme) {
    return findByName(nme, this);
  }

  /**
   * Recursively searches for a field widget having the given name.
   * @param nme Unique name of the field widget to retrieve
   * @return The found field widget or <code>null</code> if no field widget
   *         exists with the given name.
   */
  public IFieldWidget<?> getFieldWidget(String nme) {
    final IField f = findByName(nme, this);
    return f instanceof IFieldWidget<?> ? (IFieldWidget<?>) f : null;
  }

  /**
   * Recursively searches for a field widget having the given property name.
   * @param propertyName
   * @return The found field or <code>null</code> if it doesn't exist.
   */
  public IFieldWidget<?> getFieldWidgetByProperty(String propertyName) {
    return propertyName == null ? null : findByPropertyName(propertyName, this);
  }

  /**
   * Finds all {@link IFieldWidget}s whose property name matches the given regex
   * prop path token.
   * @param propPath The property path as a regular expression token. If
   *        <code>null</code>, all {@link IFieldWidget}s are included.
   * @return Set of matching fields never <code>null</code> but may be empty
   *         (when no matches found).
   */
  public Set<IFieldWidget<?>> getFieldWidgets(String propPath) {
    final Set<IFieldWidget<?>> set = new HashSet<IFieldWidget<?>>();
    findFieldWidgets(propPath, this, set);
    return set;
  }

  /**
   * Adds a field to this field group.
   * @param field The field to add
   * @throws IllegalArgumentException When this field instance already exists or
   *         another field exists with the same name.
   */
  public void addField(IField field) throws IllegalArgumentException {
    verifyAddField(field, this);
    // NOTE: the field add op should go through based on the verify routine
    if(!fields.add(field)) throw new IllegalStateException();
  }

  /**
   * Adds a field to this field group pre-pending the given parent property path
   * to the field's <em>existing</em> property name.
   * @param parentPropPath Pre-pended to the field's existing property path
   *        before the field is added. May be <code>null</code> in which case
   *        the field's property name remains un-altered.
   * @param field The field to add
   */
  public void addField(String parentPropPath, IField field) {
    setParentPropertyPath(field, parentPropPath);
    addField(field);
  }

  /**
   * Recursively pre-pends the given parent property path to all held field
   * widgets in this group.
   * @param parentPropPath
   */
  public void setParentPropertyPath(String parentPropPath) {
    setParentPropertyPath(this, parentPropPath);
  }

  /**
   * Replaces the parent property paths of all held field widgets.
   * @param existing the existing parent property path to replace
   * @param repl the replacement parent property path
   */
  public void replaceParentPropertyPath(String existing, String repl) {
    replaceParentPropertyPath(this, existing, repl);
  }

  /**
   * Adds multiple fields to this group.
   * @param flds The fields to add
   */
  public void addFields(Iterable<IField> flds) {
    if(flds != null) {
      for(final IField fld : flds) {
        addField(fld);
      }
    }
  }

  /**
   * Adds an array of fields to this group.
   * @param flds The array of fields to add
   */
  public void addFields(IField[] flds) {
    if(flds != null) {
      for(final IField fld : flds) {
        addField(fld);
      }
    }
  }

  /**
   * Adds multiple fields to this group.
   * @param parentPropPath Pre-pended to the each field's property name before
   *        the fields are added. May be <code>null</code> in which case the
   *        fields' property names remain un-altered.
   * @param flds The fields to add
   */
  public void addFields(String parentPropPath, Iterable<IField> flds) {
    if(flds != null) {
      for(final IField fld : flds) {
        addField(parentPropPath, fld);
      }
    }
  }

  /**
   * Adds multiple fields to this group.
   * @param parentPropPath Pre-pended to the each field's property name before
   *        the fields are added. May be <code>null</code> in which case the
   *        fields' property names remain un-altered.
   * @param flds The fields to add
   */
  public void addFields(String parentPropPath, IField[] flds) {
    if(flds != null) {
      for(final IField fld : flds) {
        addField(parentPropPath, fld);
      }
    }
  }

  /**
   * Recursively removes all errors bound to the given field and any and all child fields.
   * @param field the target field
   */
  private void clearErrors(IField field) {
    assert errorHandler != null;
    if(field instanceof FieldGroup) {
      for(final IField fld : ((FieldGroup) field)) {
        clearErrors(fld);
      }
    }
    else {
      errorHandler.resolveError(field, ErrorClassifier.CLIENT, ErrorDisplay.ALL_FLAGS);
    }
  }

  /**
   * Removes a field by reference searching recursively. If the given field is
   * <code>null</code> or is <em>this</em> field group, no field is removed and
   * <code>false</code> is returned.
   * @param field The field to remove.
   * @param clearErrors remove all errors bound to the field?
   * @return <code>true</code> if the field was removed, <code>false</code> if
   *         not.
   */
  public boolean removeField(IField field, final boolean clearErrors) {
    if(field != null && !(field == this)) {
      boolean rval = false;
      for(final IField fld : fields) {
        if(fld == field) {
          if(!fields.remove(field)) throw new IllegalStateException("Unable to remove field: " + field);
          rval = true;
          break;
        }
        else if(fld instanceof FieldGroup) {
          if(((FieldGroup) fld).removeField(field, clearErrors)) {
            rval = true;
            break;
          }
        }
      }
      if(rval && clearErrors && errorHandler != null) {
        // remove all associated errors for the removed field
        clearErrors(field);
      }
      return rval;
    }
    return false;
  }

  /**
   * Removes a collection of fields from this group.
   * @param clc The collection of fields to remove
   * @param clearErrors Remove errors bound to the given fields?
   */
  public void removeFields(Iterable<IField> clc, final boolean clearErrors) {
    if(clc != null) {
      for(final IField fld : clc) {
        removeField(fld, clearErrors);
      }
    }
  }

  @Override
  public void applyPropertyMetadata(IPropertyMetadataProvider provider, boolean isNewModelData) {
    for(final IField f : fields) {
      f.applyPropertyMetadata(provider, isNewModelData);
    }
  }

  @Override
  public boolean isRequired() {
    for(final IField field : fields) {
      if(field.isRequired()) return true;
    }
    return false;
  }

  @Override
  public void setRequired(boolean required) {
    for(final IField field : fields) {
      field.setRequired(required);
    }
  }

  @Override
  public boolean isReadOnly() {
    for(final IField field : fields) {
      if(!field.isReadOnly()) return false;
    }
    return true;
  }

  /**
   * Iterates over the child fields, setting their readOnly property.
   * @param readOnly true/false
   */
  @Override
  public void setReadOnly(boolean readOnly) {
    for(final IField field : fields) {
      field.setReadOnly(readOnly);
    }
  }

  @Override
  public boolean isEnabled() {
    for(final IField field : fields) {
      if(!field.isEnabled()) return false;
    }
    return true;
  }

  @Override
  public void setEnabled(boolean enabled) {
    for(final IField field : fields) {
      field.setEnabled(enabled);
    }
  }

  @Override
  public boolean isVisible() {
    for(final IField field : fields) {
      if(field.isVisible()) return true;
    }
    return false;
  }

  @Override
  public void setVisible(boolean visible) {
    for(final IField field : fields) {
      field.setVisible(visible);
    }
  }

  @Override
  public void clearValue() {
    for(final IField f : fields) {
      f.clearValue();
    }
  }

  /**
   * Removes all child fields from this group.
   */
  public void clear() {
    fields.clear();
  }

  @Override
  public void reset() {
    for(final IField f : fields) {
      f.reset();
    }
  }

  @Override
  public void validateIncrementally(boolean validate) {
    for(final IField f : fields) {
      f.validateIncrementally(validate);
    }
  }

  /**
   * @return The number of child fields.
   */
  public int size() {
    return fields.size();
  }

  public void addValidator(IValidator vldtr) {
    if(vldtr != null) {
      if(this.validator == null) {
        this.validator = new CompositeValidator();
      }
      this.validator.add(vldtr);
    }
  }

  public void removeValidator(Class<? extends IValidator> type) {
    if(validator != null) this.validator.remove(type);
  }

  public void validate() throws ValidationException {
    if(preValidationActions != null) {
      for(final Command action : preValidationActions) {
        action.execute();
      }
    }
    final ArrayList<Error> errors = new ArrayList<Error>();
    validate(errors, this, new ArrayList<FieldGroup>());
    if(errors.size() > 0) {
      throw new ValidationException(errors);
    }
  }

  @Override
  public IErrorHandler getErrorHandler() {
    return errorHandler;
  }

  @Override
  public void setErrorHandler(IErrorHandler errorHandler) {
    this.errorHandler = errorHandler;
    for(final IField f : fields) {
      f.setErrorHandler(errorHandler);
    }
  }

  @Override
  public boolean equals(Object obj) {
    if(this == obj) return true;
    if(obj == null) return false;
    if(getClass() != obj.getClass()) return false;
    final FieldGroup other = (FieldGroup) obj;
    assert name != null;
    return name.equals(other.name);
  }

  @Override
  public int hashCode() {
    assert name != null;
    return name.hashCode();
  }

  @Override
  public String toString() {
    return "FieldGroup[" + name + ']';
  }
}
TOP

Related Classes of com.tll.client.ui.field.FieldGroup

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.