package fr.openwide.core.wicket.more.markup.html.form;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.AjaxRequestTarget.AbstractListener;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.visit.ClassVisitFilter;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitFilter;
import org.apache.wicket.util.visit.IVisitor;
import org.apache.wicket.util.visit.Visits;
import org.wicketstuff.wiquery.core.events.StateEvent;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import fr.openwide.core.commons.util.functional.SerializablePredicate;
import fr.openwide.core.wicket.more.condition.Condition;
import fr.openwide.core.wicket.more.markup.html.form.observer.IFormComponentChangeObserver;
import fr.openwide.core.wicket.more.markup.html.form.observer.impl.FormComponentChangeAjaxEventBehavior;
import fr.openwide.core.wicket.more.util.model.Detachables;
import fr.openwide.core.wicket.more.util.visit.VisitFilters;
/**
* Performs abstract actions on the attached component according to the actual, client-side content of a given {@link FormComponent}.<br>
* This Behavior takes into account the fact that, in some cases, the model of a FormComponent may not reflect
* the actual value entered by the user on the client side (especially when form validation fails).<br>
* Upon the attached component's {@link #onConfigure(Component) configuration}, the Behavior perform one of two set of (abstract) actions
* based on the current value for the <code>prerequisiteField</code>, which is the {@link {@link FormComponent#getConvertedInput() converted input}
* if there was a user input, or the {@link FormComponent#getModelObject() model object} otherwise.<br>
* <br>
* This Behavior will automatically add an {@link AjaxEventBehavior} to the <code>prerequisiteField</code> to enable Ajax
* updates on the attached component when the <code>prerequisiteField</code> {@link StateEvent#CHANGE changes}. You can override {@link #getAjaxTarget(Component)}
* in order to specify which component must be added to the {@link AjaxRequestTarget target}
* (this can be useful for example when the attached component is a Select2 drop-down choice).<br>
* <br>
* The <code>prerequisiteField</code> model object is guaranteed NOT to be updated upon client-side ajax calls.
* If this is needed in your case, this Behavior is clearly overkill for you, since you do not need to inspect the <code>prerequisiteField</code>'s input.<br>
* <br>
* The sets of actions that will be performed upon the attached component's configuration are the following (each one can be overriden) :
* <ul>
* <li>{@link #setUpAttachedComponent(Component)} when {@link #shouldSetUpAttachedComponent()} is true and
* {@link #isConvertedInputSatisfyingRequirements(FormComponent, Object)} or {@link #isCurrentModelSatisfyingRequirements(FormComponent, IModel)}
* is true (which one is called depends on whether there was a user input or not).
* <li>{@link #cleanDefaultModelObject(Component)} followed by {@link #takeDownAttachedComponent(Component)} otherwise
* </ul>
*
*
* @param <T> The model object type of the <code>prerequisiteField</code>.
*/
public abstract class AbstractAjaxInputPrerequisiteBehavior<T> extends Behavior {
private static final long serialVersionUID = 4689707482303046984L;
private final FormComponent<T> prerequisiteField;
private final IFormComponentChangeObserver observer = new IFormComponentChangeObserver() {
private static final long serialVersionUID = 1L;
@Override
public void onChange(AjaxRequestTarget target) {
AbstractAjaxInputPrerequisiteBehavior.this.onPrerequisiteFieldChange(target);
}
};
private final Collection<Component> attachedComponents = Lists.newArrayList();
private Condition defaultWhenPrerequisiteInvisibleCondition = Condition.alwaysTrue();
private boolean useWicketValidation = false;
private boolean updatePrerequisiteModel = false;
private Predicate<? super T> resetAttachedModelPredicate = Predicates.alwaysFalse();
private Predicate<? super T> resetAttachedFormComponentsPredicate = Predicates.alwaysFalse();
/**
* @deprecated This should disappear soon, along with the {@link #cleanDefaultModelObject(Component)} method.
*/
@Deprecated
private boolean resetAttachedModelOnConfigure = true;
private boolean refreshParent = false;
private final Collection<AbstractListener> onChangeListeners = Lists.newArrayList();
private Predicate<? super T> objectValidPredicate = Predicates.notNull();
private Condition forceSetUpConditon = null;
private Condition forceTakeDownConditon = null;
private Resetter resetter = new DefaultResetter();
public AbstractAjaxInputPrerequisiteBehavior(FormComponent<T> prerequisiteField) {
super();
Args.notNull(prerequisiteField, "prerequisiteField");
this.prerequisiteField = prerequisiteField;
}
@Override
public void detach(Component component) {
super.detach(component);
Detachables.detach(
defaultWhenPrerequisiteInvisibleCondition,
forceSetUpConditon,
forceTakeDownConditon
);
}
/**
* Sets whether the attached component should be set up (when condition is <code>true</code>) or taken down (when condition
* is <code>false</code>) when the prerequisiteField is invisible.
* <p>Default is <code>Condition.alwaysTrue()</code>.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setDefaultWhenPrerequisiteInvisible(Condition setUpCondition) {
this.defaultWhenPrerequisiteInvisibleCondition = setUpCondition;
return this;
}
/**
* Sets whether the attached component should be set up (<code>true</code>) or taken down (<code>false</code>)
* when the prerequisiteField is invisible.
* <p>Default is <code>true</code>.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setDefaultWhenPrerequisiteInvisible(boolean defaultToSetUp) {
this.defaultWhenPrerequisiteInvisibleCondition = Condition.constant(defaultToSetUp);
return this;
}
/**
* Sets whether wicket validation ({@link FormComponent#validate()} method) is to be taken into account before
* trying to apply the {@code #setObjectValidPredicate(Predicate) objectValidPredicate}.
* <p><strong>WARNING:</strong> The wicket validation relies solely on input, and not on the model object. Thus any wicket validation
* will systematically deem a field invalid when there is no input. In particular, this means that the prerequisite field will systematically be deemed invalid on
* the first page rendering, which means this feature is not suitable for "editing" forms, where the form is filled before the first rendering.
* @deprecated Avoid using this, for the reasons mentioned above.
*/
@Deprecated
public AbstractAjaxInputPrerequisiteBehavior<T> setUseWicketValidation(boolean validatePrerequisiteInput) {
this.useWicketValidation = validatePrerequisiteInput;
return this;
}
/**
* Sets whether the prerequisite field model is to be updated when the prerequisite field input changes.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setUpdatePrerequisiteModel(boolean updatePrerequisiteModel) {
this.updatePrerequisiteModel = updatePrerequisiteModel;
return this;
}
/**
* Sets whether the attached component's models are to be set to null when the prerequisite model changes.
* @deprecated Use {@link #setResetAttachedFormComponentsIfInvalid(boolean) instead. Be aware that, on contrary to
* this method, the other also applies to descendants of the attached component and clears FormComponent inputs.
*/
@Deprecated
public AbstractAjaxInputPrerequisiteBehavior<T> setResetAttachedModel(boolean resetAttachedModel) {
this.resetAttachedModelPredicate = resetAttachedModel ? Predicates.alwaysTrue() : Predicates.alwaysFalse();
return this;
}
/**
* Sets whether the attached component's models are to be set to null when the prerequisite model is updated <em>and its new object is invalid</em>.
* @deprecated Use {@link #setResetAttachedFormComponentsIfInvalid(boolean) instead. Be aware that, on contrary to
* this method, the other also applies to descendants of the attached component and clears FormComponent inputs.
*/
@Deprecated
public AbstractAjaxInputPrerequisiteBehavior<T> setResetAttachedModelIfInvalid(boolean resetAttachedModel) {
this.resetAttachedModelPredicate = new SerializablePredicate<T>() {
private static final long serialVersionUID = 1L;
@Override
public boolean apply(T input) {
return !objectValidPredicate.apply(input);
}
};
return this;
}
/**
* Sets a predicate to determine, based on the prerequisite model value, whether the attached component's
* models are to be set to null when the prerequisite model changes.
* <p>Note that FormComponents children will not be reset (allowing the use of FormComponentPanels, which should
* handle input clearing of their children themselves).
* @deprecated Use {@link #setResetAttachedFormComponentsIfInvalid(boolean) instead. Be aware that, on contrary to
* this method, the other also applies to descendants of the attached component and clears FormComponent inputs.
*/
@Deprecated
public AbstractAjaxInputPrerequisiteBehavior<T> setResetAttachedModelPredicate(Predicate<? super T> resetAttachedModelPredicate) {
this.resetAttachedModelPredicate = resetAttachedModelPredicate;
return this;
}
/**
* Sets whether the attached component and all its children are to be reset if they are FormComponents, i.e. their
* model are to be set to null and their convertedInput are to be cleared, when the prerequisite model changes.
* <p>Note that FormComponents children will not be reset (allowing the use of FormComponentPanels, which should
* handle input clearing of their children themselves).
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setResetAttachedFormComponents() {
this.resetAttachedFormComponentsPredicate = Predicates.alwaysTrue();
return this;
}
/**
* Sets whether the attached component and all its children are to be reset if they are FormComponents, i.e. their
* model are to be set to null and their convertedInput are to be cleared, when the prerequisite model changes
* <em>and its new object is invalid</em>.
* <p>Note that FormComponents children will not be reset (allowing the use of FormComponentPanels, which should
* handle input clearing of their children themselves).
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setResetAttachedFormComponentsIfInvalid() {
this.resetAttachedFormComponentsPredicate = new SerializablePredicate<T>() {
private static final long serialVersionUID = 1L;
@Override
public boolean apply(T input) {
return !objectValidPredicate.apply(input);
}
};
return this;
}
/**
* Sets whether the attached component and all its children are to be reset if they are FormComponents, i.e. their
* model are to be set to null and their convertedInput are to be cleared, when the prerequisite model changes and
* the given predicate does not apply to the prerequisite model value anymore.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setResetAttachedFormComponentsPredicate(Predicate<? super T> resetAttachedFormComponentsPredicate) {
this.resetAttachedFormComponentsPredicate = resetAttachedModelPredicate;
return this;
}
/**
* Sets whether the attached component's model are to be set to null when calling onConfigure if the prerequisite model is invalid.
* <p>This should be used only to prevent a legacy behavior.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setNoResetAttachedModelOnConfigure() {
this.resetAttachedModelOnConfigure = false;
return this;
}
/**
* Sets the predicate used to determine whether the prerequisite field value is valid.
* <strong>Note:</strong> the predicate must be {@link Serializable}.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setObjectValidPredicate(Predicate<? super T> objectValidPredicate) {
this.objectValidPredicate = objectValidPredicate;
return this;
}
/**
* Sets the condition under which the attached component will be "set up" regardless of the value of the prerequisite field.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setForceSetUpCondition(Condition condition) {
this.forceSetUpConditon = condition;
return this;
}
/**
* Sets the condition under which the attached component will be "taken down" regardless of the value of the prerequisite field.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setForceTakeDownCondition(Condition condition) {
this.forceTakeDownConditon = condition;
return this;
}
/**
* Sets whether the ajax refresh should target the attached component's parent instead of the component itself.
* <p>This is useful for components that use javascript to generate siblings in the DOM tree, such as Select2.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setRefreshParent(boolean refreshParent) {
this.refreshParent = refreshParent;
return this;
}
/**
* Sets attached components resetter.
*/
public AbstractAjaxInputPrerequisiteBehavior<T> setResetter(Resetter resetter) {
if (resetter != null) {
this.resetter = resetter;
}
return this;
}
public AbstractAjaxInputPrerequisiteBehavior<T> onChange(AbstractListener listener) {
this.onChangeListeners.add(listener);
return this;
}
public AbstractAjaxInputPrerequisiteBehavior<T> onChange(Collection<AbstractListener> listeners) {
this.onChangeListeners.addAll(listeners);
return this;
}
@Override
public final void bind(Component component) {
if (attachedComponents.isEmpty()) {
// Make sure that the attached component will be updated when the content of the prerequisiteField changes.
FormComponentChangeAjaxEventBehavior.get(prerequisiteField).subscribe(observer);
}
attachedComponents.add(component);
}
@Override
public final void unbind(Component component) {
attachedComponents.remove(component);
if (attachedComponents.isEmpty()) {
FormComponentChangeAjaxEventBehavior.get(prerequisiteField).unsubscribe(observer);
}
}
private void onPrerequisiteFieldChange(AjaxRequestTarget target) {
T convertedInput = getPrerequisiteFieldConvertedInput();
for (Component attachedComponent : attachedComponents) {
Component reloadedComponent = getAjaxTarget(attachedComponent);
if (VisitFilters.renderedComponents().visitObject(reloadedComponent)) {
target.add(reloadedComponent);
}
boolean hasReset = false;
if (resetAttachedModelPredicate.apply(convertedInput)) {
attachedComponent.setDefaultModelObject(null);
hasReset = true;
}
if (resetAttachedFormComponentsPredicate.apply(convertedInput)) {
resetFormComponents(attachedComponent);
hasReset = true;
}
if (hasReset) {
// Handle chained prerequisites
FormComponentChangeAjaxEventBehavior behavior = FormComponentChangeAjaxEventBehavior.getExisting(attachedComponent);
if (behavior != null) {
behavior.notify(target);
}
}
}
for (AbstractListener onChangeListener : onChangeListeners) {
target.addListener(onChangeListener);
}
onPrerequisiteFieldChange(target, prerequisiteField, Collections.unmodifiableCollection(attachedComponents));
}
private void resetFormComponents(Component attachedComponent) {
visit(attachedComponent, resetFormComponentsVisitor, new ClassVisitFilter(FormComponent.class));
}
// Visits.visit is screwed: it does not accept Components, but only Iterable<Component>, on contrary to Visits.visitPostOrder
private static final <T extends Component, R> R visit(Component component, IVisitor<T, R> visitor, IVisitFilter filter) {
return Visits.visitChildren(Collections.<Component>singleton(component), visitor, filter);
}
private IVisitor<FormComponent<?>, Void> resetFormComponentsVisitor = new ResetFormComponentsVisitor();
private class ResetFormComponentsVisitor implements IVisitor<FormComponent<?>, Void>, Serializable {
private static final long serialVersionUID = 2038057558891912818L;
@Override
public void component(FormComponent<?> object, IVisit<Void> visit) {
if (attachedComponents.contains(object)) {
resetter.reset(object);
} else {
IModel<?> model = object.getDefaultModel();
if (model != null) {
model.setObject(null);
}
}
object.clearInput();
visit.dontGoDeeper();
}
}
private Component getAjaxTarget(Component componentToRender) {
if (refreshParent) {
return componentToRender.getParent();
} else {
return componentToRender;
}
}
/**
* Ajax call triggered by a change on the prerequisite field.
* Called after adding the attached components to the target, and before generating the response.
* @deprecated Use {@link #onChange(AbstractListener)} instead.
*/
@Deprecated
protected void onPrerequisiteFieldChange(AjaxRequestTarget target, FormComponent<T> prerequisiteField, Collection<Component> attachedComponents) {
}
private boolean hasPrerequisiteFieldInputChanged() {
return prerequisiteField.isEnabledInHierarchy() && prerequisiteField.isVisibleInHierarchy()
&&
(
prerequisiteField.getForm().isSubmitted()
|| FormComponentChangeAjaxEventBehavior.getExisting(prerequisiteField).isBeingSubmitted()
);
}
private T getPrerequisiteFieldConvertedInput() {
prerequisiteField.inputChanged();
prerequisiteField.validate(); // Performs input conversion
T converted = prerequisiteField.getConvertedInput();
prerequisiteField.getFeedbackMessages().clear();
return converted;
}
@Override
public final void onConfigure(Component component) {
super.onConfigure(component);
if (prerequisiteField.isVisibleInHierarchy()) {
if (forceTakeDownConditon != null && forceTakeDownConditon.applies()) {
cleanDefaultModelObject(component);
takeDownAttachedComponent(component);
} else if (forceSetUpConditon != null && forceSetUpConditon.applies()) {
setUpAttachedComponent(component);
} else {
if (hasPrerequisiteFieldInputChanged()) {
// The prerequisiteField input has changed : the rendering of the attached component was triggered either by our
// InputPrerequisiteAjaxEventBehavior or by a form submit.
// We will decide whether the attached component should be set up or taken down based on the prerequisiteField's input.
prerequisiteField.validate(); // Performs input conversion
updateModelIfNecessary(prerequisiteField);
if (isConvertedInputSatisfyingRequirements(prerequisiteField, prerequisiteField.getConvertedInput())) {
setUpAttachedComponent(component);
} else {
if (resetAttachedModelOnConfigure) {
cleanDefaultModelObject(component);
}
takeDownAttachedComponent(component);
}
// Clearing the input seems useless here, and may harm if the input is used when rendering the form component.
// prerequisiteField.clearInput();
} else {
if (useWicketValidation) {
// TODO YRO : see if anyone is still using this, and drop it if not. This is dangerous since [...]
// it may result in using wicket to validate input even though the form was not submitted.
prerequisiteField.validate(); // Performs input conversion
}
if (isCurrentModelSatisfyingRequirements(prerequisiteField, prerequisiteField.getModel())) {
setUpAttachedComponent(component);
} else {
if (resetAttachedModelOnConfigure) {
cleanDefaultModelObject(component);
}
takeDownAttachedComponent(component);
}
}
if (useWicketValidation) {
// We need to clear the message that may have been added during the validation, since they are not relevant to the user (no form was submitted)
prerequisiteField.getFeedbackMessages().clear();
}
}
} else {
if (defaultWhenPrerequisiteInvisibleCondition.applies()) {
setUpAttachedComponent(component);
} else {
takeDownAttachedComponent(component);
}
}
}
private void updateModelIfNecessary(FormComponent<T> prerequisiteField) {
if (updatePrerequisiteModel) {
prerequisiteField.updateModel();
}
}
protected final boolean isConvertedInputSatisfyingRequirements(FormComponent<T> prerequisiteField, T convertedInput) {
return (!useWicketValidation || prerequisiteField.isValid()) && isObjectValid(convertedInput);
}
protected final boolean isCurrentModelSatisfyingRequirements(FormComponent<T> prerequisiteField, IModel<T> currentModel) {
return (!useWicketValidation || prerequisiteField.isValid()) && isObjectValid(currentModel.getObject());
}
protected final boolean isObjectValid(T object) {
return objectValidPredicate.apply(object);
}
/**
* @deprecated Use the various setReset*() methods instead.
*/
@Deprecated
protected void cleanDefaultModelObject(Component attachedComponent) {
resetter.reset(attachedComponent);
}
protected abstract void setUpAttachedComponent(Component attachedComponent);
protected abstract void takeDownAttachedComponent(Component attachedComponent);
}