diff --git a/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy b/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy index ceeb25dcf6..9c6e244149 100644 --- a/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy +++ b/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy @@ -345,6 +345,143 @@ def class ObservableTests { assertEquals(6, count); } + @Test + public void testToMap1() { + Map actual = new HashMap(); + + Observable.from("a", "bb", "ccc", "dddd") + .toMap({String s -> s.length()}) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + assertEquals(expected, actual); + } + + @Test + public void testToMap2() { + Map actual = new HashMap(); + + Observable.from("a", "bb", "ccc", "dddd") + .toMap({String s -> s.length()}, {String s -> s + s}) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + assertEquals(expected, actual); + } + + @Test + public void testToMap3() { + Map actual = new HashMap(); + + LinkedHashMap last3 = new LinkedHashMap() { + public boolean removeEldestEntry(Map.Entry e) { + return size() > 3; + } + }; + + Observable.from("a", "bb", "ccc", "dddd") + .toMap({String s -> s.length()}, {String s -> s + s}, { last3 }) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + assertEquals(expected, actual); + } + @Test + public void testToMultimap1() { + Map actual = new HashMap(); + + Observable.from("a", "b", "cc", "dd") + .toMultimap({String s -> s.length()}) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + assertEquals(expected, actual); + } + + @Test + public void testToMultimap2() { + Map actual = new HashMap(); + + Observable.from("a", "b", "cc", "dd") + .toMultimap({String s -> s.length()}, {String s -> s + s}) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + assertEquals(expected, actual); + } + + @Test + public void testToMultimap3() { + Map actual = new HashMap(); + + LinkedHashMap last1 = new LinkedHashMap() { + public boolean removeEldestEntry(Map.Entry e) { + return size() > 1; + } + }; + + Observable.from("a", "b", "cc", "dd") + .toMultimap({String s -> s.length()}, {String s -> s + s}, { last1 }) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + + expected.put(2, Arrays.asList("cccc", "dddd")); + + assertEquals(expected, actual); + } + + @Test + public void testToMultimap4() { + Map actual = new HashMap(); + + LinkedHashMap last1 = new LinkedHashMap() { + public boolean removeEldestEntry(Map.Entry e) { + return size() > 2; + } + }; + + Observable.from("a", "b", "cc", "dd", "eee", "eee") + .toMultimap({String s -> s.length()}, {String s -> s + s}, { last1 }, + {i -> i == 2 ? new ArrayList() : new HashSet() }) + .toBlockingObservable() + .forEach({s -> actual.putAll(s); }); + + Map expected = new HashMap(); + + expected.put(2, Arrays.asList("cccc", "dddd")); + expected.put(3, new HashSet(Arrays.asList("eeeeee"))); + + assertEquals(expected, actual); + } def class AsyncObservable implements OnSubscribeFunc { diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 06a08ef576..1f33710e95 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -19,8 +19,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -87,6 +89,8 @@ import rx.operators.OperationTimeInterval; import rx.operators.OperationTimeout; import rx.operators.OperationTimestamp; +import rx.operators.OperationToMap; +import rx.operators.OperationToMultimap; import rx.operators.OperationToObservableFuture; import rx.operators.OperationToObservableIterable; import rx.operators.OperationToObservableList; @@ -5943,6 +5947,7 @@ public static Observable when(Plan0 p1, Plan0 p2, Plan0 p3, Plan public static Observable when(Plan0 p1, Plan0 p2, Plan0 p3, Plan0 p4, Plan0 p5, Plan0 p6, Plan0 p7, Plan0 p8, Plan0 p9) { return create(OperationJoinPatterns.when(p1, p2, p3, p4, p5, p6, p7, p8, p9)); } + /** * Correlates the elements of two sequences based on overlapping durations. * @param right The right observable sequence to join elements for. @@ -5964,5 +5969,128 @@ public Observable join(Observable< Func2 resultSelector) { return create(new OperationJoin(this, right, leftDurationSelector, rightDurationSelector, resultSelector)); } + + /** + * Return an Observable that emits a single HashMap containing all items + * emitted by the source Observable, mapped by the keys returned by the + * {@code keySelector} function. + * + * If a source item maps to the same key, the HashMap will contain the latest + * of those items. + * + * @param keySelector the function that extracts the key from the source items + * to be used as keys in the HashMap. + * @return an Observable that emits a single HashMap containing the mapped + * values of the source Observable + * @see MSDN: Observable.ToDictionary + */ + public Observable> toMap(Func1 keySelector) { + return create(OperationToMap.toMap(this, keySelector)); + } + + /** + * Return an Observable that emits a single HashMap containing elements with + * key and value extracted from the values emitted by the source Observable. + * + * If a source item maps to the same key, the HashMap will contain the latest + * of those items. + * + * @param keySelector the function that extracts the key from the source items + * to be used as key in the HashMap + * @param valueSelector the function that extracts the value from the source items + * to be used as value in the HashMap + * @return an Observable that emits a single HashMap containing the mapped + * values of the source Observable + * @see MSDN: Observable.ToDictionary + */ + public Observable> toMap(Func1 keySelector, Func1 valueSelector) { + return create(OperationToMap.toMap(this, keySelector, valueSelector)); + } + + /** + * Return an Observable that emits a single Map, returned by the mapFactory function, + * containing key and value extracted from the values emitted by the source Observable. + * + * @param keySelector the function that extracts the key from the source items + * to be used as key in the Map + * @param valueSelector the function that extracts the value from the source items + * to be used as value in the Map + * @param mapFactory the function that returns an Map instance to be used + * @return an Observable that emits a single Map containing the mapped + * values of the source Observable + */ + public Observable> toMap(Func1 keySelector, Func1 valueSelector, Func0> mapFactory) { + return create(OperationToMap.toMap(this, keySelector, valueSelector, mapFactory)); + } + + /** + * Return an Observable that emits a single HashMap containing an ArrayList of elements, + * emitted by the source Observable and keyed by the keySelector function. + * + * @param keySelector the function that extracts the key from the source items + * to be used as key in the HashMap + * @return an Observable that emits a single HashMap containing an ArrayList of elements + * mapped from the source Observable + * @see MSDN: Observable.ToLookup + */ + public Observable>> toMultimap(Func1 keySelector) { + return create(OperationToMultimap.toMultimap(this, keySelector)); + } + + /** + * Return an Observable that emits a single HashMap containing an ArrayList of values, + * extracted by the valueSelector function, emitted by the source Observable + * and keyed by the keySelector function. + * + * @param keySelector the function that extracts the key from the source items + * to be used as key in the HashMap + * @param valueSelector the function that extracts the value from the source items + * to be used as value in the Map + * @return an Observable that emits a single HashMap containing an ArrayList of elements + * mapped from the source Observable + * + * @see MSDN: Observable.ToLookup + */ + public Observable>> toMultimap(Func1 keySelector, Func1 valueSelector) { + return create(OperationToMultimap.toMultimap(this, keySelector, valueSelector)); + } + + /** + * Return an Observable that emits a single Map, returned by the mapFactory function, + * containing an ArrayList of values, extracted by the valueSelector function, + * emitted by the source Observable and keyed by the + * keySelector function. + * + * @param keySelector the function that extracts the key from the source items + * to be used as key in the Map + * @param valueSelector the function that extracts the value from the source items + * to be used as value in the Map + * @param mapFactory the function that returns an Map instance to be used + * @return an Observable that emits a single Map containing the list of mapped values + * of the source observable. + */ + public Observable>> toMultimap(Func1 keySelector, Func1 valueSelector, Func0>> mapFactory) { + return create(OperationToMultimap.toMultimap(this, keySelector, valueSelector, mapFactory)); + } + + /** + * Return an Observable that emits a single Map, returned by the mapFactory function, + * containing a custom collection of values, extracted by the valueSelector function, + * emitted by the source Observable and keyed by the + * keySelector function. + * + * @param keySelector the function that extracts the key from the source items + * to be used as key in the Map + * @param valueSelector the function that extracts the value from the source items + * to be used as value in the Map + * @param mapFactory the function that returns an Map instance to be used + * @param collectionFactory the function that returns a Collection instance for + * a particular key to be used in the Map + * @return an Observable that emits a single Map containing the collection of mapped values + * of the source observable. + */ + public Observable>> toMultimap(Func1 keySelector, Func1 valueSelector, Func0>> mapFactory, Func1> collectionFactory) { + return create(OperationToMultimap.toMultimap(this, keySelector, valueSelector, mapFactory, collectionFactory)); + } } diff --git a/rxjava-core/src/main/java/rx/operators/OperationToMap.java b/rxjava-core/src/main/java/rx/operators/OperationToMap.java new file mode 100644 index 0000000000..c52b0244f2 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperationToMap.java @@ -0,0 +1,159 @@ +/** + * Copyright 2013 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.HashMap; +import java.util.Map; +import rx.Observable; +import rx.Observable.OnSubscribeFunc; +import rx.Observer; +import rx.Subscription; +import rx.subscriptions.Subscriptions; +import rx.util.functions.Func0; +import rx.util.functions.Func1; +import rx.util.functions.Functions; + +/** + * Maps the elements of the source observable into a java.util.Map instance and + * emits that once the source observable completes. + * + * @see Issue #96 + */ +public class OperationToMap { + /** + * ToMap with key selector, identity value selector and default HashMap factory. + */ + public static OnSubscribeFunc> toMap(Observable source, + Func1 keySelector) { + return new ToMap(source, keySelector, + Functions.identity(), new DefaultToMapFactory()); + } + + /** + * ToMap with key selector, value selector and default HashMap factory. + */ + public static OnSubscribeFunc> toMap(Observable source, + Func1 keySelector, + Func1 valueSelector) { + return new ToMap(source, keySelector, + valueSelector, new DefaultToMapFactory()); + } + + /** + * ToMap with key selector, value selector and custom Map factory. + */ + public static OnSubscribeFunc> toMap(Observable source, + Func1 keySelector, + Func1 valueSelector, + Func0> mapFactory) { + return new ToMap(source, keySelector, + valueSelector, mapFactory); + } + + /** The default map factory. */ + public static class DefaultToMapFactory implements Func0> { + @Override + public Map call() { + return new HashMap(); + } + } + /** + * Maps the elements of the source observable into a java.util.Map instance + * returned by the mapFactory function by using the keySelector and + * valueSelector. + * @param the source's value type + * @param the key type + * @param the value type + */ + public static class ToMap implements OnSubscribeFunc> { + /** The source. */ + private final Observable source; + /** Key extractor. */ + private final Func1 keySelector; + /** Value extractor. */ + private final Func1 valueSelector; + /** Map factory. */ + private final Func0> mapFactory; + public ToMap( + Observable source, + Func1 keySelector, + Func1 valueSelector, + Func0> mapFactory + ) { + this.source = source; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.mapFactory = mapFactory; + + } + @Override + public Subscription onSubscribe(Observer> t1) { + Map map; + try { + map = mapFactory.call(); + } catch (Throwable t) { + t1.onError(t); + return Subscriptions.empty(); + } + return source.subscribe(new ToMapObserver( + t1, keySelector, valueSelector, map)); + } + /** + * Observer that collects the source values of T into + * a map. + */ + public static class ToMapObserver implements Observer { + /** The map. */ + Map map; + /** Key extractor. */ + private final Func1 keySelector; + /** Value extractor. */ + private final Func1 valueSelector; + /** The observer who is receiving the completed map. */ + private final Observer> t1; + + public ToMapObserver( + Observer> t1, + Func1 keySelector, + Func1 valueSelector, + Map map) { + this.map = map; + this.t1 = t1; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + } + @Override + public void onNext(T args) { + K key = keySelector.call(args); + V value = valueSelector.call(args); + map.put(key, value); + } + @Override + public void onError(Throwable e) { + map = null; + t1.onError(e); + } + @Override + public void onCompleted() { + Map map0 = map; + map = null; + t1.onNext(map0); + t1.onCompleted(); + } + } + } +} diff --git a/rxjava-core/src/main/java/rx/operators/OperationToMultimap.java b/rxjava-core/src/main/java/rx/operators/OperationToMultimap.java new file mode 100644 index 0000000000..e9cf8d9413 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperationToMultimap.java @@ -0,0 +1,206 @@ +/** + * Copyright 2013 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.HashMap; +import java.util.Map; +import rx.Observable; +import rx.Observable.OnSubscribeFunc; +import rx.Observer; +import rx.Subscription; +import rx.subscriptions.Subscriptions; +import rx.util.functions.Func0; +import rx.util.functions.Func1; +import rx.util.functions.Functions; + +/** + * Maps the elements of the source observable into a multimap + * (Map<K, Collection<V>>) where each + * key entry has a collection of the source's values. + * + * @see Issue #97 + */ +public class OperationToMultimap { + /** + * ToMultimap with key selector, identitiy value selector, + * default HashMap factory and default ArrayList collection factory. + */ + public static OnSubscribeFunc>> toMultimap( + Observable source, + Func1 keySelector + ) { + return new ToMultimap( + source, keySelector, Functions.identity(), + new DefaultToMultimapFactory(), + new DefaultMultimapCollectionFactory() + ); + } + + /** + * ToMultimap with key selector, custom value selector, + * default HashMap factory and default ArrayList collection factory. + */ + public static OnSubscribeFunc>> toMultimap( + Observable source, + Func1 keySelector, + Func1 valueSelector + ) { + return new ToMultimap( + source, keySelector, valueSelector, + new DefaultToMultimapFactory(), + new DefaultMultimapCollectionFactory() + ); + } + /** + * ToMultimap with key selector, custom value selector, + * custom Map factory and default ArrayList collection factory. + */ + public static OnSubscribeFunc>> toMultimap( + Observable source, + Func1 keySelector, + Func1 valueSelector, + Func0>> mapFactory + ) { + return new ToMultimap( + source, keySelector, valueSelector, + mapFactory, + new DefaultMultimapCollectionFactory() + ); + } + /** + * ToMultimap with key selector, custom value selector, + * custom Map factory and custom collection factory. + */ + public static OnSubscribeFunc>> toMultimap( + Observable source, + Func1 keySelector, + Func1 valueSelector, + Func0>> mapFactory, + Func1> collectionFactory + ) { + return new ToMultimap( + source, keySelector, valueSelector, + mapFactory, + collectionFactory + ); + } + /** + * The default multimap factory returning a HashMap. + */ + public static class DefaultToMultimapFactory implements Func0>> { + @Override + public Map> call() { + return new HashMap>(); + } + } + /** + * The default collection factory for a key in the multimap returning + * an ArrayList independent of the key. + */ + public static class DefaultMultimapCollectionFactory + implements Func1> { + @Override + public Collection call(K t1) { + return new ArrayList(); + } + } + /** + * Maps the elements of the source observable int a multimap customized + * by various selectors and factories. + */ + public static class ToMultimap implements OnSubscribeFunc>> { + private final Observable source; + private final Func1 keySelector; + private final Func1 valueSelector; + private final Func0>> mapFactory; + private final Func1> collectionFactory; + public ToMultimap( + Observable source, + Func1 keySelector, + Func1 valueSelector, + Func0>> mapFactory, + Func1> collectionFactory + ) { + this.source = source; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.mapFactory = mapFactory; + this.collectionFactory = collectionFactory; + } + @Override + public Subscription onSubscribe(Observer>> t1) { + Map> map; + try { + map = mapFactory.call(); + } catch (Throwable t) { + t1.onError(t); + return Subscriptions.empty(); + } + return source.subscribe(new ToMultimapObserver( + t1, keySelector, valueSelector, map, collectionFactory + )); + } + /** + * Observer that collects the source values of Ts into a multimap. + */ + public static class ToMultimapObserver implements Observer { + private final Func1 keySelector; + private final Func1 valueSelector; + private final Func1> collectionFactory; + private Map> map; + private Observer>> t1; + public ToMultimapObserver( + Observer>> t1, + Func1 keySelector, + Func1 valueSelector, + Map> map, + Func1> collectionFactory + ) { + this.t1 = t1; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.collectionFactory = collectionFactory; + this.map = map; + } + @Override + public void onNext(T args) { + K key = keySelector.call(args); + V value = valueSelector.call(args); + Collection collection = map.get(key); + if (collection == null) { + collection = collectionFactory.call(key); + map.put(key, collection); + } + collection.add(value); + } + @Override + public void onError(Throwable e) { + map = null; + t1.onError(e); + } + @Override + public void onCompleted() { + Map> map0 = map; + map = null; + t1.onNext(map0); + t1.onCompleted(); + } + } + } +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationToMapTest.java b/rxjava-core/src/test/java/rx/operators/OperationToMapTest.java new file mode 100644 index 0000000000..ab1a9fb9de --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperationToMapTest.java @@ -0,0 +1,215 @@ +/** + * Copyright 2013 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.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.Before; +import org.mockito.*; +import static org.mockito.Mockito.*; +import rx.Observable; +import rx.Observer; +import rx.util.functions.Func0; +import rx.util.functions.Func1; +import rx.util.functions.Functions; + +public class OperationToMapTest { + @Mock + Observer objectObserver; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + Func1 lengthFunc = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + Func1 duplicate = new Func1() { + @Override + public String call(String t1) { + return t1 + t1; + } + }; + @Test + public void testToMap() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + + + Observable> mapped = Observable.create(OperationToMap.toMap(source, lengthFunc)); + + Map expected = new HashMap(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + + @Test + public void testToMapWithValueSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + + Observable> mapped = Observable.create(OperationToMap.toMap(source, lengthFunc, duplicate)); + + Map expected = new HashMap(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + + @Test + public void testToMapWithError() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + + Func1 lengthFuncErr = new Func1() { + @Override + public Integer call(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + Observable> mapped = Observable.create(OperationToMap.toMap(source, lengthFuncErr)); + + Map expected = new HashMap(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + verify(objectObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void testToMapWithErrorInValueSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + + Func1 duplicateErr = new Func1() { + @Override + public String call(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Observable> mapped = Observable.create(OperationToMap.toMap(source, lengthFunc, duplicateErr)); + + Map expected = new HashMap(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + verify(objectObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void testToMapWithFactory() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + + Func0> mapFactory = new Func0>() { + @Override + public Map call() { + return new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 3; + } + }; + } + }; + + Func1 lengthFunc = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + Observable> mapped = Observable.create(OperationToMap.toMap(source, lengthFunc, Functions.identity(), mapFactory)); + + Map expected = new LinkedHashMap(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + @Test + public void testToMapWithErrorThrowingFactory() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + + Func0> mapFactory = new Func0>() { + @Override + public Map call() { + throw new RuntimeException("Forced failure"); + } + }; + + Func1 lengthFunc = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + Observable> mapped = Observable.create(OperationToMap.toMap(source, lengthFunc, Functions.identity(), mapFactory)); + + Map expected = new LinkedHashMap(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + verify(objectObserver, times(1)).onError(any(Throwable.class)); + } + +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationToMultimapTest.java b/rxjava-core/src/test/java/rx/operators/OperationToMultimapTest.java new file mode 100644 index 0000000000..f715e69c61 --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperationToMultimapTest.java @@ -0,0 +1,250 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package rx.operators; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.Before; +import org.mockito.*; +import static org.mockito.Mockito.*; +import rx.Observable; +import rx.Observer; +import rx.operators.OperationToMultimap.DefaultMultimapCollectionFactory; +import rx.operators.OperationToMultimap.DefaultToMultimapFactory; +import rx.util.functions.Func0; +import rx.util.functions.Func1; +import rx.util.functions.Functions; +public class OperationToMultimapTest { + @Mock + Observer objectObserver; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + Func1 lengthFunc = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + Func1 duplicate = new Func1() { + @Override + public String call(String t1) { + return t1 + t1; + } + }; + @Test + public void testToMultimap() { + Observable source = Observable.from("a", "b", "cc", "dd"); + + + Observable>> mapped = Observable.create(OperationToMultimap.toMultimap(source, lengthFunc)); + + Map> expected = new HashMap>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + @Test + public void testToMultimapWithValueSelector() { + Observable source = Observable.from("a", "b", "cc", "dd"); + + + Observable>> mapped = Observable.create(OperationToMultimap.toMultimap(source, lengthFunc, duplicate)); + + Map> expected = new HashMap>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + @Test + public void testToMultimapWithMapFactory() { + Observable source = Observable.from("a", "b", "cc", "dd", "eee", "fff"); + + Func0>> mapFactory = new Func0>>() { + @Override + public Map> call() { + return new LinkedHashMap>() { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > 2; + } + }; + } + }; + + Observable>> mapped = Observable.create( + OperationToMultimap.toMultimap(source, + lengthFunc, Functions.identity(), + mapFactory, new DefaultMultimapCollectionFactory())); + + Map> expected = new HashMap>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + @Test + public void testToMultimapWithCollectionFactory() { + Observable source = Observable.from("cc", "dd", "eee", "eee"); + + Func1> collectionFactory = new Func1>() { + + @Override + public Collection call(Integer t1) { + if (t1 == 2) { + return new ArrayList(); + } else { + return new HashSet(); + } + } + }; + + Observable>> mapped = Observable.create( + OperationToMultimap.toMultimap( + source, lengthFunc, Functions.identity(), + new DefaultToMultimapFactory(), collectionFactory)); + + Map> expected = new HashMap>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, new HashSet(Arrays.asList("eee"))); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onCompleted(); + } + @Test + public void testToMultimapWithError() { + Observable source = Observable.from("a", "b", "cc", "dd"); + + Func1 lengthFuncErr = new Func1() { + @Override + public Integer call(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + + Observable>> mapped = Observable.create(OperationToMultimap.toMultimap(source, lengthFuncErr)); + + Map> expected = new HashMap>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + } + @Test + public void testToMultimapWithErrorInValueSelector() { + Observable source = Observable.from("a", "b", "cc", "dd"); + + Func1 duplicateErr = new Func1() { + @Override + public String call(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Observable>> mapped = Observable.create(OperationToMultimap.toMultimap(source, lengthFunc, duplicateErr)); + + Map> expected = new HashMap>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + } + + @Test + public void testToMultimapWithMapThrowingFactory() { + Observable source = Observable.from("a", "b", "cc", "dd", "eee", "fff"); + + Func0>> mapFactory = new Func0>>() { + @Override + public Map> call() { + throw new RuntimeException("Forced failure"); + } + }; + + Observable>> mapped = Observable.create( + OperationToMultimap.toMultimap(source, lengthFunc, Functions.identity(), mapFactory)); + + Map> expected = new HashMap>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + } + @Test + public void testToMultimapWithThrowingCollectionFactory() { + Observable source = Observable.from("cc", "cc", "eee", "eee"); + + Func1> collectionFactory = new Func1>() { + + @Override + public Collection call(Integer t1) { + if (t1 == 2) { + throw new RuntimeException("Forced failure"); + } else { + return new HashSet(); + } + } + }; + + Observable>> mapped = Observable.create( + OperationToMultimap.toMultimap( + source, lengthFunc, Functions.identity(), new DefaultToMultimapFactory(), collectionFactory)); + + Map> expected = new HashMap>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Collections.singleton("eee")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onCompleted(); + } +}