Merge branch '2.7.x'
This commit is contained in:
commit
303979fb65
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
@ -16,14 +16,19 @@
|
|||
|
||||
package org.springframework.boot.build.autoconfigure;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
@ -48,6 +53,8 @@ import org.springframework.util.StringUtils;
|
|||
*/
|
||||
public class AutoConfigurationMetadata extends DefaultTask {
|
||||
|
||||
private static final String COMMENT_START = "#";
|
||||
|
||||
private SourceSet sourceSet;
|
||||
|
||||
private File outputFile;
|
||||
|
@ -57,6 +64,12 @@ public class AutoConfigurationMetadata extends DefaultTask {
|
|||
.file((Callable<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
|
||||
"META-INF/spring.factories"))
|
||||
.withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("spring.factories");
|
||||
getInputs()
|
||||
.file((Callable<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
|
||||
"META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration"))
|
||||
.withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
.withPropertyName("org.springframework.boot.autoconfigure.AutoConfiguration");
|
||||
|
||||
dependsOn((Callable<String>) () -> this.sourceSet.getProcessResourcesTaskName());
|
||||
getProject().getConfigurations()
|
||||
.maybeCreate(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME);
|
||||
|
@ -86,11 +99,9 @@ public class AutoConfigurationMetadata extends DefaultTask {
|
|||
|
||||
private Properties readAutoConfiguration() throws IOException {
|
||||
Properties autoConfiguration = CollectionFactory.createSortedProperties(true);
|
||||
Properties springFactories = readSpringFactories(
|
||||
new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
|
||||
String enableAutoConfiguration = springFactories
|
||||
.getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration");
|
||||
Set<String> classNames = StringUtils.commaDelimitedListToSet(enableAutoConfiguration);
|
||||
Set<String> classNames = new LinkedHashSet<>();
|
||||
classNames.addAll(readSpringFactories());
|
||||
classNames.addAll(readAutoConfigurationsFile());
|
||||
Set<String> publicClassNames = new LinkedHashSet<>();
|
||||
for (String className : classNames) {
|
||||
File classFile = findClassFile(className);
|
||||
|
@ -109,6 +120,57 @@ public class AutoConfigurationMetadata extends DefaultTask {
|
|||
return autoConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads auto-configurations from META-INF/spring.factories.
|
||||
* @return auto-configurations
|
||||
*/
|
||||
private Set<String> readSpringFactories() throws IOException {
|
||||
File file = new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories");
|
||||
if (!file.exists()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Properties springFactories = readSpringFactories(file);
|
||||
String enableAutoConfiguration = springFactories
|
||||
.getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration");
|
||||
return StringUtils.commaDelimitedListToSet(enableAutoConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads auto-configurations from
|
||||
* META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration.
|
||||
* @return auto-configurations
|
||||
*/
|
||||
private List<String> readAutoConfigurationsFile() throws IOException {
|
||||
File file = new File(this.sourceSet.getOutput().getResourcesDir(),
|
||||
"META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration");
|
||||
if (!file.exists()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// Nearly identical copy of
|
||||
// org.springframework.boot.autoconfigure.AutoConfigurationLoader.readAutoConfigurations
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
|
||||
List<String> autoConfigurations = new ArrayList<>();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = stripComment(line);
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
autoConfigurations.add(line);
|
||||
}
|
||||
return autoConfigurations;
|
||||
}
|
||||
}
|
||||
|
||||
private String stripComment(String line) {
|
||||
int commentStart = line.indexOf(COMMENT_START);
|
||||
if (commentStart == -1) {
|
||||
return line;
|
||||
}
|
||||
return line.substring(0, commentStart);
|
||||
}
|
||||
|
||||
private File findClassFile(String className) {
|
||||
String classFileName = className.replace(".", "/") + ".class";
|
||||
for (File classesDir : this.sourceSet.getOutput().getClassesDirs()) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
|
||||
/**
|
||||
* Indicates that a class provides configuration that can be automatically applied by
|
||||
|
@ -33,6 +34,10 @@ import org.springframework.context.annotation.Configuration;
|
|||
* {@link Configuration @Configuration} with the exception that
|
||||
* {@literal Configuration#proxyBeanMethods() proxyBeanMethods} is always {@code false}.
|
||||
* <p>
|
||||
* They are located using the {@link AutoConfigurationLoader} and the
|
||||
* {@link SpringFactoriesLoader} mechanism (keyed against
|
||||
* {@link EnableAutoConfiguration}).
|
||||
* <p>
|
||||
* Generally auto-configuration classes are marked as {@link Conditional @Conditional}
|
||||
* (most often using {@link ConditionalOnClass @ConditionalOnClass} and
|
||||
* {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.boot.autoconfigure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
|
@ -54,13 +55,20 @@ public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoad
|
|||
}
|
||||
|
||||
private boolean isAutoConfiguration(MetadataReader metadataReader) {
|
||||
return getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
|
||||
boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata()
|
||||
.isAnnotated(AutoConfiguration.class.getName());
|
||||
return annotatedWithAutoConfiguration
|
||||
|| getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
|
||||
}
|
||||
|
||||
protected List<String> getAutoConfigurations() {
|
||||
if (this.autoConfigurations == null) {
|
||||
this.autoConfigurations = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
|
||||
this.beanClassLoader);
|
||||
List<String> autoConfigurations = new ArrayList<>();
|
||||
autoConfigurations.addAll(
|
||||
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader));
|
||||
autoConfigurations
|
||||
.addAll(new AutoConfigurationLoader().loadNames(AutoConfiguration.class, this.beanClassLoader));
|
||||
this.autoConfigurations = autoConfigurations;
|
||||
}
|
||||
return this.autoConfigurations;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
@ -67,6 +67,7 @@ import org.springframework.util.StringUtils;
|
|||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @author Madhura Bhave
|
||||
* @author Moritz Halbritter
|
||||
* @since 1.3.0
|
||||
* @see EnableAutoConfiguration
|
||||
*/
|
||||
|
@ -167,7 +168,9 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
|
|||
|
||||
/**
|
||||
* Return the auto-configuration class names that should be considered. By default
|
||||
* this method will load candidates using {@link SpringFactoriesLoader} with
|
||||
* this method will load candidates using {@link AutoConfigurationLoader} with
|
||||
* {@link #getSpringFactoriesLoaderFactoryClass()}. For backward compatible reasons it
|
||||
* will also consider {@link SpringFactoriesLoader} with
|
||||
* {@link #getSpringFactoriesLoaderFactoryClass()}.
|
||||
* @param metadata the source metadata
|
||||
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
|
||||
|
@ -175,10 +178,13 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
|
|||
* @return a list of candidate configurations
|
||||
*/
|
||||
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
|
||||
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
|
||||
getBeanClassLoader());
|
||||
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
|
||||
+ "are using a custom packaging, make sure that file is correct.");
|
||||
List<String> configurations = new ArrayList<>();
|
||||
configurations.addAll(
|
||||
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
|
||||
configurations.addAll(new AutoConfigurationLoader().loadNames(AutoConfiguration.class, getBeanClassLoader()));
|
||||
Assert.notEmpty(configurations,
|
||||
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration. If you "
|
||||
+ "are using a custom packaging, make sure that file is correct.");
|
||||
return configurations;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2012-2022 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.boot.autoconfigure;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Loads the names of annotated classes, usually @{@link AutoConfiguration}.
|
||||
*
|
||||
* The names of the classes are stored in files named META-INF/spring-boot/{full qualified
|
||||
* name of the annotation}. Every line contains the full qualified class name of the
|
||||
* annotated class. Comments are supported using the # character.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @see AutoConfiguration
|
||||
* @see SpringFactoriesLoader
|
||||
*/
|
||||
class AutoConfigurationLoader {
|
||||
|
||||
private static final String LOCATION = "META-INF/spring-boot/";
|
||||
|
||||
private static final String COMMENT_START = "#";
|
||||
|
||||
/**
|
||||
* Loads the names of annotated classes.
|
||||
* @param annotation annotation to load
|
||||
* @param classLoader class loader to use for loading
|
||||
* @return list of names of annotated classes
|
||||
*/
|
||||
List<String> loadNames(Class<?> annotation, ClassLoader classLoader) {
|
||||
Assert.notNull(annotation, "'annotation' must not be null");
|
||||
ClassLoader classLoaderToUse = decideClassloader(classLoader);
|
||||
String location = LOCATION + annotation.getName();
|
||||
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
|
||||
List<String> autoConfigurations = new ArrayList<>();
|
||||
while (urls.hasMoreElements()) {
|
||||
URL url = urls.nextElement();
|
||||
autoConfigurations.addAll(readAutoConfigurations(url));
|
||||
}
|
||||
return autoConfigurations;
|
||||
}
|
||||
|
||||
private ClassLoader decideClassloader(ClassLoader classLoader) {
|
||||
if (classLoader == null) {
|
||||
return AutoConfigurationLoader.class.getClassLoader();
|
||||
}
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
private Enumeration<URL> findUrlsInClasspath(ClassLoader classLoader, String location) {
|
||||
try {
|
||||
return classLoader.getResources(location);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalArgumentException("Failed to load autoconfigurations from location [" + location + "]",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> readAutoConfigurations(URL url) {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
|
||||
List<String> autoConfigurations = new ArrayList<>();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = stripComment(line);
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
autoConfigurations.add(line);
|
||||
}
|
||||
return autoConfigurations;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalArgumentException("Unable to load autoconfigurations from location [" + url + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String stripComment(String line) {
|
||||
int commentStart = line.indexOf(COMMENT_START);
|
||||
if (commentStart == -1) {
|
||||
return line;
|
||||
}
|
||||
return line.substring(0, commentStart);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2022 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.
|
||||
|
@ -60,9 +60,9 @@ import org.springframework.core.io.support.SpringFactoriesLoader;
|
|||
* and classes can be searched.
|
||||
* <p>
|
||||
* Auto-configuration classes are regular Spring {@link Configuration @Configuration}
|
||||
* beans. They are located using the {@link SpringFactoriesLoader} mechanism (keyed
|
||||
* against this class). Generally auto-configuration beans are
|
||||
* {@link Conditional @Conditional} beans (most often using
|
||||
* beans. They are located using the {@link AutoConfigurationLoader} and the
|
||||
* {@link SpringFactoriesLoader} mechanism (keyed against this class). Generally
|
||||
* auto-configuration beans are {@link Conditional @Conditional} beans (most often using
|
||||
* {@link ConditionalOnClass @ConditionalOnClass} and
|
||||
* {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).
|
||||
*
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.util.ObjectUtils;
|
|||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector implements DeterminableImports {
|
||||
|
||||
|
@ -94,7 +95,10 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec
|
|||
}
|
||||
|
||||
protected Collection<String> loadFactoryNames(Class<?> source) {
|
||||
return SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader());
|
||||
List<String> factoryNames = new ArrayList<>();
|
||||
factoryNames.addAll(SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader()));
|
||||
factoryNames.addAll(new AutoConfigurationLoader().loadNames(source, getBeanClassLoader()));
|
||||
return factoryNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2012-2022 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.boot.autoconfigure;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class AutoConfigurationLoaderTests {
|
||||
|
||||
private AutoConfigurationLoader sut;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.sut = new AutoConfigurationLoader();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadNames() {
|
||||
List<String> classNames = this.sut.loadNames(TestAutoConfiguration.class, null);
|
||||
|
||||
assertThat(classNames).containsExactly("class1", "class2", "class3");
|
||||
}
|
||||
|
||||
@AutoConfiguration
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TestAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# A comment spanning a complete line
|
||||
class1
|
||||
|
||||
class2 # with comment at the end
|
||||
# Comment with some whitespace in front
|
||||
class3
|
|
@ -24,16 +24,18 @@ You can browse the source code of {spring-boot-autoconfigure-module-code}[`sprin
|
|||
|
||||
[[features.developing-auto-configuration.locating-auto-configuration-candidates]]
|
||||
=== Locating Auto-configuration Candidates
|
||||
Spring Boot checks for the presence of a `META-INF/spring.factories` file within your published jar.
|
||||
The file should list your configuration classes under the `EnableAutoConfiguration` key, as shown in the following example:
|
||||
|
||||
Spring Boot checks for the presence of a `META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration` file within your published jar.
|
||||
The file should list your configuration classes, as shown in the following example:
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
|
||||
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
|
||||
com.mycorp.libx.autoconfigure.LibXAutoConfiguration
|
||||
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
|
||||
----
|
||||
|
||||
TIP: You can use comments via `#` in this file.
|
||||
|
||||
NOTE: Auto-configurations must be loaded that way _only_.
|
||||
Make sure that they are defined in a specific package space and that they are never the target of component scanning.
|
||||
Furthermore, auto-configuration classes should not enable component scanning to find additional components.
|
||||
|
|
|
@ -716,13 +716,18 @@ include::code:MyJdbcTests[]
|
|||
|
||||
NOTE: Make sure to not use the regular `@Import` annotation to import auto-configurations as they are handled in a specific way by Spring Boot.
|
||||
|
||||
Alternatively, additional auto-configurations can be added for any use of a slice annotation by registering them in `META-INF/spring.factories` as shown in the following example:
|
||||
Alternatively, additional auto-configurations can be added for any use of a slice annotation by registering them in a file stored in `META-INF/spring-boot` as shown in the following example:
|
||||
|
||||
.META-INF/spring-boot/org.springframework.boot.test.autoconfigure.jdbc.JdbcTest
|
||||
[indent=0]
|
||||
----
|
||||
org.springframework.boot.test.autoconfigure.jdbc.JdbcTest=com.example.IntegrationAutoConfiguration
|
||||
com.example.IntegrationAutoConfiguration
|
||||
----
|
||||
|
||||
In this example, the `com.example.IntegrationAutoConfiguration` is enabled on every test annotated with `@JdbcTest`.
|
||||
|
||||
TIP: You can use comments via `#` in this file.
|
||||
|
||||
TIP: A slice or `@AutoConfigure...` annotation can be customized this way as long as it is meta-annotated with `@ImportAutoConfiguration`.
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue