org.jdal.ui.ViewSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.jdal.ui.ViewSupport.java

Source

/*
 * 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;
    }
}