/** * Copyright 2016 Nikita Koksharov * * 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 org.redisson.reactive; import static org.redisson.client.protocol.RedisCommands.LINDEX; import static org.redisson.client.protocol.RedisCommands.LLEN; import static org.redisson.client.protocol.RedisCommands.LREM_SINGLE; import static org.redisson.client.protocol.RedisCommands.RPUSH; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.redisson.RedissonList; import org.redisson.api.RListReactive; import org.redisson.client.codec.Codec; import org.redisson.client.protocol.RedisCommand; import org.redisson.client.protocol.RedisCommand.ValueType; import org.redisson.client.protocol.RedisCommands; import org.redisson.client.protocol.convertor.LongReplayConvertor; import org.redisson.command.CommandReactiveExecutor; import reactor.fn.BiFunction; import reactor.fn.Function; import reactor.rx.Stream; import reactor.rx.Streams; import reactor.rx.subscription.ReactiveSubscription; /** * Distributed and concurrent implementation of {@link java.util.List} * * @author Nikita Koksharov * * @param <V> the type of elements held in this collection */ public class RedissonListReactive<V> extends RedissonExpirableReactive implements RListReactive<V> { private final RedissonList<V> instance; public RedissonListReactive(CommandReactiveExecutor commandExecutor, String name) { super(commandExecutor, name); instance = new RedissonList<V>(commandExecutor, name, null); } public RedissonListReactive(Codec codec, CommandReactiveExecutor commandExecutor, String name) { super(codec, commandExecutor, name); instance = new RedissonList<V>(codec, commandExecutor, name, null); } @Override public Publisher<Long> size() { return commandExecutor.readReactive(getName(), codec, LLEN, getName()); } @Override public Publisher<V> descendingIterator() { return iterator(-1, false); } @Override public Publisher<V> iterator() { return iterator(0, true); } @Override public Publisher<V> descendingIterator(int startIndex) { return iterator(startIndex, false); } @Override public Publisher<V> iterator(int startIndex) { return iterator(startIndex, true); } private Publisher<V> iterator(final int startIndex, final boolean forward) { return new Stream<V>() { @Override public void subscribe(final Subscriber<? super V> t) { t.onSubscribe(new ReactiveSubscription<V>(this, t) { private int currentIndex = startIndex; @Override protected void onRequest(final long n) { final ReactiveSubscription<V> m = this; get(currentIndex).subscribe(new Subscriber<V>() { V currValue; @Override public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); } @Override public void onNext(V value) { currValue = value; m.onNext(value); if (forward) { currentIndex++; } else { currentIndex--; } } @Override public void onError(Throwable error) { m.onError(error); } @Override public void onComplete() { if (currValue == null) { m.onComplete(); return; } if (n-1 == 0) { return; } onRequest(n-1); } }); } }); } }; } @Override public Publisher<Long> add(V e) { return commandExecutor.writeReactive(getName(), codec, RPUSH, getName(), e); } @Override public Publisher<Boolean> remove(Object o) { return reactive(instance.removeAsync(o)); } protected Publisher<Boolean> remove(Object o, int count) { return commandExecutor.writeReactive(getName(), codec, LREM_SINGLE, getName(), count, o); } @Override public Publisher<Boolean> containsAll(Collection<?> c) { return reactive(instance.containsAllAsync(c)); } @Override public Publisher<Long> addAll(Publisher<? extends V> c) { return new PublisherAdder<V>(this) { @Override public Long sum(Long first, Long second) { return second; } }.addAll(c); } @Override public Publisher<Long> addAll(Collection<? extends V> c) { if (c.isEmpty()) { return size(); } List<Object> args = new ArrayList<Object>(c.size() + 1); args.add(getName()); args.addAll(c); return commandExecutor.writeReactive(getName(), codec, RPUSH, args.toArray()); } @Override public Publisher<Long> addAll(long index, Collection<? extends V> coll) { if (index < 0) { throw new IndexOutOfBoundsException("index: " + index); } if (coll.isEmpty()) { return size(); } if (index == 0) { // prepend elements to list List<Object> elements = new ArrayList<Object>(coll); Collections.reverse(elements); elements.add(0, getName()); return commandExecutor.writeReactive(getName(), codec, RedisCommands.LPUSH, elements.toArray()); } List<Object> args = new ArrayList<Object>(coll.size() + 1); args.add(index); args.addAll(coll); return commandExecutor.evalWriteReactive(getName(), codec, new RedisCommand<Long>("EVAL", new LongReplayConvertor(), 5, ValueType.OBJECTS), "local ind = table.remove(ARGV, 1); " + // index is the first parameter "local size = redis.call('llen', KEYS[1]); " + "assert(tonumber(ind) <= size, 'index: ' .. ind .. ' but current size: ' .. size); " + "local tail = redis.call('lrange', KEYS[1], ind, -1); " + "redis.call('ltrim', KEYS[1], 0, ind - 1); " + "for i, v in ipairs(ARGV) do redis.call('rpush', KEYS[1], v) end;" + "for i, v in ipairs(tail) do redis.call('rpush', KEYS[1], v) end;" + "return redis.call('llen', KEYS[1]);", Collections.<Object>singletonList(getName()), args.toArray()); } @Override public Publisher<Boolean> removeAll(Collection<?> c) { return reactive(instance.removeAllAsync(c)); } @Override public Publisher<Boolean> retainAll(Collection<?> c) { return reactive(instance.retainAllAsync(c)); } @Override public Publisher<V> get(long index) { return commandExecutor.readReactive(getName(), codec, LINDEX, getName(), index); } @Override public Publisher<V> set(long index, V element) { return commandExecutor.evalWriteReactive(getName(), codec, new RedisCommand<Object>("EVAL", 5), "local v = redis.call('lindex', KEYS[1], ARGV[1]); " + "redis.call('lset', KEYS[1], ARGV[1], ARGV[2]); " + "return v", Collections.<Object>singletonList(getName()), index, element); } @Override public Publisher<Void> fastSet(long index, V element) { return commandExecutor.writeReactive(getName(), codec, RedisCommands.LSET, getName(), index, element); } @Override public Publisher<Long> add(long index, V element) { return addAll(index, Collections.singleton(element)); } @Override public Publisher<V> remove(long index) { return reactive(instance.removeAsync(index)); } @Override public Publisher<Boolean> contains(Object o) { return reactive(instance.containsAsync(o)); } @Override public Publisher<Long> indexOf(Object o) { return reactive(instance.indexOfAsync(o, new LongReplayConvertor())); } @Override public Publisher<Long> lastIndexOf(Object o) { return reactive(instance.lastIndexOfAsync(o, new LongReplayConvertor())); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof RedissonListReactive)) return false; Stream<Object> e1 = Streams.wrap((Publisher<Object>)iterator()); Stream<Object> e2 = Streams.wrap(((RedissonListReactive<Object>) o).iterator()); Long count = Streams.merge(e1, e2).groupBy(new Function<Object, Object>() { @Override public Object apply(Object t) { return t; } }).count().next().poll(); boolean res = count.equals(Streams.wrap(size()).next().poll()); res &= count.equals(Streams.wrap(((RedissonListReactive<Object>) o).size()).next().poll()); return res; } @Override public int hashCode() { Integer hash = Streams.wrap(iterator()).map(new Function<V, Integer>() { @Override public Integer apply(V t) { return t.hashCode(); } }).reduce(1, new BiFunction<Integer, Integer, Integer>() { @Override public Integer apply(Integer t, Integer u) { return 31*t + u; } }).next().poll(); if (hash == null) { return 1; } return hash; } }