/* * Copyright 2009-2011 original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jdal.ui; import java.awt.Component; import java.beans.PropertyDescriptor; import java.io.Serializable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jdal.annotation.AnnotatedElementAccessor; import org.jdal.ui.bind.BinderFactory; import org.jdal.ui.bind.BinderHolder; import org.jdal.ui.bind.CompositeBinder; import org.jdal.ui.bind.ControlAccessor; import org.jdal.ui.bind.ControlAccessorFactory; import org.jdal.ui.bind.ControlChangeListener; import org.jdal.ui.bind.ControlError; import org.jdal.ui.bind.ControlEvent; import org.jdal.ui.bind.ControlInitializer; import org.jdal.ui.bind.DirectFieldAccessor; import org.jdal.ui.bind.InitializationConfig; import org.jdal.ui.bind.Initializer; import org.jdal.ui.bind.Property; import org.jdal.ui.bind.PropertyBinder; import org.jdal.ui.validation.ErrorProcessor; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.convert.TypeDescriptor; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.validation.Validator; /** * Template class that simplifies {@link View} implementation. * * <p> The central method is <code>buildPanel</code> that builds the component * that hold the view controls. You may use custom binding of the view overwriting the methods * <code>doUpdate</code> and <code>doRefresh</code>. * * <p> For common binding code, you usually use autobinding facitility, that is, using * the same name to the field control and model property name and calling to autobind() method. * When using autobinding, you may exclude model properties from binding using some * of <code>ignoreProperty</code> methods. * * <p> Manual binding is also supported via <code>bind</code> methods. When binding a control, a * the View is added to control as <code>ChangeListener</code> is added to the control for * setting dirty property on control changes. * * <p> Only <code>org.springframework.util.validation.Validator</code> validators are supported * The <code>validateView</code> method calls configured * <code>ErrorProcessors</code> to process errors found in validation. * * @author Jose Luis Martin * @since 2.0 * @see BinderFactory * @see ControlAccessorFactory * @see ErrorProcessor */ public abstract class ViewSupport<T> implements View<T>, ControlChangeListener, BinderHolder, Serializable { public final static String DEFAULT_BINDER_FACTORY_NAME = "binderFactory"; /** log */ private static final Log log = LogFactory.getLog(ViewSupport.class); /** view name */ private String name; /** binder factory to make property binders */ private BinderFactory binderFactory; /** control accessor factory */ private ControlAccessorFactory controlAccessorFactory; /** hold binders */ private CompositeBinder<T> binder = new CompositeBinder<T>(); /** Set with property names to ingnore on binding commands */ private Set<String> ignoredProperties = new HashSet<String>(); /** data model */ private T model; /** subviews list */ private List<View<T>> subViews = new ArrayList<View<T>>(); /** validator to check binding and model values */ private Validator validator; /** message source for internationalization */ @Autowired protected MessageSource messageSource; /** List of error handlers */ private List<ErrorProcessor> errorProcessors = new ArrayList<ErrorProcessor>(); /** Validation Errors */ protected BindingResult errors; /** dirty state */ private boolean dirty = false; /** initialize controls on autobind **/ private boolean initializeControls = true; protected int width = 0; protected int height = 0; /** control initializer */ private ControlInitializer controlInitializer; private List<ControlChangeListener> listeners = new ArrayList<ControlChangeListener>(); /** * Default ctor */ public ViewSupport() { this(null); } /** * Create the view and set the model * @param model model to set */ public ViewSupport(T model) { setModel(model); } /** * add a binding for control and model property name * @param component control * @param propertyName the model property path to bind * @param readOnly if true, binding only do refresh() */ public void bind(Object component, String propertyName, boolean readOnly) { checkFactories(); binder.bind(component, propertyName, readOnly); listen(component); } protected void checkFactories() { } /** * add a binding for control and model property name * @param component control * @param propertyName the model property path to bind */ public void bind(Object component, String propertyName) { bind(component, propertyName, false); } /** * {@inheritDoc} */ public abstract Object getPanel(); /** * {@inheritDoc} */ public T getModel() { return model; } /** * {@inheritDoc} */ public final void setModel(T model) { this.model = model; createBindingResult(); binder.setModel(model); // refresh subviews for (View<T> v : subViews) v.setModel(model); onSetModel(model); } /** * */ private void createBindingResult() { if (model != null) errors = new BeanPropertyBindingResult(getModel(), getModel().getClass().getSimpleName()); } /** * Callback method to handle model changes * @param model the new model */ protected void onSetModel(T model) { } /** * {@inheritDoc} */ public final void update() { clearErrors(); // do custom update doUpdate(); // update binder binder.update(); if (errors != null && binder.getBindingResult() != null && errors.getObjectName().equals(binder.getBindingResult().getObjectName())) errors.addAllErrors(binder.getBindingResult()); // update subviews for (View<T> v : subViews) { v.update(); if (errors != null && v.getBindingResult() != null && errors.getObjectName().equals(v.getBindingResult().getObjectName())) errors.addAllErrors(v.getBindingResult()); } // allow subclasses to do something after the update afterUpdate(); } /** * Clear validation erros */ private void clearErrors() { if (getModel() != null && errors != null && errors.hasErrors()) createBindingResult(); resetErrorProcessors(); } /** * Callback method on update() */ protected void doUpdate() { } /** * Callback method on update() */ protected void afterUpdate() { } /** * Add a subview, the subview is refreshed, updated and hold the same model * that this view, for adding views with other models, use bind() * @param view */ @SuppressWarnings({ "rawtypes", "unchecked" }) public void addView(View view) { subViews.add(view); view.setModel(model); } /** * {@inheritDoc} */ public final void refresh() { clearErrors(); doRefresh(); binder.refresh(); // refresh subviews for (View<T> v : subViews) v.refresh(); setDirty(false); } /** * Allow subclasses to do custom refresh */ protected void doRefresh() { } /** * Allow subclasses to do something after refresh */ protected void afterRefresh() { } /** * Listen control for changes. */ public void listen(Object control) { checkFactories(); ControlAccessor c = controlAccessorFactory.getControlAccessor(control); if (c != null) { c.addControlChangeListener(this); } } /** * {@inheritDoc} */ public void controlChange(ControlEvent e) { setDirty(true); fireControlChange(e); } /** * Gets the binder factory * @return the binder factory */ public BinderFactory getBinderFactory() { return binderFactory; } /** * Sets the binder factory, propagate it to composite binder. * @param binderFactory to set */ public void setBinderFactory(BinderFactory binderFactory) { this.binderFactory = binderFactory; binder.setBinderFactory(binderFactory); } /** * {@inheritDoc} */ public boolean validateView() { if (validator == null && !errors.hasErrors()) return true; if (validator != null) validator.validate(getModel(), errors); if (errors.hasErrors()) { for (FieldError error : errors.getFieldErrors()) { for (ErrorProcessor ep : errorProcessors ) { if (error instanceof ControlError) { ControlError ce = (ControlError) error; ep.processError(ce.getComponent(), error); } else { Binder<?> b = binder.getBinder(error.getField()); if (b instanceof PropertyBinder) { ep.processError(((PropertyBinder) b).getComponent(), error); } } } } return false; } return true; } private void resetErrorProcessors() { for (ErrorProcessor ep : errorProcessors) { ep.reset(); } } /** * Build a error message with all errors. * @return String with error message */ public String getErrorMessage() { StringBuilder sb = new StringBuilder(); if (errors.hasErrors()) { sb.append("\n"); Iterator<ObjectError> iter = errors.getAllErrors().iterator(); while (iter.hasNext()) { ObjectError oe = (ObjectError) iter.next(); sb.append("- "); sb.append(getMessage(oe)); sb.append("\n"); } } sb.append("\n"); return sb.toString(); } /** * Add a binding error * @param error error to add */ public void addError(ObjectError error) { if (errors == null) createBindingResult(); errors.addError(error); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public void clear() { T model = getModel(); if (model != null) { try { setModel((T) model.getClass().newInstance()); refresh(); } catch (InstantiationException e) { log.error(e); } catch (IllegalAccessException e) { log.error(e); } } } /** * {@inheritDoc} */ public void enableView(boolean enabled) { for (Binder<?> b : binder.getPropertyBinders()) { Object control = ((PropertyBinder) b).getComponent(); if (control instanceof Component) ((Component) control).setEnabled(enabled); else if (control instanceof View<?>) ((View<?>) control).enableView(enabled); for (View<?> v : subViews) v.enableView(enabled); } } /** * Bind controls following the same name convention or annotated with Property annotation. */ public void autobind() { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(getModel()); PropertyAccessor viewPropertyAccessor = new DirectFieldAccessor(this); // Parse Property annotations List<AnnotatedElement> elements = AnnotatedElementAccessor.findAnnotatedElements(Property.class, getClass()); for (AnnotatedElement ae : elements) { Property p = ae.getAnnotation(Property.class); InitializationConfig config = getInitializationConfig(ae.getAnnotation(Initializer.class)); bindAndInitializeControl(p.value(), viewPropertyAccessor.getPropertyValue(((Field) ae).getName()), config); this.ignoredProperties.add(p.value()); } // Iterate on model properties for (PropertyDescriptor pd : bw.getPropertyDescriptors()) { String propertyName = pd.getName(); if ( !ignoredProperties.contains(propertyName) && viewPropertyAccessor.isReadableProperty(propertyName)) { Object control = viewPropertyAccessor.getPropertyValue(propertyName); if (control != null) { if (log.isDebugEnabled()) log.debug("Found control: " + control.getClass().getSimpleName() + " for property: " + propertyName); TypeDescriptor descriptor = viewPropertyAccessor.getPropertyTypeDescriptor(propertyName); InitializationConfig config = getInitializationConfig(descriptor.getAnnotation(Initializer.class)); // do bind bindAndInitializeControl(propertyName, control, config); } } } } /** * @param Initializer * @return */ private InitializationConfig getInitializationConfig(Initializer initializer) { InitializationConfig config = new InitializationConfig(getModel().getClass()); if (initializer != null) config.setSortPropertyName(initializer.orderBy()); return config; } /** * Bind and initialize a control with property name. * @param propertyName property name * @param control control */ private void bindAndInitializeControl(String propertyName, Object control, InitializationConfig config) { bind(control, propertyName); // initialize control if (isInitializeControls() && controlInitializer != null) controlInitializer.initialize(control, propertyName, config); } /** * I18n Support * @param code message code * @return message or code if none defined */ protected String getMessage(String code) { return getMessage(code, Locale.getDefault()); } /** * I18n Support, get messae from given locale * @param code * @return */ protected String getMessage(String code, Locale locale) { try { return messageSource == null ? code : messageSource.getMessage(code, null, locale); } catch (NoSuchMessageException nsme) { log.error(nsme); } return code; } /** * I18n Support * @param msr message source resolvable * @return message or code if none defined */ protected String getMessage(MessageSourceResolvable msr) { return messageSource == null ? msr.getDefaultMessage() : messageSource.getMessage(msr, LocaleContextHolder.getLocale()); } public void addControlChangeListener(ControlChangeListener l) { if (!listeners.contains(l)) listeners.add(l); } public void removeControlChangeListener(ControlChangeListener l) { if (!listeners.contains(l)) listeners.remove(l); } /** * Notifiy Listeners that control value has changed */ protected void fireControlChange(ControlEvent e) { for (ControlChangeListener l : listeners) l.controlChange(new ControlEvent(e)); } /** * Add a property name to ignore on binding. * @param propertyName property name to ignore */ public void ignoreProperty(String propertyName) { ignoredProperties.add(propertyName); } /** * @return the ignoredProperties */ public Set<String> getIgnoredProperties() { return ignoredProperties; } /** * @param ignoredProperties the ignoredProperties to set */ public void setIgnoredProperties(Set<String> ignoredProperties) { this.ignoredProperties = ignoredProperties; } /** * Add a Collection of property names to ignore on binding * @param c Collection of property names. */ public void ignoreProperties(Collection<? extends String> c) { ignoredProperties.addAll(c); } /** * @return the validator */ public Validator getValidator() { return validator; } /** * @param validator the validator to set */ public void setValidator(Validator validator) { this.validator = validator; } /** * @return the messageSource */ public MessageSource getMessageSource() { return messageSource; } /** * @param messageSource the messageSource to set */ public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } /** * @return the width */ public int getWidth() { return width; } /** * @param width the width to set */ public void setWidth(int width) { this.width = width; } /** * @return the height */ public int getHeight() { return height; } /** * @param height the height to set */ public void setHeight(int height) { this.height = height; } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } /** * @return the errorProcessors */ public List<ErrorProcessor> getErrorProcessors() { return errorProcessors; } /** * @param errorProcessors the errorProcessors to set */ public void setErrorProcessors(List<ErrorProcessor> errorProcessors) { this.errorProcessors = errorProcessors; } /** * @return the dirty */ public boolean isDirty() { boolean d = dirty; for (View<T> v : subViews) { d = d || v.isDirty(); } return d; } /** * @param dirty the dirty to set */ public void setDirty(boolean dirty) { this.dirty = dirty; } /** * @return the controlAccessorFactory */ public ControlAccessorFactory getControlAccessorFactory() { return controlAccessorFactory; } /** * @param controlAccessorFactory the controlAccessorFactory to set */ public void setControlAccessorFactory(ControlAccessorFactory controlAccessorFactory) { this.controlAccessorFactory = controlAccessorFactory; } /** * {@inheritDoc} */ public BindingResult getBindingResult() { if (errors == null && getModel() != null) createBindingResult(); return errors; } /** * {@inheritDoc} */ public PropertyBinder getBinder(String propertyName) { return binder.getBinder(propertyName); } /** * @return the initializeControls */ public boolean isInitializeControls() { return initializeControls; } /** * @param initializeControls the initializeControls to set */ public void setInitializeControls(boolean initializeControls) { this.initializeControls = initializeControls; } /** * @return the controlInitializer */ public ControlInitializer getControlInitializer() { return controlInitializer; } /** * @param controlInitializer the controlInitializer to set */ public void setControlInitializer(ControlInitializer controlInitializer) { this.controlInitializer = controlInitializer; } }