org.tylproject.vaadin.addon.fieldbinder.FieldBinder.java Source code

Java tutorial

Introduction

Here is the source code for org.tylproject.vaadin.addon.fieldbinder.FieldBinder.java

Source

/*
 * Copyright (c) 2015 - Tyl Consulting s.a.s.
 *
 *   Authors: Edoardo Vacchi
 *   Contributors: Marco Pancotti, Daniele Zonca
 *
 * 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.tylproject.vaadin.addon.fieldbinder;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.util.BeanItem;
import com.vaadin.ui.Field;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.WrapDynaClass;
import org.tylproject.vaadin.addon.datanav.*;
import org.tylproject.vaadin.addon.datanav.events.CurrentItemChange;
import org.tylproject.vaadin.addon.datanav.events.EditingModeChange;
import org.tylproject.vaadin.addon.fieldbinder.behavior.DefaultBehaviorFactory;
import org.tylproject.vaadin.addon.fields.collectiontables.CollectionTable;
import org.tylproject.vaadin.addon.fields.collectiontables.ListTable;
import org.tylproject.vaadin.addon.fields.zoom.*;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.util.*;

/**
 * An enhanced version of Vaadin's standard {@link com.vaadin.data.fieldgroup.FieldGroup}
 *
 * The FieldBinder mimics the FieldGroup interface, but it supports more methods, and
 * it behaves in a slightly different way.
 *
 * It supports binding and unbinding elements, while still keeping track of which
 * elements are managed by the group. It also supports binding {@link com.vaadin.ui.Table} instances
 * to List values as their datasource, using {@link org.tylproject.vaadin.addon.fields.collectiontables.ListTable}
 * as their implementation.
 *
 * Example:
 *
 * <code><pre>
 *     public class Person {
 *         String name;
 *         List&lt;Address> addressList;
 *         // getters, setters
 *     }
 *     public class Address {
 *         String street, zipCode, city, state;
 *     }
 *     // ...
 *     FieldBinder&lt;Person> fieldBinder = new FieldBinder&lt;Person>(Person.class);
 *     Field&lt;String> name = fieldBinder.build("name");
 *     ListTable&lt;Address> fieldBinder.build("addressList");
 *     fieldBinder.setItemDataSource(...);
 *     fieldBinder.bindAll();
 * </pre></code>
 *
 */
public class FieldBinder<T> extends AbstractFieldBinder<FieldGroup> {

    private final WrapDynaClass dynaClass;
    private final Class<T> beanClass;
    private BasicDataNavigation navigation;
    private GridSupport gridSupport = GridSupport.UseTable;

    public FieldBinder(Class<T> beanClass) {
        super(new FieldGroup());
        this.beanClass = beanClass;
        this.dynaClass = WrapDynaClass.createDynaClass(beanClass);
        this.navigation = null;
    }

    /**
     * Creates a FieldBinder that will use the given container as the backing
     * for the values of its fields.
     *
     * {@link #getNavigation()} returns a controller on top of that container
     *
     *
     * @param beanClass
     * @param container
     */
    public FieldBinder(Class<T> beanClass, Container.Ordered container) {
        super(new FieldGroup());
        this.beanClass = beanClass;
        this.dynaClass = WrapDynaClass.createDynaClass(beanClass);

        BasicDataNavigation nav = new BasicDataNavigation(container);
        nav.setBehaviorFactory(new DefaultBehaviorFactory<>(this));

        this.navigation = nav;

    }

    /**
     * Opt-in to (very experimental!) grid support
     *
     */
    public FieldBinder<T> withGridSupport() {
        this.gridSupport = GridSupport.UseGrid;
        return this;
    }

    /**
     * set the default resource bundle.
     *
     * If a ResourceBundle is given, the captions are generated by looking up inside the bundle.
     * In particular, for a given <code>propertyId</code>, the key <code>propertyId</code>
     * is searched for. This is also true for propertyIds <i>inside</i> detail tables
     * (such as CollectionTables).
     *
     * For instance, if the entity is <code>Person(String name, List<Address> addressList)</code>
     * and <code>Address(String city)</code>, the resource bundle may define:
     *
     * <code>
     * name = ...
     * city = ...
     * </code>
     *
     * Caveat: nested properties may not currently work as expected
     */
    public FieldBinder<T> withResourceBundle(ResourceBundle resourceBundle) {
        this.getFieldFactory().setResourceBundle(resourceBundle);
        return this;
    }

    public void setItemDataSource(BeanItem<T> itemDataSource) {
        registerNestedPropertyIds(itemDataSource);
        super.setItemDataSource(itemDataSource);
    }

    private void registerNestedPropertyIds(BeanItem<T> itemDataSource) {
        for (Object propertyId : getBindingPropertyIds()) {
            if (isNestedProperty(propertyId)) {
                itemDataSource.addNestedProperty(propertyId.toString());
            }
        }
    }

    public void setBeanDataSource(T dataSource) {
        this.setItemDataSource(new BeanItem<T>(dataSource));
    }

    protected boolean isNestedProperty(Object propertyId) {
        return propertyId.toString().contains(".");
    }

    @Override
    public Item getItemDataSource() {
        return super.getItemDataSource();
    }

    // this is a bit dirty

    /**
     * Attempt to return a Bean from the currently bound Item instance.
     *
     * Caveat: this usually works when the Item is a BeanItem. In case the Item
     * is *not* a BeanItem, an attempt to retrieve the bean is done anyway:
     * the attempt is not guaranteed to succeed in this case.
     * The least requirement is that the Bean has an accessible empty constructor.
     *
     */
    public T getBeanDataSource() {
        Item item = getItemDataSource();
        if (item instanceof BeanItem) {
            return ((BeanItem<T>) this.getItemDataSource()).getBean();
        } else {
            BeanItem<T> beanItem = new BeanItem<T>(createBean());
            for (Object propId : item.getItemPropertyIds()) {
                Object value = item.getItemProperty(propId).getValue();
                beanItem.getItemProperty(propId).setValue(value);
            }
            return beanItem.getBean();
        }
    }

    protected T createBean() {
        try {
            Constructor<T> ctor = beanClass.getConstructor();
            return ctor.newInstance();
        } catch (Exception ex) {
            throw new UnsupportedOperationException(ex);
        }
    }

    /**
     * Generates a CollectionTable.
     *
     * This method automatically connects the generated CollectionTable to this FieldBinder.
     * The navigation of the FieldBinder and the navigation of the CollectionTable  will act in
     * coordination, enabling and disabling themselves when it is required.
     *
     * When the DataNavigation of the FieldBinder enters editing mode
     * ({@link org.tylproject.vaadin.addon.datanav.DataNavigation#enterEditingMode()})
     * then the navigation on the ListTable is disabled; when the navigation of the
     * ListTable enters editing mode, then the navigation of the FieldBinder is disabled.
     *
     *
     * @param containedBeanClass the class contained in the collection the propertyId refers to;
     *                           e.g., Address.class, if it is a List<Address>
     * @param propertyId         the propertyId that contains the collection (e.g., "addressList")
     *
     */
    public <U, C extends Collection<U>> CollectionTable<U, C> buildCollectionOf(Class<U> containedBeanClass,
            Object propertyId) {

        final Class<C> dataType = (Class<C>) getPropertyType(propertyId);
        final CollectionTable<U, C> collectionTable = getFieldFactory().createDetailField(dataType,
                containedBeanClass, gridSupport);

        bind(collectionTable, propertyId);

        this.getNavigation().addEditingModeChangeListener(new EditingModeSwitcher(collectionTable.getNavigation()));

        collectionTable.getNavigation().disableCrud();

        if (gridSupport == GridSupport.UseTable) {
            // field binder is currently only available for Tables!
            // propagate resourceBundle
            collectionTable.getFieldBinder().withResourceBundle(getFieldFactory().getResourceBundle());

            // the following is off by default:
            //     collectionTable.getFieldBinder().withGridSupport();
            // so no need to revert it
        }

        return collectionTable;
    }

    /**
     * Build a specialized CollectionTable to work with Lists.
     *
     * Use this version when you are sure that propertyId contains a List<T> for some T.
     * Otherwise, use {@link #buildCollectionOf(Class, Object)}.
     */
    public <U> ListTable<U> buildListOf(Class<U> containedBeanClass, Object propertyId) {
        ListTable<U> listTable = (ListTable<U>) this.<U, List<U>>buildCollectionOf(containedBeanClass, propertyId);
        return listTable;
    }

    /**
     * Build a ZoomField
     *
     * @param bindingPropertyId the propertyId that this field will be bound to
     * @param containerPropertyId the propertyId that will be displayed and selected from the ZoomDialog
     * @param zoomContainer the container instance onto which zoom
     *
     * @return the configure ZoomField instance
     */
    public TextZoomField buildZoomField(Object bindingPropertyId, Object containerPropertyId,
            Container.Indexed zoomContainer) {

        String caption = createCaptionByPropertyId(bindingPropertyId);

        TextZoomField field = new TextZoomField();
        field.setCaption(caption);
        field.withZoomDialog(makeDefaultZoomDialog(containerPropertyId, zoomContainer));

        bind(field, bindingPropertyId);

        return field;
    }

    /**
     * Build a DrillDownField.
     *
     * Equivalent to buildZoomField(bindingPropertyId, containerPropertyId, zoomContainer).drillDownOnly().
     */
    public TextZoomField buildDrillDownField(Object propertyId, Object containerPropertyId,
            Container.Indexed zoomContainer) {
        return this.buildZoomField(propertyId, containerPropertyId, zoomContainer).drillDownOnly();
    }

    /**
     * returns a GridZoomDialog if GridSupport is enabled; otherwise a TableZoomDialog
     */
    protected ZoomDialog makeDefaultZoomDialog(Object propertyId, Container.Indexed zoomCollection) {
        if (gridSupport == GridSupport.UseGrid) {
            return new GridZoomDialog(propertyId, zoomCollection);
        } else {
            return new TableZoomDialog(propertyId, zoomCollection);
        }
    }

    /**
     * Focus the first known field
     */
    public void focus() {
        if (getFields().isEmpty())
            return;
        getFields().iterator().next().focus();
    }

    /**
     * Generates a default button bar implementation binding to this FieldBinder's built-in
     * {@link org.tylproject.vaadin.addon.datanav.DataNavigation} instance
     *
     */
    public ButtonBar buildDefaultButtonBar(Container.Ordered container) {
        getNavigation().setContainer(container);
        return new ButtonBar(navigation);
    }

    /**
     * builds all the fields for all the properties of the Class {@link #getType()}
     *
     */
    public Collection<Field<?>> buildAll() {
        for (DynaProperty prop : dynaClass.getDynaProperties()) {
            build(prop.getName());
        }
        bindAll();
        return getFields();
    }

    /**
     * shorthand for invoking {@link #build(Object)} multiple times
     *
     * <code>build(propertyId1, propertyId2, propertyId3, ...)</code>
     */
    public Collection<Field<?>> buildAll(Object propertyId, Object... propertyIds) {
        build(propertyId);
        for (Object pid : propertyIds) {
            build(pid);
        }
        bindAll();
        return getFields();
    }

    /**
     * shorthand for invoking {@link #build(Object)} multiple times,
     * for the given collection of propertyIds
     */
    public Collection<Field<?>> buildAll(Collection<?> propertyIds) {
        for (Object pid : propertyIds) {
            build(pid);
        }
        bindAll();
        return getFields();
    }

    public Class<T> getType() {
        return beanClass;
    }

    /**
     * Retrieves the type of the property with the given name of the given
     * Class.
     *
     * Supports nested properties following bean naming convention.
     * e.g., "foo.bar.name"
     *
     * @see PropertyUtils#getPropertyDescriptors(Class)
     * @throws java.lang.IllegalArgumentException if the given propertyId does not exist
     */
    public Class<?> getPropertyType(Object propertyId) {
        if (propertyId == null)
            throw new IllegalArgumentException("PropertyName must not be null.");

        final String propertyName = propertyId.toString();

        final String[] path = propertyName.split("\\.");

        Class<?> propClass = beanClass;

        for (int i = 0; i < path.length; i++) {
            String propertyFragment = path[i];
            final PropertyDescriptor[] propDescs = PropertyUtils.getPropertyDescriptors(propClass);

            for (final PropertyDescriptor propDesc : propDescs)
                if (propDesc.getName().equals(propertyFragment)) {
                    propClass = propDesc.getPropertyType();
                    if (i == path.length - 1)
                        return propClass;
                }
        }

        throw new IllegalArgumentException("No such propertyId: " + propertyId);
    }

    public BasicDataNavigation getNavigation() {
        if (navigation == null)
            throw new IllegalStateException(
                    "Cannot return Navigation: no Container.Ordered instance was given at construction time");

        return navigation;
    }

    static class EditingModeSwitcher implements EditingModeChange.Listener {
        final DataNavigation otherNavigation;

        EditingModeSwitcher(DataNavigation other) {
            this.otherNavigation = other;
        }

        public void editingModeChange(EditingModeChange.Event event) {
            if (event.isEnteringEditingMode()) {
                otherNavigation.enableCrud();
            } else {
                otherNavigation.disableCrud();
            }
        }
    }
}