Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.

Bug 124403

Summary: [DataBinding] model restrictions
Product: [Eclipse Project] Platform Reporter: Mario Van Steenberghe <mario.vansteenberghe>
Component: UIAssignee: Matthew Hall <qualidafial>
Status: RESOLVED WONTFIX QA Contact:
Severity: enhancement    
Priority: P3 CC: bokowski, gmendel, qualidafial
Version: 3.2   
Target Milestone: ---   
Hardware: PC   
OS: All   
Whiteboard:
Bug Depends on: 120582    
Bug Blocks:    

Description Mario Van Steenberghe CLA 2006-01-18 17:24:02 EST
Hi, 

I have been working an a similar databinding framework for about half a year now, and would like to switch yours in the future. I have been browsing through your code and the good thing is that there are a lot of similarities with what I have, but some very important features are still missing.

Could you please give me your feedback about the following:

I would like to see a way to add restrictions to the model that are automatically validated by the framework.

For example, let's consider the following simplified model object:

Person
- firstName
- lastName
- age
- birthDate

On this model object, we could apply the following restrictions:

Person
- firstName (maxLen="20", mandatory="true")
- lastName  (maxLen="40", mandatory="true")
- age       (minVal="0", mandatory="false")
- birthDate (mask="dd/mm/yy")

When a restriction fails, the concerned fields should be marked, by a red background, a red border, ...

I've been playing a bit with the existing framework and I came up with the following solution:

public class RestrictionRegistry {

  private static RestrictionRegistry instance;
  private final Map restrictions = new HashMap();

  public static RestrictionRegistry getDefault() {
    if (instance == null)
      instance = new RestrictionRegistry();
      return instance;
    }
	
  public void associate(final Class modelType, final String property, final IRestriction restriction) {		
    getRestrictionSet(modelType, property).add(restriction);		
  }
	
  public void associate(final Class modelType, final String property, final IRestriction[] prestrictions) {
    final Set restrictionSet = getRestrictionSet(modelType, property);
    for (int i=0; i < prestrictions.length; i++) {
      restrictionSet.add(prestrictions[i]);
    }
  }
	
  public IRestriction[] getRestrictions(final Class modelType, final String property) {
    final Set restrictionSet = getRestrictionSet(modelType, property);
    return (IRestriction[]) restrictionSet.toArray(new IRestriction[restrictionSet.size()]);
  }
	
  private Set getRestrictionSet(final Class modelType, final String property) {
    Map restrictionsByProperty = (Map) this.restrictions.get(modelType);
    if (restrictionsByProperty == null) {
      restrictionsByProperty = new HashMap();
      this.restrictions.put(modelType, restrictionsByProperty);
    }
		
    Set restrictionSet = (Set) restrictionsByProperty.get(property);
    if (restrictionSet == null) {
      restrictionSet = new HashSet();
      restrictionsByProperty.put(property, restrictionSet);
    }

    return restrictionSet;
  }
}

public interface IRestriction {	
  String validate(final Object value);
  String getId();
}

public class DoubleMaxValueRestriction extends Restriction {
  public static final String ID = DoubleMaxValueRestriction.class.getName(); 
  private final double maxValue;
	
  public DoubleMaxValueRestriction(final double pmaxValue) {
    super(ID);
    this.maxValue = pmaxValue;
  }
	
  public double getMaxValue() {
    return this.maxValue;
  }

  public String validate(final Object value) {		
    String msg = null;
    final Double dval = (Double) value;
    if (dval != null && dval.doubleValue() > this.maxValue)
      msg = "max allowed value for this field is: " + this.maxValue;
    return msg;
  }  
}

Restrictions can be defined programatically as follows:

RestrictionRegistry.getDefault().associate(Employee.class, "salary", new DoubleMaxValueRestriction(10000));

Or through an XML file:

<class name="org.eclipse.databinding.sample.model.Employee">
  <property name="salary">
    <maxValue>10000</maxValue>
  </property>
</class>

The thing I'm not so sure about is where to properly integrate this restriction mechanism into the framework. Perhaps it should be achieved by the validator, but then this validator doesn't know anything about the attached model objects, except for its value.

So, for now I've created a custom 'RestrictableBeanUpdatableFactory', which returns RestrictableJavaBeanUpdatableValue objects.

The RestrictableJavaBeanUpdatableValue is very much like the JavaBeanUpdatableValue, only that it will validate restrictions before the model is updated:

public void setValue(final Object value) {
  this.updating = true;
  try {			
    final String errorMsg = validateRestrictions(value);
    if (errorMsg != null)
      throw new RestrictionException("restriction failure: " + errorMsg);
    final Object oldValue = getValue();
    final Method writeMethod = this.propertyDescriptor.getWriteMethod();
    if (!writeMethod.isAccessible()) {
      writeMethod.setAccessible(true);
    }
    writeMethod.invoke(this.object, new Object[] { value });
    fireChangeEvent(ChangeEvent.CHANGE, oldValue, getValue());
  } 
  catch (Exception e) {
    throw new RuntimeException(e);
  } 
  finally {
    this.updating = false;
  }
}

private String validateRestrictions(final Object value) {
  String msg = null;
  final IRestriction[] restrictions = RestrictionRegistry.getDefault().getRestrictions(getModelType(), this.propertyDescriptor.getName());
  for (int i=0; msg == null && restrictions != null && i < restrictions.length; i++) {
    msg = restrictions[i].validate(value);
  }
  return msg;
}

Now, the exception is propagated to the Binding object, where it should be communicated to a central error manager. 

Custom error handlers can then be implemented to listen to events from the error manager to color fields red for example.
Comment 1 Dave Orme CLA 2006-02-08 11:34:31 EST
Have you looked at the IBinding that is returned by the IDataBindingContext and how you can hook up a binding event listener to that?  In our application, that is how we hook in business model validation.

Also, I think the IDomainValidator code that landed last night might help you here.
Comment 2 Matthew Hall CLA 2009-01-21 21:21:05 EST
Mario, most of what you suggest can be accomplished by using update strategies and custom validators.

The only thing we would not support is a restriction registry.  The problem with centralized registries is that you have one place where the behavior of the entire system can be altered, and this has the potential to introduce bugs that are hard to track down.  However if this pattern works in your case there is nothing stopping you from implementing an IValidator that delegates validation to the restriction registry, and using that validator in your bindings.
Comment 3 Matthew Hall CLA 2009-01-21 21:22:41 EST
WONTFIX the suggestion for a restriction registry.  The other features can be implemented using IValidators so consider those as WORKSFORME.  :)