Support instantiating Kotlin classes with optional parameters
This commit updates BeanUtils class in order to add Kotlin optional parameters with default values support to the immutable data classes support introduced by SPR-15199. Issue: SPR-15673
This commit is contained in:
parent
5cac619e23
commit
fa4d139684
|
|
@ -517,6 +517,7 @@ project("spring-context") {
|
|||
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
|
||||
optional("org.codehaus.groovy:groovy-all:${groovyVersion}")
|
||||
optional("org.beanshell:bsh:2.0b5")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
testCompile("org.apache.commons:commons-pool2:2.4.2")
|
||||
testCompile("org.slf4j:slf4j-api:${slf4jVersion}")
|
||||
|
|
@ -755,6 +756,7 @@ project("spring-web") {
|
|||
optional("javax.xml.bind:jaxb-api:${jaxbVersion}")
|
||||
optional("javax.xml.ws:jaxws-api:${jaxwsVersion}")
|
||||
optional("javax.mail:javax.mail-api:${javamailVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
testCompile("io.projectreactor:reactor-test")
|
||||
testCompile("org.apache.taglibs:taglibs-standard-jstlel:1.2.1") {
|
||||
|
|
@ -853,6 +855,8 @@ project("spring-webmvc") {
|
|||
}
|
||||
optional('org.webjars:webjars-locator:0.32-1')
|
||||
optional("org.reactivestreams:reactive-streams")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
|
||||
testCompile("dom4j:dom4j:1.6.1") {
|
||||
exclude group: "xml-apis", module: "xml-apis"
|
||||
|
|
@ -1048,6 +1052,7 @@ project("spring-test") {
|
|||
optional("org.reactivestreams:reactive-streams")
|
||||
optional("io.projectreactor:reactor-core")
|
||||
optional("io.projectreactor:reactor-test")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
testCompile(project(":spring-context-support"))
|
||||
testCompile(project(":spring-oxm"))
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package org.springframework.beans;
|
|||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.beans.PropertyEditor;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
|
@ -27,10 +28,17 @@ import java.net.URL;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
import kotlin.reflect.full.KClasses;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
|
|
@ -53,6 +61,7 @@ import org.springframework.util.StringUtils;
|
|||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @author Sam Brannen
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public abstract class BeanUtils {
|
||||
|
||||
|
|
@ -61,6 +70,9 @@ public abstract class BeanUtils {
|
|||
private static final Set<Class<?>> unknownEditorTypes =
|
||||
Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64));
|
||||
|
||||
private static final boolean kotlinPresent =
|
||||
ClassUtils.isPresent("kotlin.Unit", BeanUtils.class.getClassLoader());
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method to instantiate a class using its no-arg constructor.
|
||||
|
|
@ -103,7 +115,12 @@ public abstract class BeanUtils {
|
|||
throw new BeanInstantiationException(clazz, "Specified class is an interface");
|
||||
}
|
||||
try {
|
||||
return instantiateClass(clazz.getDeclaredConstructor());
|
||||
Constructor<T> ctor = (kotlinPresent && isKotlinClass(clazz) ?
|
||||
KotlinDelegate.findPrimaryConstructor(clazz) : clazz.getDeclaredConstructor());
|
||||
if (ctor == null) {
|
||||
throw new BeanInstantiationException(clazz, "No default constructor found");
|
||||
}
|
||||
return instantiateClass(ctor);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new BeanInstantiationException(clazz, "No default constructor found", ex);
|
||||
|
|
@ -132,9 +149,11 @@ public abstract class BeanUtils {
|
|||
/**
|
||||
* Convenience method to instantiate a class using the given constructor.
|
||||
* <p>Note that this method tries to set the constructor accessible if given a
|
||||
* non-accessible (that is, non-public) constructor.
|
||||
* non-accessible (that is, non-public) constructor, and supports Kotlin classes
|
||||
* with optional parameters and default values.
|
||||
* @param ctor the constructor to instantiate
|
||||
* @param args the constructor arguments to apply
|
||||
* @param args the constructor arguments to apply (use null for unspecified parameter
|
||||
* if needed for Kotlin classes with optional parameters and default values)
|
||||
* @return the new instance
|
||||
* @throws BeanInstantiationException if the bean cannot be instantiated
|
||||
* @see Constructor#newInstance
|
||||
|
|
@ -143,7 +162,7 @@ public abstract class BeanUtils {
|
|||
Assert.notNull(ctor, "Constructor must not be null");
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(ctor);
|
||||
return ctor.newInstance(args);
|
||||
return (kotlinPresent && isKotlinClass(ctor.getDeclaringClass()) ? KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
|
||||
}
|
||||
catch (InstantiationException ex) {
|
||||
throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
|
||||
|
|
@ -299,6 +318,37 @@ public abstract class BeanUtils {
|
|||
return targetMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the primary constructor of the provided class (single or default constructor
|
||||
* for Java classes and primary constructor for Kotlin classes) if any.
|
||||
* @param clazz the {@link Class} of the Kotlin class
|
||||
* @see <a href="http://kotlinlang.org/docs/reference/classes.html#constructors">http://kotlinlang.org/docs/reference/classes.html#constructors</a>
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
Constructor<T> ctor = null;
|
||||
if (kotlinPresent && isKotlinClass(clazz)) {
|
||||
ctor = KotlinDelegate.findPrimaryConstructor(clazz);
|
||||
}
|
||||
else {
|
||||
Constructor<T>[] ctors = (Constructor<T>[])clazz.getConstructors();
|
||||
if (ctors.length == 1) {
|
||||
ctor = ctors[0];
|
||||
}
|
||||
else {
|
||||
try {
|
||||
ctor = clazz.getDeclaredConstructor();
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a method signature in the form {@code methodName[([arg_list])]},
|
||||
* where {@code arg_list} is an optional, comma-separated list of fully-qualified
|
||||
|
|
@ -646,4 +696,63 @@ public abstract class BeanUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the specified class is a Kotlin one.
|
||||
*/
|
||||
private static boolean isKotlinClass(Class<?> clazz) {
|
||||
for (Annotation annotation : clazz.getDeclaredAnnotations()) {
|
||||
if (annotation.annotationType().getName().equals("kotlin.Metadata")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Kotlin at runtime.
|
||||
*/
|
||||
private static class KotlinDelegate {
|
||||
|
||||
/**
|
||||
* Return the Java constructor corresponding to the Kotlin primary constructor if any.
|
||||
* @param clazz the {@link Class} of the Kotlin class
|
||||
* @see <a href="http://kotlinlang.org/docs/reference/classes.html#constructors">http://kotlinlang.org/docs/reference/classes.html#constructors</a>
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
|
||||
KFunction<T> primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
|
||||
if (primaryConstructor == null) {
|
||||
return null;
|
||||
}
|
||||
Constructor<T> constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor);
|
||||
Assert.notNull(constructor, "Can't get the Java constructor corresponding to the Kotlin primary constructor of " + clazz.getName());
|
||||
return constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a Kotlin class using the provided constructor.
|
||||
* @param ctor the constructor of the Kotlin class to instantiate
|
||||
* @param args the constructor arguments to apply (use null for unspecified parameter if needed)
|
||||
* @throws BeanInstantiationException if no primary constructor can be found
|
||||
*/
|
||||
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) {
|
||||
KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(ctor);
|
||||
if (kotlinConstructor == null) {
|
||||
throw new BeanInstantiationException(ctor.getDeclaringClass(), "No corresponding Kotlin constructor found");
|
||||
}
|
||||
List<KParameter> parameters = kotlinConstructor.getParameters();
|
||||
Map<KParameter, Object> argParameters = new HashMap<>(parameters.size());
|
||||
Assert.isTrue(args.length <= parameters.size(),
|
||||
"The number of provided arguments should be less of equals than the number of constructor parameters");
|
||||
for (int i = 0 ; i < args.length ; i++) {
|
||||
if (!(parameters.get(i).isOptional() && (args[i] == null))) {
|
||||
argParameters.put(parameters.get(i), args[i]);
|
||||
}
|
||||
}
|
||||
return kotlinConstructor.callBy(argParameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package org.springframework.beans;
|
|||
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -274,6 +275,23 @@ public class BeanUtilsTests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindDefaultConstructorAndInstantiate() {
|
||||
Constructor<Bean> ctor = BeanUtils.findPrimaryConstructor(Bean.class);
|
||||
assertNotNull(ctor);
|
||||
Bean bean = BeanUtils.instantiateClass(ctor);
|
||||
assertNotNull(bean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindSingleNonDefaultConstructorAndInstantiate() {
|
||||
Constructor<BeanWithSingleNonDefaultConstructor> ctor = BeanUtils.findPrimaryConstructor(BeanWithSingleNonDefaultConstructor.class);
|
||||
assertNotNull(ctor);
|
||||
BeanWithSingleNonDefaultConstructor bean = BeanUtils.instantiateClass(ctor, "foo");
|
||||
assertNotNull(bean);
|
||||
assertEquals("foo", bean.getName());
|
||||
}
|
||||
|
||||
private void assertSignatureEquals(Method desiredMethod, String signature) {
|
||||
assertEquals(desiredMethod, BeanUtils.resolveSignature(signature, MethodSignatureBean.class));
|
||||
|
|
@ -444,5 +462,18 @@ public class BeanUtilsTests {
|
|||
value = aValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static class BeanWithSingleNonDefaultConstructor {
|
||||
|
||||
private final String name;
|
||||
|
||||
public BeanWithSingleNonDefaultConstructor(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2002-2017 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.beans
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Kotlin tests for {@link BeanUtils}
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class BeanUtilsKotlinTests {
|
||||
|
||||
@Test
|
||||
fun `Instantiate immutable class`() {
|
||||
val constructor = BeanUtils.findPrimaryConstructor(Foo::class.java)
|
||||
val foo = BeanUtils.instantiateClass(constructor, "bar", 3) as Foo
|
||||
assertEquals("bar", foo.param1)
|
||||
assertEquals(3, foo.param2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Instantiate immutable class with optional parameter and all parameters specified`() {
|
||||
val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)
|
||||
val bar = BeanUtils.instantiateClass(constructor, "baz", 8) as Bar
|
||||
assertEquals("baz", bar.param1)
|
||||
assertEquals(8, bar.param2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Instantiate immutable class with optional parameter and only mandatory parameters specified by position`() {
|
||||
val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)
|
||||
val bar = BeanUtils.instantiateClass(constructor, "baz") as Bar
|
||||
assertEquals("baz", bar.param1)
|
||||
assertEquals(12, bar.param2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Instantiate immutable class with optional parameter specified with null value`() {
|
||||
val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)
|
||||
val bar = BeanUtils.instantiateClass(constructor, "baz", null) as Bar
|
||||
assertEquals("baz", bar.param1)
|
||||
assertEquals(12, bar.param2)
|
||||
}
|
||||
|
||||
class Foo(val param1: String, val param2: Int)
|
||||
|
||||
class Bar(val param1: String, val param2: Int = 12)
|
||||
|
||||
}
|
||||
|
|
@ -31,7 +31,6 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import kotlin.Metadata;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
|
|
@ -737,7 +736,7 @@ public class MethodParameter {
|
|||
* Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not.
|
||||
*/
|
||||
public static boolean isNullable(MethodParameter param) {
|
||||
if (param.getContainingClass().isAnnotationPresent(Metadata.class)) {
|
||||
if (isKotlinClass(param.getContainingClass())) {
|
||||
Method method = param.getMethod();
|
||||
Constructor<?> ctor = param.getConstructor();
|
||||
int index = param.getParameterIndex();
|
||||
|
|
@ -767,6 +766,16 @@ public class MethodParameter {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isKotlinClass(Class<?> clazz) {
|
||||
for (Annotation annotation : clazz.getDeclaredAnnotations()) {
|
||||
if (annotation.annotationType().getName().equals("kotlin.Metadata")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
|||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 3.1
|
||||
*/
|
||||
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
|
||||
|
|
@ -157,13 +158,13 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
protected Object createAttribute(String attributeName, MethodParameter parameter,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
Constructor<?>[] ctors = parameter.getParameterType().getConstructors();
|
||||
if (ctors.length != 1) {
|
||||
// No standard data class or standard JavaBeans arrangement ->
|
||||
// defensively go with default constructor, expecting regular bean property bindings.
|
||||
return BeanUtils.instantiateClass(parameter.getParameterType());
|
||||
Class<?> type = parameter.getParameterType();
|
||||
|
||||
Constructor<?> ctor = BeanUtils.findPrimaryConstructor(type);
|
||||
if (ctor == null) {
|
||||
throw new IllegalStateException("No primary constructor found for " + type.getName());
|
||||
}
|
||||
Constructor<?> ctor = ctors[0];
|
||||
|
||||
if (ctor.getParameterCount() == 0) {
|
||||
// A single default constructor -> clearly a standard JavaBeans arrangement.
|
||||
return BeanUtils.instantiateClass(ctor);
|
||||
|
|
@ -179,8 +180,9 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
Object[] args = new Object[paramTypes.length];
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
args[i] = binder.convertIfNecessary(
|
||||
webRequest.getParameterValues(paramNames[i]), paramTypes[i], new MethodParameter(ctor, i));
|
||||
String[] parameterValues = webRequest.getParameterValues(paramNames[i]);
|
||||
args[i] = (parameterValues != null ? binder.convertIfNecessary(parameterValues, paramTypes[i],
|
||||
new MethodParameter(ctor, i)) : null);
|
||||
}
|
||||
return BeanUtils.instantiateClass(ctor, args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2002-2017 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.mvc.method.annotation
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.springframework.mock.web.test.MockHttpServletRequest
|
||||
import org.springframework.mock.web.test.MockHttpServletResponse
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ServletAnnotationControllerHandlerMethodKotlinTests : AbstractServletHandlerMethodTests() {
|
||||
|
||||
@Test
|
||||
fun dataClassBinding() {
|
||||
initServletWithControllers(DataClassController::class.java)
|
||||
|
||||
val request = MockHttpServletRequest("GET", "/bind")
|
||||
request.addParameter("param1", "value1")
|
||||
request.addParameter("param2", "2")
|
||||
val response = MockHttpServletResponse()
|
||||
servlet.service(request, response)
|
||||
assertEquals("value1-2", response.contentAsString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataClassBindingWithOptionalParameterAndAllParameters() {
|
||||
initServletWithControllers(DataClassController::class.java)
|
||||
|
||||
val request = MockHttpServletRequest("GET", "/bind-optional-parameter")
|
||||
request.addParameter("param1", "value1")
|
||||
request.addParameter("param2", "2")
|
||||
val response = MockHttpServletResponse()
|
||||
servlet.service(request, response)
|
||||
assertEquals("value1-2", response.contentAsString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataClassBindingWithOptionalParameterAndOnlyMissingParameters() {
|
||||
initServletWithControllers(DataClassController::class.java)
|
||||
|
||||
val request = MockHttpServletRequest("GET", "/bind-optional-parameter")
|
||||
request.addParameter("param1", "value1")
|
||||
val response = MockHttpServletResponse()
|
||||
servlet.service(request, response)
|
||||
assertEquals("value1-12", response.contentAsString)
|
||||
}
|
||||
|
||||
|
||||
data class DataClass(val param1: String, val param2: Int)
|
||||
|
||||
data class DataClassWithOptionalParameter(val param1: String, val param2: Int = 12)
|
||||
|
||||
@RestController
|
||||
class DataClassController {
|
||||
|
||||
@RequestMapping("/bind")
|
||||
fun handle(data: DataClass) = "${data.param1}-${data.param2}"
|
||||
|
||||
@RequestMapping("/bind-optional-parameter")
|
||||
fun handle(data: DataClassWithOptionalParameter) = "${data.param1}-${data.param2}"
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue