package org.tessell.model.properties;
import static org.tessell.model.properties.NewProperty.booleanProperty;
import static org.tessell.util.ObjectUtils.eq;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.tessell.model.events.PropertyChangedEvent;
import org.tessell.model.events.PropertyChangedHandler;
import org.tessell.model.properties.Upstream.Capture;
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.Required;
import org.tessell.model.validation.rules.Rule;
import org.tessell.model.validation.rules.Static;
import org.tessell.model.values.DerivedValue;
import org.tessell.model.values.LambdaValue;
import org.tessell.model.values.Value;
import org.tessell.util.Inflector;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.*;
import com.google.gwt.event.shared.GwtEvent.Type;
/** Provides most of the validation/derived/etc. implementation guts of {@link Property}. */
public abstract class AbstractProperty<P, T extends AbstractProperty<P, T>> extends AbstractAbstractProperty<P> {
public static int outstandingSetInitials = 0;
public static int outstandingTouchFalses = 0;
private static final Logger log = Logger.getLogger("org.tessell.model");
// handlers
private final EventBus handlers = new SimplerEventBus();
// other properties that are validated off of our value
protected final ArrayList<Downstream> downstream = new ArrayList<Downstream>();
// rules that validate against our value and fire against our handlers
private final ArrayList<Rule<? super P>> rules = new ArrayList<Rule<? super P>>();
// outstanding errors
private final Map<Object, String> errors = new LinkedHashMap<Object, String>();
// our wrapped value
private Value<P> value;
// snapshot of the value for diff purposes (e.g. derived values)
private P lastValue;
// what we should use for null
private P defaultValue;
// the initial value
private P initialValue;
// whether the user has touched this field on the screen yet
private boolean touched;
// whether this property is required
private boolean required;
// the result of the last validate()
private boolean valid = true;
// whether we're currently reassessing
private boolean reassessing = false;
// only used if this is a derived value
private UpstreamState lastUpstream;
// only used if showing a temporary error
private Static temporaryRule = null;
// only used if someone calls .valid()
private Property<Boolean> validProperty;
// only used if someone calls .touched()
private Property<Boolean> touchedProperty;
// only used if someone calls .changed()
private Property<Boolean> changedProperty;
protected AbstractProperty() {
// the subclass should call initialize as soon as possible
}
public AbstractProperty(final Value<P> value) {
initializeValue(value);
}
/**
* Basically the constructor, but separate so that {@link DerivedProperty}
* can pass a DerivedValue anonymous class that refers to itself.
*/
protected void initializeValue(final Value<P> value) {
this.value = value;
lastValue = copyLastValue(getWithUpstreamTracking());
initialValue = copyLastValue(lastValue);
RuleHandler ruleHandler = new RuleHandler();
addRuleTriggeredHandler(ruleHandler);
addRuleUntriggeredHandler(ruleHandler);
// Fixes people calling NewProperty.booleanProperty(otherBooleanProperty).not(); I'm
// not entirely sure this is a good idea, but it passes the test.
if (value instanceof Property) {
((Property<?>) value).addDerived(this);
}
}
@SuppressWarnings("unchecked")
public void setTemporaryError(String temporaryErrorMessage) {
// Automatically touch so that the temporary error actually shows up
setTouched(true);
if (temporaryRule == null) {
temporaryRule = new Static(temporaryErrorMessage);
temporaryRule.setProperty((Property<Object>) this);
rules.add(0, temporaryRule);
temporaryRule.set(false);
} else {
// bounce the rule to retrigger it with the new message
temporaryRule.set(true);
temporaryRule.setMessage(temporaryErrorMessage);
temporaryRule.set(false);
}
}
public void clearTemporaryError() {
clearTemporaryError(true);
}
@Override
public HandlerRegistration addPropertyChangedHandler(final PropertyChangedHandler<P> handler) {
return addHandler(PropertyChangedEvent.getType(), handler);
}
@Override
public HandlerRegistration addRuleTriggeredHandler(final RuleTriggeredHandler handler) {
return addHandler(RuleTriggeredEvent.getType(), handler);
}
@Override
public HandlerRegistration addRuleUntriggeredHandler(final RuleUntriggeredHandler handler) {
return addHandler(RuleUntriggeredEvent.getType(), handler);
}
@Override
public P get() {
Upstream.addIfTracking(this);
return getWithUpstreamTracking();
}
@Override
public boolean isReadOnly() {
return value.isReadOnly();
}
@Override
public void set(final P value) {
set(value, true);
}
@Override
public void set(final P value, boolean shouldTouch) {
if (!shouldTouch) {
try {
outstandingTouchFalses++;
this.value.set(copyLastValue(value));
reassess();
} finally {
outstandingTouchFalses--;
}
} else {
this.value.set(copyLastValue(value));
if (!touched && !reassessing && !isWithinASetInitial() && !isWithinASetTouchFalse()) {
// even if unchanged, treat this as touching
setTouched(true);
} else {
reassess();
}
}
}
@Override
public void setInitialValue(final P value) {
try {
outstandingSetInitials++;
this.value.set(defaultIfNull(copyLastValue(value)));
reassess();
} finally {
outstandingSetInitials--;
}
}
@Override
public void setDefaultValue(final P value) {
if (isReadOnly()) {
throw new IllegalStateException(this + " is read only");
}
this.defaultValue = value;
setIfNull(value);
}
@Override
public void reassess() {
try {
reassessing = true;
P newValue = get();
// watch for out-of-band changes, e.g. model.merge(newDto);
if (newValue == null && defaultValue != null) {
value.set(defaultValue);
lastValue = newValue; // so that we detect/fire change
newValue = defaultValue;
}
final P oldValue = lastValue;
final boolean valueChanged = !eq(lastValue, newValue);
if (valueChanged) {
lastValue = copyLastValue(newValue);
if (isWithinASetInitial()) {
initialValue = copyLastValue(newValue);
}
}
// run validation before firing change so handlers see latest wasValid
final boolean oldValid = valid;
validate();
final boolean validChanged = oldValid != valid;
// only reassess downstream if needed. this is somewhat odd, but we reassess
// our downstream properties before firing our own change event. this is so
// that if someone listening to us is also going to check a downstream
// property's state, it would be good for them to be up to date
if (valueChanged || validChanged) {
for (final Downstream other : new ArrayList<Downstream>(downstream)) {
other.property.reassess();
}
}
if (valueChanged) {
clearTemporaryError(false);
if (changedProperty != null) {
changedProperty.set(!eq(newValue, initialValue));
}
fireChanged(oldValue, newValue);
}
} finally {
reassessing = false;
}
}
/** Allow subclasses to deep copy values if needed. */
protected P copyLastValue(P newValue) {
return newValue;
}
/**
* Track {@code other} as derived on us, so we'll forward changed/changing events to it.
*
* This is somewhat esoteric, but if you have property A and property B, B can depend on A
* (be downstream/"derived") for (at least) two reasons:
*
* 1. Because B's value is inherently based on A
* 2. Because B has a validation rule that is based on A
*
* For the 1st case, if A becomes touched, we also mark B as touched (so we pass percolateTouch=true).
*
* However, for the 2nd case, if A becomes touched, but B isn't really based on A, but instead just uses it
* for a validation rule, then we don't want to mark B as touched (so we pass percolateTouch=false).
*
* @param other the property that depends on us
* @param token a token that can be used to revoke the derivation (see {@link #removeDerived(Property, Object)}
* @param percolateTouch whether we should percolate our touched state to {@code other}
*/
@Override
public <P1 extends Property<?>> P1 addDerived(P1 other, Object token, boolean percolateTouch) {
Downstream d = findDownstreamOrNull(other);
if (d != null) {
d.tokens.add(token);
// upgrade an existing non-touch to touch
if (!d.touch && percolateTouch) {
d.touch = true;
if (touched) {
other.setTouched(touched);
}
}
} else {
d = new Downstream(other, percolateTouch);
d.tokens.add(token);
downstream.add(d);
if (percolateTouch && touched) {
other.setTouched(touched);
}
}
return other;
}
@Override
public <P1 extends Property<?>> P1 removeDerived(final P1 other, final Object token) {
Downstream d = findDownstreamOrNull(other);
if (d != null) {
d.tokens.remove(token);
if (d.tokens.size() == 0) {
downstream.remove(d);
}
}
return other;
}
@Override
public String toString() {
return value.toString();
}
// fluent method of touch + valid
public boolean touch() {
setTouched(true);
return isValid();
}
@SuppressWarnings("unchecked")
@Override
public void addRule(final Rule<? super P> rule) {
if (rules.contains(rule)) {
return;
}
if (rule.isImportant()) {
rules.add(0, rule);
} else {
rules.add(rule);
}
((Rule<P>) rule).setProperty(this);
reassess();
}
@Override
public void fireEvent(final GwtEvent<?> event) {
if (log.isLoggable(Level.FINEST)) {
log.finest(this + " firing " + event);
}
handlers.fireEventFromSource(event, this);
}
@Override
public T depends(final Property<?>... upstream) {
for (final Property<?> other : upstream) {
other.addDerived(this);
}
return getThis();
}
@Override
public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<P> handler) {
// translate PropertyChangedEvents to ValueChangedEvents
return addPropertyChangedHandler(new PropertyChangedHandler<P>() {
public void onPropertyChanged(PropertyChangedEvent<P> event) {
// need an inner class because ValueChangedEvent's cstr is protected
handler.onValueChange(new ValueChangeEvent<P>(event.getNewValue()) {
});
}
});
}
@Override
public HandlerRegistration nowAndOnChange(final PropertyValueHandler<P> handler) {
HandlerRegistration hr = addPropertyChangedHandler(new PropertyChangedHandler<P>() {
public void onPropertyChanged(PropertyChangedEvent<P> event) {
handler.onValue(event.getNewValue());
}
});
handler.onValue(get());
return hr;
}
@Override
public boolean isTouched() {
Upstream.addIfTracking(this);
return touched;
}
@Override
public void setTouched(final boolean touched) {
clearTemporaryError(false);
if (this.touched == touched) {
return;
}
this.touched = touched;
if (this.touchedProperty != null) {
this.touchedProperty.set(touched);
}
for (final Downstream other : new ArrayList<Downstream>(downstream)) {
if (other.touch) {
other.property.setTouched(touched);
}
}
reassess();
}
@Override
public boolean isRequired() {
return required;
}
@Override
public void setRequired(final boolean required) {
this.required = required;
}
@Override
public boolean isValid() {
Upstream.addIfTracking(this);
return valid;
}
@Override
public Property<Boolean> valid() {
if (validProperty == null) {
validProperty = booleanProperty(value.getName() + ".valid", valid);
}
return validProperty;
}
@Override
public Property<Boolean> touched() {
if (touchedProperty == null) {
touchedProperty = booleanProperty(value.getName() + ".touched", touched);
}
return touchedProperty;
}
@Override
public Property<Boolean> changed() {
if (changedProperty == null) {
changedProperty = booleanProperty(value.getName() + ".changed", !eq(get(), initialValue));
}
return changedProperty;
}
@Override
public P getValue() {
return get();
}
@Override
public void setValue(P value) {
set(value);
}
@Override
public void setValue(P value, boolean fire) {
// we always fire
set(value);
}
public T req() {
addRule(new Required());
return getThis();
}
public T in(final PropertyGroup group) {
group.add(this);
return getThis();
}
@Override
public String getName() {
return Inflector.humanize(value.getName());
}
@Override
public String getValueName() {
return value.getName();
}
@Override
public Map<Object, String> getErrors() {
return errors;
}
@Override
public void setIfNull(P value) {
if (get() == null) {
setInitialValue(value);
}
}
protected Value<P> getValueObject() {
return value;
}
protected abstract T getThis();
protected <H extends EventHandler> HandlerRegistration addHandler(Type<H> type, H handler) {
return handlers.addHandlerToSource(type, this, handler);
}
protected void fireChanged(P oldValue, P newValue) {
fireEvent(new PropertyChangedEvent<P>(this, oldValue, newValue));
}
/** Runs validation against our rules. */
private void validate() {
boolean oldValid = valid;
valid = true; // start out valid
for (final Rule<? super P> rule : rules) {
if (rule.validate()) {
rule.untriggerIfNeeded();
} else {
// only trigger the first invalid rule
if (valid) {
valid = false;
if (isTouched()) {
rule.triggerIfNeeded();
} else {
rule.untriggerIfNeeded();
}
} else {
rule.untriggerIfNeeded();
}
}
}
if (oldValid != valid && validProperty != null) {
validProperty.set(valid);
}
}
private P getWithUpstreamTracking() {
// this logic should probably go in DerivedValue somehow, except that
// it's only a value and does not know about it's parent property
if (value instanceof DerivedValue || value instanceof LambdaValue) {
if (lastUpstream == null) {
lastUpstream = new UpstreamState(this, true);
}
Capture c = Upstream.start();
P tempValue = value.get();
lastUpstream.update(c.finish());
return tempValue;
} else {
return value.get();
}
}
private Downstream findDownstreamOrNull(Property<?> other) {
for (Downstream downstream : this.downstream) {
if (downstream.property == other) {
return downstream;
}
}
return null;
}
private P defaultIfNull(P value) {
return (value == null) ? defaultValue : value;
}
private void clearTemporaryError(boolean resetTouched) {
if (temporaryRule != null && !temporaryRule.isValid()) {
temporaryRule.set(true);
}
if (resetTouched && touched) {
setTouched(false);
}
}
private static boolean isWithinASetInitial() {
return outstandingSetInitials > 0;
}
private static boolean isWithinASetTouchFalse() {
return outstandingTouchFalses > 0;
}
/** Remembers rules fired against us. */
private class RuleHandler implements RuleTriggeredHandler, RuleUntriggeredHandler {
public void onUntrigger(RuleUntriggeredEvent event) {
errors.remove(event.getKey());
}
public void onTrigger(RuleTriggeredEvent event) {
errors.put(event.getKey(), event.getMessage());
}
}
/** Wrapper for tracking downstream properties, plus whether we should touch them. */
static class Downstream {
final Property<?> property;
// list of what objects (e.g. UpstreamState) requested this downstream
final List<Object> tokens = new ArrayList<Object>();
boolean touch;
private Downstream(Property<?> property, boolean touch) {
this.property = property;
this.touch = touch;
}
}
}