Add auto-configuration for Jackson's JodaTime and JSR-310 modules
We now register the Jackson JodaTime module with Jackson ObjectMappers if it is on the classpath. We also register the JSR-310 module if it's on the classpath and the application is running Java 8 or better. Extracted the Jackson specific configuration previously residing in HttpMessageConvertersAutoConfiguration into a JacksonAutoConfiguration class. Added the Jackson JSR-310 module as a managed Boot dependency.
This commit is contained in:
parent
30bef1e95e
commit
6f98c63ac0
|
@ -36,6 +36,11 @@
|
|||
<artifactId>jackson-datatype-joda</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 2012-2014 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.boot.autoconfigure.jackson;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnJava;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnJava.JavaVersion;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMapperProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
|
||||
|
||||
/**
|
||||
* Auto configuration for Jackson. The following auto-configuration will get applied:
|
||||
* <ul>
|
||||
* <li>an {@link ObjectMapper} in case none is already configured.</li>
|
||||
* <li>the {@link JodaModule} registered if it's on the classpath.</li>
|
||||
* <li>the {@link JSR310Module} registered if it's on the classpath and the application is
|
||||
* running on Java 8 or better.</li>
|
||||
* <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans
|
||||
* (including the defaulted ones).</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
@EnableConfigurationProperties(HttpMapperProperties.class)
|
||||
public class JacksonAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private HttpMapperProperties properties = new HttpMapperProperties();
|
||||
|
||||
@Autowired
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Primary
|
||||
public ObjectMapper jacksonObjectMapper() {
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
if (this.properties.isJsonSortKeys()) {
|
||||
objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
|
||||
}
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
|
||||
Collection<ObjectMapper> mappers = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, ObjectMapper.class)
|
||||
.values();
|
||||
Collection<Module> modules = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
this.beanFactory, Module.class).values();
|
||||
|
||||
for (ObjectMapper mapper : mappers) {
|
||||
mapper.registerModules(modules);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass(JodaModule.class)
|
||||
static class JodaModuleAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
JodaModule jacksonJodaModule() {
|
||||
return new JodaModule();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnJava(JavaVersion.EIGHT)
|
||||
@ConditionalOnClass(JSR310Module.class)
|
||||
static class Jsr310ModuleAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
JSR310Module jacksonJsr310Module() {
|
||||
return new JSR310Module();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,38 +17,34 @@
|
|||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link HttpMessageConverter}s.
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Christian Dupuis
|
||||
* @author Piotr Maj
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(HttpMessageConverter.class)
|
||||
@Import(JacksonAutoConfiguration.class)
|
||||
public class HttpMessageConvertersAutoConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
|
@ -70,33 +66,6 @@ public class HttpMessageConvertersAutoConfiguration {
|
|||
@Autowired
|
||||
private HttpMapperProperties properties = new HttpMapperProperties();
|
||||
|
||||
@Autowired
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Collection<ObjectMapper> mappers = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, ObjectMapper.class)
|
||||
.values();
|
||||
Collection<Module> modules = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
this.beanFactory, Module.class).values();
|
||||
for (ObjectMapper mapper : mappers) {
|
||||
mapper.registerModules(modules);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Primary
|
||||
public ObjectMapper jacksonObjectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
if (this.properties.isJsonSortKeys()) {
|
||||
objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS,
|
||||
true);
|
||||
}
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(
|
||||
|
@ -106,7 +75,5 @@ public class HttpMessageConvertersAutoConfiguration {
|
|||
converter.setPrettyPrint(this.properties.isJsonPrettyPrint());
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright 2012-2014 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.boot.autoconfigure.jackson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Matchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link JacksonAutoConfiguration}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class JacksonAutoConfigurationTests {
|
||||
|
||||
AnnotationConfigApplicationContext context;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registersJodaModuleAutomatically() {
|
||||
|
||||
this.context.register(JacksonAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
|
||||
Collection<Module> modules = this.context.getBeansOfType(Module.class).values();
|
||||
assertThat(modules, is(Matchers.<Module> iterableWithSize(1)));
|
||||
assertThat(modules.iterator().next(), is(instanceOf(JodaModule.class)));
|
||||
|
||||
ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class);
|
||||
assertThat(objectMapper.canSerialize(LocalDateTime.class), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customJacksonModules() throws Exception {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(ModulesConfig.class, JacksonAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
|
||||
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
|
||||
|
||||
@SuppressWarnings({ "unchecked", "unused" })
|
||||
ObjectMapper result = verify(mapper).registerModules(
|
||||
(Iterable<Module>) argThat(hasItem(this.context.getBean("jacksonModule",
|
||||
Module.class))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doubleModuleRegistration() throws Exception {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(DoubleModulesConfig.class,
|
||||
HttpMessageConvertersAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
|
||||
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
|
||||
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo()));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class ModulesConfig {
|
||||
|
||||
@Bean
|
||||
public Module jacksonModule() {
|
||||
return new SimpleModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
return Mockito.mock(ObjectMapper.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class DoubleModulesConfig {
|
||||
|
||||
@Bean
|
||||
public Module jacksonModule() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Foo.class, new JsonSerializer<Foo>() {
|
||||
|
||||
@Override
|
||||
public void serialize(Foo value, JsonGenerator jgen,
|
||||
SerializerProvider provider) throws IOException,
|
||||
JsonProcessingException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("foo", "bar");
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
});
|
||||
return module;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(jacksonModule());
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class Foo {
|
||||
|
||||
private String name;
|
||||
|
||||
private Foo() {
|
||||
|
||||
}
|
||||
|
||||
static Foo create() {
|
||||
return new Foo();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -16,38 +16,23 @@
|
|||
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpMessageConvertersAutoConfiguration}.
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class HttpMessageConvertersAutoConfigurationTests {
|
||||
|
||||
|
@ -62,52 +47,22 @@ public class HttpMessageConvertersAutoConfigurationTests {
|
|||
|
||||
@Test
|
||||
public void customJacksonConverter() throws Exception {
|
||||
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(JacksonConfig.class,
|
||||
HttpMessageConvertersAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
|
||||
MappingJackson2HttpMessageConverter converter = this.context
|
||||
.getBean(MappingJackson2HttpMessageConverter.class);
|
||||
assertEquals(this.context.getBean(ObjectMapper.class),
|
||||
converter.getObjectMapper());
|
||||
|
||||
HttpMessageConverters converters = this.context
|
||||
.getBean(HttpMessageConverters.class);
|
||||
assertTrue(converters.getConverters().contains(converter));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJacksonModules() throws Exception {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(HttpMessageConvertersAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class);
|
||||
assertThat(objectMapper.canSerialize(LocalDateTime.class), equalTo(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customJacksonModules() throws Exception {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(ModulesConfig.class,
|
||||
HttpMessageConvertersAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
|
||||
|
||||
@SuppressWarnings({ "unchecked", "unused" })
|
||||
ObjectMapper result = verify(mapper).registerModules(
|
||||
(Iterable<Module>) argThat(hasItem(this.context.getBean("jacksonModule",
|
||||
Module.class))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doubleModuleRegistration() throws Exception {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.register(DoubleModulesConfig.class,
|
||||
HttpMessageConvertersAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
|
||||
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo()));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class JacksonConfig {
|
||||
|
||||
|
@ -122,75 +77,5 @@ public class HttpMessageConvertersAutoConfigurationTests {
|
|||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class ModulesConfig {
|
||||
|
||||
@Bean
|
||||
public Module jacksonModule() {
|
||||
return new SimpleModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
return Mockito.mock(ObjectMapper.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class DoubleModulesConfig {
|
||||
|
||||
@Bean
|
||||
public Module jacksonModule() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Foo.class, new JsonSerializer<Foo>() {
|
||||
|
||||
@Override
|
||||
public void serialize(Foo value, JsonGenerator jgen,
|
||||
SerializerProvider provider) throws IOException,
|
||||
JsonProcessingException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("foo", "bar");
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
});
|
||||
return module;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(jacksonModule());
|
||||
return mapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class Foo {
|
||||
|
||||
private String name;
|
||||
|
||||
private Foo() {
|
||||
|
||||
}
|
||||
|
||||
static Foo create() {
|
||||
return new Foo();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -370,6 +370,11 @@
|
|||
<artifactId>jackson-datatype-joda</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.gemstone.gemfire</groupId>
|
||||
<artifactId>gemfire</artifactId>
|
||||
|
|
Loading…
Reference in New Issue