Introduced MvcUriComponentsBuilder to create URIs pointing to controller methods.
MvcUriComponentsBuilder allows creating URIs that point to Spring MVC controller methods annotated with @RequestMapping. It builds them by exposing a mock method invocation API similar to Mockito, records the method invocations and thus builds up the URI by inspecting the mapping annotations and the parameters handed into the method invocations. Introduced a new SPI UriComponentsContributor that should be implemented by HandlerMethodArgumentResolvers that actually contribute path segments or query parameters to a URI. While the newly introduced MvcUriComponentsBuilder looks up those UriComponentsContributor instances from the MVC configuration. The MvcUriComponentsBuilderFactory (name to be discussed - MvcUris maybe?) prevents the multiple lookups by keeping the UriComponentsBuilder instances in an instance variable. So an instance of the factory could be exposed as Spring bean or through a HandlerMethodArgumentResolver to be injected into Controller methods. Issue: SPR-10665, SPR-8826
This commit is contained in:
committed by
Rossen Stoyanchev
parent
92a48b72d7
commit
4fd27b12fc
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* 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.springframework.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.AnnotationAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnnotationMappingDiscoverer}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class AnnotationMappingDiscovererUnitTests {
|
||||
|
||||
AnnotationMappingDiscoverer discoverer = new AnnotationMappingDiscoverer(
|
||||
RequestMapping.class);
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullAnnotationType() {
|
||||
new AnnotationMappingDiscoverer((Class<? extends Annotation>) null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullAnnotationAttribute() {
|
||||
new AnnotationMappingDiscoverer((AnnotationAttribute) null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoversTypeLevelMapping() {
|
||||
assertThat(discoverer.getMapping(MyController.class), is("/type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoversMethodLevelMapping() throws Exception {
|
||||
Method method = MyController.class.getMethod("method");
|
||||
assertThat(discoverer.getMapping(method), is("/type/method"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNullForNonExistentTypeLevelMapping() {
|
||||
assertThat(discoverer.getMapping(ControllerWithoutTypeLevelMapping.class),
|
||||
is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesMethodLevelMappingWithoutTypeLevelMapping() throws Exception {
|
||||
|
||||
Method method = ControllerWithoutTypeLevelMapping.class.getMethod("method");
|
||||
assertThat(discoverer.getMapping(method), is("/method"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesMethodLevelMappingWithSlashRootMapping() throws Exception {
|
||||
|
||||
Method method = SlashRootMapping.class.getMethod("method");
|
||||
assertThat(discoverer.getMapping(method), is("/method"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #46
|
||||
*/
|
||||
@Test
|
||||
public void treatsMissingMethodMappingAsEmptyMapping() throws Exception {
|
||||
|
||||
Method method = MyController.class.getMethod("noMethodMapping");
|
||||
assertThat(discoverer.getMapping(method), is("/type"));
|
||||
}
|
||||
|
||||
@RequestMapping("/type")
|
||||
interface MyController {
|
||||
|
||||
@RequestMapping("/method")
|
||||
void method();
|
||||
|
||||
@RequestMapping
|
||||
void noMethodMapping();
|
||||
}
|
||||
|
||||
interface ControllerWithoutTypeLevelMapping {
|
||||
|
||||
@RequestMapping("/method")
|
||||
void method();
|
||||
}
|
||||
|
||||
@RequestMapping("/")
|
||||
interface SlashRootMapping {
|
||||
|
||||
@RequestMapping("/method")
|
||||
void method();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* 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.springframework.web.servlet.hypermedia;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.format.ISODateTimeFormat;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilderUnitTests.PersonControllerImpl;
|
||||
import org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilderUnitTests.PersonsAddressesController;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilder.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MvcUriComponentsBuilderFactory}.
|
||||
*
|
||||
* @author Ricardo Gladwell
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class MvcUriComponentsBuilderFactoryUnitTests extends TestUtils {
|
||||
|
||||
List<UriComponentsContributor> contributors = Collections.emptyList();
|
||||
|
||||
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
|
||||
contributors);
|
||||
|
||||
@Test
|
||||
public void createsLinkToControllerRoot() {
|
||||
|
||||
URI link = factory.from(PersonControllerImpl.class).build().toUri();
|
||||
|
||||
assertPointsToMockServer(link);
|
||||
assertThat(link.toString(), endsWith("/people"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createsLinkToParameterizedControllerRoot() {
|
||||
|
||||
URI link = factory.from(PersonsAddressesController.class, 15).build().toUri();
|
||||
|
||||
assertPointsToMockServer(link);
|
||||
assertThat(link.toString(), endsWith("/people/15/addresses"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesParameterValueIfContributorConfigured() {
|
||||
|
||||
List<? extends UriComponentsContributor> contributors = Arrays.asList(new SampleUriComponentsContributor());
|
||||
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
|
||||
contributors);
|
||||
|
||||
SpecialType specialType = new SpecialType();
|
||||
specialType.parameterValue = "value";
|
||||
|
||||
URI link = factory.from(
|
||||
methodOn(SampleController.class).sampleMethod(1L, specialType)).build().toUri();
|
||||
assertPointsToMockServer(link);
|
||||
assertThat(link.toString(), endsWith("/sample/1?foo=value"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #57
|
||||
*/
|
||||
@Test
|
||||
public void usesDateTimeFormatForUriBinding() {
|
||||
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
|
||||
contributors);
|
||||
URI link = factory.from(methodOn(SampleController.class).sampleMethod(now)).build().toUri();
|
||||
assertThat(link.toString(),
|
||||
endsWith("/sample/" + ISODateTimeFormat.date().print(now)));
|
||||
}
|
||||
|
||||
static interface SampleController {
|
||||
|
||||
@RequestMapping("/sample/{id}")
|
||||
HttpEntity<?> sampleMethod(@PathVariable("id") Long id, SpecialType parameter);
|
||||
|
||||
@RequestMapping("/sample/{time}")
|
||||
HttpEntity<?> sampleMethod(
|
||||
@PathVariable("time") @DateTimeFormat(iso = ISO.DATE) DateTime time);
|
||||
}
|
||||
|
||||
static class SampleUriComponentsContributor implements UriComponentsContributor {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return SpecialType.class.equals(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enhance(UriComponentsBuilder builder, MethodParameter parameter,
|
||||
Object value) {
|
||||
builder.queryParam("foo", ((SpecialType) value).parameterValue);
|
||||
}
|
||||
}
|
||||
|
||||
static class SpecialType {
|
||||
|
||||
String parameterValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* 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.springframework.web.servlet.hypermedia;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilder.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MvcUriComponentsBuilder}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Dietrich Schulten
|
||||
*/
|
||||
public class MvcUriComponentsBuilderUnitTests extends TestUtils {
|
||||
|
||||
@Test
|
||||
public void createsLinkToControllerRoot() {
|
||||
|
||||
URI link = from(PersonControllerImpl.class).build().toUri();
|
||||
assertThat(link.toString(), Matchers.endsWith("/people"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createsLinkToParameterizedControllerRoot() {
|
||||
|
||||
URI link = from(PersonsAddressesController.class, 15).build().toUri();
|
||||
assertThat(link.toString(), endsWith("/people/15/addresses"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #70
|
||||
*/
|
||||
@Test
|
||||
public void createsLinkToMethodOnParameterizedControllerRoot() {
|
||||
|
||||
URI link = from(
|
||||
methodOn(PersonsAddressesController.class, 15).getAddressesForCountry(
|
||||
"DE")).build().toUri();
|
||||
assertThat(link.toString(), endsWith("/people/15/addresses/DE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createsLinkToSubResource() {
|
||||
|
||||
URI link = from(PersonControllerImpl.class).pathSegment("something").build().toUri();
|
||||
assertThat(link.toString(), endsWith("/people/something"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void rejectsControllerWithMultipleMappings() {
|
||||
from(InvalidController.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createsLinkToUnmappedController() {
|
||||
|
||||
URI link = from(UnmappedController.class).build().toUri();
|
||||
assertThat(link.toString(), is("http://localhost/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendingNullIsANoOp() {
|
||||
|
||||
URI link = from(PersonControllerImpl.class).path(null).build().toUri();
|
||||
assertThat(link.toString(), endsWith("/people"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linksToMethod() {
|
||||
|
||||
URI link = from(methodOn(ControllerWithMethods.class).myMethod(null)).build().toUri();
|
||||
assertPointsToMockServer(link);
|
||||
assertThat(link.toString(), endsWith("/something/else"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linksToMethodWithPathVariable() {
|
||||
|
||||
URI link = from(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).build().toUri();
|
||||
assertPointsToMockServer(link);
|
||||
assertThat(link.toString(), endsWith("/something/1/foo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #33
|
||||
*/
|
||||
@Test
|
||||
public void usesForwardedHostAsHostIfHeaderIsSet() {
|
||||
|
||||
request.addHeader("X-Forwarded-Host", "somethingDifferent");
|
||||
|
||||
URI link = from(PersonControllerImpl.class).build().toUri();
|
||||
assertThat(link.toString(), startsWith("http://somethingDifferent"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #26, #39
|
||||
*/
|
||||
@Test
|
||||
public void linksToMethodWithPathVariableAndRequestParams() {
|
||||
|
||||
URI link = from(
|
||||
methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, 5)).build().toUri();
|
||||
|
||||
UriComponents components = toComponents(link);
|
||||
assertThat(components.getPath(), is("/something/1/foo"));
|
||||
|
||||
MultiValueMap<String, String> queryParams = components.getQueryParams();
|
||||
assertThat(queryParams.get("limit"), contains("5"));
|
||||
assertThat(queryParams.get("offset"), contains("10"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #26, #39
|
||||
*/
|
||||
@Test
|
||||
public void linksToMethodWithPathVariableAndMultiValueRequestParams() {
|
||||
|
||||
URI link = from(
|
||||
methodOn(ControllerWithMethods.class).methodWithMultiValueRequestParams(
|
||||
"1", Arrays.asList(3, 7), 5)).build().toUri();
|
||||
|
||||
UriComponents components = toComponents(link);
|
||||
assertThat(components.getPath(), is("/something/1/foo"));
|
||||
|
||||
MultiValueMap<String, String> queryParams = components.getQueryParams();
|
||||
assertThat(queryParams.get("limit"), contains("5"));
|
||||
assertThat(queryParams.get("items"), containsInAnyOrder("3", "7"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #90
|
||||
*/
|
||||
@Test
|
||||
public void usesForwardedHostAndPortFromHeader() {
|
||||
|
||||
request.addHeader("X-Forwarded-Host", "foobar:8088");
|
||||
|
||||
URI link = from(PersonControllerImpl.class).build().toUri();
|
||||
assertThat(link.toString(), startsWith("http://foobar:8088"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #90
|
||||
*/
|
||||
@Test
|
||||
public void usesFirstHostOfXForwardedHost() {
|
||||
|
||||
request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088");
|
||||
|
||||
URI link = from(PersonControllerImpl.class).build().toUri();
|
||||
assertThat(link.toString(), startsWith("http://barfoo:8888"));
|
||||
}
|
||||
|
||||
private static UriComponents toComponents(URI link) {
|
||||
return UriComponentsBuilder.fromUri(link).build();
|
||||
}
|
||||
|
||||
static class Person {
|
||||
|
||||
Long id;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping("/people")
|
||||
interface PersonController {
|
||||
|
||||
}
|
||||
|
||||
class PersonControllerImpl implements PersonController {
|
||||
|
||||
}
|
||||
|
||||
@RequestMapping("/people/{id}/addresses")
|
||||
static class PersonsAddressesController {
|
||||
|
||||
@RequestMapping("/{country}")
|
||||
public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping({ "/persons", "/people" })
|
||||
class InvalidController {
|
||||
|
||||
}
|
||||
|
||||
class UnmappedController {
|
||||
|
||||
}
|
||||
|
||||
@RequestMapping("/something")
|
||||
static class ControllerWithMethods {
|
||||
|
||||
@RequestMapping("/else")
|
||||
HttpEntity<Void> myMethod(@RequestBody Object payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@RequestMapping("/{id}/foo")
|
||||
HttpEntity<Void> methodWithPathVariable(@PathVariable String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/{id}/foo")
|
||||
HttpEntity<Void> methodForNextPage(@PathVariable String id,
|
||||
@RequestParam Integer offset, @RequestParam Integer limit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/{id}/foo")
|
||||
HttpEntity<Void> methodWithMultiValueRequestParams(@PathVariable String id,
|
||||
@RequestParam List<Integer> items, @RequestParam Integer limit) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012 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
|
||||
*
|
||||
* 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.springframework.web.servlet.hypermedia;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class RecordedInvocationUtilsUnitTests extends TestUtils {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
|
||||
MvcUriComponentsBuilder.from(RecordedInvocationUtils.methodOn(SampleController.class).someMethod(
|
||||
1L));
|
||||
|
||||
}
|
||||
|
||||
@RequestMapping("/sample")
|
||||
static class SampleController {
|
||||
|
||||
@RequestMapping("/{id}/foo")
|
||||
HttpEntity<Void> someMethod(@PathVariable("id") Long id) {
|
||||
return new ResponseEntity<Void>(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* 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.springframework.web.servlet.hypermedia;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.springframework.mock.web.test.MockHttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Utility class to ease tesing.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class TestUtils {
|
||||
|
||||
protected MockHttpServletRequest request;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
request = new MockHttpServletRequest();
|
||||
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||
}
|
||||
|
||||
protected void assertPointsToMockServer(URI link) {
|
||||
assertThat(link.toString(), startsWith("http://localhost"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user