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 4 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,111 @@
/*
* 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.<p>
*
* For example:<p>
* <pre>Subscription s = property.subscribe(System.out::println)</pre>
hjohn marked this conversation as resolved.
Show resolved Hide resolved
* 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();
hjohn marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 null
* @return a combined {@link Subscription} which will cancel both when
* cancelled, never null
*/
default Subscription and(Subscription other) {
Objects.requireNonNull(other);
hjohn marked this conversation as resolved.
Show resolved Hide resolved

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 null
* @return a {@link Subscription} which can be used to cancel this
* subscription, never null
*/
static <T> Subscription subscribe(ObservableValue<T> observableValue, Consumer<? super T> subscriber) {
ChangeListener<T> listener = (obs, old, current) -> subscriber.accept(current);
hjohn marked this conversation as resolved.
Show resolved Hide resolved

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 null
* @return a {@link Subscription} which can be used to cancel this
* subscription, never null
*/
static Subscription subscribeInvalidations(ObservableValue<?> observableValue, Runnable runnable) {
InvalidationListener listener = obs -> runnable.run();

observableValue.addListener(listener);

return () -> observableValue.removeListener(listener);
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
}
}

Expand All @@ -182,6 +189,33 @@ 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}
*/
hjohn marked this conversation as resolved.
Show resolved Hide resolved
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.
* <p>
* The default implementation always allows bindings to become valid.
*
* @return {@code true} if this binding is allowed to become valid, otherwise
* {@code false}
hjohn marked this conversation as resolved.
Show resolved Hide resolved
*/
protected boolean allowValidation() {
return true;
}

/**
* Calculates the current value of this binding.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2021 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 javafx.beans.value;

import java.util.Objects;
import java.util.function.Function;

import com.sun.javafx.binding.Subscription;

class FlatMappedBinding<S, T> extends LazyObjectBinding<T> {

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

private Subscription mappedSubscription = Subscription.EMPTY;

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> mapped = value == null ? null : mapper.apply(value);

if (isObserved()) {
mappedSubscription.unsubscribe();
mappedSubscription = mapped == null ? Subscription.EMPTY : Subscription.subscribeInvalidations(mapped, this::invalidate);
}

return mapped == null ? null : mapped.getValue();
}

@Override
protected Subscription observeInputs() {
Subscription subscription = Subscription.subscribeInvalidations(source, this::invalidate);

return () -> {
subscription.unsubscribe();
mappedSubscription.unsubscribe();
mappedSubscription = Subscription.EMPTY;
};
}
}
hjohn marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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 javafx.beans.value;

import com.sun.javafx.binding.Subscription;

import javafx.beans.InvalidationListener;
import javafx.beans.binding.ObjectBinding;

/**
* 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);

updateSubcriptionAfterAdd();
}

@Override
public void removeListener(ChangeListener<? super T> listener) {
super.removeListener(listener);

updateSubcriptionAfterRemove();
}

@Override
public void addListener(InvalidationListener listener) {
super.addListener(listener);

updateSubcriptionAfterAdd();
}

@Override
public void removeListener(InvalidationListener listener) {
super.removeListener(listener);

updateSubcriptionAfterRemove();
}

@Override
protected boolean allowValidation() {
return isObserved();
}

/**
* Called after a listener was added to start observing inputs if they're not observed already.
*/
private void updateSubcriptionAfterAdd() {
hjohn marked this conversation as resolved.
Show resolved Hide resolved
if (!wasObserved) { // was first observer registered?
subscription = observeInputs(); // 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.
hjohn marked this conversation as resolved.
Show resolved Hide resolved
*/

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 updateSubcriptionAfterRemove() {
hjohn marked this conversation as resolved.
Show resolved Hide resolved
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 observeInputs();
}
Loading