提交 2913964b 编写于 作者: S Sam Brannen

[SPR-7960][SPR-8386] Supporting declarative configuration of bean definition...

[SPR-7960][SPR-8386] Supporting declarative configuration of bean definition profiles in the TestContext framework:
- TextContext now works with MergedContextConfiguration instead of locations and loader
- TextContext now builds context caching key from MergedContextConfiguration
- Test context caching is now based on locations, classes, active profiles, and context loader
- TextContext now delegates to SmartContextLoader or ContextLoader as appropriate
- AbstractContextLoader now implements SmartContextLoader
- AbstractGenericContextLoader now sets active profiles in the GenericApplicationContext 
- Introduced integration tests for profile support in the TCF for both XML and annotation config
上级 ab704fda
......@@ -10,8 +10,14 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.core.springbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.springframework.ide.eclipse.core.springnature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
<?xml version="1.0" encoding="UTF-8"?>
<beansProjectDescription>
<version>1</version>
<pluginVersion><![CDATA[2.6.0.201104111100-PATCH]]></pluginVersion>
<configSuffixes>
<configSuffix><![CDATA[xml]]></configSuffix>
</configSuffixes>
<enableImports><![CDATA[false]]></enableImports>
<configs>
<config>src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml</config>
</configs>
<configSets>
</configSets>
</beansProjectDescription>
......@@ -46,8 +46,7 @@ class ContextCache {
/**
* Map of context keys to Spring ApplicationContext instances.
*/
private final Map<String, ApplicationContext> contextKeyToContextMap =
new ConcurrentHashMap<String, ApplicationContext>();
private final Map<String, ApplicationContext> contextKeyToContextMap = new ConcurrentHashMap<String, ApplicationContext>();
private int hitCount;
......@@ -134,7 +133,7 @@ class ContextCache {
}
/**
* Explicitly add a ApplicationContext instance to the cache under the given key.
* Explicitly add an ApplicationContext instance to the cache under the given key.
* @param key the context key (never <code>null</code>)
* @param context the ApplicationContext instance (never <code>null</code>)
*/
......@@ -188,11 +187,11 @@ class ContextCache {
* as the {@link #hitCount hit} and {@link #missCount miss} counts.
*/
public String toString() {
return new ToStringCreator(this)
.append("size", size())
.append("hitCount", getHitCount())
.append("missCount",getMissCount())
.toString();
return new ToStringCreator(this)//
.append("size", size())//
.append("hitCount", getHitCount())//
.append("missCount", getMissCount())//
.toString();
}
}
......@@ -16,7 +16,6 @@
package org.springframework.test.context;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
......@@ -43,9 +42,9 @@ public class TestContext extends AttributeAccessorSupport {
private final ContextCache contextCache;
private final ContextLoader contextLoader;
private final String contextKey;
private final String[] locations;
private final MergedContextConfiguration mergedContextConfiguration;
private final Class<?> testClass;
......@@ -88,28 +87,28 @@ public class TestContext extends AttributeAccessorSupport {
Assert.notNull(testClass, "Test class must not be null");
Assert.notNull(contextCache, "ContextCache must not be null");
MergedContextConfiguration mergedContextConfiguration;
ContextConfiguration contextConfiguration = testClass.getAnnotation(ContextConfiguration.class);
ContextLoader contextLoader = null;
String[] locations = null;
if (contextConfiguration == null) {
if (logger.isInfoEnabled()) {
logger.info(String.format("@ContextConfiguration not found for class [%s]", testClass));
}
mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null);
}
else {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ContextConfiguration [%s] for class [%s]", contextConfiguration,
testClass));
}
contextLoader = ContextLoaderUtils.resolveContextLoader(testClass, defaultContextLoaderClassName);
locations = ContextLoaderUtils.resolveContextLocations(contextLoader, testClass);
mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
defaultContextLoaderClassName);
}
this.testClass = testClass;
this.contextCache = contextCache;
this.contextLoader = contextLoader;
this.locations = locations;
this.contextKey = generateContextKey(mergedContextConfiguration);
this.mergedContextConfiguration = mergedContextConfiguration;
this.testClass = testClass;
}
/**
......@@ -118,19 +117,44 @@ public class TestContext extends AttributeAccessorSupport {
* @throws Exception if an error occurs while loading the application context
*/
private ApplicationContext loadApplicationContext() throws Exception {
Assert.notNull(this.contextLoader, "Can not load an ApplicationContext with a NULL 'contextLoader'. "
+ "Consider annotating your test class with @ContextConfiguration.");
Assert.notNull(this.locations, "Can not load an ApplicationContext with a NULL 'locations' array. "
ContextLoader contextLoader = mergedContextConfiguration.getContextLoader();
Assert.notNull(contextLoader, "Can not load an ApplicationContext with a NULL 'contextLoader'. "
+ "Consider annotating your test class with @ContextConfiguration.");
return this.contextLoader.loadContext(this.locations);
ApplicationContext applicationContext;
if (contextLoader instanceof SmartContextLoader) {
SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
applicationContext = smartContextLoader.loadContext(mergedContextConfiguration);
}
else {
String[] locations = mergedContextConfiguration.getLocations();
Assert.notNull(locations, "Can not load an ApplicationContext with a NULL 'locations' array. "
+ "Consider annotating your test class with @ContextConfiguration.");
applicationContext = contextLoader.loadContext(locations);
}
return applicationContext;
}
/**
* Convert the supplied context <code>key</code> to a String representation
* for use in caching, logging, etc.
* Generates a context <code>key</code> from information stored in the
* {@link MergedContextConfiguration} for this <code>TestContext</code>.
*/
private String contextKeyString(Serializable key) {
return ObjectUtils.nullSafeToString(key);
private String generateContextKey(MergedContextConfiguration mergedContextConfiguration) {
String[] locations = mergedContextConfiguration.getLocations();
Class<?>[] classes = mergedContextConfiguration.getClasses();
String[] activeProfiles = mergedContextConfiguration.getActiveProfiles();
ContextLoader contextLoader = mergedContextConfiguration.getContextLoader();
String locationsKey = ObjectUtils.nullSafeToString(locations);
String classesKey = ObjectUtils.nullSafeToString(classes);
String activeProfilesKey = ObjectUtils.nullSafeToString(activeProfiles);
String contextLoaderKey = contextLoader == null ? "null" : contextLoader.getClass().getName();
return String.format("locations = [%s], classes = [%s], activeProfiles = [%s], contextLoader = [%s]",
locationsKey, classesKey, activeProfilesKey, contextLoaderKey);
}
/**
......@@ -141,13 +165,12 @@ public class TestContext extends AttributeAccessorSupport {
* application context
*/
public ApplicationContext getApplicationContext() {
synchronized (this.contextCache) {
String contextKeyString = contextKeyString(this.locations);
ApplicationContext context = this.contextCache.get(contextKeyString);
synchronized (contextCache) {
ApplicationContext context = contextCache.get(contextKey);
if (context == null) {
try {
context = loadApplicationContext();
this.contextCache.put(contextKeyString, context);
contextCache.put(contextKey, context);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load ApplicationContext", ex);
......@@ -162,7 +185,7 @@ public class TestContext extends AttributeAccessorSupport {
* @return the test class (never <code>null</code>)
*/
public final Class<?> getTestClass() {
return this.testClass;
return testClass;
}
/**
......@@ -172,7 +195,7 @@ public class TestContext extends AttributeAccessorSupport {
* @see #updateState(Object,Method,Throwable)
*/
public final Object getTestInstance() {
return this.testInstance;
return testInstance;
}
/**
......@@ -182,7 +205,7 @@ public class TestContext extends AttributeAccessorSupport {
* @see #updateState(Object, Method, Throwable)
*/
public final Method getTestMethod() {
return this.testMethod;
return testMethod;
}
/**
......@@ -194,7 +217,7 @@ public class TestContext extends AttributeAccessorSupport {
* @see #updateState(Object, Method, Throwable)
*/
public final Throwable getTestException() {
return this.testException;
return testException;
}
/**
......@@ -204,7 +227,7 @@ public class TestContext extends AttributeAccessorSupport {
* replacing a bean definition).
*/
public void markApplicationContextDirty() {
this.contextCache.setDirty(contextKeyString(this.locations));
contextCache.setDirty(contextKey);
}
/**
......@@ -227,11 +250,11 @@ public class TestContext extends AttributeAccessorSupport {
@Override
public String toString() {
return new ToStringCreator(this)//
.append("testClass", this.testClass)//
.append("locations", this.locations)//
.append("testInstance", this.testInstance)//
.append("testMethod", this.testMethod)//
.append("testException", this.testException)//
.append("testClass", testClass)//
.append("testInstance", testInstance)//
.append("testMethod", testMethod)//
.append("testException", testException)//
.append("mergedContextConfiguration", mergedContextConfiguration)//
.toString();
}
......
......@@ -20,6 +20,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.ResourceTypeAwareContextLoader;
import org.springframework.test.context.SmartContextLoader;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
......@@ -38,7 +39,7 @@ import org.springframework.util.StringUtils;
* @see #generateDefaultLocations
* @see #modifyLocations
*/
public abstract class AbstractContextLoader implements ResourceTypeAwareContextLoader {
public abstract class AbstractContextLoader implements SmartContextLoader, ResourceTypeAwareContextLoader {
/**
* If the supplied <code>locations</code> are <code>null</code> or
......
......@@ -20,9 +20,12 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
......@@ -45,6 +48,33 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
protected static final Log logger = LogFactory.getLog(AbstractGenericContextLoader.class);
/**
* TODO Document loadContext().
*
* @see org.springframework.test.context.SmartContextLoader#loadContext(org.springframework.test.context.MergedContextConfiguration)
*/
public final ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Loading ApplicationContext for merged context configuration [%s].",
mergedContextConfiguration));
}
String[] locations = mergedContextConfiguration.getLocations();
Assert.notNull(locations, "Can not load an ApplicationContext with a NULL 'locations' array. "
+ "Consider annotating your test class with @ContextConfiguration.");
GenericApplicationContext context = new GenericApplicationContext();
context.getEnvironment().setActiveProfiles(mergedContextConfiguration.getActiveProfiles());
prepareContext(context);
customizeBeanFactory(context.getDefaultListableBeanFactory());
loadBeanDefinitions(context, locations);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
customizeContext(context);
context.refresh();
context.registerShutdownHook();
return context;
}
/**
* Loads a Spring ApplicationContext from the supplied <code>locations</code>.
* <p>Implementation details:
......
......@@ -25,6 +25,10 @@ import org.springframework.test.context.junit4.annotation.AnnotationConfigSpring
import org.springframework.test.context.junit4.annotation.DefaultConfigClassesBaseTests;
import org.springframework.test.context.junit4.annotation.DefaultConfigClassesInheritedTests;
import org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests;
import org.springframework.test.context.junit4.profile.annotation.DefaultProfileAnnotationConfigTests;
import org.springframework.test.context.junit4.profile.annotation.DevProfileAnnotationConfigTests;
import org.springframework.test.context.junit4.profile.xml.DefaultProfileXmlConfigTests;
import org.springframework.test.context.junit4.profile.xml.DevProfileXmlConfigTests;
/**
* <p>
......@@ -54,6 +58,10 @@ StandardJUnit4FeaturesTests.class,//
AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests.class,//
DefaultConfigClassesBaseTests.class,//
DefaultConfigClassesInheritedTests.class,//
DefaultProfileAnnotationConfigTests.class,//
DevProfileAnnotationConfigTests.class, //
DefaultProfileXmlConfigTests.class,//
DevProfileXmlConfigTests.class, //
ExpectedExceptionSpringRunnerTests.class,//
TimedSpringRunnerTests.class,//
RepeatedSpringRunnerTests.class,//
......
/*
* Copyright 2011 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.test.context.junit4.profile.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.Employee;
import org.springframework.beans.Pet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
/**
* @author Sam Brannen
* @since 3.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { DefaultProfileConfig.class, DevProfileConfig.class }, loader = AnnotationConfigContextLoader.class)
public class DefaultProfileAnnotationConfigTests {
@Autowired
protected Pet pet;
@Autowired(required = false)
protected Employee employee;
@Test
public void pet() {
assertNotNull(pet);
assertEquals("Fido", pet.getName());
}
@Test
public void employee() {
assertNull("employee bean should not be created for the default profile", employee);
}
}
/*
* Copyright 2011 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.test.context.junit4.profile.annotation;
import org.springframework.beans.Pet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Sam Brannen
* @since 3.1
*/
@Configuration
public class DefaultProfileConfig {
@Bean
public Pet pet() {
return new Pet("Fido");
}
}
/*
* Copyright 2011 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.test.context.junit4.profile.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.springframework.test.context.ActiveProfiles;
/**
* @author Sam Brannen
* @since 3.1
*/
@ActiveProfiles("dev")
public class DevProfileAnnotationConfigTests extends DefaultProfileAnnotationConfigTests {
@Test
public void employee() {
assertNotNull("employee bean should be loaded for the 'dev' profile", employee);
assertEquals("John Smith", employee.getName());
}
}
/*
* Copyright 2011 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.test.context.junit4.profile.annotation;
import org.springframework.beans.Employee;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* @author Sam Brannen
* @since 3.1
*/
@Profile("dev")
@Configuration
public class DevProfileConfig {
@Bean
public Employee employee() {
Employee employee = new Employee();
employee.setName("John Smith");
employee.setAge(42);
employee.setCompany("Acme Widgets, Inc.");
return employee;
}
}
/*
* Copyright 2002-2011 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.test.context.junit4.profile.annotation;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
/**
* JUnit test suite for <em>bean definition profile</em> support in the
* Spring TestContext Framework with XML-based configuration.
*
* @author Sam Brannen
* @since 3.1
*/
@RunWith(Suite.class)
// Note: the following 'multi-line' layout is for enhanced code readability.
@SuiteClasses({//
DefaultProfileAnnotationConfigTests.class,//
DevProfileAnnotationConfigTests.class //
})
public class ProfileAnnotationConfigTestSuite {
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean id="pet" class="org.springframework.beans.Pet">
<constructor-arg value="Fido" />
</bean>
<beans profile="dev">
<bean id="employee" class="org.springframework.beans.Employee">
<property name="name" value="John Smith" />
<property name="age" value="42" />
<property name="company" value="Acme Widgets, Inc." />
</bean>
</beans>
</beans>
/*
* Copyright 2011 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.test.context.junit4.profile.xml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.Employee;
import org.springframework.beans.Pet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* TODO Document DefaultProfileXmlConfigTests.
*
* @author Sam Brannen
* @since 3.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class DefaultProfileXmlConfigTests {
@Autowired
protected Pet pet;
@Autowired(required = false)
protected Employee employee;
@Test
public void pet() {
assertNotNull(pet);
assertEquals("Fido", pet.getName());
}
@Test
public void employee() {
assertNull("employee bean should not be created for the default profile", employee);
}
}
/*
* Copyright 2011 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.test.context.junit4.profile.xml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.springframework.test.context.ActiveProfiles;
/**
* TODO Document DefaultProfileXmlConfigTests.
*
* @author Sam Brannen
* @since 3.1
*/
@ActiveProfiles("dev")
public class DevProfileXmlConfigTests extends DefaultProfileXmlConfigTests {
@Test
public void employee() {
assertNotNull("employee bean should be loaded for the 'dev' profile", employee);
assertEquals("John Smith", employee.getName());
}
}
/*
* Copyright 2002-2011 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.test.context.junit4.profile.xml;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
/**
* JUnit test suite for <em>bean definition profile</em> support in the
* Spring TestContext Framework with XML-based configuration.
*
* @author Sam Brannen
* @since 3.1
*/
@RunWith(Suite.class)
// Note: the following 'multi-line' layout is for enhanced code readability.
@SuiteClasses({//
DefaultProfileXmlConfigTests.class,//
DevProfileXmlConfigTests.class //
})
public class ProfileXmlConfigTestSuite {
}
......@@ -15,8 +15,14 @@
<level value="warn" />
</logger>
<logger name="org.springframework.binding">
<level value="debug" />
<logger name="org.springframework.test.context.TestContext">
<level value="warn" />
</logger>
<logger name="org.springframework.test.context.ContextLoaderUtils">
<level value="warn" />
</logger>
<logger name="org.springframework.test.context.support.AbstractGenericContextLoader">
<level value="warn" />
</logger>
<!-- Root Logger -->
......@@ -24,5 +30,5 @@
<priority value="warn" />
<appender-ref ref="console" />
</root>
</log4j:configuration>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册