DATAGEODE-317 - Add PoolResolver implementation to compose a collection of PoolResolvers.
This commit is contained in:
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.data.gemfire.client.support;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.apache.geode.cache.client.Pool;
|
||||||
|
|
||||||
|
import org.springframework.data.gemfire.client.PoolResolver;
|
||||||
|
import org.springframework.data.gemfire.util.ArrayUtils;
|
||||||
|
import org.springframework.data.gemfire.util.CollectionUtils;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite of {@link PoolResolver PoolResolvers} functioning as a single {@link PoolResolver}.
|
||||||
|
*
|
||||||
|
* @author John Blum
|
||||||
|
* @see org.apache.geode.cache.client.Pool
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
public class ComposablePoolResolver implements PoolResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null-safe factory method to compose an array of {@link PoolResolver} objects.
|
||||||
|
*
|
||||||
|
* Preserves order in the composition.
|
||||||
|
*
|
||||||
|
* @param poolResolvers array of {@link PoolResolver} objects to compose.
|
||||||
|
* @return a composition from the array of {@link PoolResolver} objects; may return {@literal null}.
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
* @see #compose(Iterable)
|
||||||
|
*/
|
||||||
|
public static @Nullable PoolResolver compose(@Nullable PoolResolver... poolResolvers) {
|
||||||
|
return compose(Arrays.asList(ArrayUtils.nullSafeArray(poolResolvers, PoolResolver.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null-safe factory method to compose an {@link Iterable} of {@link PoolResolver} objects.
|
||||||
|
*
|
||||||
|
* Preserves order in the composition if the {@link Iterable} collection-like data structure is ordered,
|
||||||
|
* like a {@link java.util.List}).
|
||||||
|
*
|
||||||
|
* @param poolResolvers {@link Iterable} of {@link PoolResolver} objects to compose.
|
||||||
|
* @return a composition from the {@link Iterable} of {@link PoolResolver} objects; may return {@literal null}.
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
* @see java.lang.Iterable
|
||||||
|
* @see #compose(PoolResolver, PoolResolver)
|
||||||
|
*/
|
||||||
|
public static @Nullable PoolResolver compose(@Nullable Iterable<PoolResolver> poolResolvers) {
|
||||||
|
|
||||||
|
PoolResolver current = null;
|
||||||
|
|
||||||
|
for (PoolResolver poolResolver : CollectionUtils.nullSafeIterable(poolResolvers)) {
|
||||||
|
current = compose(current, poolResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null-safe factory method to compose two {@link PoolResolver} objects in a composition.
|
||||||
|
*
|
||||||
|
* @param one first {@link PoolResolver} in the composition.
|
||||||
|
* @param two second {@link PoolResolver} in the composition.
|
||||||
|
* @return a composition from the two {@link PoolResolver} objects. Returns the first {@link PoolResolver}
|
||||||
|
* if the second {@link PoolResolver} is {@literal null}. Returns the second {@link PoolResolver} if the first
|
||||||
|
* {@link PoolResolver} is {@literal null}. Returns {@literal null} if both {@link PoolResolver} arguments
|
||||||
|
* are {@literal null}.
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
* @see #ComposablePoolResolver(PoolResolver, PoolResolver)
|
||||||
|
*/
|
||||||
|
public static @Nullable PoolResolver compose(@Nullable PoolResolver one, @Nullable PoolResolver two) {
|
||||||
|
return one == null ? two : two == null ? one : new ComposablePoolResolver(one, two);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PoolResolver poolResolverOne;
|
||||||
|
private final PoolResolver poolResolverTwo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of {@link ComposablePoolResolver} initialized and composed of two {@link PoolResolver}
|
||||||
|
* implementations that will function as one.
|
||||||
|
*
|
||||||
|
* @param poolResolverOne first {@link PoolResolver} in the composition order.
|
||||||
|
* @param poolResolverTwo second {@link PoolResolver} in the composition order.
|
||||||
|
* @throws IllegalArgumentException if either the first or second {@link PoolResolver} are {@literal null}.
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
*/
|
||||||
|
protected ComposablePoolResolver(PoolResolver poolResolverOne, PoolResolver poolResolverTwo) {
|
||||||
|
|
||||||
|
Assert.notNull(poolResolverOne, "PoolResolver 1 must not be null");
|
||||||
|
Assert.notNull(poolResolverTwo, "PoolResolver 2 must not be null");
|
||||||
|
|
||||||
|
this.poolResolverOne = poolResolverOne;
|
||||||
|
this.poolResolverTwo = poolResolverTwo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reference to the first {@link PoolResolver} in the composition.
|
||||||
|
*
|
||||||
|
* @return a reference to the first {@link PoolResolver} in the composition.
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
*/
|
||||||
|
protected @NonNull PoolResolver getPoolResolverOne() {
|
||||||
|
return this.poolResolverOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reference to the second {@link PoolResolver} in the composition.
|
||||||
|
*
|
||||||
|
* @return a reference to the second {@link PoolResolver} in the composition.
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
*/
|
||||||
|
protected @NonNull PoolResolver getPoolResolverTwo() {
|
||||||
|
return this.poolResolverTwo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to resolve a {@link Pool} with the given {@link String name} by delegating to the composed
|
||||||
|
* {@link PoolResolver} objects.
|
||||||
|
*
|
||||||
|
* The first {@link PoolResolver} in the composition to resolve a {@link Pool} with the given {@link String name}
|
||||||
|
* stops the resolution process and returns the target {@link Pool}. If no {@link Pool} with the given
|
||||||
|
* {@link String name} can be resolved by any {@link PoolResolver} in the composition, then {@literal null}
|
||||||
|
* will be returned.
|
||||||
|
*
|
||||||
|
* @param poolName {@link String name} of the {@link Pool} to resolve.
|
||||||
|
* @return the resolved {@link Pool} or {@literal null} if a {@link Pool} with {@link String name}
|
||||||
|
* cannot be resolved.
|
||||||
|
* @see org.apache.geode.cache.client.Pool
|
||||||
|
* @see #getPoolResolverOne()
|
||||||
|
* @see #getPoolResolverTwo()
|
||||||
|
*/
|
||||||
|
@Nullable @Override
|
||||||
|
public Pool resolve(@Nullable String poolName) {
|
||||||
|
|
||||||
|
Pool pool = getPoolResolverOne().resolve(poolName);
|
||||||
|
|
||||||
|
return pool != null ? pool : getPoolResolverTwo().resolve(poolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.data.gemfire.client.support;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import org.apache.geode.cache.client.Pool;
|
||||||
|
|
||||||
|
import org.springframework.data.gemfire.client.PoolResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit Tests for {@link ComposablePoolResolver}.
|
||||||
|
*
|
||||||
|
* @author John Blum
|
||||||
|
* @see org.junit.Test
|
||||||
|
* @see org.mockito.Mock
|
||||||
|
* @see org.mockito.Mockito
|
||||||
|
* @see org.mockito.junit.MockitoJUnitRunner
|
||||||
|
* @see org.apache.geode.cache.client.Pool
|
||||||
|
* @see org.springframework.data.gemfire.client.PoolResolver
|
||||||
|
* @see org.springframework.data.gemfire.client.support.ComposablePoolResolver
|
||||||
|
* @since 2.3.0
|
||||||
|
*/
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class ComposablePoolResolverUnitTests {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Pool mockPool;
|
||||||
|
|
||||||
|
@Mock(name = "one")
|
||||||
|
private PoolResolver mockPoolResolverOne;
|
||||||
|
|
||||||
|
@Mock(name = "two")
|
||||||
|
private PoolResolver mockPoolResolverTwo;
|
||||||
|
|
||||||
|
@Mock(name = "three")
|
||||||
|
private PoolResolver mockPoolResolverThree;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public void composeWithArrayOfPoolResolvers() {
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver
|
||||||
|
.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo, this.mockPoolResolverThree);
|
||||||
|
|
||||||
|
assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class);
|
||||||
|
|
||||||
|
PoolResolver one = ((ComposablePoolResolver) poolResolver).getPoolResolverOne();
|
||||||
|
PoolResolver two = ((ComposablePoolResolver) poolResolver).getPoolResolverTwo();
|
||||||
|
|
||||||
|
assertThat(one).isInstanceOf(ComposablePoolResolver.class);
|
||||||
|
assertThat(((ComposablePoolResolver) one).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne);
|
||||||
|
assertThat(((ComposablePoolResolver) one).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo);
|
||||||
|
assertThat(two).isEqualTo(this.mockPoolResolverThree);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithArrayContainingSinglePoolResolver() {
|
||||||
|
|
||||||
|
assertThat(ComposablePoolResolver.compose(this.mockPoolResolverOne))
|
||||||
|
.isEqualTo(this.mockPoolResolverOne);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithArrayContainingNonNullPoolResolverAndNullPoolResolver() {
|
||||||
|
|
||||||
|
PoolResolver[] poolResolvers = { this.mockPoolResolverOne, null };
|
||||||
|
|
||||||
|
assertThat(ComposablePoolResolver.compose(poolResolvers)).isEqualTo(this.mockPoolResolverOne);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public void composeWithArrayContainingNullAndNonNullPoolResolvers() {
|
||||||
|
|
||||||
|
PoolResolver[] poolResolvers = { null, this.mockPoolResolverOne, null, this.mockPoolResolverTwo, null };
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(poolResolvers);
|
||||||
|
|
||||||
|
assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class);
|
||||||
|
assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne);
|
||||||
|
assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public void composeWithArrayContainingOrganizedNullAndNonNullPoolResolvers() {
|
||||||
|
|
||||||
|
PoolResolver[] poolResolvers = {
|
||||||
|
null, this.mockPoolResolverOne, this.mockPoolResolverTwo, null, this.mockPoolResolverThree, null, null
|
||||||
|
};
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(poolResolvers);
|
||||||
|
|
||||||
|
assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class);
|
||||||
|
|
||||||
|
PoolResolver one = ((ComposablePoolResolver) poolResolver).getPoolResolverOne();
|
||||||
|
PoolResolver two = ((ComposablePoolResolver) poolResolver).getPoolResolverTwo();
|
||||||
|
|
||||||
|
assertThat(one).isInstanceOf(ComposablePoolResolver.class);
|
||||||
|
assertThat(((ComposablePoolResolver) one).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne);
|
||||||
|
assertThat(((ComposablePoolResolver) one).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo);
|
||||||
|
assertThat(two).isEqualTo(this.mockPoolResolverThree);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithEmptyArrayReturnsNull() {
|
||||||
|
assertThat(ComposablePoolResolver.compose()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithNullArrayIsNullSafe() {
|
||||||
|
assertThat(ComposablePoolResolver.compose((PoolResolver[]) null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithNullIterableIsNullSafe() {
|
||||||
|
assertThat(ComposablePoolResolver.compose((Iterable<PoolResolver>) null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithAllNullPoolResolvers() {
|
||||||
|
assertThat(ComposablePoolResolver.compose(null, null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithSingleNullPoolResolver() {
|
||||||
|
|
||||||
|
assertThat(ComposablePoolResolver.compose(this.mockPoolResolverOne, null))
|
||||||
|
.isEqualTo(this.mockPoolResolverOne);
|
||||||
|
|
||||||
|
assertThat(ComposablePoolResolver.compose(null, this.mockPoolResolverTwo))
|
||||||
|
.isEqualTo(this.mockPoolResolverTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composeWithTwoNonNullPoolResolvers() {
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo);
|
||||||
|
|
||||||
|
assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class);
|
||||||
|
assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne);
|
||||||
|
assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructComposablePoolResolver() {
|
||||||
|
|
||||||
|
ComposablePoolResolver poolResolver =
|
||||||
|
new ComposablePoolResolver(this.mockPoolResolverOne, this.mockPoolResolverTwo);
|
||||||
|
|
||||||
|
assertThat(poolResolver).isNotNull();
|
||||||
|
assertThat(poolResolver.getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne);
|
||||||
|
assertThat(poolResolver.getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testConstructComposablePoolResolverWithNullPoolResolver(Supplier<ComposablePoolResolver> constructor,
|
||||||
|
String expectedMessage) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
constructor.get();
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException expected) {
|
||||||
|
|
||||||
|
assertThat(expected).hasMessage(expectedMessage);
|
||||||
|
assertThat(expected).hasNoCause();
|
||||||
|
|
||||||
|
throw expected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void constructComposablePoolResolverWithNullPoolResolverAsFirstArgument() {
|
||||||
|
testConstructComposablePoolResolverWithNullPoolResolver(
|
||||||
|
() -> new ComposablePoolResolver(null, this.mockPoolResolverTwo),
|
||||||
|
"PoolResolver 1 must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void constructComposablePoolResolverWithNullPoolResolverAsSecondArgument() {
|
||||||
|
testConstructComposablePoolResolverWithNullPoolResolver(
|
||||||
|
() -> new ComposablePoolResolver(this.mockPoolResolverOne, null),
|
||||||
|
"PoolResolver 2 must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveReturnsFromFirstPoolResolver() {
|
||||||
|
|
||||||
|
when(this.mockPoolResolverOne.resolve(anyString())).thenReturn(this.mockPool);
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo);
|
||||||
|
|
||||||
|
assertThat(poolResolver.resolve("TestPool")).isEqualTo(this.mockPool);
|
||||||
|
|
||||||
|
verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool"));
|
||||||
|
verify(this.mockPoolResolverTwo, never()).resolve(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveReturnsFromSecondPoolResolver() {
|
||||||
|
|
||||||
|
when(this.mockPoolResolverTwo.resolve(anyString())).thenReturn(this.mockPool);
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo);
|
||||||
|
|
||||||
|
assertThat(poolResolver.resolve("TestPool")).isEqualTo(this.mockPool);
|
||||||
|
|
||||||
|
verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool"));
|
||||||
|
verify(this.mockPoolResolverTwo, times(1)).resolve(eq("TestPool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public void resolveReturnsPoolFromLastPoolResolverInOrder() {
|
||||||
|
|
||||||
|
when(this.mockPoolResolverTwo.resolve(anyString())).thenReturn(this.mockPool);
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(null, this.mockPoolResolverThree,
|
||||||
|
null , null, this.mockPoolResolverOne, null, this.mockPoolResolverTwo, null, null);
|
||||||
|
|
||||||
|
assertThat(poolResolver.resolve("TestPool")).isEqualTo(this.mockPool);
|
||||||
|
|
||||||
|
InOrder ordered = inOrder(this.mockPoolResolverThree, this.mockPoolResolverOne, this.mockPoolResolverTwo);
|
||||||
|
|
||||||
|
ordered.verify(this.mockPoolResolverThree, times(1)).resolve(eq("TestPool"));
|
||||||
|
ordered.verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool"));
|
||||||
|
ordered.verify(this.mockPoolResolverTwo, times(1)).resolve(eq("TestPool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveReturnsNull() {
|
||||||
|
|
||||||
|
PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo);
|
||||||
|
|
||||||
|
assertThat(poolResolver.resolve("TestPool")).isNull();
|
||||||
|
|
||||||
|
verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool"));
|
||||||
|
verify(this.mockPoolResolverTwo, times(1)).resolve(eq("TestPool"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user