package org.tessell.model.commands;
import static java.lang.Boolean.FALSE;
import static org.tessell.model.properties.NewProperty.booleanProperty;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.tessell.model.properties.BooleanProperty;
import org.tessell.model.properties.HasRuleTriggers;
import org.tessell.model.properties.Property;
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 com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.event.shared.SimplerEventBus;
/**
* Codifies a UI action that has an enabled state and optional validation rules
* that must pass before it can execute.
*
* When {@link #execute()} is called, any dependent properties will be touched
* to potentially trigger validation rules. If any of them fail validation or
* return false, the execute is skipped.
*/
public abstract class UiCommand implements HasRuleTriggers, Runnable {
private final BooleanProperty enabled = booleanProperty("enabled", true);
private final EventBus handlers = new SimplerEventBus();
private final Map<String, HasHandlers> errors = new HashMap<String, HasHandlers>();
private final List<Property<Boolean>> onlyIf = new ArrayList<Property<Boolean>>();
/**
* Executes the UI command, first triggering any "only if" validation,
* and then calling {@code doExecute} if they all pass.
*
* @throws IllegalStateException if the command is disabled
*/
public final void execute() {
if (!enabled.isTrue()) {
throw new IllegalStateException("Command is disabled");
}
clearErrors();
if (canExecute()) {
doExecute();
}
}
/** An alias for {@link #execute()}. */
public void run() {
execute();
}
/**
* Adds a property that conditionalizes whether this command can be executed.
*
* {@code onlyIf} will be touched on {@link #execute()} and the execution
* skipped if it is invalid or returns {@code false}.
*/
public void addOnlyIf(Property<Boolean> onlyIf) {
this.onlyIf.add(onlyIf);
// TODO onlyIf.addDerived()
}
public void removeOnlyIf(Property<Boolean> onlyIf) {
this.onlyIf.remove(onlyIf);
}
/** Fires an error message against this command's handlers. */
public void error(String message) {
error(handlers, message);
}
/** Fires an error message against the {@code errorTarget}'s handlers. */
public void error(HasHandlers errorTarget, String message) {
if (errors.containsKey(message)) {
return;
}
errors.put(message, errorTarget);
errorTarget.fireEvent(new RuleTriggeredEvent(message, message, new Boolean[] { false }));
}
/**
* Clears any errors that were fired against this instance (by {@link #error(String)).
*
* Note that this is automatically called by {@link #execute()} so that any now-invalid
* messages are removed before the rules are rerun.
*/
public void clearErrors() {
for (Map.Entry<String, HasHandlers> e : errors.entrySet()) {
e.getValue().fireEvent(new RuleUntriggeredEvent(e.getKey(), e.getKey()));
}
errors.clear();
}
/**
* @return the property of whether this command is enabled or not
*/
public Property<Boolean> enabled() {
return enabled;
}
@Override
public HandlerRegistration addRuleTriggeredHandler(RuleTriggeredHandler handler) {
return handlers.addHandler(RuleTriggeredEvent.getType(), handler);
}
@Override
public HandlerRegistration addRuleUntriggeredHandler(RuleUntriggeredHandler handler) {
return handlers.addHandler(RuleUntriggeredEvent.getType(), handler);
}
/**
* @return the list of only if conditions; purposefully modifiable so it can be bound against
*/
public List<Property<Boolean>> getOnlyIf() {
return onlyIf;
}
/**
* Method for subclasses to perform the command's logic once the validation rules have passed.
*/
protected abstract void doExecute();
/** @return {@code true} if each onlyIf property, after touching, is valid. */
private boolean canExecute() {
boolean allValid = true;
for (Property<Boolean> p : onlyIf) {
if (!p.touch() || FALSE.equals(p.get())) {
allValid = false; // purposefully do not early return
}
}
return allValid;
}
}