package org.nocket.gen.page.element.synchronizer; import gengui.annotations.Eager; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.form.AbstractSingleSelectChoice; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.util.lang.Objects; import org.apache.wicket.validation.IValidatable; import org.apache.wicket.validation.IValidator; import org.apache.wicket.validation.validator.AbstractValidator; import org.nocket.component.form.JSR303Validator; import org.nocket.gen.page.element.ButtonElement; import org.nocket.gen.page.element.CheckboxInputElement; import org.nocket.gen.page.element.PageElementI; import org.nocket.gen.page.element.RadioInputElement; import org.nocket.gen.page.element.SelectElement; import org.nocket.gen.page.element.TextAreaElement; import org.nocket.gen.page.element.TextInputElement; import org.nocket.gen.page.visitor.bind.builder.components.GeneratedBeanValidationForm; import org.nocket.gen.page.visitor.bind.builder.components.GeneratedButton; import org.nocket.util.Assert; /** * This behavior class manipulates components in several ways.<br/> * <br/> * - controls setVisible and setEnabled states for a component<br/> * - invokes custom validator methods<br/> * - add ajax for handling {@link Eager} fields<br/> * - adds tool tips for input fields<br/> * - switches TextFields and TextAreas to read-only<br/> */ public class DomainComponentBehaviour extends AbstractValidator<Object> { private static final long serialVersionUID = 1L; private static final Logger log = Logger.getLogger(DomainComponentBehaviour.class); private static final String TITLE = "title"; private static final String ONCHANGE = "onchange"; private static final String ONCLICK = "onclick"; private static final String READONLY = "readonly"; private static final String DISABLED = "disabled"; private final Component component; private final SynchronizerHelper helper; private final boolean hideButton; private List<IValidator<Object>> validators; /** * The constructor for this class. * * @param element * {@link PageElementI}. * @param component * {@link Component}. */ public DomainComponentBehaviour(PageElementI<?> element, final Component component) { this.helper = new SynchronizerHelper(element); this.hideButton = element instanceof ButtonElement; this.component = component; // Add validators addValidatorsForComponent(); addTooltipForField(component); if (helper.isEager()) { enableAjaxForEagerField(element, component); } addNullValidToChoiceField(element, component); switchDisabledTextFieldsToReadonly(element, component); } private void addValidatorsForComponent() { validators = new ArrayList<IValidator<Object>>(); // Throw exception is validator required without validator there. helper.assertValidate(); if (helper.isProperty()) { validators.add(new JSR303Validator<Object>(helper)); } validators.add(new NocketValidateMethodValidator<Object>(helper)); } /** * @see org.apache.wicket.behavior.Behavior#onConfigure(org.apache.wicket.Component) */ @Override public void onConfigure(Component component) { super.onConfigure(component); component.setEnabled(helper.isEnabled()); if (hideButton && helper.getHiderMethod() != null) { component.setVisible(helper.invokeHiderMethod() == null); } } /** * @see org.apache.wicket.validation.validator.AbstractValidator#onValidate(org.apache.wicket.validation.IValidatable) */ @Override protected void onValidate(IValidatable<Object> validatable) { if (performValidation(validatable)) { for (IValidator<Object> v : validators) { v.validate(validatable); } } } private boolean performValidation(IValidatable<Object> validatable) { GeneratedBeanValidationForm<?> form = getParentForm(); if (form.isEagerProcessing()) { if (form.isForcedProcessing()) { return false; } if (isEqual(validatable.getValue(), component.getDefaultModelObject())) { if (log.isDebugEnabled()) { log.debug(MessageFormat.format("No validation for component: wicket id={0}, old value={1}, new value={2}.", helper.getWicketId(), component.getDefaultModelObject(), validatable.getValue())); } return false; } else { if (log.isDebugEnabled()) { log.debug(MessageFormat.format("Validation required for component: wicket id={0}, old value={1}, new value={2}.", helper.getWicketId(), component.getDefaultModelObject(), validatable.getValue())); } return true; } } return true; } private GeneratedBeanValidationForm<?> getParentForm() { GeneratedBeanValidationForm<?> parentForm = component.findParent(GeneratedBeanValidationForm.class); Assert.notNull(parentForm, "Form for the component " + component.getId() + " cannot be found."); return parentForm; } /** * @see org.apache.wicket.validation.validator.AbstractValidator#validateOnNullValue() */ @Override public boolean validateOnNullValue() { return true; } /** * Switches TextFields and TextAreas from disabled to readonly. * * Select drops down boxes are also manipulated. They are switched to * invisible via css, then the dropdown is replaced by a input field. The * input field is injected using the ComponentTag name, which hold the * string "select". We replace select with the parsed content of the * SELECT_HACK constant. (evil IE convinience hack...) * * @param element * {@link PageElementI}. * @param component * {@link Component}. */ private void switchDisabledTextFieldsToReadonly(PageElementI<?> element, final Component component) { // Replace "disabled" for TextAreas and Input Fields with "readonly". if (element instanceof TextInputElement || element instanceof TextAreaElement) { component.add(new Behavior() { private static final long serialVersionUID = 1L; @Override public void onComponentTag(Component component, ComponentTag tag) { // Switch TextFields and TextAreas from disabled to readonly if (tag.getAttributes().containsKey(DISABLED)) { tag.getAttributes().remove(DISABLED); tag.getAttributes().put(READONLY, READONLY); } super.onComponentTag(component, tag); } }); } else if (element instanceof SelectElement) { component.add(new Behavior() { private static final long serialVersionUID = 1L; @Override public void onComponentTag(Component component, ComponentTag tag) { if (tag.getAttributes().containsKey(DISABLED)) { tag.getAttributes().put(READONLY, READONLY); } super.onComponentTag(component, tag); } }); } } /** * Oh boy, this is one line of code for a veeeery special trick. * <p> * This method forces the bound attribute's setter to be invoked at the very * end of the synchronization phase in case the synchronization was trigger * by an eager-update of the behaviour's component. We assume that a * component is only eager-annotated when its setter performs a piece of * relevant application logic. Invoking the setter *at last* of all * attributes ensures that its logic can rely on all other attributes being * already in sync with the user's input.<br> * The method is called "reinvoke" because the setter will also be * automatically called earlier by Wicket, but we can't tell at which time * in relation to all the other attributes. So this is definitely the second * time which should not cause any performance issues as the setters of * eager-updated attributes must perform their job very fast anyway.<br> * The setter is called with the current attribute's value, which should be * OK for a re-invokation according to the * "Stang'sches Synchronisierungskalk�l". If the attribute's value may be * influenced by other attributes' setters, these other attributes should * also be eager-updated. This in turn means that their appropriate GUI * components won't change their value in this phase here and therefore * their setters won't be invoked. So the current attribute's value will * only be changed by its own GUI component here and can therefore be set to * the same value when being set for the second time. * <p> * The whole thing looks so strange because it is a poor-man's solution. The * proper solution would be to change the order of model updates within * Wicket so that the setter must only be called only *once*. But all * methods in the Form class like internalUpdateFormComponentModels and * updateFormComponentModels and the internal class FormModelUpdateVisitor * are all private and final and so on :-( */ protected void reinvokeSetter() { helper.invokeSetterMethod(helper.invokeGetterMethod()); } /** * Add eager behavior. * * @param element * {@link PageElementI}. * @param component * {@link Component}. */ private void enableAjaxForEagerField(PageElementI<?> element, final Component component) { component.add(new EagerAjaxFormSubmitBehavior(getOnChangeEventName(element), helper)); } /** * This method checks whether the form contains an invisible or disabled * button that should now be re-enabled or re-displayed by sending it with * the AjaxRequestTarget * * @param target * @return */ protected boolean formCreatesInvisibleOrDisabledButton(AjaxRequestTarget target) { for (Component comp : target.getComponents()) { if (comp instanceof GeneratedButton) { GeneratedButton button = (GeneratedButton) comp; return button.isEnabled() || button.determineVisibility(); } } return false; } /** * Gets the correct name of the onChange Element. * * @param element * {@link PageElementI}. * @return String. */ private String getOnChangeEventName(PageElementI<?> element) { final String event; if (element instanceof CheckboxInputElement || element instanceof RadioInputElement) { event = ONCLICK; // IE fix } else { event = ONCHANGE; } return event; } /** * Add tooltip handler. * * @param component * {@link Component}. */ private void addTooltipForField(final Component component) { component.add(new AttributeModifier(TITLE, new AbstractReadOnlyModel<String>() { private static final long serialVersionUID = 1L; @Override public String getObject() { return helper.getFieldTooltip(); } })); } /** * Add null valid behaviour. * * @param element * {@link PageElementI}. * @param component * {@link Component}. */ private void addNullValidToChoiceField(PageElementI<?> element, final Component component) { if (element instanceof SelectElement && component instanceof AbstractSingleSelectChoice) { component.add(new Behavior() { private static final long serialVersionUID = 1L; private final AbstractSingleSelectChoice<?> choice = (AbstractSingleSelectChoice<?>) component; @Override public void onConfigure(Component component) { choice.setNullValid(helper.isChoicesNullValid()); } }); } } private boolean isEqual(Object o1, Object o2) { if (o1 == null && o2 == null) { return true; } if (o1 == null || o2 == null) { return false; } return o1.equals(o2); } }