Skip to content

Commit

Permalink
8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue
Browse files Browse the repository at this point in the history
Reviewed-by: nlisker, mstrauss, kcr
  • Loading branch information
hjohn authored and kevinrushforth committed Jul 8, 2022
1 parent 178d898 commit 60c75b8
Show file tree
Hide file tree
Showing 12 changed files with 1,923 additions and 4 deletions.
@@ -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;
}
}
@@ -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();
}
@@ -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
}
}
@@ -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
}
}

1 comment on commit 60c75b8

@openjdk-notifier
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.