Skip to content

Commit 60c75b8

Browse files
hjohnkevinrushforth
authored andcommitted
8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue
Reviewed-by: nlisker, mstrauss, kcr
1 parent 178d898 commit 60c75b8

12 files changed

Lines changed: 1923 additions & 4 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package com.sun.javafx.binding;
27+
28+
import java.util.Objects;
29+
import java.util.function.Function;
30+
31+
import javafx.beans.value.ObservableValue;
32+
33+
/**
34+
* A binding holding the value of an indirect source. The indirect source results from
35+
* applying a mapping to the given source.
36+
*
37+
* <p>Implementation:
38+
*
39+
* <p>In a flat mapped binding there are always two subscriptions involved:
40+
* <ul>
41+
* <li>The subscription on its source</li>
42+
* <li>The subscription on the value resulting from the mapping of the source: the indirect source</li>
43+
* </ul>
44+
* The subscription on its given source is present when this binding itself is observed and not present otherwise.
45+
*
46+
* <p>The subscription on the indirect source must change whenever the value of the given source changes or is invalidated. More
47+
* specifically, when the given source is invalidated the indirect subscription should be removed, and when it is revalidated it
48+
* should resubscribe to the newly calculated indirect source. The binding avoids resubscribing when only the value of
49+
* the indirect source changes.
50+
*
51+
* @param <S> the type of the source
52+
* @param <T> the type of the resulting binding
53+
*/
54+
public class FlatMappedBinding<S, T> extends LazyObjectBinding<T> {
55+
56+
private final ObservableValue<S> source;
57+
private final Function<? super S, ? extends ObservableValue<? extends T>> mapper;
58+
59+
private Subscription indirectSourceSubscription = Subscription.EMPTY;
60+
private ObservableValue<? extends T> indirectSource;
61+
62+
public FlatMappedBinding(ObservableValue<S> source, Function<? super S, ? extends ObservableValue<? extends T>> mapper) {
63+
this.source = Objects.requireNonNull(source, "source cannot be null");
64+
this.mapper = Objects.requireNonNull(mapper, "mapper cannot be null");
65+
}
66+
67+
@Override
68+
protected T computeValue() {
69+
S value = source.getValue();
70+
ObservableValue<? extends T> newIndirectSource = value == null ? null : mapper.apply(value);
71+
72+
if (isObserved() && indirectSource != newIndirectSource) { // only resubscribe when observed and the indirect source changed
73+
indirectSourceSubscription.unsubscribe();
74+
indirectSourceSubscription = newIndirectSource == null ? Subscription.EMPTY : Subscription.subscribeInvalidations(newIndirectSource, this::invalidate);
75+
indirectSource = newIndirectSource;
76+
}
77+
78+
return newIndirectSource == null ? null : newIndirectSource.getValue();
79+
}
80+
81+
@Override
82+
protected Subscription observeSources() {
83+
Subscription subscription = Subscription.subscribeInvalidations(source, this::invalidateAll);
84+
85+
return () -> {
86+
subscription.unsubscribe();
87+
unsubscribeIndirectSource();
88+
};
89+
}
90+
91+
/**
92+
* Called when the primary source changes. Invalidates this binding and unsubscribes the indirect source
93+
* to avoid holding a strong reference to it. If the binding becomes valid later, {@link #computeValue()} will
94+
* subscribe to a newly calculated indirect source.
95+
*
96+
* <p>Note that this only needs to be called for changes of the primary source; changes in the indirect
97+
* source only need to invalidate this binding without also unsubscribing, as it would be wasteful to resubscribe
98+
* to the same indirect source for each invalidation of that source.
99+
*/
100+
private void invalidateAll() {
101+
unsubscribeIndirectSource();
102+
invalidate();
103+
}
104+
105+
private void unsubscribeIndirectSource() {
106+
indirectSourceSubscription.unsubscribe();
107+
indirectSourceSubscription = Subscription.EMPTY;
108+
indirectSource = null;
109+
}
110+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package com.sun.javafx.binding;
27+
28+
import javafx.beans.InvalidationListener;
29+
import javafx.beans.binding.ObjectBinding;
30+
import javafx.beans.value.ChangeListener;
31+
32+
/**
33+
* Extends {@link ObjectBinding} with the ability to lazily register and eagerly unregister listeners on its
34+
* dependencies.
35+
*
36+
* @param <T> the type of the wrapped {@code Object}
37+
*/
38+
abstract class LazyObjectBinding<T> extends ObjectBinding<T> {
39+
40+
private Subscription subscription;
41+
private boolean wasObserved;
42+
43+
@Override
44+
public void addListener(ChangeListener<? super T> listener) {
45+
super.addListener(listener);
46+
47+
updateSubscriptionAfterAdd();
48+
}
49+
50+
@Override
51+
public void removeListener(ChangeListener<? super T> listener) {
52+
super.removeListener(listener);
53+
54+
updateSubscriptionAfterRemove();
55+
}
56+
57+
@Override
58+
public void addListener(InvalidationListener listener) {
59+
super.addListener(listener);
60+
61+
updateSubscriptionAfterAdd();
62+
}
63+
64+
@Override
65+
public void removeListener(InvalidationListener listener) {
66+
super.removeListener(listener);
67+
68+
updateSubscriptionAfterRemove();
69+
}
70+
71+
@Override
72+
protected boolean allowValidation() {
73+
return isObserved();
74+
}
75+
76+
/**
77+
* Called after a listener was added to start observing inputs if they're not observed already.
78+
*/
79+
private void updateSubscriptionAfterAdd() {
80+
if (!wasObserved) { // was first observer registered?
81+
subscription = observeSources(); // start observing source
82+
83+
/*
84+
* Although the act of registering a listener already attempts to make
85+
* this binding valid, allowValidation won't allow it as the binding is
86+
* not observed yet. This is because isObserved will not yet return true
87+
* when the process of registering the listener hasn't completed yet.
88+
*
89+
* As the binding must be valid after it becomes observed the first time
90+
* 'get' is called again.
91+
*
92+
* See com.sun.javafx.binding.ExpressionHelper (which is used
93+
* by ObjectBinding) where it will do a call to ObservableValue#getValue
94+
* BEFORE adding the actual listener. This results in ObjectBinding#get
95+
* to be called in which the #allowValidation call will block it from
96+
* becoming valid as the condition is "isObserved()"; this is technically
97+
* correct as the listener wasn't added yet, but means we must call
98+
* #get again to make this binding valid.
99+
*/
100+
101+
get(); // make binding valid as source wasn't tracked until now
102+
wasObserved = true;
103+
}
104+
}
105+
106+
/**
107+
* Called after a listener was removed to stop observing inputs if this was the last listener
108+
* observing this binding.
109+
*/
110+
private void updateSubscriptionAfterRemove() {
111+
if (wasObserved && !isObserved()) { // was last observer unregistered?
112+
subscription.unsubscribe();
113+
subscription = null;
114+
invalidate(); // make binding invalid as source is no longer tracked
115+
wasObserved = false;
116+
}
117+
}
118+
119+
/**
120+
* Called when this binding was previously not observed and a new observer was added. Implementors must return a
121+
* {@link Subscription} which will be cancelled when this binding no longer has any observers.
122+
*
123+
* @return a {@link Subscription} which will be cancelled when this binding no longer has any observers, never null
124+
*/
125+
protected abstract Subscription observeSources();
126+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package com.sun.javafx.binding;
27+
28+
import java.util.Objects;
29+
import java.util.function.Function;
30+
31+
import javafx.beans.value.ObservableValue;
32+
33+
public class MappedBinding<S, T> extends LazyObjectBinding<T> {
34+
35+
private final ObservableValue<S> source;
36+
private final Function<? super S, ? extends T> mapper;
37+
38+
public MappedBinding(ObservableValue<S> source, Function<? super S, ? extends T> mapper) {
39+
this.source = Objects.requireNonNull(source, "source cannot be null");
40+
this.mapper = Objects.requireNonNull(mapper, "mapper cannot be null");
41+
}
42+
43+
@Override
44+
protected T computeValue() {
45+
S value = source.getValue();
46+
47+
return value == null ? null : mapper.apply(value);
48+
}
49+
50+
@Override
51+
protected Subscription observeSources() {
52+
return Subscription.subscribeInvalidations(source, this::invalidate); // start observing source
53+
}
54+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package com.sun.javafx.binding;
27+
28+
import java.util.Objects;
29+
30+
import javafx.beans.value.ObservableValue;
31+
32+
public class OrElseBinding<T> extends LazyObjectBinding<T> {
33+
34+
private final ObservableValue<T> source;
35+
private final T constant;
36+
37+
public OrElseBinding(ObservableValue<T> source, T constant) {
38+
this.source = Objects.requireNonNull(source, "source cannot be null");
39+
this.constant = constant;
40+
}
41+
42+
@Override
43+
protected T computeValue() {
44+
T value = source.getValue();
45+
46+
return value == null ? constant : value;
47+
}
48+
49+
@Override
50+
protected Subscription observeSources() {
51+
return Subscription.subscribeInvalidations(source, this::invalidate); // start observing source
52+
}
53+
}

0 commit comments

Comments
 (0)