jfxtras.labs.scene.control.BeanPathAdapter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jfxtras-labs Show documentation
Show all versions of jfxtras-labs Show documentation
Experimental components for JavaFX 2
/**
* 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:
*
* -
* 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());
*
*
*
* -
* 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());
*
*
*
* -
* 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);
*
*
*
* -
* 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);
*
*
*
* -
* 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);
*
*
*
* -
* 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");
*
*
*
* -
* 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");
*
*
*
* -
* 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);
*
*
*
* -
* 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);
*
*
*
* -
* 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);
* }
* });
*
*
*
* -
* {@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);
*
*
*
* -
* {@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);
*
*
*
* -
* 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);
* }
* });
*
*
*
*
*
* @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, PT> 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, PT> 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, PT> 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