Java tutorial
/* * 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<Address> addressList; * // getters, setters * } * public class Address { * String street, zipCode, city, state; * } * // ... * FieldBinder<Person> fieldBinder = new FieldBinder<Person>(Person.class); * Field<String> name = fieldBinder.build("name"); * ListTable<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(); } } } }