PropertyChangeListeners.java

package sprouts.impl;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.*;

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

/**
 *  This class is technically an internal class and should not be used directly.
 *  If you use this class directly, most likely, you are at risk of your code breaking
 *  in future releases of Sprouts.
 * @param <T> The type of the property that this listener listens to.
 */
public final class PropertyChangeListeners<T> implements ChangeListeners.OwnerCallableForCleanup<ValDelegate<T>>
{
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(PropertyChangeListeners.class);

    private Association<Channel, ChangeListeners<ValDelegate<T>>> _channelsToListeners = (Association)Association.betweenLinked(Channel.class, ChangeListeners.class);


    /**
     *  Creates a new instance of {@link PropertyChangeListeners}, without any listeners.
     */
    public PropertyChangeListeners() {}

    /**
     *  Creates a new instance of {@link PropertyChangeListeners} by copying the listeners from another instance.
     *  This is useful for when a property inherits listeners from another property,
     *
     * @param other The other instance to copy the listeners from.
     */
    public PropertyChangeListeners( PropertyChangeListeners<T> other ) {
        _channelsToListeners = other._channelsToListeners;
    }

    /**
     *  Adds a change listener for the given channel.
     *  This method is used to register a listener that will be notified when the property changes.
     *
     * @param channel The channel on which the change listener will be registered.
     * @param action The action to be performed when the property changes.
     */
    public void onChange( Channel channel, Action<ValDelegate<T>> action ) {
        Objects.requireNonNull(channel);
        Objects.requireNonNull(action);
        _updateActionsFor(channel, it->it.add(action, channel, this));
    }

    @Override
    public void updateState(@Nullable Channel channel, Function<ChangeListeners<ValDelegate<T>>, ChangeListeners<ValDelegate<T>>> updater) {
        if ( channel != null )
            _updateActionsFor(channel, it -> updater.apply(_getActionsFor(channel)));
    }

    /**
     *  Adds a plain observer (has no delegate) as a change listener for the default channel,
     *  which is defined by {@link SproutsFactory#defaultObservableChannel()}.
     *  The listener will be notified when the property changes, or when it
     *  is triggered explicitly through ({@link Val#fireChange(Channel)}).
     *
     * @param observer The observer to be registered as a change listener.
     */
    public void onChange( Observer observer ) {
        this.onChange(Sprouts.factory().defaultObservableChannel(), new ObserverAsActionImpl<>(observer) );
    }

    /**
     *  Unsubscribes a {@link Subscriber} from the change listeners.
     *  This method is used to remove a listener that was previously registered.
     *  Note that the {@link Subscriber} is a marker interface as well as common
     *  super type for all listeners that are registered to the change listeners.
     *
     * @param subscriber The subscriber to be removed from the change listeners.
     */
    public void unsubscribe( Subscriber subscriber ) {
        updateActions( it -> it.unsubscribe(subscriber ) );
    }

    /**
     *  Unsubscribes all change listeners from this {@link PropertyChangeListeners}.
     *  This method is used to remove all listeners that were previously registered.
     *  Note that this will remove all listeners, regardless of the channel they were registered on.
     */
    public void unsubscribeAll() {
        updateActions(ChangeListeners::unsubscribeAll);
    }

    private void updateActions(Function<ChangeListeners<ValDelegate<T>>, ChangeListeners<ValDelegate<T>>> updater) {
        _channelsToListeners = (Association)
                _channelsToListeners.entrySet()
                .stream()
                .map( entry -> {
                    return entry.withSecond(updater.apply(entry.second()));
                })
                .collect(Association.collectorOfLinked(Channel.class, ChangeListeners.class));
    }

    /**
     *  Fires a change event for the given property and channel.
     *  This method is used to notify all listeners that a change has occurred.
     *  <b>This is not currently used internally, but may be useful for
     *  deeply integrated libraries built on top of sprouts.</b>
     *
     * @param owner The owner of the property that changed.
     * @param channel The channel on which the change occurred.
     * @param newValue The new value of the property.
     * @param oldValue The old value of the property.
     */
    public void fireChange( Val<T> owner, Channel channel, @Nullable T newValue, @Nullable T oldValue ) {
        fireChange(owner, channel, new ItemPair<>(owner.type(), newValue, oldValue));
    }

    void fireChange(
        Val<T> owner,
        Channel channel,
        ItemPair<T> pair
    ) {
        if ( _channelsToListeners.isEmpty() )
            return;
        Supplier<ValDelegate<T>> lazilyCreatedDelegate = new Supplier<ValDelegate<T>>() {
            private @Nullable ValDelegate<T> delegate = null;
            @Override
            public ValDelegate<T> get() {
                if ( delegate == null )
                    delegate = Sprouts.factory().delegateOf(owner, channel, pair.change(), pair.newValue(), pair.oldValue());
                return delegate;
            }
        };
        // We clone this property to avoid concurrent modification
        if ( channel == From.ALL)
            for ( Channel key : _channelsToListeners.keySet() )
                _getActionsFor(key).fireChange( lazilyCreatedDelegate );
        else {
            _getActionsFor(channel).fireChange( lazilyCreatedDelegate );
            _getActionsFor(From.ALL).fireChange( lazilyCreatedDelegate );
        }
    }

    /**
     *  Returns the number of change listeners that are currently registered.
     *  This is useful for debugging purposes.
     *
     * @return The number of change listeners that are currently registered.
     */
    public long numberOfChangeListeners() {
        return _channelsToListeners.values()
                            .stream()
                            .mapToLong(ChangeListeners::numberOfChangeListeners)
                            .sum();
    }

    private ChangeListeners<ValDelegate<T>> _getActionsFor( Channel channel ) {
        if ( !_channelsToListeners.containsKey(channel) ) {
            _channelsToListeners = _channelsToListeners.put(channel, new ChangeListeners<>());
        }
        return _channelsToListeners.get(channel).get();
    }

    private void _updateActionsFor(Channel channel, Function<ChangeListeners<ValDelegate<T>>, ChangeListeners<ValDelegate<T>>> updater) {
        ChangeListeners<ValDelegate<T>> listeners = _getActionsFor(channel);
        listeners = updater.apply(listeners);
        _channelsToListeners = _channelsToListeners.put(channel, listeners);
    }

    @Override
    public final String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(this.getClass().getSimpleName()).append("[");
        for ( Channel key : _channelsToListeners.keySet() ) {
            try {
                sb.append(key).append("->").append(_channelsToListeners.get(key).get()).append(", ");
            } catch ( Exception e ) {
                _logError("An error occurred while trying to get the number of change listeners for channel '{}'", key, e);
            }
        }
        sb.append("]");
        return sb.toString();
    }


    private static void _logError(String message, @Nullable Object... args) {
        Util._logError(log, message, args);
    }

}