T checkNotNull(final T reference, final Object errorMessage) {
23 | if (reference == null) {
24 | throw new NullPointerException(String.valueOf(errorMessage));
25 | }
26 | return reference;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/org/killbill/billing/client/util/TreeMapSetMultimap.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Equinix, Inc
3 | * Copyright 2014-2022 The Billing Project, LLC
4 | *
5 | * The Billing Project licenses this file to you under the Apache License, version 2.0
6 | * (the "License"); you may not use this file except in compliance with the
7 | * License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | */
17 |
18 | package org.killbill.billing.client.util;
19 |
20 | import java.util.Collection;
21 | import java.util.Map;
22 | import java.util.SortedMap;
23 | import java.util.TreeMap;
24 | import java.util.TreeSet;
25 |
26 | /**
27 | * Implementation of {@link Multimap}:
28 | *
29 | * - backed by {@link SortedMap} and use {@link TreeSet} as a values
30 | * - key cannot be {@code null}
31 | * - values cannot be {@code null}
32 | * - {@link #asMap()} return backed map (the {@code SortedMap}).
33 | *
34 | */
35 | public class TreeMapSetMultimap implements Multimap {
36 |
37 | private final SortedMap> delegate = new TreeMap<>();
38 |
39 | public TreeMapSetMultimap() {
40 | }
41 |
42 | public TreeMapSetMultimap(final Map> map) {
43 | if (map != null && !map.isEmpty()) {
44 | delegate.putAll(map);
45 | }
46 | }
47 |
48 | @Override
49 | public void put(final K key, final V value) {
50 | Preconditions.checkNotNull(key, "Cannot #put() with null key");
51 | Preconditions.checkNotNull(value, "Cannot #put() with null value");
52 |
53 | if (delegate.containsKey(key)) {
54 | delegate.get(key).add(value);
55 | } else {
56 | final Collection list = new TreeSet<>();
57 | list.add(value);
58 | delegate.put(key, list);
59 | }
60 | }
61 |
62 | @Override
63 | public void putAll(final K key, final Collection values) {
64 | Preconditions.checkNotNull(key, "Cannot #putAll() with null key");
65 | Preconditions.checkNotNull(values, "Cannot #putAll() with null values");
66 |
67 | if (!values.isEmpty()) {
68 | if (delegate.containsKey(key)) {
69 | delegate.get(key).addAll(values);
70 | } else {
71 | delegate.put(key, new TreeSet<>(values));
72 | }
73 | }
74 | }
75 |
76 | @Override
77 | public void remove(final K key) {
78 | Preconditions.checkNotNull(key, "Cannot #remove() with null key");
79 | delegate.remove(key);
80 | }
81 |
82 | @Override
83 | public Map> asMap() {
84 | return delegate;
85 | }
86 |
87 | @Override
88 | public String toString() {
89 | return "TreeMapSetMultimap {" + delegate + '}';
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/test/java/org/killbill/billing/client/TestRequestOptions.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Equinix, Inc
3 | * Copyright 2014-2022 The Billing Project, LLC
4 | *
5 | * The Billing Project licenses this file to you under the Apache License, version 2.0
6 | * (the "License"); you may not use this file except in compliance with the
7 | * License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | */
17 |
18 | package org.killbill.billing.client;
19 |
20 | import java.util.ArrayList;
21 | import java.util.Collection;
22 | import java.util.HashMap;
23 | import java.util.List;
24 | import java.util.Map;
25 |
26 | import org.killbill.billing.client.RequestOptions.RequestOptionsBuilder;
27 | import org.killbill.billing.client.util.Multimap;
28 | import org.killbill.billing.client.util.TreeMapSetMultimap;
29 | import org.testng.Assert;
30 | import org.testng.annotations.Test;
31 |
32 | public class TestRequestOptions {
33 |
34 | @Test(groups = "fast")
35 | public void test() {
36 | // requestOptionsWithImmutableMapValues();
37 | requestOptionsWithMutableMapValues();
38 | }
39 |
40 | void requestOptionsWithImmutableMapValues() {
41 | // Let's consider scenario a user using the java client:
42 | final Map> queryParams = new HashMap<>();
43 | // User can generously do this ...
44 | queryParams.put("key1", new ArrayList<>(List.of("1.1", "1.2", "1.3")));
45 | // .... or accidentally do this (because after guava removed, this is the "straight forward" approach)
46 | queryParams.put("key2", List.of("2.1", "2.2", "2.3"));
47 | // So far, good.
48 | final RequestOptions requestOptions = new RequestOptionsBuilder()
49 | .withQueryParams(queryParams)
50 | .build();
51 |
52 | // .... Then our internal generated API classes have something like this: (eg: InvoiceApi)
53 | // (Also note that as per '34b59c4c' commit, InvoiceApi still use internal #addToMapValues() method, which is do the same thing)
54 | final Multimap queryParamsInApi = new TreeMapSetMultimap<>(requestOptions.getQueryParams());
55 |
56 | // This one Ok, because user pass mutable object (wrap List.of() to new ArrayList)
57 | queryParamsInApi.put("key1", "1.4");
58 | try {
59 | // This one not Ok, because user pass immutable object first (only use List.of() )
60 | queryParamsInApi.put("key2", "2.4");
61 | Assert.fail("Make sure that RequestOptionBuilder#withQueryParams NOT calling toMutableMapValues() method");
62 | } catch (final UnsupportedOperationException ignored) {
63 | }
64 | }
65 |
66 | // Simulating the same thing as "requestOptionsWithImmutableMapValues()", but expect that
67 | // RequestOptions#withQueryParams() call Req method.
68 |
69 | /**
70 | * Simulating the same thing as {@link #requestOptionsWithImmutableMapValues()}, but expect that
71 | */
72 | private void requestOptionsWithMutableMapValues() {
73 | final Map> queryParams = new HashMap<>();
74 | queryParams.put("key1", new ArrayList<>(List.of("1.1", "1.2", "1.3")));
75 | queryParams.put("key2", List.of("2.1", "2.2", "2.3"));
76 |
77 | final RequestOptions requestOptions = new RequestOptionsBuilder()
78 | .withQueryParams(queryParams)
79 | .build();
80 |
81 | final Multimap queryParamsInApi = new TreeMapSetMultimap<>(requestOptions.getQueryParams());
82 |
83 | queryParamsInApi.put("key1", "1.4");
84 | queryParamsInApi.put("key2", "2.4");
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/test/java/org/killbill/billing/client/api/gen/TestInvoiceApi.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Equinix, Inc
3 | * Copyright 2014-2022 The Billing Project, LLC
4 | *
5 | * The Billing Project licenses this file to you under the Apache License, version 2.0
6 | * (the "License"); you may not use this file except in compliance with the
7 | * License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | */
17 |
18 | package org.killbill.billing.client.api.gen;
19 |
20 | import java.util.UUID;
21 |
22 | import org.killbill.billing.client.KillBillClientException;
23 | import org.killbill.billing.client.KillBillHttpClient;
24 | import org.killbill.billing.client.RequestOptions;
25 | import org.killbill.billing.client.RequestOptions.RequestOptionsBuilder;
26 | import org.killbill.billing.client.util.Multimap;
27 | import org.killbill.billing.client.util.TreeMapSetMultimap;
28 | import org.mockito.Mockito;
29 | import org.testng.annotations.Test;
30 |
31 | public class TestInvoiceApi {
32 |
33 | // make sure that withQueryParams() will not throw the same problem as before
34 | // adding RequestOptionsBuilder#toMutableMapValues()
35 | @Test(groups = "fast")
36 | public void getPaymentsForInvoice() throws KillBillClientException {
37 | final KillBillHttpClient httpClient = Mockito.mock(KillBillHttpClient.class);
38 |
39 | final Multimap queryParams = new TreeMapSetMultimap<>();
40 | queryParams.put("key1", "value1");
41 |
42 | final RequestOptions requestOptions = new RequestOptionsBuilder()
43 | .withQueryParams(queryParams.asMap())
44 | .build();
45 |
46 | final InvoiceApi api = new InvoiceApi(httpClient);
47 | api.getPaymentsForInvoice(UUID.randomUUID(), requestOptions);
48 |
49 | Mockito.verify(httpClient, Mockito.times(1)).doGet(Mockito.anyString(), (Class>) Mockito.any(), Mockito.any());
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/java/org/killbill/billing/client/util/TestTreeMapSetMultimap.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Equinix, Inc
3 | * Copyright 2014-2022 The Billing Project, LLC
4 | *
5 | * The Billing Project licenses this file to you under the Apache License, version 2.0
6 | * (the "License"); you may not use this file except in compliance with the
7 | * License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | */
17 |
18 | package org.killbill.billing.client.util;
19 |
20 | import java.util.Collection;
21 | import java.util.Collections;
22 | import java.util.List;
23 | import java.util.Map;
24 | import java.util.Set;
25 |
26 | import org.testng.Assert;
27 | import org.testng.annotations.Test;
28 |
29 | public class TestTreeMapSetMultimap {
30 |
31 | @Test(groups = "fast")
32 | public void put() {
33 | final Multimap multimap = new TreeMapSetMultimap<>();
34 | multimap.put("key", "value");
35 | Assert.assertEquals(multimap.asMap().size(), 1);
36 | Assert.assertTrue(multimap.asMap().keySet().stream().allMatch("key"::equals));
37 | Assert.assertTrue(multimap.asMap().values().stream().allMatch(values -> values.contains("value")));
38 |
39 | try {
40 | multimap.put(null, "any");
41 | Assert.fail("Null key should not supported");
42 | } catch (final NullPointerException ignored) {
43 | }
44 |
45 | try {
46 | multimap.put(null, "any");
47 | Assert.fail("Null values should not supported");
48 | } catch (final NullPointerException ignored) {
49 | }
50 | }
51 |
52 | @Test(groups = "fast")
53 | public void putAll() {
54 | final Multimap multimap = new TreeMapSetMultimap<>();
55 | multimap.putAll("key1", List.of("1.1", "1.2", "1.3"));
56 |
57 | Assert.assertTrue(multimap.asMap().keySet().stream().allMatch("key1"::equals));
58 | Assert.assertTrue(multimap.asMap().values().stream().allMatch(values -> values.containsAll(List.of("1.1", "1.2", "1.3"))));
59 |
60 | // No matter what user supplied in values, it should be able to put more.
61 | multimap.put("key1", "1.4");
62 | multimap.putAll("key1", Collections.singleton("1.5"));
63 | multimap.putAll("key1", Set.of("1.5", "1.6")); // 1.5 purposely duplicated.
64 | multimap.putAll("key2", List.of("2.1", "2.2"));
65 | multimap.putAll("key1", Collections.emptyList());
66 |
67 | Assert.assertEquals(multimap.asMap().get("key1").size(), 6);
68 | Assert.assertTrue(multimap.asMap().get("key1").containsAll(List.of("1.1", "1.2", "1.3", "1.4", "1.5", "1.6")));
69 | }
70 |
71 | @Test
72 | public void remove() {
73 | final Multimap multimap = new TreeMapSetMultimap<>();
74 | multimap.put("key1", "val1");
75 | multimap.put("key2", "val2");
76 |
77 | Assert.assertEquals(multimap.asMap().size(), 2);
78 |
79 | multimap.remove("key1");
80 |
81 | Assert.assertEquals(multimap.asMap().size(), 1);
82 | }
83 |
84 | @Test(groups = "fast")
85 | public void asMap() {
86 | final Multimap multimap = new TreeMapSetMultimap<>();
87 | multimap.putAll("key1", List.of("1.1", "1.2", "1.3"));
88 |
89 | final Map> asMap = multimap.asMap();
90 |
91 | Assert.assertEquals(asMap.size(), 1);
92 | Assert.assertEquals(asMap.get("key1").size(), 3);
93 |
94 | multimap.put("key1", "1.4");
95 | multimap.putAll("key1", List.of("1.5", "1.6"));
96 | multimap.put("key2", "2.1");
97 |
98 | Assert.assertEquals(asMap.get("key2"), List.of("2.1"));
99 | Assert.assertEquals(asMap.size(), 2);
100 | Assert.assertEquals(asMap.get("key1").size(), 6);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------