All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jfxtras.labs.scene.control.BeanPathAdapter Maven / Gradle / Ivy

There is a newer version: 9.0-r1
Show newest version
/**
 * BeanPathAdapter.java
 *
 * Copyright (c) 2011-2015, JFXtras
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the organization nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package jfxtras.labs.scene.control;

import java.io.Serializable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.WeakReference;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.MapProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.scene.control.SelectionModel;
import javafx.util.StringConverter;

/**
 * An adapter that takes a POJO bean and internally and recursively
 * binds/un-binds it's fields to other {@link Property} components. It allows a
 * . separated field path to be traversed on a bean until
 * the final field name is found (last entry in the .
 * separated field path). Each field will have a corresponding {@link Property}
 * that is automatically generated and reused in the binding process. Each
 * {@link Property} is bean-aware and will dynamically update it's values and
 * bindings as different beans are set on the adapter. Bean's set on the adapter
 * do not need to instantiate all the sub-beans in the path(s) provided as long
 * as they contain a no-argument constructor they will be instantiated as
 * path(s) are traversed.
 * 
 * 

Examples:

*
    *
  1. * Binding bean fields to multiple JavaFX control properties of different * types: * *
     * // Assuming "age" is a double field in person we can bind it to a
     * // Slider#valueProperty() of type double, but we can also bind it
     * // to a TextField#valueProperty() of type String.
     * Person person = new Person();
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * Slider sl = new Slider();
     * TextField tf = new TextField();
     * personPA.bindBidirectional("age", sl.valueProperty());
     * personPA.bindBidirectional("age", tf.valueProperty());
     * 
    * *
  2. *
  3. * Binding beans within beans: * *
     * // Binding a bean (Person) field called "address" that contains another
     * // bean (Address) that also contains a field called "location" with a
     * // bean (Location) field of "state" (the chain can be virtually endless
     * // with all beans being instantiated along the way when null).
     * Person person = new Person();
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * TextField tf = new TextField();
     * personPA.bindBidirectional("address.location.state", tf.valueProperty());
     * 
    * *
  4. *
  5. * Binding non-primitive bean paths to JavaFX control properties of the same * non-primitive type: * *
     * // Assuming "address" is an "Address" field in a "Person" class we can bind it
     * // to a ComboBox#valueProperty() of the same type. The "Address" class should
     * // override the "toString()" method in order to show a meaningful selection
     * // value in the example ComboBox.
     * Address a1 = new Address();
     * Address a2 = new Address();
     * a1.setStreet("1st Street");
     * a2.setStreet("2nd Street");
     * ComboBox<Address> cb = new ComboBox<>();
     * cb.getItems().addAll(a1, a2);
     * Person person = new Person();
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * personPA.bindBidirectional("address", cb.valueProperty(), Address.class);
     * 
    * *
  6. *
  7. * Binding collections/maps fields to/from observable collections/maps (i.e. * items in a JavaFX control): * *
     * // Assuming "allLanguages" is a collection/map field in person we can
     * // bind it to a JavaFX observable collection/map
     * Person person = new Person();
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * ListView<String> lv = new ListView<>();
     * personPA.bindContentBidirectional("allLanguages", null, String.class,
     * 		lv.getItems(), String.class, null, null);
     * 
    * *
  8. *
  9. * Binding collections/maps fields to/from observable collections/maps * selections (i.e. selections in a JavaFX control): * *
     * // Assuming "languages" is a collection/map field in person we can
     * // bind it to a JavaFX observable collection/map selections
     * Person person = new Person();
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * ListView<String> lv = new ListView<>();
     * personPA.bindContentBidirectional("languages", null, String.class, lv
     * 		.getSelectionModel().getSelectedItems(), String.class, lv
     * 		.getSelectionModel(), null);
     * 
    * *
  10. *
  11. * Binding collection/map fields to/from observable collections/maps * selections using an items from another observable collection/map as a * reference (i.e. selections in a JavaFX control that contain the same * instances as what are in the items being selected from): * *
     * // Assuming "languages" and "allLanguages" are a collection/map
     * // fields in person we can bind "languages" to selections made from
     * // the items in "allLanguages" to a JavaFX observable collection/map
     * // selection Person person = new Person();
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * ListView<String> lv = new ListView<>();
     * personPA.bindContentBidirectional("languages", null, String.class, lv
     * 		.getSelectionModel().getSelectedItems(), String.class, lv
     * 		.getSelectionModel(), "allLanguages");
     * 
    * *
  12. *
  13. * Binding complex bean collection/map fields to/from observable * collections/maps selections and items (i.e. selections in a JavaFX control * that contain the same bean instances as what are in the items being * selected): * *
     * // Assuming "hobbies" and
     * "allHobbies" are a collection/map // fields in person and each element within
     * them contain an // instance of Hobby that has it's own field called "name" //
     * we can bind "allHobbies" and "hobbies" to the Hobby "name"s // for each Hobby
     * in the items/selections (respectively) to/from // a ListView wich will only
     * contain the String name of each Hobby // as it's items and selections Person
     * person = new Person(); BeanPathAdapter<Person> personPA = new
     * BeanPathAdapter<>(person); ListView<String> lv = new ListView<>();
     * // bind items 
     * personPA.bindContentBidirectional("allHobbies", "name", Hobby.class,
     * 		lv.getItems(), String.class, null, null);
     * // bind selections that reference the same instances within the items
     * personPA.bindContentBidirectional("languages", "name", Hobby.class,
     * 		lv.getSelectionModel().getSelectedItems(), String.class,
     * 		lv.getSelectionModel(), "allHobbiess");
     * 
    * *
  14. *
  15. * Binding bean collection/map fields to/from multiple JavaFX control * observable collections/maps of the same type (via bean collection/map): * *
     * Person person = new Person();
     * Hobby hobby1 = new Hobby();
     * hobby1.setName("Hobby 1");
     * Hobby hobby2 = new Hobby();
     * hobby2.setName("Hobby 2");
     * person.setAllHobbies(new LinkedHashSet<Hobby>());
     * person.getAllHobbies().add(hobby1);
     * person.getAllHobbies().add(hobby2);
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * ListView<String> lv = new ListView<>();
     * personPA.bindContentBidirectional("allHobbies", "name", Hobby.class,
     * 		lv.getItems(), String.class, null, null);
     * ListView<String> lv2 = new ListView<>();
     * personPA.bindContentBidirectional("allHobbies", "name", Hobby.class,
     * 		lv2.getItems(), String.class, null, null);
     * 
    * *
  16. *
  17. * Binding bean collection/map fields to/from multiple JavaFX control * observable collections/maps of the same type (via JavaFX control observable * collection/map): * *
     * // When the bean collection/map field is empty/null and it is
     * // bound to a non-empty observable collection/map, the values
     * // of the observable are used to instantiate each item bean
     * // and set the item value (Hobby#setName in this case)
     * Person person = new Person();
     * final ObservableList<String> oc = FXCollections.observableArrayList("Hobby 1",
     * 		"Hobby 2", "Hobby 3");
     * BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * ListView<String> lv = new ListView<>(oc);
     * personPA.bindContentBidirectional("allHobbies", "name", Hobby.class,
     * 		lv.getItems(), String.class, null, null);
     * ListView<String> lv2 = new ListView<>(); // <-- notice that oc is not passed
     * personPA.bindContentBidirectional("allHobbies", "name", Hobby.class,
     * 		lv2.getItems(), String.class, null, null);
     * 
    * *
  18. *
  19. * Switching beans: * *
     * // Assuming "age" is a double field in person...
     * final Person person1 = new Person();
     * person1.setAge(1D);
     * final Person person2 = new Person();
     * person2.setAge(2D);
     * final BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person1);
     * TextField tf = new TextField();
     * personPA.bindBidirectional("age", tf.valueProperty());
     * Button btn = new Button("Toggle People");
     * btn.setOnMouseClicked(new EventHandler<MouseEvent>() {
     * 	public void handle(MouseEvent event) {
     * 		// all bindings will show relevant person data and changes made
     * 		// to the bound controls will be reflected in the bean that is
     * 		// set at the time of the change
     * 		personPA.setBean(personPA.getBean() == person1 ? person2 : person1);
     * 	}
     * });
     * 
    * *
  20. *
  21. * {@link Date}/{@link Calendar} binding: * *
     * // Assuming "dob" is a java.util.Date or java.util.Calendar field
     * // in person it can be bound to a java.util.Date or
     * // java.util.Calendar JavaFX control property. Example uses a
     * // jfxtras.labs.scene.control.CalendarPicker
     * final Person person = new Person();
     * final BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * CalendarPicker calendarPicker = new CalendarPicker();
     * personPA.bindBidirectional("dob", calendarPicker.calendarProperty(),
     * 		Calendar.class);
     * 
    * *
  22. *
  23. * {@link javafx.scene.control.TableView} binding: * *
     * // Assuming "name"/"description" are a java.lang.String fields in Hobby
     * // and "hobbies" is a List/Set/Map in Person
     * final Person person = new Person();
     * final BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * TableView<Hobby> table = new TableView<>();
     * TableColumn<Hobby, String> nameCol = new TableColumn<>("Hobby Name");
     * nameCol.setMinWidth(100);
     * nameCol.setCellValueFactory(new PropertyValueFactory<Hobby, String>("name"));
     * TableColumn<Hobby, String> descCol = new TableColumn<>("Hobby Desc");
     * descCol.setMinWidth(100);
     * descCol.setCellValueFactory(new PropertyValueFactory<Hobby, String>(
     * 		"description"));
     * table.getColumns().addAll(nameCol, descCol);
     * personPA.bindContentBidirectional("hobbies", null, String.class,
     * 		table.getItems(), Hobby.class, null, null);
     * 
    * *
  24. *
  25. * Listening for global changes: * *
     * final BeanPathAdapter<Person> personPA = new BeanPathAdapter<>(person);
     * // use the following to eliminate unwanted notifications
     * // personPA.removeFieldPathValueTypes(FieldPathValueType.BEAN_CHANGE, ...)
     * personPA.fieldPathValueProperty().addListener(
     * 		new ChangeListener<FieldPathValue>() {
     * 			@Override
     * 			public void changed(
     * 					final ObservableValue<? extends FieldPathValue> observable,
     * 					final FieldPathValue oldValue, final FieldPathValue newValue) {
     * 				System.out.println("Value changed from: " + oldValue + " to: "
     * 						+ newValue);
     * 			}
     * 		});
     * 
    * *
  26. *
* * @see #bindBidirectional(String, Property) * @see #bindContentBidirectional(String, String, Class, ObservableList, Class, * SelectionModel, String) * @see #bindContentBidirectional(String, String, Class, ObservableSet, Class, * SelectionModel, String) * @see #bindContentBidirectional(String, String, Class, ObservableMap, Class, * SelectionModel, String) * @param * the bean type */ public class BeanPathAdapter { public static final char PATH_SEPARATOR = '.'; public static final char COLLECTION_ITEM_PATH_SEPARATOR = '#'; private FieldBean root; private FieldPathValueProperty fieldPathValueProperty = new FieldPathValueProperty(); /** * Constructor * * @param bean * the bean the {@link BeanPathAdapter} is for */ public BeanPathAdapter(final B bean) { setBean(bean); } /** * @see #bindBidirectional(String, Property, Class) */ public void bindBidirectional(final String fieldPath, final BooleanProperty property) { bindBidirectional(fieldPath, property, Boolean.class); } /** * @see #bindBidirectional(String, Property, Class) */ public void bindBidirectional(final String fieldPath, final StringProperty property) { bindBidirectional(fieldPath, property, String.class); } /** * @see #bindBidirectional(String, Property, Class) */ public void bindBidirectional(final String fieldPath, final Property property) { bindBidirectional(fieldPath, property, null); } /** * Binds a {@link ObservableList} by traversing the bean's field tree. An * additional item path can be specified when the path points to a * {@link Collection} that contains beans that also need traversed in order * to establish the final value. For example: If a field path points to * phoneNumbers (relative to the {@link #getBean()}) where * phoneNumbers is a {@link Collection} that contains * PhoneNumber instances which in turn have a field called * areaCode then an item path can be passed in addition to the * field path with areaCode as it's value. * * @param fieldPath * the . separated field paths relative to * the {@link #getBean()} that will be traversed * @param itemFieldPath * the . separated field paths relative to * each item in the bean's underlying {@link Collection} that * will be traversed (empty/null when each item value does not * need traversed) * @param itemFieldPathType * the {@link Class} of that the item path points to * @param list * the {@link ObservableList} to bind to the field class type of * the property * @param listValueType * the class type of the {@link ObservableList} value * @param selectionModel * the {@link SelectionModel} used to set the values within the * {@link ObservableList} only applicable when the * {@link ObservableList} is used for selection(s) and therefore * cannot be updated directly because it is read-only * @param selectionModelItemMasterPath * when binding to {@link SelectionModel} items, this will be the * optional path to the collection field that contains all the * items to select from */ public void bindContentBidirectional(final String fieldPath, final String itemFieldPath, final Class itemFieldPathType, final ObservableList list, final Class listValueType, final SelectionModel selectionModel, final String selectionModelItemMasterPath) { FieldProperty itemMaster = null; if (selectionModelItemMasterPath != null && !selectionModelItemMasterPath.isEmpty()) { itemMaster = getRoot().performOperation( selectionModelItemMasterPath, list, listValueType, itemFieldPath, itemFieldPathType, null, null, FieldBeanOperation.CREATE_OR_FIND); } getRoot().performOperation(fieldPath, list, listValueType, itemFieldPath, itemFieldPathType, selectionModel, itemMaster, FieldBeanOperation.BIND); } /** * Binds a {@link ObservableSet} by traversing the bean's field tree. An * additional item path can be specified when the path points to a * {@link Collection} that contains beans that also need traversed in order * to establish the final value. For example: If a field path points to * phoneNumbers (relative to the {@link #getBean()}) where * phoneNumbers is a {@link Collection} that contains * PhoneNumber instances which in turn have a field called * areaCode then an item path can be passed in addition to the * field path with areaCode as it's value. * * @param fieldPath * the . separated field paths relative to * the {@link #getBean()} that will be traversed * @param itemFieldPath * the . separated field paths relative to * each item in the bean's underlying {@link Collection} that * will be traversed (empty/null when each item value does not * need traversed) * @param itemFieldPathType * the {@link Class} of that the item path points to * @param set * the {@link ObservableSet} to bind to the field class type of * the property * @param setValueType * the class type of the {@link ObservableSet} value * @param selectionModel * the {@link SelectionModel} used to set the values within the * {@link ObservableSet} only applicable when the * {@link ObservableSet} is used for selection(s) and therefore * cannot be updated directly because it is read-only * @param selectionModelItemMasterPath * when binding to {@link SelectionModel} items, this will be the * optional path to the collection field that contains all the * items to select from */ public void bindContentBidirectional(final String fieldPath, final String itemFieldPath, final Class itemFieldPathType, final ObservableSet set, final Class setValueType, final SelectionModel selectionModel, final String selectionModelItemMasterPath) { FieldProperty itemMaster = null; if (selectionModelItemMasterPath != null && !selectionModelItemMasterPath.isEmpty()) { itemMaster = getRoot().performOperation( selectionModelItemMasterPath, set, setValueType, itemFieldPath, itemFieldPathType, null, null, FieldBeanOperation.CREATE_OR_FIND); } getRoot().performOperation(fieldPath, set, setValueType, itemFieldPath, itemFieldPathType, selectionModel, itemMaster, FieldBeanOperation.BIND); } /** * Binds a {@link ObservableMap} by traversing the bean's field tree. An * additional item path can be specified when the path points to a * {@link Collection} that contains beans that also need traversed in order * to establish the final value. For example: If a field path points to * phoneNumbers (relative to the {@link #getBean()}) where * phoneNumbers is a {@link Collection} that contains * PhoneNumber instances which in turn have a field called * areaCode then an item path can be passed in addition to the * field path with areaCode as it's value. * * @param fieldPath * the . separated field paths relative to * the {@link #getBean()} that will be traversed * @param itemFieldPath * the . separated field paths relative to * each item in the bean's underlying {@link Collection} that * will be traversed (empty/null when each item value does not * need traversed) * @param itemFieldPathType * the {@link Class} of that the item path points to * @param map * the {@link ObservableMap} to bind to the field class type of * the property * @param mapValueType * the class type of the {@link ObservableMap} value * @param selectionModel * the {@link SelectionModel} used to set the values within the * {@link ObservableMap} only applicable when the * {@link ObservableMap} is used for selection(s) and therefore * cannot be updated directly because it is read-only * @param selectionModelItemMasterPath * when binding to {@link SelectionModel} items, this will be the * optional path to the collection field that contains all the * items to select from */ public void bindContentBidirectional(final String fieldPath, final String itemFieldPath, final Class itemFieldPathType, final ObservableMap map, final Class mapValueType, final SelectionModel selectionModel, final String selectionModelItemMasterPath) { FieldProperty itemMaster = null; if (selectionModelItemMasterPath != null && !selectionModelItemMasterPath.isEmpty()) { itemMaster = getRoot().performOperation( selectionModelItemMasterPath, map, mapValueType, itemFieldPath, itemFieldPathType, null, null, FieldBeanOperation.CREATE_OR_FIND); } getRoot().performOperation(fieldPath, map, mapValueType, itemFieldPath, itemFieldPathType, selectionModel, itemMaster, FieldBeanOperation.BIND); } /** * Binds a {@link Property} by traversing the bean's field tree * * @param fieldPath * the . separated field paths relative to * the {@link #getBean()} that will be traversed * @param property * the {@link Property} to bind to the field class type of the * property * @param propertyType * the class type of the {@link Property} value */ @SuppressWarnings("unchecked") public void bindBidirectional(final String fieldPath, final Property property, final Class propertyType) { Class clazz = propertyType != null ? propertyType : propertyValueClass(property); if (clazz == null && property.getValue() != null) { clazz = (Class) property.getValue().getClass(); } if (clazz == null || clazz == Object.class) { throw new UnsupportedOperationException(String.format( "Unable to determine property value class for %1$s " + "and declared type %2$s", property, propertyType)); } getRoot().performOperation(fieldPath, property, clazz, FieldBeanOperation.BIND); } /** * Unbinds a {@link Property} by traversing the bean's field tree * * @param fieldPath * the . separated field paths relative to * the {@link #getBean()} that will be traversed * @param property * the {@link Property} to bind to the field class type of the * property */ public void unBindBidirectional(final String fieldPath, final Property property) { getRoot().performOperation(fieldPath, property, null, FieldBeanOperation.UNBIND); } /** * @return the bean of the {@link BeanPathAdapter} */ public B getBean() { return getRoot().getBean(); } /** * Sets the root bean of the {@link BeanPathAdapter}. Any existing * properties will be updated with the values relative to the paths within * the bean. * * @param bean * the bean to set */ public void setBean(final B bean) { if (bean == null) { throw new NullPointerException(); } if (getRoot() == null) { this.root = new FieldBean<>(null, bean, null, fieldPathValueProperty); } else { getRoot().setBean(bean); } if (hasFieldPathValueTypes(FieldPathValueType.BEAN_CHANGE)) { fieldPathValueProperty.set(new FieldPathValue(null, getBean(), getBean(), FieldPathValueType.BEAN_CHANGE)); } } /** * @return the root/top level {@link FieldBean} */ protected final FieldBean getRoot() { return this.root; } /** * @see #addFieldPathValueTypes(FieldPathValueType...) * @see #removeFieldPathValueTypes(FieldPathValueType...) * @see #hasFieldPathValueTypes(FieldPathValueType...) * @return the {@link ReadOnlyObjectProperty} that contains the last path * that was changed in the {@link BeanPathAdapter}. For * notifications for items bound using content bindings * (collections/maps) */ public final ReadOnlyObjectProperty fieldPathValueProperty() { return fieldPathValueProperty.getReadOnlyProperty(); } /** * Provides the underlying value class for a given {@link Property} * * @param property * the {@link Property} to check * @return the value class of the {@link Property} */ @SuppressWarnings("unchecked") protected static Class propertyValueClass(final Property property) { Class clazz = null; if (property != null) { if (StringProperty.class.isAssignableFrom(property.getClass())) { clazz = (Class) String.class; } else if (IntegerProperty.class.isAssignableFrom(property .getClass())) { clazz = (Class) Integer.class; } else if (BooleanProperty.class.isAssignableFrom(property .getClass())) { clazz = (Class) Boolean.class; } else if (DoubleProperty.class.isAssignableFrom(property .getClass())) { clazz = (Class) Double.class; } else if (FloatProperty.class .isAssignableFrom(property.getClass())) { clazz = (Class) Float.class; } else if (LongProperty.class.isAssignableFrom(property.getClass())) { clazz = (Class) Long.class; } else if (ListProperty.class.isAssignableFrom(property.getClass())) { clazz = (Class) List.class; } else if (MapProperty.class.isAssignableFrom(property.getClass())) { clazz = (Class) Map.class; } else { clazz = (Class) Object.class; } } return clazz; } /** * Adds {@link FieldPathValueType}(s) {@link FieldPathValueType}(s) that * {link notifyProperty()} will use * * @param types * the {@link FieldPathValueType} to add */ public void addFieldPathValueTypes(final FieldPathValueType... types) { fieldPathValueProperty.addRemoveTypes(true, types); } /** * Removes {@link FieldPathValueType}(s) {@link FieldPathValueType}(s) that * {link notifyProperty()} will use * * @param types * the {@link FieldPathValueType}(s) to remove */ public void removeFieldPathValueTypes(final FieldPathValueType... types) { fieldPathValueProperty.addRemoveTypes(false, types); } /** * Determines if the {@link FieldPathValueType}(s) are being used by the * {link notifyProperty()} * * @param types * the {@link FieldPathValueType}(s) to check for * @return true if all of the specified {@link FieldPathValueType}(s) exist */ public boolean hasFieldPathValueTypes(final FieldPathValueType... types) { return fieldPathValueProperty.hasTypes(types); } /** * The {@link ReadOnlyObjectWrapper} that contains the last path that was * changed in the {@link BeanPathAdapter} */ static class FieldPathValueProperty extends ReadOnlyObjectWrapper { private final Set types; /** * Constructor */ public FieldPathValueProperty() { super(); this.types = new HashSet<>(); addRemoveTypes(true, FieldPathValueType.values()); } /** * Adds/Removes {@link FieldPathValueType}(s) * * @param add * true to add, false to remove * @param types * the {@link FieldPathValueType}(s) to add/remove */ public void addRemoveTypes(final boolean add, final FieldPathValueType... types) { if (types.length <= 0) { return; } if (add) { Collections.addAll(this.types, types); } else { for (final FieldPathValueType t : types) { this.types.remove(t); } } } /** * Determines if the {link getTypes()} has all of the specified * {@link FieldPathValueType}(s) * * @param types * the {@link FieldPathValueType}(s) to check for * @return true if all of the specified {@link FieldPathValueType}(s) * exist */ public boolean hasTypes(final FieldPathValueType... types) { if (types.length <= 0) { return false; } for (final FieldPathValueType type : types) { if (!this.types.contains(type)) { return false; } } return true; } } /** * Field {@link #getPath()}/{@link #getValue()} */ public static class FieldPathValue { private final String path; private final Object bean; private final Object value; private final FieldPathValueType type; /** * Constructor * * @param path * the {@link #getPath()} * @param bean * the {@link #getBean()} * @param value * the {@link #getValue()} * @param type */ public FieldPathValue(final String path, final Object bean, final Object value, final FieldPathValueType type) { this.path = path; this.bean = bean; this.value = value; this.type = type; } /** * Generates a hash code using {@link #getPath()}, {@link #getBean()}, * {@link #getValue()}, and {link isFromItemSelection()} * * @return the hash code */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((bean == null) ? 0 : bean.hashCode()); result = prime * result + ((path == null) ? 0 : path.hashCode()); result = prime * result + ((value == null) ? 0 : value.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } /** * Determines equality based upon {@link #getPath()}, {@link #getBean()} * , {@link #getValue()}, and {link isFromItemSelection()} * * @param obj * the {@link Object} to check for equality * @return true when equal */ @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } FieldPathValue other = (FieldPathValue) obj; if (bean == null) { if (other.bean != null) { return false; } } else if (!bean.equals(other.bean)) { return false; } if (path == null) { if (other.path != null) { return false; } } else if (!path.equals(other.path)) { return false; } if (value == null) { if (other.value != null) { return false; } } else if (!value.equals(other.value)) { return false; } if (type != other.type) { return false; } return true; } /** * {@inheritDoc} */ @Override public String toString() { return FieldPathValue.class.getSimpleName() + " [path=" + path + ", value=" + value + ", type=" + type + ", bean=" + bean + "]"; } /** * @return the {@link BeanPathAdapter#PATH_SEPARATOR} separated path of * the field value that changed. When the path involves an item * within a collection the path to the item will be separated * with {@link BeanPathAdapter#COLLECTION_ITEM_PATH_SEPARATOR} */ public String getPath() { return path; } /** * @return the bean that the {@link #getValue()} belongs to */ public Object getBean() { return bean; } /** * @return value of the field path that changed */ public Object getValue() { return value; } /** * @return the {@link FieldPathValueType} */ public FieldPathValueType getType() { return type; } } /** * {@link FieldPathValue} types used for {@link FieldPathValueProperty} * changes */ public static enum FieldPathValueType { /** * Root bean change (from a {@link BeanPathAdapter#setBean(Object)} * operation) */ BEAN_CHANGE, /** * General field binding change (not from a * {@link BeanPathAdapter#setBean(Object)} operation) */ FIELD_CHANGE, /** Item added via content binding */ CONTENT_ITEM_ADD, /** Item removed via content binding */ CONTENT_ITEM_REMOVE, /** Selection item added via content binding */ CONTENT_ITEM_ADD_SELECT, /** Selection item removed via content binding */ CONTENT_ITEM_REMOVE_SELECT; } /** * {@link FieldBean} operations */ public static enum FieldBeanOperation { BIND, UNBIND, CREATE_OR_FIND; } /** * A POJO bean extension that allows binding based upon a . * separated field path that will be traversed on a bean until the * final field name is found. Each bean may contain child {@link FieldBean}s * when an operation is perfomed with a direct descendant field that is a * non-primitive type. Any primitive types are added as a * {@link FieldProperty} reference to the {@link FieldBean}. * * @param * the parent bean type * @param * the bean type */ protected static class FieldBean implements Serializable { private static final long serialVersionUID = 7397535724568852021L; private final FieldPathValueProperty notifyProperty; private final Map> fieldBeans = new HashMap<>(); private final Map> fieldProperties = new HashMap<>(); private final Map> fieldSelectionProperties = new HashMap<>(); private final Map, FieldStringConverter> stringConverters = new HashMap<>(); private FieldHandle fieldHandle; private final FieldBean parent; private BT bean; /** * Creates a {@link FieldBean} * * @param parent * the parent {@link FieldBean} (should not be null) * @param fieldHandle * the {@link FieldHandle} (should not be null) * @param notifyProperty * the {@link FieldPathValueProperty} that will be set every * time the {@link FieldBean#setBean(Object)} is changed */ protected FieldBean(final FieldBean parent, final FieldHandle fieldHandle, final FieldPathValueProperty notifyProperty) { this.parent = parent; this.fieldHandle = fieldHandle; this.bean = this.fieldHandle.setDerivedValueFromAccessor(); this.notifyProperty = notifyProperty; if (getParent() != null) { getParent().addFieldBean(this); } } /** * Creates a {@link FieldBean} with a generated {@link FieldHandle} that * targets the supplied bean and is projected on the parent * {@link FieldBean}. It assumes that the supplied {@link FieldBean} has * been set on the parent {@link FieldBean}. * * @see #createFieldHandle(Object, Object, String) * @param parent * the parent {@link FieldBean} (null when it's the root) * @param bean * the bean that the {@link FieldBean} is for * @param fieldName * the field name of the parent {@link FieldBean} for which * the new {@link FieldBean} is for * @param notifyProperty * the {@link FieldPathValueProperty} that will be set every * time the {@link FieldBean#setBean(Object)} is changed */ protected FieldBean(final FieldBean parent, final BT bean, final String fieldName, final FieldPathValueProperty notifyProperty) { if (bean == null) { throw new NullPointerException("Bean cannot be null"); } this.parent = parent; this.bean = bean; this.notifyProperty = notifyProperty; this.fieldHandle = getParent() != null ? createFieldHandle( getParent().getBean(), bean, fieldName) : null; if (getParent() != null) { getParent().addFieldBean(this); } } /** * Generates a {@link FieldHandle} that targets the supplied bean and is * projected on the parent {@link FieldBean} that has * * @param parentBean * the parent bean * @param bean * the child bean * @param fieldName * the field name of the child within the parent * @return the {@link FieldHandle} */ @SuppressWarnings("unchecked") protected FieldHandle createFieldHandle(final PT parentBean, final BT bean, final String fieldName) { return new FieldHandle(parentBean, fieldName, (Class) getBean().getClass()); } /** * @see #setParentBean(Object) * @return the bean that the {@link FieldBean} represents */ public BT getBean() { return bean; } /** * Adds a child {@link FieldBean} if it doesn't already exist. NOTE: It * does NOT ensure the child bean has been set on the parent. * * @param fieldBean * the {@link FieldBean} to add */ protected void addFieldBean(final FieldBean fieldBean) { if (!getFieldBeans().containsKey(fieldBean.getFieldName())) { getFieldBeans().put(fieldBean.getFieldName(), fieldBean); } } /** * Adds or updates a child {@link FieldProperty}. When the child already * exists it will {@link FieldProperty#setTarget(Object)} using the bean * of the {@link FieldProperty}. * * @param fieldProperty * the {@link FieldProperty} to add or update */ protected void addOrUpdateFieldProperty( final FieldProperty fieldProperty) { final String pkey = fieldProperty.getName(); if (getFieldProperties().containsKey(pkey)) { getFieldProperties().get(pkey).setTarget( fieldProperty.getBean()); } else if (getFieldSelectionProperties().containsKey(pkey)) { getFieldSelectionProperties().get(pkey).setTarget( fieldProperty.getBean()); } else if (fieldProperty.hasItemMaster()) { getFieldSelectionProperties().put(pkey, fieldProperty); } else { getFieldProperties().put(pkey, fieldProperty); } } /** * Sets the bean of the {@link FieldBean} and it's underlying * {@link #getFieldBeans()}, {@link #getFieldProperties()}, and * {@link #getFieldSelectionProperties()} * * @see #setParentBean(Object) * @param bean * the bean to set */ public void setBean(final BT bean) { if (bean == null) { throw new NullPointerException("Bean cannot be null"); } this.bean = bean; for (final Map.Entry> fn : getFieldBeans() .entrySet()) { fn.getValue().setParentBean(getBean()); } // selections need to be set before non-selections so that item // master listeners in the selection properties will have the // updated values by the time changes are detected on the item // masters for (final Map.Entry> fp : getFieldSelectionProperties() .entrySet()) { fp.getValue().setTarget(getBean()); } for (final Map.Entry> fp : getFieldProperties() .entrySet()) { fp.getValue().setTarget(getBean()); } } /** * Binds a parent bean to the {@link FieldBean} and it's underlying * {@link #getFieldBeans()}, {@link #getFieldProperties()}, and * {@link #getFieldSelectionProperties()} * * @see #setBean(Object) * @param bean * the parent bean to bind to */ public void setParentBean(final PT bean) { if (bean == null) { throw new NullPointerException("Cannot bind to a null bean"); } else if (fieldHandle == null) { throw new IllegalStateException("Cannot bind to a root " + FieldBean.class.getSimpleName()); } fieldHandle.setTarget(bean); setBean(fieldHandle.setDerivedValueFromAccessor()); } /** * @see BeanPathAdapter.FieldBean#performOperation(String, String, * Class, String, Observable, Class, SelectionModel, FieldProperty, * FieldBeanOperation) */ public FieldProperty performOperation( final String fieldPath, final Property property, final Class propertyValueClass, final FieldBeanOperation operation) { return performOperation(fieldPath, fieldPath, propertyValueClass, null, (Observable) property, null, null, null, operation); } /** * @see BeanPathAdapter.FieldBean#performOperation(String, String, * Class, String, Observable, Class, SelectionModel, FieldProperty, * FieldBeanOperation) */ public FieldProperty performOperation( final String fieldPath, final ObservableList observableList, final Class listValueClass, final String collectionItemPath, final Class collectionItemPathType, final SelectionModel selectionModel, final FieldProperty itemMaster, final FieldBeanOperation operation) { return performOperation(fieldPath, fieldPath, listValueClass, collectionItemPath, (Observable) observableList, collectionItemPathType, selectionModel, itemMaster, operation); } /** * @see BeanPathAdapter.FieldBean#performOperation(String, String, * Class, String, Observable, Class, SelectionModel, FieldProperty, * FieldBeanOperation) */ public FieldProperty performOperation( final String fieldPath, final ObservableSet observableSet, final Class setValueClass, final String collectionItemPath, final Class collectionItemPathType, final SelectionModel selectionModel, final FieldProperty itemMaster, final FieldBeanOperation operation) { return performOperation(fieldPath, fieldPath, setValueClass, collectionItemPath, (Observable) observableSet, collectionItemPathType, selectionModel, itemMaster, operation); } /** * @see BeanPathAdapter.FieldBean#performOperation(String, String, * Class, String, Observable, Class, SelectionModel, FieldProperty, * FieldBeanOperation) */ public FieldProperty performOperation( final String fieldPath, final ObservableMap observableMap, final Class mapValueClass, final String collectionItemPath, final Class collectionItemPathType, final SelectionModel selectionModel, final FieldProperty itemMaster, final FieldBeanOperation operation) { return performOperation(fieldPath, fieldPath, mapValueClass, collectionItemPath, (Observable) observableMap, collectionItemPathType, selectionModel, itemMaster, operation); } /** * Performs a {@link FieldBeanOperation} by generating a * {@link FieldProperty} based upon the supplied . * separated path to the field by traversing the matching children of * the {@link FieldBean} until the corresponding {@link FieldProperty} * is found (target bean uses the POJO from {@link FieldBean#getBean()} * ). If the operation is bind and the {@link FieldProperty} doesn't * exist all relative {@link FieldBean}s in the path will be * instantiated using a no-argument constructor until the * {@link FieldProperty} is created and bound to the supplied * {@link Property}. The process is reciprocated until all path * {@link FieldBean} and {@link FieldProperty} attributes of the field * path are extinguished. * * @see Bindings#bindBidirectional(Property, Property) * @see Bindings#unbindBidirectional(Property, Property) * @param fullFieldPath * the full . separated field names (used in * recursion of method call to maintain the original path and * should not be used in initial method invocation) * @param fieldPath * the . separated field names * @param propertyValueClass * the class of the {@link Property} value type (only needed * when binding) * @param collectionItemPath * the the . separated field names of the * {@link Observable} collection (only applicable when the * {@link Observable} is a {@link ObservableList}, * {@link ObservableSet}, or {@link ObservableMap}) * @param observable * the {@link Property}, {@link ObservableList}, * {@link ObservableSet}, or {@link ObservableMap} to perform * the {@link FieldBeanOperation} on * @param collectionItemType * the {@link Observable} {@link Class} of each item in the * {@link Observable} collection (only applicable when the * {@link Observable} is a {@link ObservableList}, * {@link ObservableSet}, or {@link ObservableMap}) * @param selectionModel * the {@link SelectionModel} used to set the values within * the {@link Observable} only applicable when the * {@link Observable} is used for selection(s) and therefore * cannot be updated directly because it is read-only * @param itemMaster * the {@link FieldProperty} that contains the item(s) that * the {@link SelectionModel} can select from * @param operation * the {@link FieldBeanOperation} * @return the {@link FieldProperty} the operation was performed on * (null when the operation was not performed on any * {@link FieldProperty} */ protected FieldProperty performOperation( final String fullFieldPath, final String fieldPath, final Class propertyValueClass, final String collectionItemPath, final Observable observable, final Class collectionItemType, final SelectionModel selectionModel, final FieldProperty itemMaster, final FieldBeanOperation operation) { final String[] fieldNames = fieldPath.split("\\" + PATH_SEPARATOR); final boolean isField = fieldNames.length == 1; final String pkey = isField ? fieldNames[0] : ""; final boolean isFieldProp = isField && getFieldProperties().containsKey(pkey); final boolean isFieldSelProp = isField && !isFieldProp && getFieldSelectionProperties().containsKey(pkey); if (isFieldProp || isFieldSelProp) { final FieldProperty fp = isFieldSelProp ? getFieldSelectionProperties() .get(pkey) : getFieldProperties().get(pkey); performOperation(fp, observable, propertyValueClass, operation); return fp; } else if (!isField && getFieldBeans().containsKey(fieldNames[0])) { // progress to the next child field/bean in the path chain final String nextFieldPath = fieldPath.replaceFirst( fieldNames[0] + PATH_SEPARATOR, ""); return getFieldBeans().get(fieldNames[0]).performOperation( fullFieldPath, nextFieldPath, propertyValueClass, collectionItemPath, observable, collectionItemType, selectionModel, itemMaster, operation); } else if (operation != FieldBeanOperation.UNBIND) { // add a new bean/property chain if (isField) { final Class fieldClass = FieldHandle.getAccessorType( getBean(), fieldNames[0]); final FieldProperty childProp = new FieldProperty/*won't compile in JDK8: <>*/( getBean(), fullFieldPath, fieldNames[0], notifyProperty, propertyValueClass == fieldClass ? fieldClass : Object.class, collectionItemPath, observable, collectionItemType, selectionModel, itemMaster); addOrUpdateFieldProperty(childProp); return performOperation(fullFieldPath, fieldNames[0], propertyValueClass, collectionItemPath, observable, collectionItemType, selectionModel, itemMaster, operation); } else { // create a handle to set the bean as a child of the current // bean // if the child bean exists on the bean it will remain // unchanged final FieldHandle pfh = new FieldHandle<>( getBean(), fieldNames[0], Object.class); final FieldBean childBean = new FieldBean<>(this, pfh, notifyProperty); // progress to the next child field/bean in the path chain final String nextFieldPath = fieldPath.substring(fieldPath .indexOf(PATH_SEPARATOR + fieldNames[1]) + 1); return childBean.performOperation(fullFieldPath, nextFieldPath, propertyValueClass, collectionItemPath, observable, collectionItemType, selectionModel, itemMaster, operation); } } return null; } /** * Performs a {@link FieldBeanOperation} on a {@link FieldProperty} and * an {@link Observable} * * @param fp * the {@link FieldProperty} * @param observable * the {@link Property}, {@link ObservableList}, * {@link ObservableSet}, or {@link ObservableMap} to perform * the {@link FieldBeanOperation} on * @param observableValueClass * the {@link Class} of the {@link Observable} value * @param operation * the {@link FieldBeanOperation} */ @SuppressWarnings("unchecked") protected void performOperation(final FieldProperty fp, final Observable observable, final Class observableValueClass, final FieldBeanOperation operation) { if (operation == FieldBeanOperation.CREATE_OR_FIND) { return; } // because of the inverse relationship of the bidirectional // bind the initial value needs to be captured and reset as // a dirty value or the bind operation will overwrite the // initial value with the value of the passed property final Object val = fp.getDirty(); if (Property.class.isAssignableFrom(observable.getClass())) { if (operation == FieldBeanOperation.UNBIND) { Bindings.unbindBidirectional((Property) fp, (Property) observable); } else if (operation == FieldBeanOperation.BIND) { if (fp.getFieldType() == fp.getDeclaredFieldType()) { Bindings.bindBidirectional((Property) fp, (Property) observable); } else { Bindings.bindBidirectional( (Property) fp, (Property) observable, (StringConverter) getFieldStringConverter(observableValueClass)); } } } else if (fp.getCollectionObservable() != null && observable != null && fp.getCollectionObservable() != observable) { // handle scenario where multiple observable collections/maps // are being bound to the same field property if (operation == FieldBeanOperation.UNBIND) { Bindings.unbindContentBidirectional( fp.getCollectionObservable(), observable); } else if (operation == FieldBeanOperation.BIND) { if (FieldProperty.isObservableList(observable) && fp.isObservableList()) { Bindings.bindContentBidirectional( (ObservableList) observable, (ObservableList) fp .getCollectionObservable()); } else if (FieldProperty.isObservableSet(observable) && fp.isObservableSet()) { Bindings.bindContentBidirectional( (ObservableSet) observable, (ObservableSet) fp .getCollectionObservable()); } else if (FieldProperty.isObservableMap(observable) && fp.isObservableMap()) { Bindings.bindContentBidirectional( (ObservableMap) observable, (ObservableMap) fp .getCollectionObservable()); } else { throw new UnsupportedOperationException( String.format( "Incompatible observable collection/map types cannot be bound %1$s and %2$s", fp.getCollectionObservable(), observable)); } } } else if (operation == FieldBeanOperation.UNBIND) { fp.set(null); } // reset initial dirty value final Object currVal = fp.getDirty(); if (val != null && val.toString() != null && !val.toString().isEmpty() && !val.equals(currVal) && !fp.hasDefaultDerived()) { fp.setDirty(val); } } /** * @return the field name that the {@link FieldBean} represents in it's * parent (null when the {@link FieldBean} is root) */ public String getFieldName() { return fieldHandle != null ? fieldHandle.getFieldName() : null; } /** * @return the parent {@link FieldBean} (null when the {@link FieldBean} * is root) */ public FieldBean getParent() { return parent; } /** * @see #getFieldProperties() * @see #getFieldSelectionProperties() * @return the {@link Map} of fields that belong to the * {@link FieldBean} that are not a {@link FieldProperty}, but * rather exist as a {@link FieldBean} that may or may not * contain their own {@link FieldProperty} instances */ protected Map> getFieldBeans() { return fieldBeans; } /** * @see #getFieldSelectionProperties() * @see #getFieldBeans() * @return the {@link Map} of fields that belong to the * {@link FieldBean} that are not {@link FieldBean}s, but rather * exist as a {@link FieldProperty} and are not * {@link #getFieldSelectionProperties()} */ protected Map> getFieldProperties() { return fieldProperties; } /** * @see #getFieldProperties() * @see #getFieldBeans() * @return the {@link Map} of fields that belong to the * {@link FieldBean} that are not {@link FieldBean}s, but rather * exist as a {@link FieldProperty} that are not * {@link #getFieldProperties()} */ protected Map> getFieldSelectionProperties() { return fieldSelectionProperties; } /** * @see #getFieldProperties() * @see #getFieldSelectionProperties() * @return the {@link FieldProperty} with the given name that belongs to * the {@link FieldBean} (null when the name does not exist) */ public FieldProperty getFieldProperty( final String proptertyName) { if (getFieldProperties().containsKey(proptertyName)) { return getFieldProperties().get(proptertyName); } else if (getFieldSelectionProperties().containsKey(proptertyName)) { return getFieldSelectionProperties().get(proptertyName); } return null; } /** * Gets/Creates (if not already created) a {@link FieldStringConverter}. * * @param targetClass * the target class of the {@link FieldStringConverter} * @return the {@link FieldStringConverter} */ @SuppressWarnings("unchecked") public FieldStringConverter getFieldStringConverter( final Class targetClass) { if (stringConverters.containsKey(targetClass)) { return (FieldStringConverter) stringConverters .get(targetClass); } else { final FieldStringConverter fsc = new FieldStringConverter<>( targetClass); stringConverters.put(targetClass, fsc); return fsc; } } } /** * Coercible {@link StringConverter} that handles conversions between * strings and a target class when used in the binding process * {@link Bindings#bindBidirectional(Property, Property, StringConverter)} * * @see StringConverter * @param * the target class type that is used in the coercion of the * string */ protected static class FieldStringConverter extends StringConverter { public static final SimpleDateFormat SDF = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssz"); private final Class targetClass; /** * Constructor * * @param targetClass * the class that the {@link FieldStringConverter} is * targeting */ public FieldStringConverter(final Class targetClass) { this.targetClass = targetClass; } /** * {@inheritDoc} */ @Override public T fromString(final String string) { return coerce(string, targetClass); } /** * {@inheritDoc} */ @Override public String toString(final T object) { return coerceToString(object); } /** * @return the target class that is used in the coercion of the string */ public Class getTargetClass() { return targetClass; } /** * Attempts to coerce a value into a {@link String} * * @param v * the value to coerce * @return the coerced value (null when value failed to be coerced) */ public static String coerceToString(final VT v) { String cv = null; if (v != null && SelectionModel.class.isAssignableFrom(v.getClass())) { cv = ((SelectionModel) v).getSelectedItem() != null ? ((SelectionModel) v) .getSelectedItem().toString() : null; } else if (v != null && (Calendar.class.isAssignableFrom(v.getClass()) || Date.class .isAssignableFrom(v.getClass()))) { final Date date = Date.class.isAssignableFrom(v.getClass()) ? (Date) v : ((Calendar) v).getTime(); cv = SDF.format(date); } else if (v != null) { cv = v.toString(); } return cv; } /** * Attempts to coerce a value into the specified class * * @param v * the value to coerce * @param targetClass * the class to coerce to * @return the coerced value (null when value failed to be coerced) */ @SuppressWarnings("unchecked") public static VT coerce(final Object v, final Class targetClass) { if (targetClass == Object.class) { return (VT) v; } VT val; final boolean isStringType = targetClass.equals(String.class); if (v == null || (!isStringType && v.toString() != null && v.toString() .isEmpty())) { val = (VT) FieldHandle.defaultValue(targetClass); } else if (isStringType || (v != null && targetClass.isAssignableFrom(v.getClass()))) { val = (VT) targetClass.cast(v); } else if (v != null && Date.class.isAssignableFrom(targetClass)) { if (Calendar.class.isAssignableFrom(v.getClass())) { val = (VT) ((Calendar) v).getTime(); } else if (java.util.Date.class.isAssignableFrom(v.getClass())) { val = (VT) (Date) v; } else { try { val = (VT) SDF.parse(v.toString()); } catch (final Throwable t) { throw new IllegalArgumentException(String.format( "Unable to convert %1$s to %2$s", v, targetClass), t); } } } else if (v != null && Calendar.class.isAssignableFrom(targetClass)) { final Calendar cal = Calendar.getInstance(); Date date = null; try { date = Date.class.isAssignableFrom(v.getClass()) ? (Date) v : SDF.parse(v.toString()); cal.setTime(date); val = (VT) cal; } catch (final Throwable t) { throw new IllegalArgumentException(String.format( "Unable to convert %1$s to %2$s", v, targetClass), t); } } else { val = FieldHandle.valueOf(targetClass, v.toString()); } return val; } } /** * A {@link Property} extension that uses a bean's getter/setter to define * the {@link Property}'s value. * * @param * the bean type * @param * the field type * @param * the {@link FieldProperty#get()} type */ public static class FieldProperty extends ObjectPropertyBase implements ListChangeListener, SetChangeListener, MapChangeListener, ChangeListener { private final FieldPathValueProperty notifyProperty; private final String fullPath; private final FieldHandle fieldHandle; private boolean isDirty; private boolean isDirtyCollection; private boolean isCollectionListening; private final String collectionItemPath; private final WeakReference collectionObservable; private final Class collectionType; private final SelectionModel collectionSelectionModel; private final FieldProperty itemMaster; /** * Constructor * * @param bean * the bean that the path belongs to * @param fullPath * the full . separated path to the * {@link FieldProperty} * @param fieldName * the name of the field within the bean * @param notifyProperty * the {@link FieldPathValueProperty} that will be set every * time the {@link FieldProperty#setValue(Object)} is * performed or an item within the value is changed * @param declaredFieldType * the declared {@link Class} of the field * @param collectionItemPath * the the . separated field names of the * {@link Observable} collection (only applicable when the * {@link Observable} is a {@link ObservableList}, * {@link ObservableSet}, or {@link ObservableMap}) * @param collectionObservable * the {@link Observable} {@link Collection} used to bind to * the {@link FieldProperty} OR when the * {@link SelectionModel} is specified this is the * {@link Observable} {@link Collection} of available items * to select from * @param collectionType * the {@link Collection} {@link Class} used to attempt to * transform the underlying field {@link Observable} * {@link Collection} to the {@link Collection} {@link Class} * (only applicable when the actual field is a * {@link Collection}) * @param collectionSelectionModel * the {@link SelectionModel} used to set the values within * the {@link Observable} only applicable when the * {@link Observable} is used for selection(s) and therefore * cannot be updated directly because it is read-only * @param itemMaster * the {@link FieldProperty} that contains the item(s) that * the {@link SelectionModel} can select from */ @SuppressWarnings("unchecked") protected FieldProperty(final BT bean, final String fullPath, final String fieldName, final FieldPathValueProperty notifyProperty, final Class declaredFieldType, final String collectionItemPath, final Observable collectionObservable, final Class collectionType, final SelectionModel collectionSelectionModel, final FieldProperty itemMaster) { super(); this.fullPath = fullPath; this.notifyProperty = notifyProperty; this.fieldHandle = new FieldHandle(bean, fieldName, declaredFieldType); this.itemMaster = itemMaster; this.collectionObservable = new WeakReference( collectionObservable); this.collectionItemPath = collectionItemPath; this.collectionType = collectionType; this.collectionSelectionModel = (SelectionModel) collectionSelectionModel; if (this.collectionSelectionModel != null && this.itemMaster != null) { this.itemMaster.addListener(this); } setDerived(); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public PT get() { try { final Object dv = getDirty(); if (dv != null && getDeclaredFieldType() != getFieldType()) { return (PT) FieldStringConverter.coerceToString(dv); } return (PT) dv; } catch (final Throwable t) { throw new RuntimeException("Unable to get value", t); } } /** * Sets the {@link FieldHandle#deriveValueFromAccessor()} value */ protected void setDerived() { final T derived = fieldHandle.deriveValueFromAccessor(true); set(derived); } /** * Flags the {@link Property} value as dirty and calls * {@link #set(Object)} * * @param v * the value to set */ public void setDirty(final Object v) { isDirty = true; set(v); } /** * Sets an {@link Object} value * * @param v * the value to set */ @Override public void set(final Object v) { try { final Object cv = fieldHandle.getAccessor().invoke(); final Class clazz = cv != null ? cv.getClass() : fieldHandle .getFieldType(); if (v != null && (Collection.class.isAssignableFrom(v.getClass()) || Map.class .isAssignableFrom(v.getClass()))) { fieldHandle.getSetter().invoke(v); postSet(cv); } else if (isDirty || cv != v) { final Object val = FieldStringConverter.coerce(v, clazz); fieldHandle.getSetter().invoke(val); postSet(cv); } } catch (final Throwable t) { throw new IllegalArgumentException(String.format( "Unable to set object value: %1$s on %2$s", v, fieldHandle.getFieldName()), t); } } /** * Executes any post processing that needs to take place after set * operation takes place * * @param prevValue * the {@link #getValue()} before {@link #setValue(Object)} * was called * @throws Throwable * thrown when any errors occur when processing a post set * operation */ protected final void postSet(final Object prevValue) throws Throwable { final Boolean colChanged = populateObservableCollection(); if (colChanged == null || colChanged) { invalidated(); fireValueChangedEvent(); } try { // all collection/map item value changes will be captured at the // collection/map level unless the collection/map level types // are not registered (in which case a normal change will be // evaluated if (!isDirty && (fullPath.indexOf(COLLECTION_ITEM_PATH_SEPARATOR) < 0 || (notifyProperty .hasTypes(FieldPathValueType.FIELD_CHANGE) && !hasFieldPathValueTypeAddOrRemove(true) && !hasFieldPathValueTypeAddOrRemove(false)))) { final Object cv = getDirty(); if ((cv == null && prevValue != null) || (cv != null && !cv.equals(prevValue))) { notifyProperty .set(new FieldPathValue(fullPath, getBean(), cv, FieldPathValueType.FIELD_CHANGE)); } } } finally { isDirty = false; } } /** * Updates the {@link Observable} when the field represents a supported * {@link Collection}. If the {@link #collectionType} is defined an * attempt will be made to transform the {@link Observable} * {@link Collection} to it. * * @throws Throwable * thrown when {@link FieldHandle#getSetter()} cannot be * invoked, the {@link #getDirty()} cannot be cast to * {@link FieldHandle#getFieldType()}, or the * {@link #getDirty()} cannot be transformed using the * {@link #collectionType} * @return true when the collection has been populated */ private Boolean populateObservableCollection() throws Throwable { final Observable oc = getCollectionObservable(); Boolean changed = null; if (isList() || isSet()) { addRemoveCollectionListener(oc, false); Collection items = (Collection) getDirty(); if (items == null) { items = new LinkedHashSet<>(); fieldHandle.getSetter().invoke(items); } changed = syncCollectionValues(items, false, false, null, null, null); } else if (isMap()) { addRemoveCollectionListener(oc, false); Map items = (Map) getDirty(); if (items == null) { items = new HashMap<>(); fieldHandle.getSetter().invoke(items); } changed = syncCollectionValues(items, false, false, null, null, null); } else { return changed; } addRemoveCollectionListener(oc, true); return changed; } /** * Synchronizes the collection items used in the {@link FieldProperty} * and {@link Observable} collection. * {@link Bindings#bindContentBidirectional(ObservableList, ObservableList)} * variants are/cannot be not used. * * @param values * the {@link List}, {@link Set}, or {@link Map} that should * be synchronized * @param toField * true when synchronization needs to occur on the * {@link FieldProperty} collection, false when * synchronization needs to occur on the {@link Observable} * collection * @param fromItemMasterChange * true when the synchronization is from a change made to the * {@link #itemMaster} * @param listChange * any {@link ListChangeListener.Change} * @param setChange * any {@link SetChangeListener.Change} * @param mapChange * any {@link MapChangeListener.Change} * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ @SuppressWarnings("unchecked") private boolean syncCollectionValues(final Object values, final boolean toField, final boolean fromItemMasterChange, final ListChangeListener.Change listChange, final SetChangeListener.Change setChange, final MapChangeListener.Change mapChange) { boolean changed = false; if (isDirtyCollection) { return changed; } if (collectionSelectionModel != null && itemMaster != null && itemMaster.isDirtyCollection && (listChange != null || setChange != null || mapChange != null)) { // selections shouldn't get synchronized while the item master // is in the middle of getting synchronized return changed; } try { isDirtyCollection = true; // TODO : Use a more elegant technique to synchronize the // observable // and the bean collections that doesn't require clearing and // resetting them? (see commented onChange methods from revision // 204) if (this.collectionObservable.get() != null && Collection.class .isAssignableFrom(this.collectionObservable .get().getClass())) { final Collection oc = (Collection) this.collectionObservable .get(); if (Collection.class.isAssignableFrom(values.getClass())) { final Collection col = (Collection) values; if (toField) { changed = syncCollectionValuesFromObservable(col, oc); } else { final boolean wasColEmpty = col.isEmpty(); if (collectionSelectionModel != null) { collectionSelectionModel.clearSelection(); } else if (!wasColEmpty || isDirty) { oc.clear(); changed = true; } else { changed = syncCollectionValuesFromObservable(col, oc); } if (!wasColEmpty) { syncObservableFromCollectionValues(col, oc); } } } else if (Map.class.isAssignableFrom(values.getClass())) { final Map map = (Map) values; if (toField) { changed = syncCollectionValuesFromObservable(map, oc); } else { final boolean wasColEmpty = map.isEmpty(); if (collectionSelectionModel != null) { collectionSelectionModel.clearSelection(); } else if (!wasColEmpty || isDirty) { oc.clear(); changed = true; } else { changed = syncCollectionValuesFromObservable(map, oc); } if (!wasColEmpty) { syncObservableFromCollectionValues(map, oc); } } } } else if (this.collectionObservable.get() instanceof ObservableMap) { final ObservableMap oc = (ObservableMap) this.collectionObservable .get(); if (Collection.class.isAssignableFrom(values.getClass())) { final Collection col = (Collection) values; if (toField) { changed = syncCollectionValuesFromObservable(col, oc); } else { final boolean wasColEmpty = col.isEmpty(); if (collectionSelectionModel != null) { collectionSelectionModel.clearSelection(); } else if (!wasColEmpty || isDirty) { oc.clear(); changed = true; } else { changed = syncCollectionValuesFromObservable(col, oc); } if (!wasColEmpty) { syncObservableFromCollectionValues(col, oc); } } } else if (Map.class.isAssignableFrom(values.getClass())) { final Map map = (Map) values; if (toField) { changed = syncCollectionValuesFromObservable(map, oc); } else { final boolean wasColEmpty = map.isEmpty(); if (collectionSelectionModel != null) { collectionSelectionModel.clearSelection(); } else if (!wasColEmpty || isDirty) { oc.clear(); changed = true; } else { changed = syncCollectionValuesFromObservable(map, oc); } if (!wasColEmpty) { syncObservableFromCollectionValues(map, oc); } } } } return changed || listChange != null || setChange != null || mapChange != null; } finally { isDirtyCollection = false; } } /** * Synchronizes the {@link Collection} values to the supplied * {@link Observable} {@link Collection} * * @param fromCol * the {@link Collection} that synchronization will derive * from * @param oc * the {@link Observable} {@link Collection} that should be * synchronized to * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncObservableFromCollectionValues( final Collection fromCol, final Collection oc) { boolean changed = false; boolean missing = false; FieldProperty fp; Object fpv; int i = -1; final boolean isOcList = List.class.isAssignableFrom(oc.getClass()); for (final Object item : fromCol) { fp = genFieldProperty(item, null); fpv = fp != null ? fp.getDirty() : item; missing = !oc.contains(fpv); changed = !changed ? missing : changed; if (collectionSelectionModel == null) { if (isOcList) { ((List) oc).add(++i, fpv); } else { oc.add(fpv); } } else { selectCollectionValue(fpv); } } return changed; } /** * Synchronizes the {@link Collection} values to the supplied * {@link Observable} {@link Map} * * @param fromCol * the {@link Collection} that synchronization will derive * from * @param oc * the {@link Observable} {@link Map} that should be * synchronized to * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncObservableFromCollectionValues( final Collection fromCol, final Map oc) { boolean changed = false; boolean missing = false; FieldProperty fp; Object fpv; int i = -1; for (final Object item : fromCol) { fp = genFieldProperty(item, null); fpv = fp != null ? fp.getDirty() : item; missing = !oc.containsValue(fpv); changed = !changed ? missing : changed; if (collectionSelectionModel == null) { oc.put(++i, fpv); } else { selectCollectionValue(fpv); } } return changed; } /** * Synchronizes the {@link Map} values to the supplied * {@link Observable} {@link Collection} * * @param fromMap * the {@link Map} that synchronization will derive from * @param oc * the {@link Observable} {@link Collection} that should be * synchronized to * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncObservableFromCollectionValues( final Map fromMap, final Collection oc) { boolean changed = false; boolean missing = false; FieldProperty fp; Object fpv; int i = -1; final boolean isOcList = List.class.isAssignableFrom(oc.getClass()); for (final Object item : fromMap.values()) { fp = genFieldProperty(item, null); fpv = fp != null ? fp.getDirty() : item; missing = !oc.contains(fpv); changed = !changed ? missing : changed; if (collectionSelectionModel == null) { if (isOcList) { ((List) oc).add(++i, fpv); } else { oc.add(fpv); } } else { selectCollectionValue(fpv); } } return changed; } /** * Synchronizes the {@link Map} values to the supplied * {@link Observable} {@link Map} * * @param fromMap * the {@link Map} that synchronization will derive from * @param oc * the {@link Observable} {@link Map} that should be * synchronized to * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncObservableFromCollectionValues( final Map fromMap, final Map oc) { boolean changed = false; boolean missing = false; FieldProperty fp; Object fpv; int i = -1; for (final Map.Entry item : fromMap.entrySet()) { fp = genFieldProperty(item.getValue(), null); fpv = fp != null ? fp.getDirty() : item.getValue(); missing = !oc.containsValue(fpv); changed = !changed ? missing : changed; if (collectionSelectionModel == null) { oc.put(++i, fpv); } else { selectCollectionValue(fpv); } } return changed; } /** * Calls the {@link SelectionModel#select(Object)} the specified value * * @param value * the value to select */ private void selectCollectionValue(final Object value) { if (collectionSelectionModel == null) { return; } collectionSelectionModel.select(value); } /** * Synchronizes the {@link Observable} {@link Collection} values to the * supplied {@link Collection} * * @param toCol * the {@link Collection} that should be synchronized to * @param oc * the {@link Observable} {@link Collection} that * synchronization will derive from * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncCollectionValuesFromObservable( final Collection toCol, final Collection oc) { boolean changed = false; boolean missing = false; final List fvs = new ArrayList<>(); FieldProperty fp; Object fpv; final List nc = new ArrayList<>(); for (final Object item : oc) { if (item != null) { fp = genFieldProperty(null, item); fpv = fp == null ? item : fp.getBean(); missing = !toCol.contains(fpv); changed = !changed ? missing : changed; nc.add(fpv); if (missing && hasFieldPathValueTypeAddOrRemove(true)) { fvs.add(newSyncCollectionFieldPathValue(fp, fpv, true)); } } } if (hasFieldPathValueTypeAddOrRemove(false)) { for (final Object item : toCol) { if (!nc.contains(item)) { fp = genFieldProperty(item, null); fpv = fp == null ? item : fp.getBean(); fvs.add(newSyncCollectionFieldPathValue(fp, fpv, false)); } } } toCol.clear(); toCol.addAll(nc); setFieldPathValues(fvs); return changed; } /** * Synchronizes the {@link Observable} {@link Collection} values to the * supplied {@link Map} * * @param toMap * the {@link Map} that should be synchronized to * @param oc * the {@link Observable} {@link Map} that synchronization * will derive from * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncCollectionValuesFromObservable( final Map toMap, final Collection oc) { boolean changed = false; boolean missing = false; final List fvs = new ArrayList<>(); FieldProperty fp; Object fpv; int i = -1; final Map nc = new HashMap<>(); for (final Object item : oc) { if (item != null) { fp = genFieldProperty(null, item); fpv = fp == null ? item : fp.getBean(); missing = !toMap.containsValue(fpv); changed = !changed ? missing : changed; nc.put(++i, fpv); if (missing && hasFieldPathValueTypeAddOrRemove(true)) { fvs.add(newSyncCollectionFieldPathValue(fp, fpv, true)); } } } if (hasFieldPathValueTypeAddOrRemove(false)) { for (final Object item : toMap.values()) { if (!nc.containsValue(item)) { fp = genFieldProperty(item, null); fpv = fp == null ? item : fp.getBean(); fvs.add(newSyncCollectionFieldPathValue(fp, fpv, false)); } } } toMap.clear(); toMap.putAll(nc); setFieldPathValues(fvs); return changed; } /** * Synchronizes the {@link Observable} {@link Map} values to the * supplied {@link Collection} * * @param toCol * the {@link Collection} that should be synchronized to * @param oc * the {@link Observable} {@link Map} that synchronization * will derive from * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncCollectionValuesFromObservable( final Collection toCol, final ObservableMap oc) { boolean changed = false; boolean missing = false; final List fvs = new ArrayList<>(); FieldProperty fp; Object fpv; final List nc = new ArrayList<>(); for (final Map.Entry item : oc.entrySet()) { if (item != null && item.getValue() != null) { fp = genFieldProperty(null, item.getValue()); fpv = fp == null ? item.getValue() : fp.getBean(); missing = !toCol.contains(fpv); changed = !changed ? missing : changed; nc.add(fpv); if (missing && hasFieldPathValueTypeAddOrRemove(true)) { fvs.add(newSyncCollectionFieldPathValue(fp, fpv, true)); } } } if (hasFieldPathValueTypeAddOrRemove(false)) { for (final Object item : toCol) { if (!nc.contains(item)) { fp = genFieldProperty(item, null); fpv = fp == null ? item : fp.getBean(); fvs.add(newSyncCollectionFieldPathValue(fp, fpv, false)); } } } toCol.clear(); toCol.addAll(nc); setFieldPathValues(fvs); return changed; } /** * Synchronizes the {@link Observable} {@link Map} values to the * supplied {@link Map} * * @param toMap * the {@link Map} that should be synchronized to * @param oc * the {@link Observable} {@link Collection} that * synchronization will derive from * @return true when the synchronization resulted in a change to the * {@link Collection}/{@link Map} */ private boolean syncCollectionValuesFromObservable( final Map toMap, final ObservableMap oc) { boolean changed = false; boolean missing = false; final List fvs = new ArrayList<>(); FieldProperty fp; Object fpv; int i = -1; final Map nc = new HashMap<>(); for (final Map.Entry item : oc.entrySet()) { if (item != null && item.getValue() != null) { fp = genFieldProperty(null, item.getValue()); fpv = fp == null ? item.getValue() : fp.getBean(); missing = !toMap.containsValue(fpv); changed = !changed ? missing : changed; nc.put(i, fpv); if (missing && hasFieldPathValueTypeAddOrRemove(true)) { fvs.add(newSyncCollectionFieldPathValue(fp, fpv, true)); } } } if (hasFieldPathValueTypeAddOrRemove(false)) { for (final Object item : toMap.values()) { if (!nc.containsValue(item)) { fp = genFieldProperty(item, null); fpv = fp == null ? item : fp.getBean(); fvs.add(newSyncCollectionFieldPathValue(fp, fpv, false)); } } } toMap.clear(); toMap.putAll(nc); setFieldPathValues(fvs); return changed; } /** * Creates a new {@link FieldPathValue} using specified * {@link FieldProperty} or the current {@link FieldProperty} when the * specified {@link FieldProperty} is null. * * @param fp * the {@link FieldProperty} (optional) * @param fpv * the {@link FieldProperty#getValue()} * @param isAdd * true when adding an item * @return the {@link FieldPathValue} */ protected FieldPathValue newSyncCollectionFieldPathValue( final FieldProperty fp, final Object fpv, final boolean isAdd) { FieldPathValueType type; if (collectionSelectionModel != null) { type = isAdd ? FieldPathValueType.CONTENT_ITEM_ADD_SELECT : FieldPathValueType.CONTENT_ITEM_REMOVE_SELECT; } else { type = isAdd ? FieldPathValueType.CONTENT_ITEM_ADD : FieldPathValueType.CONTENT_ITEM_REMOVE; } if (fp == null) { return new FieldPathValue(fullPath, getBean(), fpv, type); } else { return new FieldPathValue(fp.fullPath, fp.getBean(), fpv, type); } } /** * Determines if the {@link FieldPathValueProperty} is registered for * adds or removals * * @param add * true to check for add, false to check for remove * @return true when the {@link FieldPathValueProperty} is registered * for the add or remove */ protected boolean hasFieldPathValueTypeAddOrRemove(final boolean add) { return (add && collectionSelectionModel != null && notifyProperty .hasTypes(FieldPathValueType.CONTENT_ITEM_ADD_SELECT)) || (add && collectionSelectionModel == null && notifyProperty .hasTypes(FieldPathValueType.CONTENT_ITEM_ADD)) || (!add && collectionSelectionModel != null && notifyProperty .hasTypes(FieldPathValueType.CONTENT_ITEM_REMOVE_SELECT)) || (!add && collectionSelectionModel == null && notifyProperty .hasTypes(FieldPathValueType.CONTENT_ITEM_REMOVE)); } /** * Sets a {@link Collection} of {@link FieldPathValue}(s) on the * {@link #notifyProperty} * * @param fieldPathValues * the {@link Collection} of {@link FieldPathValue}(s) to set */ protected void setFieldPathValues( final Collection fieldPathValues) { if (notifyProperty != null) { for (final FieldPathValue o : fieldPathValues) { notifyProperty.set(o); } } } /** * Generates a {@link FieldProperty} using the specified bean and sets * the optional value on the bean (when the value is not null). The * returned {@link FieldProperty} will contain the same value instance * contained with the item master. When the item master is not available * an attempt will be made to get the item value from the * {@link #getDirty()} collection/map. * * @param itemBeanValue * the collection {@link FieldBean} value to add/update. when * {@code null} the existing {@link FieldBean} value will * will be used unless it is {@code null} as well- in which * case an attempt will be made to instantiate a new instance * of the bean using a no-argument constructor * @param itemBeanPropertyValue * the collection {@link FieldBean}'s {@link FieldProperty} * value to add/update (null when no update should be made to * the {@link FieldBean}'s {@link FieldProperty} value) * @return the {@link FieldProperty} for the collection item (null when * none is required) */ protected FieldProperty genFieldProperty( final Object itemBeanValue, final Object itemBeanPropertyValue) { try { // simple collection items that do not have a path do not // require an update if (collectionItemPath == null || collectionItemPath.isEmpty()) { return null; } else if (itemBeanValue == null && itemBeanPropertyValue == null) { throw new NullPointerException( "Both itemBeanValue and itemBeanPropertyValue cannot be null"); } Object value = itemBeanPropertyValue; Object bean = itemBeanValue == null ? collectionType .newInstance() : itemBeanValue; FieldProperty fp = genCollectionFieldProperty(bean); if (value != null) { fp.setDirty(value); } else { value = fp.getDirty(); } if (itemBeanPropertyValue != null) { // ensure that any selection values come from the item // master and any updates to an existing bean return a field // property of the same bean reference/target Object im = itemMaster != null ? itemMaster.getDirty() : getDirty(); FieldProperty imfp; if (Collection.class.isAssignableFrom(im.getClass())) { for (final Object ib : (Collection) im) { imfp = genCollectionFieldProperty(ib); if (imfp.getDirty() == value) { return imfp; } } } else if (Map.class.isAssignableFrom(im.getClass())) { for (final Map.Entry ib : ((Map) im) .entrySet()) { imfp = genCollectionFieldProperty(ib.getValue()); if (imfp.getDirty() == value) { return imfp; } } } } return fp; } catch (InstantiationException | IllegalAccessException e) { throw new UnsupportedOperationException(String.format( "Cannot create collection item bean using %1$s", collectionType), e); } } /** * Generates a {@link FieldProperty} for a * {@link #getCollectionItemPath()} and * {@link #getCollectionSelectionModel()} when applicable * * @param bean * the bean to generate a {@link FieldProperty} * @return the generated {@link FieldProperty} */ protected FieldProperty genCollectionFieldProperty( final Object bean) { FieldBean fb; FieldProperty fp; fb = new FieldBean<>(null, bean, null, notifyProperty); fp = fb.performOperation(fullPath + COLLECTION_ITEM_PATH_SEPARATOR + collectionItemPath, collectionItemPath, Object.class, null, null, null, collectionSelectionModel, null, FieldBeanOperation.CREATE_OR_FIND); return fp; } /** * Updates the underlying collection item value * * see updateCollectionItemBean(int, Object, Object) * @param itemBeanPropertyValue * the collection {@link FieldBean}'s {@link FieldProperty} * value to add/update * @return {@link FieldProperty#getBean()} when the collection item has * it's own bean path, the * itemBeanPropertyValue when it does not */ protected Object updateCollectionItemProperty( final Object itemBeanPropertyValue) { final FieldProperty fp = genFieldProperty(null, itemBeanPropertyValue); return fp == null ? itemBeanPropertyValue : fp.getBean(); } /** * Adds/Removes the {@link FieldProperty} as a collection listener * * @param observable * the {@link Observable} collection/map to listen for * changes on * @param add * true to add, false to remove */ protected void addRemoveCollectionListener(final Observable observable, final boolean add) { final boolean isCol = getCollectionObservable() == observable; if (isCol && ((this.isCollectionListening && add) || (this.isCollectionListening && !add))) { return; } Boolean change = null; if (observable instanceof ObservableList) { final ObservableList ol = (ObservableList) observable; if (add) { ol.addListener(this); change = true; } else { ol.removeListener(this); change = false; } } else if (observable instanceof ObservableSet) { final ObservableSet os = (ObservableSet) observable; if (add) { os.addListener(this); change = true; } else { os.removeListener(this); change = false; } } else if (observable instanceof ObservableMap) { final ObservableMap om = (ObservableMap) observable; if (add) { om.addListener(this); change = true; } else { om.removeListener(this); change = false; } } else if (observable == null) { throw new IllegalStateException(String.format( "Observable collection/map bound to %1$s (item path: %2$s) " + "has been garbage collected", this.fieldHandle.getFieldName(), this.collectionItemPath, observable, this.getFieldType())); } else { throw new UnsupportedOperationException(String.format( "%1$s (item path: %2$s) of type \"%4$s\" " + "must be bound to a supported " + "observable collection/map type... " + "Found observable: %3$s", this.fieldHandle.getFieldName(), this.collectionItemPath, observable, this.getFieldType())); } if (isCol && change != null) { this.isCollectionListening = change; } } /** * Detects {@link #itemMaster} changes for selection synchronization */ @Override public void changed(final ObservableValue observable, final Object oldValue, final Object newValue) { syncCollectionValues(getDirty(), false, true, null, null, null); } /** * {@inheritDoc} */ @Override public final void onChanged( ListChangeListener.Change change) { syncCollectionValues(getDirty(), true, false, change, null, null); } /** * {@inheritDoc} */ @Override public final void onChanged( SetChangeListener.Change change) { syncCollectionValues(getDirty(), true, false, null, change, null); } /** * {@inheritDoc} */ @Override public final void onChanged( MapChangeListener.Change change) { syncCollectionValues(getDirty(), true, false, null, null, change); } /** * @return the dirty value before conversion takes place */ public Object getDirty() { try { return fieldHandle.getAccessor().invoke(); } catch (final Throwable t) { throw new RuntimeException("Unable to get dirty value", t); } } /** * Binds a new target to the {@link FieldHandle} * * @param bean * the target bean to bind to */ protected void setTarget(final BT bean) { isDirty = true; fieldHandle.setTarget(bean); setDerived(); } /** * {@inheritDoc} */ @Override public BT getBean() { return fieldHandle.getTarget(); } /** * {@inheritDoc} */ @Override public String getName() { return fieldHandle.getFieldName(); } /** * @return the {@link FieldHandle#getFieldType()} */ @SuppressWarnings("unchecked") public Class getFieldType() { return (Class) fieldHandle.getFieldType(); } /** * @return the {@link FieldHandle#getDeclaredFieldType()} */ public Class getDeclaredFieldType() { return fieldHandle.getDeclaredFieldType(); } /** * @return the {@link FieldHandle#hasDefaultDerived()} */ protected boolean hasDefaultDerived() { return fieldHandle.hasDefaultDerived(); } /** * @return true when the {@link FieldProperty} is for a {@link List} */ public boolean isList() { return List.class.isAssignableFrom(this.fieldHandle.getFieldType()); } /** * @return true when the {@link FieldProperty} is for a {@link Set} */ public boolean isSet() { return Set.class.isAssignableFrom(this.fieldHandle.getFieldType()); } /** * @return true when the {@link FieldProperty} is for a {@link Map} */ public boolean isMap() { return Map.class.isAssignableFrom(this.fieldHandle.getFieldType()); } /** * @return true when the {@link FieldProperty} is bound to an * {@link ObservableList} */ protected boolean isObservableList() { return isObservableList(getCollectionObservable()); } /** * @return true when the {@link FieldProperty} is bound to an * {@link ObservableSet} */ protected boolean isObservableSet() { return isObservableSet(getCollectionObservable()); } /** * @return true when the {@link FieldProperty} is bound to an * {@link ObservableMap} */ protected boolean isObservableMap() { return isObservableMap(getCollectionObservable()); } /** * @param observable * the {@link Observable} to check * @return true when the {@link Observable} is an {@link ObservableList} */ protected static boolean isObservableList(final Observable observable) { return observable != null && ObservableList.class.isAssignableFrom(observable .getClass()); } /** * @param observable * the {@link Observable} to check * @return true when the {@link Observable} is an {@link ObservableSet} */ protected static boolean isObservableSet(final Observable observable) { return observable != null && ObservableSet.class.isAssignableFrom(observable .getClass()); } /** * @param observable * the {@link Observable} to check * @return true when the {@link Observable} is an {@link ObservableMap} */ protected static boolean isObservableMap(final Observable observable) { return observable != null && ObservableMap.class.isAssignableFrom(observable .getClass()); } /** * Extracts the collections {@link FieldProperty} from an associated * {@link FieldBean} * * @param fieldBean * the {@link FieldBean} to extract from * @return the extracted {@link FieldProperty} */ protected FieldProperty extractCollectionItemFieldProperty( final FieldBean fieldBean) { final String[] cip = collectionItemPath.split("\\."); return fieldBean.getFieldProperty(cip[cip.length - 1]); } /** * @return true when each item in the underlying collection has a path * to it's own field value */ public boolean hasCollectionItemPath() { return this.collectionItemPath != null && !this.collectionItemPath.isEmpty(); } /** * @return a . separated field name that represents each * item in the underlying collection */ public String getCollectionItemPath() { return this.collectionItemPath; } /** * @return a {@link SelectionModel} for the {@link FieldProperty} when * the field references a collection/map for item selection or * {@code null} when not a selection {@link FieldProperty} */ protected SelectionModel getCollectionSelectionModel() { return collectionSelectionModel; } /** * @return an {@link Observable} used to represent an * {@link ObservableList}, {@link ObservableSet}, or * {@link ObservableMap} (null when either the observable * collection has been garbage collected or the * {@link FieldProperty} does not represent a collection) */ protected Observable getCollectionObservable() { return this.collectionObservable.get(); } /** * @return true when the {@link FieldProperty} has an item master that * it's using to reference {@link #get()} for */ public boolean hasItemMaster() { return this.itemMaster != null; } } /** * Field handle to {@link FieldHandle#getAccessor()} and * {@link FieldHandle#getSetter()} for a given * {@link FieldHandle#getTarget()}. * * @param * the {@link FieldHandle#getTarget()} type * @param * the {@link FieldHandle#getDeclaredFieldType()} type */ protected static class FieldHandle { private static final Map, Class> PRIMS = new HashMap<>(); static { PRIMS.put(boolean.class, Boolean.class); PRIMS.put(char.class, Character.class); PRIMS.put(double.class, Double.class); PRIMS.put(float.class, Float.class); PRIMS.put(long.class, Long.class); PRIMS.put(int.class, Integer.class); PRIMS.put(short.class, Short.class); PRIMS.put(long.class, Long.class); PRIMS.put(byte.class, Byte.class); } private static final Map, Object> DFLTS = new HashMap<>(); static { DFLTS.put(Boolean.class, Boolean.FALSE); DFLTS.put(boolean.class, false); DFLTS.put(Byte.class, Byte.valueOf("0")); DFLTS.put(byte.class, Byte.valueOf("0").byteValue()); DFLTS.put(Number.class, 0L); DFLTS.put(Short.class, Short.valueOf("0")); DFLTS.put(short.class, Short.valueOf("0").shortValue()); DFLTS.put(Character.class, Character.valueOf(' ')); DFLTS.put(char.class, ' '); DFLTS.put(Integer.class, Integer.valueOf(0)); DFLTS.put(int.class, 0); DFLTS.put(Long.class, Long.valueOf(0)); DFLTS.put(long.class, 0L); DFLTS.put(Float.class, Float.valueOf(0F)); DFLTS.put(float.class, 0F); DFLTS.put(Double.class, Double.valueOf(0D)); DFLTS.put(double.class, 0D); DFLTS.put(BigInteger.class, BigInteger.valueOf(0L)); DFLTS.put(BigDecimal.class, BigDecimal.valueOf(0D)); } private final String fieldName; private MethodHandle accessor; private MethodHandle setter; private final Class declaredFieldType; private T target; private boolean hasDefaultDerived; /** * Constructor * * @param target * the {@link #getTarget()} for the {@link MethodHandle}s * @param fieldName * the field name defined in the {@link #getTarget()} * @param declaredFieldType * the declared field type for the {@link #getFieldName()} */ protected FieldHandle(final T target, final String fieldName, final Class declaredFieldType) { super(); this.fieldName = fieldName; this.declaredFieldType = declaredFieldType; this.target = target; updateMethodHandles(); } /** * Updates the {@link #getAccessor()} and {@link #getSetter()} using the * current {@link #getTarget()} and {@link #getFieldName()}. * {@link MethodHandle}s are immutable so new ones are created. */ protected void updateMethodHandles() { this.accessor = buildAccessorWithLikelyPrefixes(getTarget(), getFieldName()); this.setter = buildSetter(getAccessor(), getTarget(), getFieldName()); } /** * Gets the {@link #buildAccessorWithLikelyPrefixes(Object, String)} * {@link MethodHandle#type()} * * @param target * the accessor target * @param fieldName * the field name of the target * @return the accessor return type */ public static Class getAccessorType(final Object target, final String fieldName) { return buildAccessorWithLikelyPrefixes(target, fieldName).type() .returnType(); } /** * Attempts to build a {@link MethodHandle} accessor for the field name * using common prefixes used for methods to access a field * * @param target * the target object that the accessor is for * @param fieldName * the field name that the accessor is for * @return the accessor {@link MethodHandle} */ protected static MethodHandle buildAccessorWithLikelyPrefixes( final Object target, final String fieldName) { final MethodHandle mh = buildAccessor(target, fieldName, "get", "is", "has", "use"); if (mh == null) { // throw new NoSuchMethodException(fieldName + " on " + target); throw new IllegalArgumentException(fieldName + " on " + target); } return mh; } /** * Attempts to build a {@link MethodHandle} accessor for the field name * using common prefixes used for methods to access a field * * @param target * the target object that the accessor is for * @param fieldName * the field name that the accessor is for * @return the accessor {@link MethodHandle} * @param fieldNamePrefix * the prefix of the method for the field name * @return the accessor {@link MethodHandle} */ protected static MethodHandle buildAccessor(final Object target, final String fieldName, final String... fieldNamePrefix) { final String accessorName = buildMethodName(fieldNamePrefix[0], fieldName); try { return MethodHandles .lookup() .findVirtual( target.getClass(), accessorName, MethodType.methodType(target.getClass() .getMethod(accessorName) .getReturnType())).bindTo(target); } catch (final NoSuchMethodException e) { return fieldNamePrefix.length <= 1 ? null : buildAccessor( target, fieldName, Arrays.copyOfRange(fieldNamePrefix, 1, fieldNamePrefix.length)); } catch (final Throwable t) { throw new IllegalArgumentException( "Unable to resolve accessor " + accessorName, t); } } /** * Builds a setter {@link MethodHandle} * * @param accessor * the field's accesssor that will be used as the parameter * type for the setter * @param target * the target object that the setter is for * @param fieldName * the field name that the setter is for * @return the setter {@link MethodHandle} */ protected static MethodHandle buildSetter(final MethodHandle accessor, final Object target, final String fieldName) { try { final MethodHandle mh1 = MethodHandles .lookup() .findVirtual( target.getClass(), buildMethodName("set", fieldName), MethodType.methodType(void.class, accessor .type().returnType())).bindTo(target); return mh1; } catch (final Throwable t) { throw new IllegalArgumentException("Unable to resolve setter " + fieldName, t); } } /** * Attempts to invoke a valueOf using the * {@link #getDeclaredFieldType()} class * * @param value * the value to invoke the valueOf method on * @return the result (null if the operation fails) */ public F valueOf(final String value) { return valueOf(getDeclaredFieldType(), value); } /** * Attempts to invoke a valueOf using the specified class * * @param valueOfClass * the class to attempt to invoke a valueOf * method on * @param value * the value to invoke the valueOf method on * @return the result (null if the operation fails) */ @SuppressWarnings("unchecked") public static VT valueOf(final Class valueOfClass, final Object value) { if (value != null && String.class.isAssignableFrom(valueOfClass)) { return (VT) value.toString(); } final Class clazz = PRIMS.containsKey(valueOfClass) ? PRIMS .get(valueOfClass) : valueOfClass; MethodHandle mh1 = null; try { mh1 = MethodHandles.lookup().findStatic(clazz, "valueOf", MethodType.methodType(clazz, String.class)); } catch (final Throwable t) { // class doesn't support it- do nothing } if (mh1 != null) { try { return (VT) mh1.invoke(value); } catch (final Throwable t) { throw new IllegalArgumentException(String.format( "Unable to invoke valueOf on %1$s using %2$s", value, valueOfClass), t); } } return null; } /** * Determines if a {@link Class} has a default value designated * * @param clazz * the {@link Class} to check * @return true when a {@link Class} has a default value designated for * it */ public static boolean hasDefault(final Class clazz) { return clazz == null ? false : DFLTS.containsKey(clazz); } /** * Gets a default value for the {@link #getDeclaredFieldType()} * * @return the default value */ public F defaultValue() { return defaultValue(getDeclaredFieldType()); } /** * Gets a default value for the specified class * * @param clazz * the class * @return the default value */ @SuppressWarnings("unchecked") public static VT defaultValue(final Class clazz) { return (VT) (DFLTS.containsKey(clazz) ? DFLTS.get(clazz) : null); } /** * Builds a method name using a prefix and a field name * * @param prefix * the method's prefix * @param fieldName * the method's field name * @return the method name */ protected static String buildMethodName(final String prefix, final String fieldName) { return (fieldName.startsWith(prefix) ? fieldName : prefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1)); } /** * Sets the derived value from {@link #deriveValueFromAccessor()} using * {@link #getSetter()} * * @see #deriveValueFromAccessor() * @return the accessor's return target value */ public F setDerivedValueFromAccessor() { F derived = null; try { derived = deriveValueFromAccessor(false); getSetter().invoke(derived); } catch (final Throwable t) { throw new RuntimeException(String.format( "Unable to set %1$s on %2$s", derived, getTarget()), t); } return derived; } /** * Gets an accessor's return target value obtained by calling the * accessor's {@link MethodHandle#invoke(Object...)} method. When the * value returned is null an attempt will be made to * instantiate it using either by using a default value from * {@link #DFLTS} (for primatives) or {@link Class#newInstance()} on the * accessor's {@link MethodType#returnType()} method. * * @return the accessor's return target value */ @SuppressWarnings("unchecked") protected F deriveValueFromAccessor(boolean isNullable) { F targetValue = null; try { targetValue = (F) getAccessor().invoke(); } catch (final Throwable t) { targetValue = null; } if (targetValue == null) { try { if (DFLTS.containsKey(getFieldType())) { targetValue = (F) DFLTS.get(getFieldType()); } else { final Class clazz = (Class) getAccessor().type() .returnType(); if (List.class.isAssignableFrom(clazz)) { targetValue = (F) new ArrayList<>(); } else if (Set.class.isAssignableFrom(clazz)) { targetValue = (F) new LinkedHashSet<>(); } else if (Map.class.isAssignableFrom(clazz)) { targetValue = (F) new HashMap<>(); } else if (!Calendar.class .isAssignableFrom(getFieldType()) && !String.class .isAssignableFrom(getFieldType())) { if (isNullable) { targetValue = null; } else { targetValue = clazz.newInstance(); } } } hasDefaultDerived = true; } catch (final Exception e) { throw new IllegalArgumentException( String.format( "Unable to get accessor return instance for %1$s using %2$s.", getAccessor(), getAccessor().type() .returnType())); } } else { hasDefaultDerived = false; } return targetValue; } /** * Binds a new target to the {@link FieldHandle} * * @param target * the target to bind to */ public void setTarget(final T target) { if (getTarget().equals(target)) { return; } this.target = target; updateMethodHandles(); } public T getTarget() { return target; } public String getFieldName() { return fieldName; } /** * @return the getter */ protected MethodHandle getAccessor() { return accessor; } /** * @return the setter */ protected MethodHandle getSetter() { return setter; } /** * @return the declared field type of the property value */ public Class getDeclaredFieldType() { return declaredFieldType; } /** * @return the field type from {@link #getAccessor()} of the property * value */ public Class getFieldType() { return getAccessor().type().returnType(); } /** * @return true if a default value has been derived */ public boolean hasDefaultDerived() { return hasDefaultDerived; } } }