diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/binding/FlatMappedBinding.java b/modules/javafx.base/src/main/java/com/sun/javafx/binding/FlatMappedBinding.java
new file mode 100644
index 00000000000..870ba859075
--- /dev/null
+++ b/modules/javafx.base/src/main/java/com/sun/javafx/binding/FlatMappedBinding.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.binding;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+import javafx.beans.value.ObservableValue;
+
+/**
+ * A binding holding the value of an indirect source. The indirect source results from
+ * applying a mapping to the given source.
+ *
+ *
Implementation:
+ *
+ *
In a flat mapped binding there are always two subscriptions involved:
+ *
+ *
The subscription on its source
+ *
The subscription on the value resulting from the mapping of the source: the indirect source
+ *
+ * The subscription on its given source is present when this binding itself is observed and not present otherwise.
+ *
+ *
The subscription on the indirect source must change whenever the value of the given source changes or is invalidated. More
+ * specifically, when the given source is invalidated the indirect subscription should be removed, and when it is revalidated it
+ * should resubscribe to the newly calculated indirect source. The binding avoids resubscribing when only the value of
+ * the indirect source changes.
+ *
+ * @param the type of the source
+ * @param the type of the resulting binding
+ */
+public class FlatMappedBinding extends LazyObjectBinding {
+
+ private final ObservableValue source;
+ private final Function super S, ? extends ObservableValue extends T>> mapper;
+
+ private Subscription indirectSourceSubscription = Subscription.EMPTY;
+ private ObservableValue extends T> indirectSource;
+
+ public FlatMappedBinding(ObservableValue source, Function super S, ? extends ObservableValue extends T>> mapper) {
+ this.source = Objects.requireNonNull(source, "source cannot be null");
+ this.mapper = Objects.requireNonNull(mapper, "mapper cannot be null");
+ }
+
+ @Override
+ protected T computeValue() {
+ S value = source.getValue();
+ ObservableValue extends T> newIndirectSource = value == null ? null : mapper.apply(value);
+
+ if (isObserved() && indirectSource != newIndirectSource) { // only resubscribe when observed and the indirect source changed
+ indirectSourceSubscription.unsubscribe();
+ indirectSourceSubscription = newIndirectSource == null ? Subscription.EMPTY : Subscription.subscribeInvalidations(newIndirectSource, this::invalidate);
+ indirectSource = newIndirectSource;
+ }
+
+ return newIndirectSource == null ? null : newIndirectSource.getValue();
+ }
+
+ @Override
+ protected Subscription observeSources() {
+ Subscription subscription = Subscription.subscribeInvalidations(source, this::invalidateAll);
+
+ return () -> {
+ subscription.unsubscribe();
+ unsubscribeIndirectSource();
+ };
+ }
+
+ /**
+ * Called when the primary source changes. Invalidates this binding and unsubscribes the indirect source
+ * to avoid holding a strong reference to it. If the binding becomes valid later, {@link #computeValue()} will
+ * subscribe to a newly calculated indirect source.
+ *
+ *
Note that this only needs to be called for changes of the primary source; changes in the indirect
+ * source only need to invalidate this binding without also unsubscribing, as it would be wasteful to resubscribe
+ * to the same indirect source for each invalidation of that source.
+ */
+ private void invalidateAll() {
+ unsubscribeIndirectSource();
+ invalidate();
+ }
+
+ private void unsubscribeIndirectSource() {
+ indirectSourceSubscription.unsubscribe();
+ indirectSourceSubscription = Subscription.EMPTY;
+ indirectSource = null;
+ }
+}
diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/binding/LazyObjectBinding.java b/modules/javafx.base/src/main/java/com/sun/javafx/binding/LazyObjectBinding.java
new file mode 100644
index 00000000000..d35030ec307
--- /dev/null
+++ b/modules/javafx.base/src/main/java/com/sun/javafx/binding/LazyObjectBinding.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.binding;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.value.ChangeListener;
+
+/**
+ * Extends {@link ObjectBinding} with the ability to lazily register and eagerly unregister listeners on its
+ * dependencies.
+ *
+ * @param the type of the wrapped {@code Object}
+ */
+abstract class LazyObjectBinding extends ObjectBinding {
+
+ private Subscription subscription;
+ private boolean wasObserved;
+
+ @Override
+ public void addListener(ChangeListener super T> listener) {
+ super.addListener(listener);
+
+ updateSubscriptionAfterAdd();
+ }
+
+ @Override
+ public void removeListener(ChangeListener super T> listener) {
+ super.removeListener(listener);
+
+ updateSubscriptionAfterRemove();
+ }
+
+ @Override
+ public void addListener(InvalidationListener listener) {
+ super.addListener(listener);
+
+ updateSubscriptionAfterAdd();
+ }
+
+ @Override
+ public void removeListener(InvalidationListener listener) {
+ super.removeListener(listener);
+
+ updateSubscriptionAfterRemove();
+ }
+
+ @Override
+ protected boolean allowValidation() {
+ return isObserved();
+ }
+
+ /**
+ * Called after a listener was added to start observing inputs if they're not observed already.
+ */
+ private void updateSubscriptionAfterAdd() {
+ if (!wasObserved) { // was first observer registered?
+ subscription = observeSources(); // start observing source
+
+ /*
+ * Although the act of registering a listener already attempts to make
+ * this binding valid, allowValidation won't allow it as the binding is
+ * not observed yet. This is because isObserved will not yet return true
+ * when the process of registering the listener hasn't completed yet.
+ *
+ * As the binding must be valid after it becomes observed the first time
+ * 'get' is called again.
+ *
+ * See com.sun.javafx.binding.ExpressionHelper (which is used
+ * by ObjectBinding) where it will do a call to ObservableValue#getValue
+ * BEFORE adding the actual listener. This results in ObjectBinding#get
+ * to be called in which the #allowValidation call will block it from
+ * becoming valid as the condition is "isObserved()"; this is technically
+ * correct as the listener wasn't added yet, but means we must call
+ * #get again to make this binding valid.
+ */
+
+ get(); // make binding valid as source wasn't tracked until now
+ wasObserved = true;
+ }
+ }
+
+ /**
+ * Called after a listener was removed to stop observing inputs if this was the last listener
+ * observing this binding.
+ */
+ private void updateSubscriptionAfterRemove() {
+ if (wasObserved && !isObserved()) { // was last observer unregistered?
+ subscription.unsubscribe();
+ subscription = null;
+ invalidate(); // make binding invalid as source is no longer tracked
+ wasObserved = false;
+ }
+ }
+
+ /**
+ * Called when this binding was previously not observed and a new observer was added. Implementors must return a
+ * {@link Subscription} which will be cancelled when this binding no longer has any observers.
+ *
+ * @return a {@link Subscription} which will be cancelled when this binding no longer has any observers, never null
+ */
+ protected abstract Subscription observeSources();
+}
diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/binding/MappedBinding.java b/modules/javafx.base/src/main/java/com/sun/javafx/binding/MappedBinding.java
new file mode 100644
index 00000000000..5eb039ac381
--- /dev/null
+++ b/modules/javafx.base/src/main/java/com/sun/javafx/binding/MappedBinding.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.binding;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+import javafx.beans.value.ObservableValue;
+
+public class MappedBinding extends LazyObjectBinding {
+
+ private final ObservableValue source;
+ private final Function super S, ? extends T> mapper;
+
+ public MappedBinding(ObservableValue source, Function super S, ? extends T> mapper) {
+ this.source = Objects.requireNonNull(source, "source cannot be null");
+ this.mapper = Objects.requireNonNull(mapper, "mapper cannot be null");
+ }
+
+ @Override
+ protected T computeValue() {
+ S value = source.getValue();
+
+ return value == null ? null : mapper.apply(value);
+ }
+
+ @Override
+ protected Subscription observeSources() {
+ return Subscription.subscribeInvalidations(source, this::invalidate); // start observing source
+ }
+}
diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/binding/OrElseBinding.java b/modules/javafx.base/src/main/java/com/sun/javafx/binding/OrElseBinding.java
new file mode 100644
index 00000000000..fd0de0197d0
--- /dev/null
+++ b/modules/javafx.base/src/main/java/com/sun/javafx/binding/OrElseBinding.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.binding;
+
+import java.util.Objects;
+
+import javafx.beans.value.ObservableValue;
+
+public class OrElseBinding extends LazyObjectBinding {
+
+ private final ObservableValue source;
+ private final T constant;
+
+ public OrElseBinding(ObservableValue source, T constant) {
+ this.source = Objects.requireNonNull(source, "source cannot be null");
+ this.constant = constant;
+ }
+
+ @Override
+ protected T computeValue() {
+ T value = source.getValue();
+
+ return value == null ? constant : value;
+ }
+
+ @Override
+ protected Subscription observeSources() {
+ return Subscription.subscribeInvalidations(source, this::invalidate); // start observing source
+ }
+}
diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/binding/Subscription.java b/modules/javafx.base/src/main/java/com/sun/javafx/binding/Subscription.java
new file mode 100644
index 00000000000..be89bcc1e59
--- /dev/null
+++ b/modules/javafx.base/src/main/java/com/sun/javafx/binding/Subscription.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.binding;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+
+/**
+ * A subscription encapsulates how to cancel it without having
+ * to keep track of how it was created.
+ *
+ *
For example:
+ *
+ *
{@code Subscription s = property.subscribe(System.out::println)}
+ *
+ *
The function passed in to {@code subscribe} does not need to be stored
+ * in order to clean up the subscription later.
+ */
+@FunctionalInterface
+public interface Subscription {
+
+ /**
+ * An empty subscription. Does nothing when cancelled.
+ */
+ static final Subscription EMPTY = () -> {};
+
+ /**
+ * Cancels this subscription.
+ */
+ void unsubscribe();
+
+ /**
+ * Combines this {@link Subscription} with the given {@code Subscription}
+ * and returns a new {@code Subscription} which will cancel both when
+ * cancelled.
+ *
+ * @param other another {@link Subscription}, cannot be {@code null}
+ * @return a combined {@link Subscription} which will cancel both when
+ * cancelled, never {@code null}
+ * @throws NullPointerException when {@code other} is {@code null}
+ */
+ default Subscription and(Subscription other) {
+ Objects.requireNonNull(other);
+
+ return () -> {
+ unsubscribe();
+ other.unsubscribe();
+ };
+ }
+
+ /**
+ * Creates a {@link Subscription} on this {@link ObservableValue} which
+ * immediately provides its current value to the given {@code subscriber},
+ * followed by any subsequent changes in value.
+ *
+ * @param subscriber a {@link Consumer} to supply with the values of this
+ * {@link ObservableValue}, cannot be {@code null}
+ * @return a {@link Subscription} which can be used to cancel this
+ * subscription, never {@code null}
+ * @throws NullPointerException when {@code observableValue} or {@code subscriber} is {@code null}
+ */
+ static Subscription subscribe(ObservableValue observableValue, Consumer super T> subscriber) {
+ Objects.requireNonNull(observableValue);
+ Objects.requireNonNull(subscriber);
+
+ ChangeListener listener = (obs, old, current) -> subscriber.accept(current);
+
+ subscriber.accept(observableValue.getValue()); // eagerly send current value
+ observableValue.addListener(listener);
+
+ return () -> observableValue.removeListener(listener);
+ }
+
+ /**
+ * Creates a {@link Subscription} on this {@link ObservableValue} which
+ * calls the given {@code runnable} whenever this {@code ObservableValue}
+ * becomes invalid.
+ *
+ * @param runnable a {@link Runnable} to call whenever this
+ * {@link ObservableValue} becomes invalid, cannot be @{code null}
+ * @return a {@link Subscription} which can be used to cancel this
+ * subscription, never @{code null}
+ * @throws NullPointerException when {@code observableValue} or {@code runnable} is {@code null}
+ */
+ static Subscription subscribeInvalidations(ObservableValue> observableValue, Runnable runnable) {
+ Objects.requireNonNull(observableValue);
+ Objects.requireNonNull(runnable);
+
+ InvalidationListener listener = obs -> runnable.run();
+
+ observableValue.addListener(listener);
+
+ return () -> observableValue.removeListener(listener);
+ }
+}
diff --git a/modules/javafx.base/src/main/java/javafx/beans/binding/Bindings.java b/modules/javafx.base/src/main/java/javafx/beans/binding/Bindings.java
index 45212231100..e4eb8bf2ab4 100644
--- a/modules/javafx.base/src/main/java/javafx/beans/binding/Bindings.java
+++ b/modules/javafx.base/src/main/java/javafx/beans/binding/Bindings.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -437,6 +437,10 @@ public ObservableList> getDependencies() {
*
* Note: since 8.0, JavaBeans properties are supported and might be in the chain.
*
+ *
+ * Since 19, it is recommended to use {@link ObservableValue#flatMap(java.util.function.Function)}
+ * to select a nested member of an {@link ObservableValue}.
+ *
*
* @param the type of the wrapped {@code Object}
* @param root
@@ -444,6 +448,7 @@ public ObservableList> getDependencies() {
* @param steps
* The property names to reach the final property
* @return the created {@link ObjectBinding}
+ * @see ObservableValue#flatMap(java.util.function.Function)
*/
public static ObjectBinding select(ObservableValue> root, String... steps) {
return new SelectBinding.AsObject(root, steps);
diff --git a/modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java b/modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
index 6368df52f1c..84f62a8416c 100644
--- a/modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
+++ b/modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -154,7 +154,13 @@ public ObservableList> getDependencies() {
@Override
public final T get() {
if (!valid) {
- value = computeValue();
+ T computed = computeValue();
+
+ if (!allowValidation()) {
+ return computed;
+ }
+
+ value = computed;
valid = true;
}
return value;
@@ -174,6 +180,7 @@ public final void invalidate() {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
+ value = null; // clear cached value to avoid hard reference to stale data
}
}
@@ -182,6 +189,35 @@ public final boolean isValid() {
return valid;
}
+ /**
+ * Checks if the binding has at least one listener registered on it. This
+ * is useful for subclasses which want to conserve resources when not observed.
+ *
+ * @return {@code true} if this binding currently has one or more
+ * listeners registered on it, otherwise {@code false}
+ * @since 19
+ */
+ protected final boolean isObserved() {
+ return helper != null;
+ }
+
+ /**
+ * Checks if the binding is allowed to become valid. Overriding classes can
+ * prevent a binding from becoming valid. This is useful in subclasses which
+ * do not always listen for invalidations of their dependencies and prefer to
+ * recompute the current value instead. This can also be useful if caching of
+ * the current computed value is not desirable.
+ *
+ * The default implementation always allows bindings to become valid.
+ *
+ * @return {@code true} if this binding is allowed to become valid, otherwise
+ * {@code false}
+ * @since 19
+ */
+ protected boolean allowValidation() {
+ return true;
+ }
+
/**
* Calculates the current value of this binding.
*
diff --git a/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java b/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
index f8c8d439f99..dba9e23f399 100644
--- a/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
+++ b/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,12 @@
package javafx.beans.value;
+import java.util.function.Function;
+
+import com.sun.javafx.binding.FlatMappedBinding;
+import com.sun.javafx.binding.MappedBinding;
+import com.sun.javafx.binding.OrElseBinding;
+
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -137,4 +143,112 @@ public interface ObservableValue extends Observable {
* @return The current value
*/
T getValue();
+
+ /**
+ * Returns an {@code ObservableValue} that holds the result of applying the
+ * given mapping function on this value. The result is updated when this
+ * {@code ObservableValue} changes. If this value is {@code null}, no
+ * mapping is applied and the resulting value is also {@code null}.
+ *
+ * For example, mapping a string to an upper case string:
+ *
+ *
+ * @param the type of values held by the resulting {@code ObservableValue}
+ * @param mapper the mapping function to apply to a value, cannot be {@code null}
+ * @return an {@code ObservableValue} that holds the result of applying the given
+ * mapping function on this value, or {@code null} when it
+ * is {@code null}; never returns {@code null}
+ * @throws NullPointerException if the mapping function is {@code null}
+ * @since 19
+ */
+ default ObservableValue map(Function super T, ? extends U> mapper) {
+ return new MappedBinding<>(this, mapper);
+ }
+
+ /**
+ * Returns an {@code ObservableValue} that holds this value, or the given constant if
+ * it is {@code null}. The result is updated when this {@code ObservableValue} changes. This
+ * method, when combined with {@link #map(Function)}, allows handling of all values
+ * including {@code null} values.
+ *
+ * For example, mapping a string to an upper case string, but leaving it blank
+ * if the input is {@code null}:
+ *
+ *
+ * @param constant the value to use when this {@code ObservableValue}
+ * holds {@code null}; can be {@code null}
+ * @return an {@code ObservableValue} that holds this value, or the given constant if
+ * it is {@code null}; never returns {@code null}
+ * @since 19
+ */
+ default ObservableValue orElse(T constant) {
+ return new OrElseBinding<>(this, constant);
+ }
+
+ /**
+ * Returns an {@code ObservableValue} that holds the value of an {@code ObservableValue}
+ * produced by applying the given mapping function on this value. The result is updated
+ * when either this {@code ObservableValue} or the {@code ObservableValue} produced by
+ * the mapping changes. If this value is {@code null}, no mapping is applied and the
+ * resulting value is {@code null}. If the mapping resulted in {@code null}, then the
+ * resulting value is also {@code null}.
+ *
+ * This method is similar to {@link #map(Function)}, but the mapping function is
+ * one whose result is already an {@code ObservableValue}, and if invoked, {@code flatMap} does
+ * not wrap it within an additional {@code ObservableValue}.
+ *
+ * For example, a property that is only {@code true} when a UI element is part of a {@code Scene}
+ * that is part of a {@code Window} that is currently shown on screen:
+ *
+ * Changes in any of the values of: the scene of {@code listView}, the window of that scene, or
+ * the showing of that window, will update the boolean value {@code isShowing}.
+ *
+ * This method is preferred over {@link javafx.beans.binding.Bindings#select Bindings} methods
+ * since it is type safe.
+ *
+ * @param the type of values held by the resulting {@code ObservableValue}
+ * @param mapper the mapping function to apply to a value, cannot be {@code null}
+ * @return an {@code ObservableValue} that holds the value of an {@code ObservableValue}
+ * produced by applying the given mapping function on this value, or
+ * {@code null} when the value is {@code null}; never returns {@code null}
+ * @throws NullPointerException if the mapping function is {@code null}
+ * @since 19
+ */
+ default ObservableValue flatMap(Function super T, ? extends ObservableValue extends U>> mapper) {
+ return new FlatMappedBinding<>(this, mapper);
+ }
}
diff --git a/modules/javafx.base/src/shims/java/com/sun/javafx/binding/LazyObjectBindingStub.java b/modules/javafx.base/src/shims/java/com/sun/javafx/binding/LazyObjectBindingStub.java
new file mode 100644
index 00000000000..55e58d0b23a
--- /dev/null
+++ b/modules/javafx.base/src/shims/java/com/sun/javafx/binding/LazyObjectBindingStub.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.binding;
+
+/**
+ * Stub to allow testing of package private LazyObjectBinding.
+ */
+public class LazyObjectBindingStub extends LazyObjectBinding {
+
+ public int computeValueCalls;
+ public int startObservingCalls;
+ public int stopObservingCalls;
+
+ @Override
+ protected T computeValue() {
+ computeValueCalls++;
+
+ return null;
+ }
+
+ @Override
+ protected Subscription observeSources() {
+ startObservingCalls++;
+
+ return () -> {
+ stopObservingCalls++;
+ };
+ }
+}
diff --git a/modules/javafx.base/src/test/java/test/javafx/beans/value/LazyObjectBindingTest.java b/modules/javafx.base/src/test/java/test/javafx/beans/value/LazyObjectBindingTest.java
new file mode 100644
index 00000000000..72da81b3c26
--- /dev/null
+++ b/modules/javafx.base/src/test/java/test/javafx/beans/value/LazyObjectBindingTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package test.javafx.beans.value;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.sun.javafx.binding.LazyObjectBindingStub;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.value.ChangeListener;
+
+public class LazyObjectBindingTest {
+
+ private LazyObjectBindingStub binding = new LazyObjectBindingStub<>();
+
+ void resetCounters() {
+ binding.startObservingCalls = 0;
+ binding.computeValueCalls = 0;
+ binding.stopObservingCalls = 0;
+ }
+
+ @Test
+ void shouldBeInvalidInitially() {
+ assertFalse(binding.isValid());
+ }
+
+ @Nested
+ class WhenObservedWithInvalidationListener {
+ private InvalidationListener invalidationListener = obs -> {};
+
+ {
+ binding.addListener(invalidationListener);
+ }
+
+ @Test
+ void shouldBeValid() {
+ assertTrue(binding.isValid());
+ }
+
+ @Test
+ void shouldStartObservingSource() {
+ assertEquals(1, binding.startObservingCalls);
+ }
+
+ @Test
+ void shouldNotStopObservingSource() {
+ assertEquals(0, binding.stopObservingCalls);
+ }
+
+ @Test
+ void shouldCallComputeValueOneOrTwoTimes() {
+
+ /*
+ * The binding is made valid twice currently, once when
+ * the listener is registered, and again after the observing of
+ * inputs starts. The first time the binding does not become
+ * valid because it is not yet considered "observed" as the
+ * computeValue call occurs in the middle of the listener
+ * registration process.
+ *
+ * See also the explanation in LazyObjectBinding#updateSubcriptionAfterAdd
+ */
+
+ assertTrue(binding.computeValueCalls >= 1 && binding.computeValueCalls <= 2);
+ }
+
+ @Nested
+ class AndWhenObservedAgain {
+ private ChangeListener changeListener = (obs, old, current) -> {};
+
+ {
+ resetCounters();
+ binding.addListener(changeListener);
+ }
+
+ @Test
+ void shouldStillBeValid() {
+ assertTrue(binding.isValid());
+ }
+
+ @Test
+ void shouldNotStartObservingSourceAgain() {
+ assertEquals(0, binding.startObservingCalls);
+ }
+
+ @Test
+ void shouldNotStopObservingSource() {
+ assertEquals(0, binding.stopObservingCalls);
+ }
+
+ @Test
+ void shouldNotComputeValueAgain() {
+ assertEquals(0, binding.computeValueCalls);
+ }
+
+ @Nested
+ class AndThenOneObserverIsRemoved {
+ {
+ resetCounters();
+ binding.removeListener(changeListener);
+ }
+
+ @Test
+ void shouldStillBeValid() {
+ assertTrue(binding.isValid());
+ }
+
+ @Test
+ void shouldNotStartObservingSourceAgain() {
+ assertEquals(0, binding.startObservingCalls);
+ }
+
+ @Test
+ void shouldNotStopObservingSource() {
+ assertEquals(0, binding.stopObservingCalls);
+ }
+
+ @Test
+ void shouldNotComputeValueAgain() {
+ assertEquals(0, binding.computeValueCalls);
+ }
+
+ @Nested
+ class AndThenTheLastObserverIsRemoved {
+ {
+ resetCounters();
+ binding.removeListener(invalidationListener);
+ }
+
+ @Test
+ void shouldNotStartObservingSource() {
+ assertEquals(0, binding.startObservingCalls);
+ }
+
+ @Test
+ void shouldStopObservingSource() {
+ assertEquals(1, binding.stopObservingCalls);
+ }
+
+ @Test
+ void shouldNotComputeValue() {
+ assertEquals(0, binding.computeValueCalls);
+ }
+
+ @Test
+ void shouldNoLongerBeValid() {
+ assertFalse(binding.isValid());
+ }
+
+ @Nested
+ class AndTheListenerIsRemovedAgain {
+ {
+ resetCounters();
+ binding.removeListener(invalidationListener);
+ }
+
+ @Test
+ void shouldNotStartObservingSource() {
+ assertEquals(0, binding.startObservingCalls);
+ }
+
+ @Test
+ void shouldNotStopObservingSource() {
+ assertEquals(0, binding.stopObservingCalls);
+ }
+
+ @Test
+ void shouldNotComputeValue() {
+ assertEquals(0, binding.computeValueCalls);
+ }
+
+ @Test
+ void shouldNotBeValid() {
+ assertFalse(binding.isValid());
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/modules/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java b/modules/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
new file mode 100644
index 00000000000..b6722f7dad5
--- /dev/null
+++ b/modules/javafx.base/src/test/java/test/javafx/beans/value/ObservableValueFluentBindingsTest.java
@@ -0,0 +1,987 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package test.javafx.beans.value;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+
+public class ObservableValueFluentBindingsTest {
+ private int invalidations;
+
+ private final StringProperty property = new SimpleStringProperty("Initial");
+ private final List values = new ArrayList<>();
+ private final ChangeListener changeListener = (obs, old, current) -> values.add(current);
+ private final InvalidationListener invalidationListener = obs -> invalidations++;
+
+ @Nested
+ class When_map_Called {
+
+ @Nested
+ class WithNull {
+
+ @Test
+ void shouldThrowNullPointerException() {
+ assertThrows(NullPointerException.class, () -> property.map(null));
+ }
+ }
+
+ @Nested
+ class WithNotNullReturns_ObservableValue_Which {
+ private ObservableValue observableValue = property.map(v -> v + "+map");
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class When_getValue_Called {
+
+ @Test
+ void shouldReturnPropertyValuesWithOperationApplied() {
+ assertEquals("Initial+map", observableValue.getValue());
+
+ property.set("Left");
+
+ assertEquals("Left+map", observableValue.getValue());
+ }
+
+ @Test
+ void shouldNotOperateOnNull() {
+ property.set(null);
+
+ assertEquals((String) null, observableValue.getValue());
+ }
+ }
+
+ @Nested
+ class WhenObservedForInvalidations {
+ {
+ startObservingInvalidations(observableValue);
+ }
+
+ @Test
+ void shouldOnlyInvalidateOnce() {
+ assertNotInvalidated();
+
+ property.set("Left");
+
+ assertInvalidated();
+
+ property.set("Right");
+
+ assertNotInvalidated();
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingInvalidations(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNotInvalidated();
+
+ property.set("Left");
+ property.set("Right");
+
+ assertNotInvalidated();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyOperation() {
+ assertNothingIsObserved();
+
+ property.set("Right");
+
+ assertObserved("Right+map");
+ }
+
+ @Test
+ void shouldNotOperateOnNull() {
+ property.set(null);
+
+ assertObserved((String) null); // map operation is skipped (as it would NPE otherwise) and the resulting value is null
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Right");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+
+ @Nested
+ class When_orElse_CalledReturns_ObservableValue_Which {
+ {
+ observableValue = observableValue.orElse("Empty");
+ }
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyMapThenOrElseOperation() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertObserved("Left+map");
+
+ property.set(null);
+
+ assertObserved("Empty");
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+ }
+
+ @Nested
+ class When_map_CalledAgainReturns_ObservableValue_Which {
+ {
+ observableValue = observableValue.map(v -> v + "+map2");
+ }
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyMapThenSecondMapOperation() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertObserved("Left+map+map2");
+
+ property.set(null);
+
+ assertObserved((String) null);
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Nested
+ class When_orElse_CalledReturns_ObservableValue_Which {
+ private ObservableValue observableValue = property.orElse("Empty");
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class When_getValue_Called {
+
+ @Test
+ void shouldReturnPropertyValuesWithOperationApplied() {
+ assertEquals("Initial", observableValue.getValue());
+
+ property.set(null);
+
+ assertEquals("Empty", observableValue.getValue());
+ }
+ }
+
+ @Nested
+ class WhenObservedForInvalidations {
+ {
+ startObservingInvalidations(observableValue);
+ }
+
+ @Test
+ void shouldOnlyInvalidateOnce() {
+ assertNotInvalidated();
+
+ property.set("Left");
+
+ assertInvalidated();
+
+ property.set(null);
+
+ assertNotInvalidated();
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingInvalidations(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNotInvalidated();
+
+ property.set("Left");
+ property.set(null);
+
+ assertNotInvalidated();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyOperation() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertObserved("Left");
+
+ property.set(null);
+
+ assertObserved("Empty");
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+ }
+
+ @Nested
+ class When_flatMap_Called {
+
+ @Nested
+ class WithNull {
+
+ @Test
+ void shouldThrowNullPointerException() {
+ assertThrows(NullPointerException.class, () -> property.flatMap(null));
+ }
+ }
+
+ @Nested
+ class WithNotNullReturns_ObservableValue_Which {
+ private int subscribeCount;
+ private int unsubscribeCount;
+
+ private StringProperty left = new SimpleStringProperty("LEFT");
+ private StringProperty right = new SimpleStringProperty("RIGHT");
+ private StringProperty unknown = new SimpleStringProperty("UNKNOWN") {
+ @Override
+ public void addListener(InvalidationListener listener) {
+ super.addListener(listener);
+
+ subscribeCount++;
+ };
+
+ @Override
+ public void removeListener(InvalidationListener listener) {
+ super.removeListener(listener);
+
+ unsubscribeCount++;
+ };
+ };
+
+ private ObservableValue observableValue =
+ property.flatMap(v -> "Left".equals(v) ? left : "Right".equals(v) ? right : unknown);
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class When_getValue_Called {
+
+ @Test
+ void shouldReturnPropertyValuesWithOperationApplied() {
+ assertEquals("UNKNOWN", observableValue.getValue()); // initially it is not left or right, so unknown
+
+ property.set("Right");
+
+ assertEquals("RIGHT", observableValue.getValue());
+
+ right.setValue("RIGHT+1");
+
+ assertEquals("RIGHT+1", observableValue.getValue());
+
+ left.setValue("LEFT+1");
+ unknown.setValue("UNKNOWN+1");
+
+ assertEquals("RIGHT+1", observableValue.getValue()); // changing left or unknown value should have no effect
+
+ property.set("Left");
+
+ assertEquals("LEFT+1", observableValue.getValue()); // after switching to left, it switches to the left value
+ }
+
+ @Test
+ void shouldNotOperateOnNull() {
+ property.set(null);
+
+ assertNull(observableValue.getValue());
+ }
+
+ @Test
+ void shouldIgnoreFlatMapsToNull() {
+ unknown = null;
+
+ assertNull(observableValue.getValue());
+ }
+ }
+
+ @Nested
+ class WhenObservedForInvalidations {
+ {
+ startObservingInvalidations(observableValue);
+ }
+
+ @Test
+ void shouldOnlyInvalidateOnce() {
+ assertNotInvalidated();
+
+ unknown.set("UNKNOWN+1");
+
+ assertInvalidated();
+
+ property.set("Right");
+
+ assertNotInvalidated();
+ }
+
+ @Test
+ void shouldNotResubscribeToMappedPropertyOnEachValidation() {
+ assertEquals(1, subscribeCount);
+ assertEquals(0, unsubscribeCount);
+
+ unknown.set("A");
+ observableValue.getValue();
+ unknown.set("B");
+ observableValue.getValue();
+ unknown.set("C");
+ observableValue.getValue();
+
+ assertEquals(1, subscribeCount);
+ assertEquals(0, unsubscribeCount);
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Test
+ void shouldStronglyReferMappedProperty() {
+ ReferenceAsserts.testIfStronglyReferenced(unknown, () -> unknown = null);
+ }
+
+ @Test
+ void shouldNotStronglyReferOldMappedProperty() {
+ property.set("Right");
+
+ ReferenceAsserts.testIfNotStronglyReferenced(unknown, () -> unknown = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingInvalidations(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNotInvalidated();
+
+ property.set("Left");
+ property.set("Right");
+ property.set("Unknown");
+
+ assertNotInvalidated();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ ReferenceAsserts.testIfNotStronglyReferenced(unknown, () -> unknown = null);
+ }
+ }
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyOperation() {
+ assertNothingIsObserved();
+
+ unknown.set("UNKNOWN+1");
+
+ assertObserved("UNKNOWN+1"); // as it initially is unknown, changing the unknown property results in a change
+
+ property.set("Right");
+
+ assertObserved("RIGHT"); // switching to right gives the value of the right property
+
+ unknown.set("UNKNOWN+2");
+ left.set("LEFT+1");
+
+ assertNothingIsObserved(); // changing left or unknown has no effect when currently observing right
+
+ right.set("RIGHT+1");
+
+ assertObserved("RIGHT+1"); // changing right value has an effect as right is observed
+
+ property.set("Left");
+
+ assertObserved("LEFT+1"); // switching to left sees latest left value
+ }
+
+ @Test
+ void shouldNotResubscribeToMappedPropertyOnEachValidation() {
+ assertEquals(1, subscribeCount);
+ assertEquals(0, unsubscribeCount);
+
+ unknown.set("A");
+ unknown.set("B");
+ unknown.set("C");
+
+ assertEquals(1, subscribeCount);
+ assertEquals(0, unsubscribeCount);
+ }
+
+ @Test
+ void shouldNotOperateOnNull() {
+ property.set(null);
+
+ assertObserved((String)null); // flatMap operation is skipped (as it would NPE otherwise) and the resulting value is null
+ }
+
+ @Test
+ void shouldIgnoreFlatMapsToNull() {
+ right = null;
+
+ property.set("Right");
+
+ assertObserved((String)null); // flatMap maps to right property which is now null, this results in null
+ }
+
+ @Test
+ void shouldObserveNullWhenFlatMappedPropertyIsSetToNull() {
+ property.set("Right");
+
+ assertObserved("RIGHT");
+
+ property.set(null);
+
+ assertObserved((String)null);
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Test
+ void shouldStronglyReferMappedProperty() {
+ ReferenceAsserts.testIfStronglyReferenced(unknown, () -> unknown = null);
+ }
+
+ @Test
+ void shouldNotStronglyReferOldMappedProperty() {
+ property.set("Right");
+
+ ReferenceAsserts.testIfNotStronglyReferenced(unknown, () -> unknown = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+ property.set("Right");
+ property.set("Unknown");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ ReferenceAsserts.testIfNotStronglyReferenced(unknown, () -> unknown = null);
+ }
+ }
+ }
+
+ @Nested
+ class When_map_CalledReturns_ObservableValue_Which {
+ {
+ observableValue = observableValue.map(v -> v + "+map");
+ }
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyFlatMapThenMapOperation() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertObserved("LEFT+map");
+
+ property.set("Right");
+
+ assertObserved("RIGHT+map");
+
+ left.set("LEFT-LEFT"); // should have no effect
+
+ assertNothingIsObserved();
+
+ right.set("RIGHT-RIGHT");
+
+ assertObserved("RIGHT-RIGHT+map");
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+ }
+
+ @Nested
+ class When_orElse_CalledReturns_ObservableValue_Which {
+ {
+ observableValue = observableValue.orElse("Empty");
+ }
+
+ @Test
+ void shouldNotBeNull() {
+ assertNotNull(observableValue);
+ }
+
+ @Test
+ void shouldNotBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class WhenObservedForChanges {
+ {
+ startObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldApplyFlatMapThenMapOperation() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertObserved("LEFT");
+
+ property.set("Right");
+
+ assertObserved("RIGHT");
+
+ left.set("LEFT-LEFT"); // should have no effect as right branch is observed
+
+ assertNothingIsObserved();
+
+ right.set("RIGHT-RIGHT");
+
+ assertObserved("RIGHT-RIGHT");
+
+ right.set(null);
+
+ assertObserved("Empty");
+
+ property.set("Left");
+
+ assertObserved("LEFT-LEFT");
+
+ property.set(null);
+
+ assertObserved("Empty");
+ }
+
+ @Test
+ void shouldBeStronglyReferenced() {
+ ReferenceAsserts.testIfStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+
+ @Nested
+ class AndWhenUnobserved {
+ {
+ stopObservingChanges(observableValue);
+ }
+
+ @Test
+ void shouldNoLongerBeCalled() {
+ assertNothingIsObserved();
+
+ property.set("Left");
+
+ assertNothingIsObserved();
+ }
+
+ @Test
+ void shouldNoLongerBeStronglyReferenced() {
+ ReferenceAsserts.testIfNotStronglyReferenced(observableValue, () -> observableValue = null);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Ensures nothing has been observed since the last check.
+ */
+ private void assertNothingIsObserved() {
+ assertObserved();
+ }
+
+ /**
+ * Ensures that given values have been observed since last call.
+ *
+ * @param expectedValues an array of expected values
+ */
+ private void assertObserved(String... expectedValues) {
+ assertEquals(values, Arrays.asList(expectedValues));
+ values.clear();
+ }
+
+ /**
+ * Starts observing the given observable value for changes. This will do
+ * a sanity check that the observer is currently not working before adding it.
+ *
+ * @param observableValue an {@code ObservableValue}, cannot be {@code null}
+ */
+ private void startObservingChanges(ObservableValue observableValue) {
+ values.clear();
+
+ property.setValue("Left");
+ property.setValue("Right");
+ property.setValue("Initial");
+
+ assertTrue(values.isEmpty());
+
+ observableValue.addListener(changeListener);
+ }
+
+ /**
+ * Stops observing the given observable value for changes. This will do a
+ * sanity check that the observer is currently working before removing it.
+ *
+ * @param observableValue an {@code ObservableValue}, cannot be {@code null}
+ */
+ private void stopObservingChanges(ObservableValue observableValue) {
+ values.clear();
+
+ property.setValue("Left");
+ property.setValue("Right");
+
+ assertEquals(2, values.size());
+
+ values.clear();
+
+ observableValue.removeListener(changeListener);
+ }
+
+ /**
+ * Ensures no invalidations occurred since the last check.
+ */
+ private void assertNotInvalidated() {
+ assertEquals(0, invalidations);
+ }
+
+ /**
+ * Ensures that an invalidation occurred since last check.
+ */
+ private void assertInvalidated() {
+ assertEquals(1, invalidations);
+ invalidations = 0;
+ }
+
+ /**
+ * Starts observing the given observable value for invalidations. This will do
+ * a sanity check that the observer is currently not working before adding it.
+ *
+ * @param observableValue an {@code ObservableValue}, cannot be {@code null}
+ */
+ private void startObservingInvalidations(ObservableValue observableValue) {
+ invalidations = 0;
+
+ property.getValue();
+ property.setValue("Left");
+ property.setValue("Right");
+ property.setValue("Initial");
+
+ assertNotInvalidated();
+
+ observableValue.addListener(invalidationListener);
+ }
+
+ /**
+ * Stops observing the given observable value for invalidations. This will do a
+ * sanity check that the observer is currently working before removing it.
+ *
+ * @param observableValue an {@code ObservableValue}, cannot be {@code null}
+ */
+ private void stopObservingInvalidations(ObservableValue observableValue) {
+ invalidations = 0;
+
+ property.getValue();
+ property.setValue("Left");
+ property.setValue("Right");
+ property.setValue("Initial");
+
+ assertInvalidated();
+
+ observableValue.removeListener(invalidationListener);
+ }
+}
diff --git a/modules/javafx.base/src/test/java/test/javafx/beans/value/ReferenceAsserts.java b/modules/javafx.base/src/test/java/test/javafx/beans/value/ReferenceAsserts.java
new file mode 100644
index 00000000000..a04691ef3cf
--- /dev/null
+++ b/modules/javafx.base/src/test/java/test/javafx/beans/value/ReferenceAsserts.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package test.javafx.beans.value;
+
+import java.lang.ref.WeakReference;
+
+import test.util.memory.JMemoryBuddy;
+
+public class ReferenceAsserts {
+
+ public static void testIfStronglyReferenced(Object obj, Runnable clearRefs) {
+ WeakReference