Commit ccc3afb4 authored by nobodyiam's avatar nobodyiam

support multiple spring contexts

parent 2a926ea2
...@@ -65,7 +65,7 @@ public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFac ...@@ -65,7 +65,7 @@ public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFac
Set<String> keys = placeholderHelper.extractPlaceholderKeys(placeholder); Set<String> keys = placeholderHelper.extractPlaceholderKeys(placeholder);
for (String key : keys) { for (String key : keys) {
SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true); SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true);
springValueRegistry.register(key, springValue); springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue); logger.debug("Monitoring {}", springValue);
} }
} }
...@@ -102,7 +102,7 @@ public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFac ...@@ -102,7 +102,7 @@ public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFac
for (String key : keys) { for (String key : keys) {
SpringValue springValue = new SpringValue(key, apolloJsonValue.value(), bean, beanName, SpringValue springValue = new SpringValue(key, apolloJsonValue.value(), bean, beanName,
method, true); method, true);
springValueRegistry.register(key, springValue); springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue); logger.debug("Monitoring {}", springValue);
} }
} }
......
...@@ -19,9 +19,12 @@ import org.slf4j.Logger; ...@@ -19,9 +19,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
/** /**
...@@ -30,7 +33,7 @@ import org.springframework.context.annotation.Bean; ...@@ -30,7 +33,7 @@ import org.springframework.context.annotation.Bean;
* @author github.com/zhegexiaohuozi seimimaster@gmail.com * @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2017/12/20. * @since 2017/12/20.
*/ */
public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor { public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor, BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class); private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class);
...@@ -38,21 +41,22 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory ...@@ -38,21 +41,22 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
private final PlaceholderHelper placeholderHelper; private final PlaceholderHelper placeholderHelper;
private final SpringValueRegistry springValueRegistry; private final SpringValueRegistry springValueRegistry;
private static Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions = private BeanFactory beanFactory;
LinkedListMultimap.create(); private Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions;
public SpringValueProcessor() { public SpringValueProcessor() {
configUtil = ApolloInjector.getInstance(ConfigUtil.class); configUtil = ApolloInjector.getInstance(ConfigUtil.class);
placeholderHelper = SpringInjector.getInstance(PlaceholderHelper.class); placeholderHelper = SpringInjector.getInstance(PlaceholderHelper.class);
springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class); springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class);
beanName2SpringValueDefinitions = LinkedListMultimap.create();
} }
@Override @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException { throws BeansException {
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() && beanFactory instanceof BeanDefinitionRegistry) {
beanName2SpringValueDefinitions = SpringValueDefinitionProcessor beanName2SpringValueDefinitions = SpringValueDefinitionProcessor
.getBeanName2SpringValueDefinitions(); .getBeanName2SpringValueDefinitions((BeanDefinitionRegistry) beanFactory);
} }
} }
...@@ -82,7 +86,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory ...@@ -82,7 +86,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
for (String key : keys) { for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false); SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
springValueRegistry.register(key, springValue); springValueRegistry.register(beanFactory, key, springValue);
logger.debug("Monitoring {}", springValue); logger.debug("Monitoring {}", springValue);
} }
} }
...@@ -112,7 +116,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory ...@@ -112,7 +116,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
for (String key : keys) { for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method, false); SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method, false);
springValueRegistry.register(key, springValue); springValueRegistry.register(beanFactory, key, springValue);
logger.info("Monitoring {}", springValue); logger.info("Monitoring {}", springValue);
} }
} }
...@@ -135,7 +139,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory ...@@ -135,7 +139,7 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
} }
SpringValue springValue = new SpringValue(definition.getKey(), definition.getPlaceholder(), SpringValue springValue = new SpringValue(definition.getKey(), definition.getPlaceholder(),
bean, beanName, method, false); bean, beanName, method, false);
springValueRegistry.register(definition.getKey(), springValue); springValueRegistry.register(beanFactory, definition.getKey(), springValue);
logger.debug("Monitoring {}", springValue); logger.debug("Monitoring {}", springValue);
} catch (Throwable ex) { } catch (Throwable ex) {
logger.error("Failed to enable auto update feature for {}.{}", bean.getClass(), logger.error("Failed to enable auto update feature for {}.{}", bean.getClass(),
...@@ -147,4 +151,8 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory ...@@ -147,4 +151,8 @@ public class SpringValueProcessor extends ApolloProcessor implements BeanFactory
beanName2SpringValueDefinitions.removeAll(beanName); beanName2SpringValueDefinitions.removeAll(beanName);
} }
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
} }
...@@ -11,9 +11,11 @@ import com.google.common.collect.Multimap; ...@@ -11,9 +11,11 @@ import com.google.common.collect.Multimap;
import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService; import com.ctrip.framework.apollo.ConfigService;
import com.google.common.collect.Sets;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.Set;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware; import org.springframework.context.EnvironmentAware;
...@@ -40,7 +42,7 @@ import org.springframework.core.env.PropertySource; ...@@ -40,7 +42,7 @@ import org.springframework.core.env.PropertySource;
*/ */
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered { public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
private static final Multimap<Integer, String> NAMESPACE_NAMES = LinkedHashMultimap.create(); private static final Multimap<Integer, String> NAMESPACE_NAMES = LinkedHashMultimap.create();
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); private static final Set<BeanFactory> AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES = Sets.newConcurrentHashSet();
private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector
.getInstance(ConfigPropertySourceFactory.class); .getInstance(ConfigPropertySourceFactory.class);
...@@ -53,11 +55,8 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir ...@@ -53,11 +55,8 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
@Override @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (INITIALIZED.compareAndSet(false, true)) { initializePropertySources();
initializePropertySources(); initializeAutoUpdatePropertiesFeature(beanFactory);
initializeAutoUpdatePropertiesFeature(beanFactory);
}
} }
private void initializePropertySources() { private void initializePropertySources() {
...@@ -80,6 +79,9 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir ...@@ -80,6 +79,9 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
} }
} }
// clean up
NAMESPACE_NAMES.clear();
// add after the bootstrap property source or to the first // add after the bootstrap property source or to the first
if (environment.getPropertySources() if (environment.getPropertySources()
.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) { .contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
...@@ -110,7 +112,8 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir ...@@ -110,7 +112,8 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
} }
private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) { private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() ||
!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {
return; return;
} }
...@@ -129,12 +132,6 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir ...@@ -129,12 +132,6 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
this.environment = (ConfigurableEnvironment) environment; this.environment = (ConfigurableEnvironment) environment;
} }
//only for test
private static void reset() {
NAMESPACE_NAMES.clear();
INITIALIZED.set(false);
}
@Override @Override
public int getOrder() { public int getOrder() {
//make it as early as possible //make it as early as possible
......
...@@ -51,7 +51,7 @@ public class AutoUpdateConfigChangeListener implements ConfigChangeListener{ ...@@ -51,7 +51,7 @@ public class AutoUpdateConfigChangeListener implements ConfigChangeListener{
} }
for (String key : keys) { for (String key : keys) {
// 1. check whether the changed key is relevant // 1. check whether the changed key is relevant
Collection<SpringValue> targetValues = springValueRegistry.get(key); Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
if (targetValues == null || targetValues.isEmpty()) { if (targetValues == null || targetValues.isEmpty()) {
continue; continue;
} }
......
package com.ctrip.framework.apollo.spring.property; package com.ctrip.framework.apollo.spring.property;
import com.ctrip.framework.apollo.spring.util.SpringInjector; import com.ctrip.framework.apollo.spring.util.SpringInjector;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValue;
...@@ -30,9 +32,9 @@ import com.google.common.collect.Multimap; ...@@ -30,9 +32,9 @@ import com.google.common.collect.Multimap;
* </pre> * </pre>
*/ */
public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPostProcessor { public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPostProcessor {
private static final Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions = private static final Map<BeanDefinitionRegistry, Multimap<String, SpringValueDefinition>> beanName2SpringValueDefinitions =
LinkedListMultimap.create(); Maps.newConcurrentMap();
private static final AtomicBoolean initialized = new AtomicBoolean(false); private static final Set<BeanDefinitionRegistry> PROPERTY_VALUES_PROCESSED_BEAN_FACTORIES = Sets.newConcurrentHashSet();
private final ConfigUtil configUtil; private final ConfigUtil configUtil;
private final PlaceholderHelper placeholderHelper; private final PlaceholderHelper placeholderHelper;
...@@ -54,16 +56,27 @@ public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPos ...@@ -54,16 +56,27 @@ public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPos
} }
public static Multimap<String, SpringValueDefinition> getBeanName2SpringValueDefinitions() { public static Multimap<String, SpringValueDefinition> getBeanName2SpringValueDefinitions(BeanDefinitionRegistry registry) {
return beanName2SpringValueDefinitions; Multimap<String, SpringValueDefinition> springValueDefinitions = beanName2SpringValueDefinitions.get(registry);
if (springValueDefinitions == null) {
springValueDefinitions = LinkedListMultimap.create();
}
return springValueDefinitions;
} }
private void processPropertyValues(BeanDefinitionRegistry beanRegistry) { private void processPropertyValues(BeanDefinitionRegistry beanRegistry) {
if (!initialized.compareAndSet(false, true)) { if (!PROPERTY_VALUES_PROCESSED_BEAN_FACTORIES.add(beanRegistry)) {
// already initialized // already initialized
return; return;
} }
if (!beanName2SpringValueDefinitions.containsKey(beanRegistry)) {
beanName2SpringValueDefinitions.put(beanRegistry, LinkedListMultimap.<String, SpringValueDefinition>create());
}
Multimap<String, SpringValueDefinition> springValueDefinitions = beanName2SpringValueDefinitions.get(beanRegistry);
String[] beanNames = beanRegistry.getBeanDefinitionNames(); String[] beanNames = beanRegistry.getBeanDefinitionNames();
for (String beanName : beanNames) { for (String beanName : beanNames) {
BeanDefinition beanDefinition = beanRegistry.getBeanDefinition(beanName); BeanDefinition beanDefinition = beanRegistry.getBeanDefinition(beanName);
...@@ -82,16 +95,9 @@ public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPos ...@@ -82,16 +95,9 @@ public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPos
} }
for (String key : keys) { for (String key : keys) {
beanName2SpringValueDefinitions.put(beanName, springValueDefinitions.put(beanName, new SpringValueDefinition(key, placeholder, propertyValue.getName()));
new SpringValueDefinition(key, placeholder, propertyValue.getName()));
} }
} }
} }
} }
//only for test
private static void reset() {
initialized.set(false);
beanName2SpringValueDefinitions.clear();
}
} }
package com.ctrip.framework.apollo.spring.property; package com.ctrip.framework.apollo.spring.property;
import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import org.springframework.beans.factory.BeanFactory;
public class SpringValueRegistry { public class SpringValueRegistry {
private final Multimap<String, SpringValue> registry = LinkedListMultimap.create();
public void register(String key, SpringValue springValue) { private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
registry.put(key, springValue); private final Object LOCK = new Object();
public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
if (!registry.containsKey(beanFactory)) {
synchronized (LOCK) {
if (!registry.containsKey(beanFactory)) {
registry.put(beanFactory, LinkedListMultimap.<String, SpringValue>create());
}
}
}
registry.get(beanFactory).put(key, springValue);
} }
public Collection<SpringValue> get(String key) { public Collection<SpringValue> get(BeanFactory beanFactory, String key) {
return registry.get(key); Multimap<String, SpringValue> beanFactorySpringValues = registry.get(beanFactory);
if (beanFactorySpringValues == null) {
return null;
}
return beanFactorySpringValues.get(key);
} }
} }
...@@ -33,16 +33,10 @@ import com.google.common.collect.Maps; ...@@ -33,16 +33,10 @@ import com.google.common.collect.Maps;
*/ */
public abstract class AbstractSpringIntegrationTest { public abstract class AbstractSpringIntegrationTest {
private static final Map<String, Config> CONFIG_REGISTRY = Maps.newHashMap(); private static final Map<String, Config> CONFIG_REGISTRY = Maps.newHashMap();
private static Method PROPERTY_SOURCES_PROCESSOR_CLEAR;
private static Method SPRING_VALUE_DEFINITION_PROCESS_CLEAR;
private static Method CONFIG_SERVICE_RESET; private static Method CONFIG_SERVICE_RESET;
static { static {
try { try {
PROPERTY_SOURCES_PROCESSOR_CLEAR = PropertySourcesProcessor.class.getDeclaredMethod("reset");
ReflectionUtils.makeAccessible(PROPERTY_SOURCES_PROCESSOR_CLEAR);
SPRING_VALUE_DEFINITION_PROCESS_CLEAR = SpringValueDefinitionProcessor.class.getDeclaredMethod("reset");
ReflectionUtils.makeAccessible(SPRING_VALUE_DEFINITION_PROCESS_CLEAR);
CONFIG_SERVICE_RESET = ConfigService.class.getDeclaredMethod("reset"); CONFIG_SERVICE_RESET = ConfigService.class.getDeclaredMethod("reset");
ReflectionUtils.makeAccessible(CONFIG_SERVICE_RESET); ReflectionUtils.makeAccessible(CONFIG_SERVICE_RESET);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
...@@ -112,10 +106,6 @@ public abstract class AbstractSpringIntegrationTest { ...@@ -112,10 +106,6 @@ public abstract class AbstractSpringIntegrationTest {
} }
protected static void doSetUp() { protected static void doSetUp() {
//as PropertySourcesProcessor has some static states, so we must manually clear its state
ReflectionUtils.invokeMethod(PROPERTY_SOURCES_PROCESSOR_CLEAR, null);
//as SpringValueDefinitionProcessor has some static states, so we must manually clear its state
ReflectionUtils.invokeMethod(SPRING_VALUE_DEFINITION_PROCESS_CLEAR, null);
//as ConfigService is singleton, so we must manually clear its container //as ConfigService is singleton, so we must manually clear its container
ReflectionUtils.invokeMethod(CONFIG_SERVICE_RESET, null); ReflectionUtils.invokeMethod(CONFIG_SERVICE_RESET, null);
MockInjector.reset(); MockInjector.reset();
......
...@@ -37,8 +37,6 @@ public class EmbeddedApollo extends ExternalResource { ...@@ -37,8 +37,6 @@ public class EmbeddedApollo extends ExternalResource {
private static final Type notificationType = new TypeToken<List<ApolloConfigNotification>>() { private static final Type notificationType = new TypeToken<List<ApolloConfigNotification>>() {
}.getType(); }.getType();
private static Method PROPERTY_SOURCES_PROCESSOR_CLEAR;
private static Method SPRING_VALUE_DEFINITION_PROCESS_CLEAR;
private static Method CONFIG_SERVICE_LOCATOR_CLEAR; private static Method CONFIG_SERVICE_LOCATOR_CLEAR;
private static ConfigServiceLocator CONFIG_SERVICE_LOCATOR; private static ConfigServiceLocator CONFIG_SERVICE_LOCATOR;
...@@ -51,10 +49,6 @@ public class EmbeddedApollo extends ExternalResource { ...@@ -51,10 +49,6 @@ public class EmbeddedApollo extends ExternalResource {
static { static {
try { try {
System.setProperty("apollo.longPollingInitialDelayInMills", "0"); System.setProperty("apollo.longPollingInitialDelayInMills", "0");
PROPERTY_SOURCES_PROCESSOR_CLEAR = PropertySourcesProcessor.class.getDeclaredMethod("reset");
PROPERTY_SOURCES_PROCESSOR_CLEAR.setAccessible(true);
SPRING_VALUE_DEFINITION_PROCESS_CLEAR = SpringValueDefinitionProcessor.class.getDeclaredMethod("reset");
SPRING_VALUE_DEFINITION_PROCESS_CLEAR.setAccessible(true);
CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class); CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class);
CONFIG_SERVICE_LOCATOR_CLEAR = ConfigServiceLocator.class.getDeclaredMethod("initConfigServices"); CONFIG_SERVICE_LOCATOR_CLEAR = ConfigServiceLocator.class.getDeclaredMethod("initConfigServices");
CONFIG_SERVICE_LOCATOR_CLEAR.setAccessible(true); CONFIG_SERVICE_LOCATOR_CLEAR.setAccessible(true);
...@@ -105,9 +99,6 @@ public class EmbeddedApollo extends ExternalResource { ...@@ -105,9 +99,6 @@ public class EmbeddedApollo extends ExternalResource {
private void clear() throws Exception { private void clear() throws Exception {
resetOverriddenProperties(); resetOverriddenProperties();
// clear Apollo states
PROPERTY_SOURCES_PROCESSOR_CLEAR.invoke(null);
SPRING_VALUE_DEFINITION_PROCESS_CLEAR.invoke(null);
} }
private void mockConfigServiceUrl(String url) throws Exception { private void mockConfigServiceUrl(String url) throws Exception {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment