Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue #675

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d9bfefe
Initial proposal
hjohn Oct 7, 2021
312fb50
Upgrade tests to JUnit 5
hjohn Dec 15, 2021
30e8cea
Apply changes suggested in review and updated copyright years to 2022
hjohn Jan 5, 2022
040bfe4
Fix grammar mistakes and did some small rephrases
hjohn Jan 10, 2022
b013e2d
Change code according to review comments
hjohn Jan 27, 2022
14048a9
Clean up some missed asserts and some nested class names
hjohn Jan 27, 2022
30733cc
Fix wrong test values
hjohn Jan 27, 2022
29dc2af
Process review comments
hjohn Mar 10, 2022
8f9bf89
Process review comments (2)
hjohn Mar 10, 2022
72cd24d
Add line feed at last line where it was missing
hjohn Mar 18, 2022
711ea90
Fix code blocks
hjohn Mar 18, 2022
8ba9e92
Clean up docs in Subscription
hjohn Mar 18, 2022
49e1f81
Add missing javadoc tags
hjohn Mar 18, 2022
6a5358d
Reword flat map docs a bit and fixed a link
hjohn Mar 18, 2022
cb01f11
Update API docs for ObservableValue
hjohn Mar 21, 2022
e09186c
Small wording change in API of ObservableValue after proof reading
hjohn Mar 21, 2022
e2703e6
Fix wording
hjohn Mar 22, 2022
a880666
Add since tags to all new API
hjohn May 28, 2022
c352390
Expand flatMap javadoc with additional wording from Optional#flatMap
hjohn May 28, 2022
3ac734b
Rename observeInputs to observeSources
hjohn Jun 30, 2022
baa97bd
Fix typos in LazyObjectBinding
hjohn Jun 30, 2022
abe1333
Fix bug invalidation bug in FlatMappedBinding
hjohn Jun 30, 2022
a93b826
Add note to Bindings#select to consider ObservableValue#flatMap
hjohn Jun 30, 2022
2c3a9d9
Move private binding classes to com.sun.javafx.binding package
hjohn Jun 30, 2022
60b3311
Update copyrights
hjohn Jul 1, 2022
6ae74d1
Add null checks in Subscription
hjohn Jul 1, 2022
d66f2ba
Merge branch 'openjdk:master' into feature/fluent-bindings
hjohn Jul 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Implementation:
*
* <p>In a flat mapped binding there are always two subscriptions involved:
* <ul>
* <li>The subscription on its source</li>
* <li>The subscription on the value resulting from the mapping of the source: the indirect source</li>
* </ul>
* The subscription on its given source is present when this binding itself is observed and not present otherwise.
*
* <p>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 <S> the type of the source
* @param <T> the type of the resulting binding
*/
public class FlatMappedBinding<S, T> extends LazyObjectBinding<T> {

private final ObservableValue<S> source;
private final Function<? super S, ? extends ObservableValue<? extends T>> mapper;

private Subscription indirectSourceSubscription = Subscription.EMPTY;
private ObservableValue<? extends T> indirectSource;

public FlatMappedBinding(ObservableValue<S> 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.
*
* <p>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;
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> the type of the wrapped {@code Object}
*/
abstract class LazyObjectBinding<T> extends ObjectBinding<T> {

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();
}
Original file line number Diff line number Diff line change
@@ -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<S, T> extends LazyObjectBinding<T> {

private final ObservableValue<S> source;
private final Function<? super S, ? extends T> mapper;

public MappedBinding(ObservableValue<S> 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
}
}
Original file line number Diff line number Diff line change
@@ -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<T> extends LazyObjectBinding<T> {

private final ObservableValue<T> source;
private final T constant;

public OrElseBinding(ObservableValue<T> 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
}
}
Loading