package org.tessell.model.properties;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.tessell.model.validation.events.RuleTriggeredEvent;
import org.tessell.model.validation.events.RuleTriggeredHandler;
import org.tessell.model.validation.events.RuleUntriggeredEvent;
import org.tessell.model.validation.events.RuleUntriggeredHandler;
import org.tessell.model.validation.rules.Custom;
import org.tessell.model.values.SetValue;
import com.google.gwt.event.shared.HandlerRegistration;
/** Groups a set of {@link Property}s together. */
public class PropertyGroup extends AbstractProperty<Boolean, PropertyGroup> {
// All of the properties in this group
private final ArrayList<PropertyWithHandlers> properties = new ArrayList<PropertyWithHandlers>();
// Any outstanding errors from properties in this group
private final ArrayList<PropertyError> invalid = new ArrayList<PropertyError>();
private Snapshot snapshot;
public PropertyGroup(final String name) {
super(new SetValue<Boolean>(name, true));
// add a rule that fires whenever we're false (and touched)
addRule(new Custom(name + " invalid", this));
// We always want to consider ourselves "touched", so that our
// error message fires right away, and the UI/parent property
// group can see it. However, we don't want to touch our children
// right away, we only do that when the client calls setTouched.
super.setTouched(true);
}
@Override
public void reassess() {
((SetValue<Boolean>) getValueObject()).set(invalid.size() == 0);
super.reassess();
}
/** Adds properties to the group to validate as a group. */
public void add(final Property<?>... properties) {
for (final Property<?> property : properties) {
this.properties.add(new PropertyWithHandlers(property));
for (Map.Entry<Object, String> e : property.getErrors().entrySet()) {
invalid.add(new PropertyError(property, e.getKey()));
}
}
reassess();
}
/** Removes {@code property} from the group. */
public void remove(Property<?> property) {
for (Iterator<PropertyWithHandlers> i = properties.iterator(); i.hasNext();) {
PropertyWithHandlers pwd = i.next();
if (pwd.property == property) {
pwd.removeHandlers();
i.remove();
}
}
for (Iterator<PropertyError> i = invalid.iterator(); i.hasNext();) {
if (i.next().property == property) {
i.remove();
}
}
reassess();
}
public ArrayList<Property<?>> getProperties() {
ArrayList<Property<?>> properties = new ArrayList<Property<?>>();
for (PropertyWithHandlers pwh : this.properties) {
properties.add(pwh.property);
}
return properties;
}
@Override
public void setTouched(final boolean touched) {
for (final PropertyWithHandlers other : new ArrayList<PropertyWithHandlers>(properties)) {
other.property.setTouched(touched);
}
// Per comment in the constructor, we don't actually want
// to toggle our touched state, it should always be true.
// So don't call super, but copy/paste some of it's logic
// here to be at least somewhat consistent.
// super.setTouched(touched);
for (final Downstream other : downstream) {
if (other.touch) {
other.property.setTouched(touched);
}
}
reassess();
}
public void capture() {
Snapshot s = new Snapshot();
for (Property<?> p : getProperties()) {
s.save(p);
}
snapshot = s;
}
public void restore() {
for (Property<?> p : getProperties()) {
snapshot.restore(p);
}
}
public void reassessAll() {
for (Property<?> p : getProperties()) {
p.reassess();
}
}
@Override
protected PropertyGroup getThis() {
return this;
}
/** Remembers the state of all of the properties in our group so that we can roll back when needed (e.g. on cancel). */
private static class Snapshot {
private final Map<Property<?>, Object> state = new LinkedHashMap<Property<?>, Object>();
private <P> void save(Property<P> p) {
state.put(p, p.get());
}
@SuppressWarnings("unchecked")
private <P> void restore(Property<P> p) {
p.set((P) state.get(p));
}
}
/** Holds a property + its handler registrations (in case we have to remove it). */
private class PropertyWithHandlers {
private final Property<?> property;
private final HandlerRegistration triggered;
private final HandlerRegistration untriggered;
private PropertyWithHandlers(final Property<?> property) {
this.property = property;
triggered = property.addRuleTriggeredHandler(new RuleTriggeredHandler() {
public void onTrigger(final RuleTriggeredEvent event) {
invalid.add(new PropertyError(property, event.getKey()));
reassess();
}
});
untriggered = property.addRuleUntriggeredHandler(new RuleUntriggeredHandler() {
public void onUntrigger(final RuleUntriggeredEvent event) {
for (Iterator<PropertyError> i = invalid.iterator(); i.hasNext();) {
if (i.next().key.equals(event.getKey())) {
i.remove();
}
}
reassess();
}
});
}
private void removeHandlers() {
triggered.removeHandler();
untriggered.removeHandler();
}
@Override
public String toString() {
return property.toString();
}
}
/** Holds a rule triggered key + the property that caused it (in case we have to remove it). */
private class PropertyError {
private final Property<?> property;
private final Object key;
private PropertyError(Property<?> property, Object key) {
this.property = property;
this.key = key;
}
}
}