/******************************************************************************* * Copyright (c) 2007, 2014 compeople AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * compeople AG - initial API and implementation *******************************************************************************/ package org.eclipse.riena.ui.ridgets; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.databinding.AggregateValidationStatus; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.UpdateValueStrategy; import org.eclipse.core.databinding.beans.BeansObservables; import org.eclipse.core.databinding.beans.PojoObservables; import org.eclipse.core.databinding.conversion.IConverter; import org.eclipse.core.databinding.observable.value.ComputedValue; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.IValueChangeListener; import org.eclipse.core.databinding.observable.value.ValueChangeEvent; import org.eclipse.core.databinding.validation.IValidator; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.riena.core.marker.IMarkable; import org.eclipse.riena.core.util.StringUtils; import org.eclipse.riena.ui.core.marker.ErrorMarker; import org.eclipse.riena.ui.core.marker.ErrorMessageMarker; import org.eclipse.riena.ui.core.marker.IMessageMarker; import org.eclipse.riena.ui.core.marker.MessageMarker; import org.eclipse.riena.ui.core.marker.ValidationTime; import org.eclipse.riena.ui.ridgets.databinding.RidgetUpdateValueStrategy; import org.eclipse.riena.ui.ridgets.marker.ValidationMessageMarker; import org.eclipse.riena.ui.ridgets.validation.IValidationCallback; import org.eclipse.riena.ui.ridgets.validation.ValidationRuleStatus; import org.eclipse.riena.ui.ridgets.validation.ValidatorCollection; /** * Helper class for Ridgets to delegate their value binding issues to. */ public class ValueBindingSupport { /** * This rule fails if the ridget is marked with an error marker */ private final IValidator noErrorsRule = new IValidator() { public IStatus validate(final Object value) { final boolean isOk = markable.getMarkersOfType(ErrorMarker.class).isEmpty(); return isOk ? Status.OK_STATUS : Status.CANCEL_STATUS; } @Override public String toString() { return "NO_ERRORS_RULE"; //$NON-NLS-1$ } }; private final ValidatorCollection afterGetValidators; private final ValidatorCollection onEditValidators; private final ValidatorCollection afterSetValidators; private Map<IValidator, Set<ValidationMessageMarker>> rule2messages; private Map<IValidator, IStatus> rule2status; private Map<IValidator, ErrorMessageMarker> rule2error; private DataBindingContext context; private IObservableValue targetOV; private IObservableValue modelOV; private String valuePropertyName; private Object valueHolder; private Binding modelBinding; private IConverter uiControlToModelConverter; private IConverter modelToUIControlConverter; private AggregateValidationStatus validationStatus; private IMarkable markable; public ValueBindingSupport(final IObservableValue target) { bindToTarget(target); afterGetValidators = new ValidatorCollection(); onEditValidators = new ValidatorCollection(); afterSetValidators = new ValidatorCollection(); } public ValueBindingSupport(final IObservableValue target, final IObservableValue model) { this(target); bindToModel(model); } public IConverter getUIControlToModelConverter() { return uiControlToModelConverter; } public void setUIControlToModelConverter(final IConverter uiControlToModelConverter) { this.uiControlToModelConverter = uiControlToModelConverter; } public IConverter getModelToUIControlConverter() { return modelToUIControlConverter; } public void setModelToUIControlConverter(final IConverter modelToUIControlConverter) { this.modelToUIControlConverter = modelToUIControlConverter; } public Collection<IValidator> getValidationRules() { final List<IValidator> allValidationRules = new ArrayList<IValidator>(onEditValidators.getValidators()); allValidationRules.addAll(afterGetValidators.getValidators()); allValidationRules.addAll(afterSetValidators.getValidators()); return allValidationRules; } /** * Return all validation rules kept by this instance. * * @return a ValidatorCollection with all rules; never null; may be empty. * * @since 1.2 */ public ValidatorCollection getAllValidators() { final ValidatorCollection result = new ValidatorCollection(); addAll(onEditValidators, result); addAll(afterGetValidators, result); addAll(afterSetValidators, result); return result; } /** * Return all 'on edit' validation rules kept by this instance. * * @return a ValidatorCollection; never null; may be empty. */ public ValidatorCollection getOnEditValidators() { return onEditValidators; } /** * Return all 'on update' validation rules kept by this instance. * * @return a ValidatorCollection; never null; may be empty. */ public ValidatorCollection getAfterGetValidators() { return afterGetValidators; } /** * Return all 'after update' validation rules kept by this instance. * * @return a ValidatorCollection; never null; may be empty. * @since 4.0 */ public ValidatorCollection getAfterSetValidators() { return afterSetValidators; } /** * Adds a validation rule. * * @param validationRule * The validation rule to add (non null) * @param validationTime * a value specifying when to evalute the validationRule (non null) * @return true, if the onEditValidators were changed, false otherwise * @see #getOnEditValidators() * @throws RuntimeException * if validationRule is null, or an unsupported ValidationTime is used */ public boolean addValidationRule(final IValidator validationRule, final ValidationTime validationTime) { Assert.isNotNull(validationRule); Assert.isNotNull(validationTime); switch (validationTime) { case ON_UI_CONTROL_EDIT: onEditValidators.add(validationRule); return true; case ON_UPDATE_TO_MODEL: afterGetValidators.add(validationRule); return false; case AFTER_UPDATE_TO_MODEL: afterSetValidators.add(validationRule); return false; default: throw new UnsupportedOperationException("Unknown validationTime: " + validationTime); //$NON-NLS-1$ } } /** * Removes a validation rule. * * @param validationRule * The validation rule to remove * @return true, if the onEditValidators were changed, false otherwise * @see #getOnEditValidators() */ public boolean removeValidationRule(final IValidator validationRule) { if (validationRule == null) { return false; } removeErrorMarker(validationRule); removeMessages(validationRule); clearStatus(validationRule); // first remove in the list of afterGetValidators and afterSetValidators afterGetValidators.remove(validationRule); afterSetValidators.remove(validationRule); // if it is in the list of On_edit validators, also remove and return true return onEditValidators.remove(validationRule); } public void bindToTarget(final IObservableValue observableValue) { targetOV = observableValue; rebindToModel(); } /** * @see org.eclipse.riena.ui.ridgets.IValueRidget#bindToModel(org.eclipse.core .databinding.observable.value.IObservableValue) */ public void bindToModel(final IObservableValue observableValue) { this.valuePropertyName = null; this.valueHolder = null; modelOV = observableValue; rebindToModel(); } /** * @see org.eclipse.riena.ui.ridgets.IValueRidget#bindToModel(java.lang.Object, java.lang.String) */ public void bindToModel(final Object valueHolder, final String valuePropertyName) { this.valueHolder = valueHolder; this.valuePropertyName = valuePropertyName; if (isBean(valueHolder.getClass())) { modelOV = BeansObservables.observeValue(valueHolder, valuePropertyName); } else { modelOV = PojoObservables.observeValue(valueHolder, valuePropertyName); } rebindToModel(); } /** * @return the targetOV * @since 4.0 */ public IObservableValue getTargetOV() { return targetOV; } /** * Binds (first time or again) the model to the control. */ public void rebindToModel() { if (modelOV == null || targetOV == null) { return; } final RidgetUpdateValueStrategy uiControlToModelStrategy = new RidgetUpdateValueStrategy(this, UpdateValueStrategy.POLICY_UPDATE); final RidgetUpdateValueStrategy modelToUIControlStrategy = new RidgetUpdateValueStrategy(this, UpdateValueStrategy.POLICY_ON_REQUEST); uiControlToModelStrategy.setAfterGetValidator(afterGetValidators); uiControlToModelStrategy.setAfterSetValidator(afterSetValidators); if (uiControlToModelConverter != null) { if ((targetOV.getValueType() == uiControlToModelConverter.getFromType()) && (modelOV.getValueType() == uiControlToModelConverter.getToType())) { uiControlToModelStrategy.setConverter(uiControlToModelConverter); } } if (modelToUIControlConverter != null) { if ((targetOV.getValueType() == modelToUIControlConverter.getToType()) && (modelOV.getValueType() == modelToUIControlConverter.getFromType())) { modelToUIControlStrategy.setConverter(modelToUIControlConverter); } } if (modelBinding != null) { // MUST dispose previous binding, otherwise code like this: // ridget.bind(modelA); // ridget.bind(modelB); // causes the ridget to be bound to two models and ui changes are // synched with both! modelBinding.dispose(); } if (validationStatus != null) { // must dispose old instance, see performance Bug 327684 validationStatus.dispose(); } modelBinding = getContext().bindValue(targetOV, modelOV, uiControlToModelStrategy, modelToUIControlStrategy); validationStatus = new AggregateValidationStatus(getContext().getBindings(), AggregateValidationStatus.MAX_SEVERITY); validationStatus.addValueChangeListener(new IValueChangeListener() { public void handleValueChange(final ValueChangeEvent event) { final IStatus newStatus = (IStatus) ((ComputedValue) event.getSource()).getValue(); updateValidationMessages(newStatus); } private void updateValidationMessages(final IStatus newStatus) { if (targetOV != null) { final Object value = targetOV.getValue(); updateValidationStatus(noErrorsRule, newStatus); for (final IValidator rule : getAfterGetValidators()) { updateValidationStatusForRule(value, rule); } for (final IValidator rule : getAfterSetValidators()) { updateValidationStatusForRule(value, rule); } } } /** * if validation has already been performed by this rule, the validation result is directly reused */ protected void updateValidationStatusForRule(final Object value, final IValidator rule) { if (rule2status.containsKey(rule)) { updateValidationStatus(rule, rule2status.get(rule)); } else { updateValidationStatus(rule, rule.validate(value)); } } }); } public IObservableValue getModelObservable() { return modelOV; } public Binding getModelBinding() { return modelBinding; } /** * Get the value property name that has been specified with the <i>bindToModel</i> method. * * @return the value property name * * @since 4.0 */ public String getValuePropertyName() { return valuePropertyName; } public void setMarkable(final IMarkable markable) { this.markable = markable; } public void updateFromModel() { if (valueHolder != null) { if (!isBean(valueHolder.getClass()) && isNestedProperty(valuePropertyName)) { bindToModel(valueHolder, valuePropertyName); } } if (modelBinding != null) { modelBinding.updateModelToTarget(); } } /** * Returns whether the given name for the property is a conjunction of properties. The nested properties must be separated with a dot (e.g. "parent.name"). * * @param propertyName * the property name * @return {@code true} if the property name contains nested properties; otherwise {@code false} */ private boolean isNestedProperty(final String propertyName) { return StringUtils.isGiven(propertyName) && (propertyName.indexOf('.') != -1); } public void updateFromTarget() { if (modelBinding != null) { modelBinding.updateTargetToModel(); } } public DataBindingContext getContext() { if (context == null) { context = new DataBindingContext(); } return context; } public void addValidationMessage(final String message) { addValidationMessage(message, noErrorsRule); } public void addValidationMessage(final IMessageMarker messageMarker) { addValidationMessage(messageMarker, noErrorsRule); } public void addValidationMessage(final String message, final IValidator validationRule) { addValidationMessage(new MessageMarker(message), validationRule); } public void addValidationMessage(final IMessageMarker messageMarker, final IValidator validationRule) { Assert.isNotNull(messageMarker, "messageMarker cannot be null"); //$NON-NLS-1$ Assert.isNotNull(validationRule, "validationRule cannot be null"); //$NON-NLS-1$ final ValidationMessageMarker validationMessageMarker = new ValidationMessageMarker(messageMarker, validationRule); if (rule2messages == null) { rule2messages = new HashMap<IValidator, Set<ValidationMessageMarker>>(); } Set<ValidationMessageMarker> messages = rule2messages.get(validationRule); if (messages == null) { messages = new HashSet<ValidationMessageMarker>(); rule2messages.put(validationRule, messages); } messages.add(validationMessageMarker); updateValidationMessageMarker(validationRule); } public void removeValidationMessage(final String message) { removeValidationMessage(message, noErrorsRule); } public void removeValidationMessage(final IMessageMarker messageMarker) { removeValidationMessage(messageMarker, noErrorsRule); } public void removeValidationMessage(final String message, final IValidator validationRule) { removeValidationMessage(new MessageMarker(message), validationRule); } public void removeValidationMessage(final IMessageMarker messageMarker, final IValidator validationRule) { final ValidationMessageMarker validationMessageMarker = new ValidationMessageMarker(messageMarker, validationRule); if (rule2messages != null) { final Set<ValidationMessageMarker> messages = rule2messages.get(validationRule); messages.remove(validationMessageMarker); markable.removeMarker(validationMessageMarker); if (messages.isEmpty()) { rule2messages.remove(validationRule); } } } /** * Updates the aggregate error status (i.e. the sum of all rules). This will update the error state and messages attached to the aggregate error status -- * see {@link #addValidationMessage(String)}. * <p> * Implementation note: This should be invoked when the aggregate state of 'on_edit' rules has changed, since the status of such rules is not * * @param status * an IStatus instance with the aggregate status; never null * @see IValidationCallback * @since 1.2 */ public void updateValidationStatus(final IStatus status) { updateValidationStatus(noErrorsRule, status); } /** * Update the status of a specific rule. This will update the error state and messages attached to that rule -- see * {@link #addValidationMessage(String, IValidator)}. * * @param validationRule * an IValidator instance; never null * @param status * an IStatus instance; never null * @see IValidationCallback * @since 1.2 */ public void updateValidationStatus(final IValidator validationRule, final IStatus status) { // trace("updating rule " + validationRule + " with " + status); storeStatus(validationRule, status); if (!status.isOK()) { addErrorMarker(validationRule, status); addMessages(validationRule); } else { removeErrorMarker(validationRule); removeMessages(validationRule); } } // helping methods ////////////////// private void addAll(final ValidatorCollection source, final ValidatorCollection target) { for (final IValidator validationRule : source) { target.add(validationRule); } } private void addErrorMarker(final IValidator validationRule, final IStatus status) { if (isBlocked(validationRule, status) || validationRule == noErrorsRule) { return; } if (rule2error == null) { rule2error = new HashMap<IValidator, ErrorMessageMarker>(); } ErrorMessageMarker errorMarker = rule2error.get(validationRule); if (errorMarker == null) { errorMarker = new ErrorMessageMarker(status.getMessage()); rule2error.put(validationRule, errorMarker); } else { errorMarker.setMessage(status.getMessage()); } markable.addMarker(errorMarker); // trace("+EM " + errorMarker + " " + markable.getMarkers().size()); } private void addMessages(final IValidator validationRule) { if (rule2messages != null && rule2messages.containsKey(validationRule)) { final Set<ValidationMessageMarker> messages = rule2messages.get(validationRule); for (final ValidationMessageMarker message : messages) { // trace("+VMM " + message); markable.addMarker(message); } } } private void clearStatus(final IValidator validationRule) { if (rule2status != null) { rule2status.remove(validationRule); } } private IStatus getStatus(final IValidator validationRule) { return rule2status != null ? rule2status.get(validationRule) : null; } private boolean isBean(final Class<?> clazz) { boolean result; try { // next line throws NoSuchMethodException, if no matching method found clazz.getMethod("addPropertyChangeListener", PropertyChangeListener.class); //$NON-NLS-1$ result = true; // have bean } catch (final NoSuchMethodException e) { result = false; // have pojo } return result; } private boolean isBlocked(final IValidator validationRule, final IStatus status) { return status.getCode() == ValidationRuleStatus.ERROR_BLOCK_WITH_FLASH && onEditValidators.contains(validationRule); } private void storeStatus(final IValidator validationRule, final IStatus status) { if (rule2status == null) { rule2status = new HashMap<IValidator, IStatus>(); } rule2status.put(validationRule, status); } private void removeErrorMarker(final IValidator validationRule) { if (rule2error != null) { final ErrorMessageMarker errorMarker = rule2error.remove(validationRule); if (errorMarker != null) { markable.removeMarker(errorMarker); // trace("-EM " + errorMarker + " " + (size - 1)); } } } private void removeMessages(final IValidator validationRule) { if (rule2messages != null && rule2messages.containsKey(validationRule)) { final Set<ValidationMessageMarker> messages = rule2messages.get(validationRule); for (final ValidationMessageMarker message : messages) { markable.removeMarker(message); // trace("-VMM " + message); } } } private void updateValidationMessageMarker(final IValidator validationRule) { final IStatus status = getStatus(validationRule); if (status != null) { updateValidationStatus(validationRule, status); } } // private void trace(String message) { // System.out.println(message); // } }