All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rx.operators.OperationReplay Maven / Gradle / Ivy

There is a newer version: 0.20.7
Show newest version
/**
 * Copyright 2014 Netflix, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package rx.operators;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observable.OnSubscribeFunc;
import rx.Observer;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Func1;
import rx.functions.Functions;
import rx.schedulers.Timestamped;
import rx.subjects.Subject;
import rx.subscriptions.Subscriptions;

/**
 * Replay with limited buffer and/or time constraints.
 * 
 * 
 * @see MSDN: Observable.Replay overloads
 */
public final class OperationReplay {
    /** Utility class. */
    private OperationReplay() {
        throw new IllegalStateException("No instances!");
    }

    /**
     * Create a BoundedReplaySubject with the given buffer size.
     */
    public static  Subject replayBuffered(int bufferSize) {
        return CustomReplaySubject.create(bufferSize);
    }

    /**
     * Creates a subject whose client observers will observe events
     * propagated through the given wrapped subject.
     */
    public static  Subject createScheduledSubject(Subject subject, Scheduler scheduler) {
        final Observable observedOn = subject.observeOn(scheduler);
        SubjectWrapper s = new SubjectWrapper(new OnSubscribe() {

            @Override
            public void call(Subscriber o) {
                // TODO HACK between OnSubscribeFunc and Action1
                subscriberOf(observedOn).onSubscribe(o);
            }

        }, subject);
        return s;
    }

    /**
     * Create a CustomReplaySubject with the given time window length
     * and optional buffer size.
     * 
     * @param 
     *            the source and return type
     * @param time
     *            the length of the time window
     * @param unit
     *            the unit of the time window length
     * @param bufferSize
     *            the buffer size if >= 0, otherwise, the buffer will be unlimited
     * @param scheduler
     *            the scheduler from where the current time is retrieved. The
     *            observers will not observe on this scheduler.
     * @return a Subject with the required replay behavior
     */
    public static  Subject replayWindowed(long time, TimeUnit unit, int bufferSize, final Scheduler scheduler) {
        final long ms = unit.toMillis(time);
        if (ms <= 0) {
            throw new IllegalArgumentException("The time window is less than 1 millisecond!");
        }
        Func1> timestamp = new Func1>() {
            @Override
            public Timestamped call(T t1) {
                return new Timestamped(scheduler.now(), t1);
            }
        };
        Func1, T> untimestamp = new Func1, T>() {
            @Override
            public T call(Timestamped t1) {
                return t1.getValue();
            }
        };

        ReplayState, T> state;

        if (bufferSize >= 0) {
            state = new ReplayState, T>(new VirtualBoundedList>(bufferSize), untimestamp);
        } else {
            state = new ReplayState, T>(new VirtualArrayList>(), untimestamp);
        }
        final ReplayState, T> fstate = state;
        // time based eviction when a value is added
        state.onValueAdded = new Action0() {
            @Override
            public void call() {
                long now = scheduler.now();
                long before = now - ms;
                for (int i = fstate.values.start(); i < fstate.values.end(); i++) {
                    Timestamped v = fstate.values.get(i);
                    if (v.getTimestampMillis() >= before) {
                        fstate.values.removeBefore(i);
                        break;
                    }
                }
            }
        };
        // time based eviction when a client subscribes
        state.onSubscription = state.onValueAdded;

        final CustomReplaySubject, T> brs = new CustomReplaySubject, T>(
                new CustomReplaySubjectSubscribeFunc, T>(state), state, timestamp
                );

        return brs;
    }

    /**
     * Return an OnSubscribeFunc which delegates the subscription to the given observable.
     */
    public static  OnSubscribeFunc subscriberOf(final Observable target) {
        return new OnSubscribeFunc() {
            @Override
            public Subscription onSubscribe(Observer t1) {
                return target.subscribe(t1);
            }
        };
    }

//    /**
//     * Subject that wraps another subject and uses a mapping function
//     * to transform the received values.
//     */
//    public static final class MappingSubject extends Subject {
//        private final Subject subject;
//        private final Func1 selector;
//        private final OnSubscribe func;
//
//        public MappingSubject(OnSubscribe func, Subject subject, Func1 selector) {
//            this.func = func;
//            this.subject = subject;
//            this.selector = selector;
//        }
//
//        @Override
//        public Observable toObservable() {
//            return Observable.create(func);
//        }
//
//        @Override
//        public void onNext(T args) {
//            subject.onNext(selector.call(args));
//        }
//
//        @Override
//        public void onError(Throwable e) {
//            subject.onError(e);
//        }
//
//        @Override
//        public void onCompleted() {
//            subject.onCompleted();
//        }
//
//    }

    /**
     * A subject that wraps another subject.
     */
    public static final class SubjectWrapper extends Subject {
        /** The wrapped subject. */
        final Subject subject;

        public SubjectWrapper(OnSubscribe func, Subject subject) {
            super(func);
            this.subject = subject;
        }

        @Override
        public void onNext(T args) {
            subject.onNext(args);
        }

        @Override
        public void onError(Throwable e) {
            subject.onError(e);
        }

        @Override
        public void onCompleted() {
            subject.onCompleted();
        }

    }

    /** Base state with lock. */
    static class BaseState {
        /** The lock to protect the other fields. */
        private final Lock lock = new ReentrantLock();

        /** Lock. */
        public void lock() {
            lock.lock();
        }

        /** Unlock. */
        public void unlock() {
            lock.unlock();
        }

    }

    /**
     * Base interface for logically indexing a list.
     * 
     * @param 
     *            the value type
     */
    public interface VirtualList {
        /** @return the number of elements in this list */
        int size();

        /**
         * Add an element to the list.
         * 
         * @param value
         *            the value to add
         */
        void add(T value);

        /**
         * Retrieve an element at the specified logical index.
         * 
         * @param index
         * @return
         */
        T get(int index);

        /**
         * Remove elements up before the given logical index and move
         * the start() to this index.
         * 

* For example, a list contains 3 items. Calling removeUntil 2 will * remove the first two items. * * @param index */ void removeBefore(int index); /** * Clear the elements of this list and increase the * start by the number of elements. */ void clear(); /** * Returns the current head index of this list. * * @return */ int start(); /** * Returns the current tail index of this list (where the next value would appear). * * @return */ int end(); /** * Clears and resets the indexes of the list. */ void reset(); /** * Returns the current content as a list. * * @return */ List toList(); } /** * Behaves like a normal, unbounded ArrayList but with virtual index. */ public static final class VirtualArrayList implements VirtualList { /** The backing list . */ final List list = new ArrayList(); /** The virtual start index of the list. */ int startIndex; @Override public int size() { return list.size(); } @Override public void add(T value) { list.add(value); } @Override public T get(int index) { return list.get(index - startIndex); } @Override public void removeBefore(int index) { int j = index - startIndex; if (j > 0 && j <= list.size()) { list.subList(0, j).clear(); } startIndex = index; } @Override public void clear() { startIndex += list.size(); list.clear(); } @Override public int start() { return startIndex; } @Override public int end() { return startIndex + list.size(); } @Override public void reset() { list.clear(); startIndex = 0; } @Override public List toList() { return new ArrayList(list); } } /** * A bounded list which increases its size up to a maximum capacity, then * behaves like a circular buffer with virtual indexes. */ public static final class VirtualBoundedList implements VirtualList { /** A list that grows up to maxSize. */ private final List list = new ArrayList(); /** The maximum allowed size. */ private final int maxSize; /** The logical start index of the list. */ int startIndex; /** The head index inside the list, where the first readable value sits. */ int head; /** The tail index inside the list, where the next value will be added. */ int tail; /** The number of items in the list. */ int count; /** * Construct a VirtualBoundedList with the given maximum number of elements. * * @param maxSize */ public VirtualBoundedList(int maxSize) { if (maxSize < 0) { throw new IllegalArgumentException("maxSize < 0"); } this.maxSize = maxSize; } @Override public int start() { return startIndex; } @Override public int end() { return startIndex + count; } @Override public void clear() { startIndex += count; list.clear(); head = 0; tail = 0; count = 0; } @Override public int size() { return count; } @Override public void add(T value) { if (list.size() == maxSize) { list.set(tail, value); head = (head + 1) % maxSize; tail = (tail + 1) % maxSize; startIndex++; } else { list.add(value); tail = (tail + 1) % maxSize; count++; } } @Override public T get(int index) { if (index < start() || index >= end()) { throw new ArrayIndexOutOfBoundsException(index); } int idx = (head + (index - startIndex)) % maxSize; return list.get(idx); } @Override public void removeBefore(int index) { if (index <= start()) { return; } if (index >= end()) { clear(); startIndex = index; return; } int rc = index - startIndex; int head2 = head + rc; for (int i = head; i < head2; i++) { list.set(i % maxSize, null); count--; } startIndex = index; head = head2 % maxSize; } @Override public List toList() { List r = new ArrayList(list.size() + 1); for (int i = head; i < head + count; i++) { int idx = i % maxSize; r.add(list.get(idx)); } return r; } @Override public void reset() { list.clear(); count = 0; head = 0; tail = 0; } } /** * The state class. * * @param * the intermediate type stored in the values buffer * @param * the result type transformed via the resultSelector */ static final class ReplayState extends BaseState { /** The values observed so far. */ final VirtualList values; /** The result selector. */ final Func1 resultSelector; /** The received error. */ Throwable error; /** General completion indicator. */ boolean done; /** The map of replayers. */ final Map replayers = new LinkedHashMap(); /** * Callback once a value has been added but before it is replayed * (I.e, run a time based eviction policy). *

* Called while holding the state lock. */ protected Action0 onValueAdded = new Action0() { @Override public void call() { } }; /** * Callback once an error has been called but before it is replayed * (I.e, run a time based eviction policy). *

* Called while holding the state lock. */ protected Action0 onErrorAdded = new Action0() { @Override public void call() { } }; /** * Callback once completed has been called but before it is replayed * (I.e, run a time based eviction policy). *

* Called while holding the state lock. */ protected Action0 onCompletedAdded = new Action0() { @Override public void call() { } }; /** * Callback to pre-manage the values if an observer unsubscribes * (I.e, run a time based eviction policy). *

* Called while holding the state lock. */ protected Action0 onSubscription = new Action0() { @Override public void call() { } }; /** * Construct a ReplayState with the supplied buffer and result selectors. * * @param values * @param resultSelector */ public ReplayState(final VirtualList values, final Func1 resultSelector) { this.values = values; this.resultSelector = resultSelector; } /** * Returns a live collection of the observers. *

* Caller should hold the lock. * * @return */ Collection replayers() { return new ArrayList(replayers.values()); } /** * Add a replayer to the replayers and create a Subscription for it. *

* Caller should hold the lock. * * @param obs * @return */ Subscription addReplayer(Observer obs) { Subscription s = new Subscription() { final AtomicBoolean once = new AtomicBoolean(); @Override public void unsubscribe() { if (once.compareAndSet(false, true)) { remove(this); } } @Override public boolean isUnsubscribed() { return once.get(); } }; Replayer rp = new Replayer(obs, s); replayers.put(s, rp); rp.replayTill(values.start() + values.size()); return s; } /** The replayer that holds a value where the given observer is currently at. */ final class Replayer { protected final Observer wrapped; /** Where this replayer was in reading the list. */ protected int index; /** To cancel and unsubscribe this replayer and observer. */ protected final Subscription cancel; protected Replayer(Observer wrapped, Subscription cancel) { this.wrapped = wrapped; this.cancel = cancel; } /** * Replay up to the given index * * @param limit */ void replayTill(int limit) { int si = values.start(); if (index < si) { index = si; } while (index < limit) { TIntermediate value = values.get(index); index++; try { wrapped.onNext(resultSelector.call(value)); } catch (Throwable t) { replayers.remove(cancel); wrapped.onError(t); return; } } if (done) { if (error != null) { wrapped.onError(error); } else { wrapped.onCompleted(); } } } } /** * Remove the subscription. * * @param s */ void remove(Subscription s) { lock(); try { replayers.remove(s); } finally { unlock(); } } /** * Add a notification value and limit the size of values. *

* Caller should hold the lock. * * @param value */ void add(TIntermediate value) { values.add(value); } /** Clears the value list. */ void clearValues() { lock(); try { values.clear(); } finally { unlock(); } } } /** * A customizable replay subject with support for transformations. * * @param * the Observer side's value type * @param * the type of the elements in the replay buffer * @param * the value type of the observers subscribing to this subject */ public static final class CustomReplaySubject extends Subject { /** * Return a subject that retains all events and will replay them to an {@link Observer} that subscribes. * * @return a subject that retains all events and will replay them to an {@link Observer} that subscribes. */ public static CustomReplaySubject create() { ReplayState state = new ReplayState(new VirtualArrayList(), Functions. identity()); return new CustomReplaySubject( new CustomReplaySubjectSubscribeFunc(state), state, Functions. identity()); } /** * Create a bounded replay subject with the given maximum buffer size. * * @param maxSize * the maximum size in number of onNext notifications * @return */ public static CustomReplaySubject create(int maxSize) { ReplayState state = new ReplayState(new VirtualBoundedList(maxSize), Functions. identity()); return new CustomReplaySubject( new CustomReplaySubjectSubscribeFunc(state), state, Functions. identity()); } /** The replay state. */ protected final ReplayState state; /** The result selector. */ protected final Func1 intermediateSelector; private CustomReplaySubject( final OnSubscribeFunc onSubscribe, ReplayState state, Func1 intermediateSelector) { super(new OnSubscribe() { @Override public void call(Subscriber sub) { onSubscribe.onSubscribe(sub); } }); this.state = state; this.intermediateSelector = intermediateSelector; } @Override public void onCompleted() { state.lock(); try { if (state.done) { return; } state.done = true; state.onCompletedAdded.call(); replayValues(); } finally { state.unlock(); } } @Override public void onError(Throwable e) { state.lock(); try { if (state.done) { return; } state.done = true; state.error = e; state.onErrorAdded.call(); replayValues(); } finally { state.unlock(); } } @Override public void onNext(TInput args) { state.lock(); try { if (state.done) { return; } state.add(intermediateSelector.call(args)); state.onValueAdded.call(); replayValues(); } finally { state.unlock(); } } /** * Replay values up to the current index. */ protected void replayValues() { int s = state.values.start() + state.values.size(); for (ReplayState.Replayer rp : state.replayers()) { rp.replayTill(s); } } } /** * The subscription function. * * @param * the type of the elements in the replay buffer * @param * the value type of the observers subscribing to this subject */ protected static final class CustomReplaySubjectSubscribeFunc implements Observable.OnSubscribeFunc { private final ReplayState state; protected CustomReplaySubjectSubscribeFunc(ReplayState state) { this.state = state; } @Override public Subscription onSubscribe(Observer t1) { VirtualList values; Throwable error; state.lock(); try { if (!state.done) { state.onSubscription.call(); return state.addReplayer(t1); } values = state.values; error = state.error; } finally { state.unlock(); } // fully replay the subject for (int i = values.start(); i < values.end(); i++) { try { t1.onNext(state.resultSelector.call(values.get(i))); } catch (Throwable t) { t1.onError(t); return Subscriptions.empty(); } } if (error != null) { t1.onError(error); } else { t1.onCompleted(); } return Subscriptions.empty(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy