提交 f435f27d 编写于 作者: J Jason Song

Add config file controller to support get config as plain properties file

上级 f5bc9f83
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
......@@ -4,7 +4,7 @@
<parent>
<artifactId>apollo</artifactId>
<groupId>com.ctrip.framework.apollo</groupId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>apollo-biz</artifactId>
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
package com.ctrip.framework.apollo.internals;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.core.utils.PropertiesUtil;
import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
import com.ctrip.framework.apollo.util.ExceptionUtil;
import com.dianping.cat.Cat;
......@@ -8,8 +9,6 @@ import com.dianping.cat.Cat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
......@@ -38,13 +37,10 @@ public class PropertiesConfigFile extends AbstractConfigFile {
if (m_configProperties.get() == null) {
return null;
}
StringWriter writer = new StringWriter();
try {
m_configProperties.get().store(writer, null);
StringBuffer stringBuffer = writer.getBuffer();
filterPropertiesComment(stringBuffer);
return stringBuffer.toString();
} catch (IOException ex) {
return PropertiesUtil.toString(m_configProperties.get());
} catch (Throwable ex) {
ApolloConfigException exception =
new ApolloConfigException(String
.format("Parse properties file content failed for namespace: %s, cause: %s",
......@@ -54,25 +50,6 @@ public class PropertiesConfigFile extends AbstractConfigFile {
}
}
/**
* filter out the first comment line
* @param stringBuffer the string buffer
* @return true if filtered successfully, false otherwise
*/
boolean filterPropertiesComment(StringBuffer stringBuffer) {
//check whether has comment in the first line
if (stringBuffer.charAt(0) != '#') {
return false;
}
int commentLineIndex = stringBuffer.indexOf("\n");
if (commentLineIndex == -1) {
return false;
}
stringBuffer.delete(0, commentLineIndex + 1);
return true;
}
@Override
public boolean hasContent() {
return m_configProperties.get() != null && !m_configProperties.get().isEmpty();
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
package com.ctrip.framework.apollo.configservice;
import com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner;
import com.ctrip.framework.apollo.configservice.controller.ConfigFileController;
import com.ctrip.framework.apollo.configservice.controller.NotificationController;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -14,10 +15,14 @@ import org.springframework.context.annotation.Configuration;
public class ConfigServiceAutoConfiguration {
@Autowired
private NotificationController notificationController;
@Autowired
private ConfigFileController configFileController;
@Bean
public ReleaseMessageScanner releaseMessageScanner() {
ReleaseMessageScanner releaseMessageScanner = new ReleaseMessageScanner();
//handle server cache first
releaseMessageScanner.addMessageListener(configFileController);
releaseMessageScanner.addMessageListener(notificationController);
return releaseMessageScanner;
}
......
package com.ctrip.framework.apollo.configservice.controller;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.cache.Weigher;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener;
import com.ctrip.framework.apollo.biz.message.Topics;
import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
import com.ctrip.framework.apollo.core.utils.PropertiesUtil;
import com.dianping.cat.Cat;
import org.hibernate.cache.spi.CacheKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletResponse;
/**
* @author Jason Song(song_s@ctrip.com)
*/
@RestController
@RequestMapping("/configfiles")
public class ConfigFileController implements ReleaseMessageListener{
private static final Logger logger = LoggerFactory.getLogger(ConfigFileController.class);
private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
private static final long MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
private static final long EXPIRE_AFTER_WRITE = 10;
private final HttpHeaders responseHeaders;
private final ResponseEntity<String> NOT_FOUND_RESPONSE;
private Cache<String, String> localCache;
private final Multimap<String, String>
watchedKeys2CacheKey = Multimaps.synchronizedSetMultimap(HashMultimap.create());
private final Multimap<String, String>
cacheKey2WatchedKeys = Multimaps.synchronizedSetMultimap(HashMultimap.create());
@Autowired
private ConfigController configController;
@Autowired
private NamespaceUtil namespaceUtil;
@Autowired
private WatchKeysUtil watchKeysUtil;
public ConfigFileController() {
localCache = CacheBuilder.newBuilder()
.expireAfterWrite(EXPIRE_AFTER_WRITE, TimeUnit.MINUTES)
.weigher(new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return value == null ? 0 : value.length();
}
})
.maximumWeight(MAX_CACHE_SIZE)
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> notification) {
String cacheKey = notification.getKey();
logger.debug("removing cache key: {}", cacheKey);
if (!cacheKey2WatchedKeys.containsKey(cacheKey)) {
return;
}
//create a new list to avoid ConcurrentModificationException
List<String> watchedKeys = new ArrayList<>(cacheKey2WatchedKeys.get(cacheKey));
for (String watchedKey : watchedKeys) {
watchedKeys2CacheKey.remove(watchedKey, cacheKey);
}
cacheKey2WatchedKeys.removeAll(cacheKey);
logger.debug("removed cache key: {}", cacheKey);
}
})
.build();
responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "text/plain;charset=UTF-8");
NOT_FOUND_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
@RequestMapping(value = "/{appId}/{clusterName}/{namespace:.+}", method = RequestMethod.GET)
public ResponseEntity<String> queryConfigAsFile(@PathVariable String appId,
@PathVariable String clusterName,
@PathVariable String namespace,
@RequestParam(value = "dataCenter", required = false) String dataCenter,
@RequestParam(value = "ip", required = false) String clientIp,
HttpServletResponse response) throws IOException {
//strip out .properties suffix
namespace = namespaceUtil.filterNamespaceName(namespace);
//TODO add clientIp as key parts?
String cacheKey = assembleCacheKey(appId, clusterName, namespace, dataCenter);
String result = localCache.getIfPresent(cacheKey);
if (Strings.isNullOrEmpty(result)) {
ApolloConfig apolloConfig =
configController
.queryConfig(appId, clusterName, namespace, dataCenter, "-1", clientIp,
response);
if (apolloConfig == null || apolloConfig.getConfigurations() == null) {
return NOT_FOUND_RESPONSE;
}
Properties properties = new Properties();
properties.putAll(apolloConfig.getConfigurations());
result = PropertiesUtil.toString(properties);
localCache.put(cacheKey, result);
logger.debug("adding cache for key: {}", cacheKey);
Set<String> watchedKeys =
watchKeysUtil.assembleAllWatchKeys(appId, clusterName, namespace, dataCenter);
for (String watchedKey : watchedKeys) {
watchedKeys2CacheKey.put(watchedKey, cacheKey);
}
cacheKey2WatchedKeys.putAll(cacheKey, watchedKeys);
logger.debug("added cache for key: {}", cacheKey);
}
return new ResponseEntity<>(result, responseHeaders,
HttpStatus.OK);
}
String assembleCacheKey(String appId, String clusterName, String namespace,
String dataCenter) {
List<String> keyParts = Lists.newArrayList(appId, clusterName, namespace);
if (!Strings.isNullOrEmpty(dataCenter)) {
keyParts.add(dataCenter);
}
return STRING_JOINER.join(keyParts);
}
@Override
public void handleMessage(ReleaseMessage message, String channel) {
logger.info("message received - channel: {}, message: {}", channel, message);
String content = message.getMessage();
if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
return;
}
if (!watchedKeys2CacheKey.containsKey(content)) {
return;
}
//create a new list to avoid ConcurrentModificationException
List<String> cacheKeys = new ArrayList<>(watchedKeys2CacheKey.get(content));
for (String cacheKey : cacheKeys) {
logger.debug("invalidate cache key: {}", cacheKey);
localCache.invalidate(cacheKey);
}
}
}
package com.ctrip.framework.apollo.configservice.controller;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.ctrip.framework.apollo.common.entity.AppNamespace;
import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener;
import com.ctrip.framework.apollo.biz.message.Topics;
import com.ctrip.framework.apollo.biz.service.AppNamespaceService;
import com.ctrip.framework.apollo.biz.service.ReleaseMessageService;
import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil;
import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
import com.dianping.cat.Cat;
......@@ -33,7 +30,6 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
......@@ -48,12 +44,11 @@ public class NotificationController implements ReleaseMessageListener {
deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create());
private static final ResponseEntity<ApolloConfigNotification>
NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
private static final Splitter STRING_SPLITTER =
Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings();
@Autowired
private AppNamespaceService appNamespaceService;
private WatchKeysUtil watchKeysUtil;
@Autowired
private ReleaseMessageService releaseMessageService;
......@@ -75,12 +70,7 @@ public class NotificationController implements ReleaseMessageListener {
//strip out .properties suffix
namespace = namespaceUtil.filterNamespaceName(namespace);
Set<String> watchedKeys = assembleWatchKeys(appId, cluster, namespace, dataCenter);
//Listen on more namespaces if it's a public namespace
if (!namespaceBelongsToAppId(appId, namespace)) {
watchedKeys.addAll(this.findPublicConfigWatchKey(appId, cluster, namespace, dataCenter));
}
Set<String> watchedKeys = watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespace, dataCenter);
DeferredResult<ResponseEntity<ApolloConfigNotification>> deferredResult =
new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE);
......@@ -117,52 +107,13 @@ public class NotificationController implements ReleaseMessageListener {
});
logWatchedKeysToCat(watchedKeys, "Apollo.LongPoll.RegisteredKeys");
logger.info("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
watchedKeys, appId, cluster, namespace, dataCenter);
}
return deferredResult;
}
private String assembleKey(String appId, String cluster, String namespace) {
return STRING_JOINER.join(appId, cluster, namespace);
}
private Set<String> findPublicConfigWatchKey(String applicationId, String clusterName,
String namespace,
String dataCenter) {
AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);
//check whether the namespace's appId equals to current one
if (Objects.isNull(appNamespace) || Objects.equals(applicationId, appNamespace.getAppId())) {
return Sets.newHashSet();
}
String publicConfigAppId = appNamespace.getAppId();
return assembleWatchKeys(publicConfigAppId, clusterName, namespace, dataCenter);
}
private Set<String> assembleWatchKeys(String appId, String clusterName, String namespace,
String dataCenter) {
Set<String> watchedKeys = Sets.newHashSet();
//watch specified cluster config change
if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, clusterName)) {
watchedKeys.add(assembleKey(appId, clusterName, namespace));
}
//watch data center config change
if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, clusterName)) {
watchedKeys.add(assembleKey(appId, dataCenter, namespace));
}
//watch default cluster config change
watchedKeys.add(assembleKey(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace));
return watchedKeys;
}
@Override
public void handleMessage(ReleaseMessage message, String channel) {
logger.info("message received - channel: {}, message: {}", channel, message);
......@@ -183,26 +134,18 @@ public class NotificationController implements ReleaseMessageListener {
new ResponseEntity<>(
new ApolloConfigNotification(keys.get(2), message.getId()), HttpStatus.OK);
if (!deferredResults.containsKey(content)) {
return;
}
//create a new list to avoid ConcurrentModificationException
List<DeferredResult<ResponseEntity<ApolloConfigNotification>>> results =
Lists.newArrayList(deferredResults.get(content));
logger.info("Notify {} clients for key {}", results.size(), content);
logger.debug("Notify {} clients for key {}", results.size(), content);
for (DeferredResult<ResponseEntity<ApolloConfigNotification>> result : results) {
result.setResult(notification);
}
logger.info("Notification completed");
}
private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
//Every app has an 'application' namespace
if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
return true;
}
AppNamespace appNamespace = appNamespaceService.findOne(appId, namespaceName);
return appNamespace != null;
logger.debug("Notification completed");
}
private void logWatchedKeysToCat(Set<String> watchedKeys, String eventName) {
......
package com.ctrip.framework.apollo.configservice.util;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.ctrip.framework.apollo.biz.service.AppNamespaceService;
import com.ctrip.framework.apollo.common.entity.AppNamespace;
import com.ctrip.framework.apollo.core.ConfigConsts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.Set;
/**
* @author Jason Song(song_s@ctrip.com)
*/
@Component
public class WatchKeysUtil {
private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
@Autowired
private AppNamespaceService appNamespaceService;
public Set<String> assembleAllWatchKeys(String appId, String clusterName, String namespace,
String dataCenter) {
Set<String> watchedKeys = assembleWatchKeys(appId, clusterName, namespace, dataCenter);
//Listen on more namespaces if it's a public namespace
if (!namespaceBelongsToAppId(appId, namespace)) {
watchedKeys.addAll(this.findPublicConfigWatchKey(appId, clusterName, namespace, dataCenter));
}
return watchedKeys;
}
private Set<String> findPublicConfigWatchKey(String applicationId, String clusterName,
String namespace,
String dataCenter) {
AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);
//check whether the namespace's appId equals to current one
if (Objects.isNull(appNamespace) || Objects.equals(applicationId, appNamespace.getAppId())) {
return Sets.newHashSet();
}
String publicConfigAppId = appNamespace.getAppId();
return assembleWatchKeys(publicConfigAppId, clusterName, namespace, dataCenter);
}
private String assembleKey(String appId, String cluster, String namespace) {
return STRING_JOINER.join(appId, cluster, namespace);
}
private Set<String> assembleWatchKeys(String appId, String clusterName, String namespace,
String dataCenter) {
Set<String> watchedKeys = Sets.newHashSet();
//watch specified cluster config change
if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, clusterName)) {
watchedKeys.add(assembleKey(appId, clusterName, namespace));
}
//watch data center config change
if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, clusterName)) {
watchedKeys.add(assembleKey(appId, dataCenter, namespace));
}
//watch default cluster config change
watchedKeys.add(assembleKey(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace));
return watchedKeys;
}
private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
//Every app has an 'application' namespace
if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
return true;
}
AppNamespace appNamespace = appNamespaceService.findOne(appId, namespaceName);
return appNamespace != null;
}
}
package com.ctrip.framework.apollo.configservice;
import com.ctrip.framework.apollo.configservice.controller.ConfigControllerTest;
import com.ctrip.framework.apollo.configservice.controller.ConfigFileControllerTest;
import com.ctrip.framework.apollo.configservice.controller.NotificationControllerTest;
import com.ctrip.framework.apollo.configservice.integration.ConfigControllerIntegrationTest;
import com.ctrip.framework.apollo.configservice.integration.ConfigFileControllerIntegrationTest;
import com.ctrip.framework.apollo.configservice.integration.NotificationControllerIntegrationTest;
import com.ctrip.framework.apollo.configservice.util.NamespaceUtilTest;
......@@ -13,7 +15,8 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({ConfigControllerTest.class, NotificationControllerTest.class,
ConfigControllerIntegrationTest.class, NotificationControllerIntegrationTest.class,
NamespaceUtilTest.class})
NamespaceUtilTest.class, ConfigFileControllerTest.class,
ConfigFileControllerIntegrationTest.class})
public class AllTests {
}
package com.ctrip.framework.apollo.configservice.controller;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
import com.ctrip.framework.apollo.biz.message.Topics;
import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Jason Song(song_s@ctrip.com)
*/
@RunWith(MockitoJUnitRunner.class)
public class ConfigFileControllerTest {
@Mock
private ConfigController configController;
@Mock
private WatchKeysUtil watchKeysUtil;
@Mock
private NamespaceUtil namespaceUtil;
private ConfigFileController configFileController;
private String someAppId;
private String someClusterName;
private String someNamespace;
private String someDataCenter;
private String someClientIp;
@Mock
private HttpServletResponse someResponse;
Multimap<String, String> watchedKeys2CacheKey;
Multimap<String, String> cacheKey2WatchedKeys;
@Before
public void setUp() throws Exception {
configFileController = new ConfigFileController();
ReflectionTestUtils.setField(configFileController, "configController", configController);
ReflectionTestUtils.setField(configFileController, "watchKeysUtil", watchKeysUtil);
ReflectionTestUtils.setField(configFileController, "namespaceUtil", namespaceUtil);
someAppId = "someAppId";
someClusterName = "someClusterName";
someNamespace = "someNamespace";
someDataCenter = "someDataCenter";
someClientIp = "10.1.1.1";
when(namespaceUtil.filterNamespaceName(someNamespace)).thenReturn(someNamespace);
watchedKeys2CacheKey =
(Multimap<String, String>) ReflectionTestUtils
.getField(configFileController, "watchedKeys2CacheKey");
cacheKey2WatchedKeys =
(Multimap<String, String>) ReflectionTestUtils
.getField(configFileController, "cacheKey2WatchedKeys");
}
@Test
public void testQueryConfigAsFile() throws Exception {
String someKey = "someKey";
String someValue = "someValue";
String anotherKey = "anotherKey";
String anotherValue = "anotherValue";
String someWatchKey = "someWatchKey";
String anotherWatchKey = "anotherWatchKey";
Set<String> watchKeys = Sets.newHashSet(someWatchKey, anotherWatchKey);
String cacheKey =
configFileController
.assembleCacheKey(someAppId, someClusterName, someNamespace, someDataCenter);
Map<String, String> configurations =
ImmutableMap.of(someKey, someValue, anotherKey, anotherValue);
ApolloConfig someApolloConfig = mock(ApolloConfig.class);
when(someApolloConfig.getConfigurations()).thenReturn(configurations);
when(configController
.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp,
someResponse)).thenReturn(someApolloConfig);
when(watchKeysUtil
.assembleAllWatchKeys(someAppId, someClusterName, someNamespace, someDataCenter))
.thenReturn(watchKeys);
ResponseEntity<String> response =
configFileController
.queryConfigAsFile(someAppId, someClusterName, someNamespace, someDataCenter,
someClientIp, someResponse);
assertEquals(2, watchedKeys2CacheKey.size());
assertEquals(2, cacheKey2WatchedKeys.size());
assertTrue(watchedKeys2CacheKey.containsEntry(someWatchKey, cacheKey));
assertTrue(watchedKeys2CacheKey.containsEntry(anotherWatchKey, cacheKey));
assertTrue(cacheKey2WatchedKeys.containsEntry(cacheKey, someWatchKey));
assertTrue(cacheKey2WatchedKeys.containsEntry(cacheKey, anotherWatchKey));
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(response.getBody().contains(String.format("%s=%s", someKey, someValue)));
assertTrue(response.getBody().contains(String.format("%s=%s", anotherKey, anotherValue)));
ResponseEntity<String> anotherResponse =
configFileController
.queryConfigAsFile(someAppId, someClusterName, someNamespace, someDataCenter,
someClientIp, someResponse);
assertEquals(response, anotherResponse);
verify(configController, times(1))
.queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp,
someResponse);
}
@Test
public void testHandleMessage() throws Exception {
String someWatchKey = "someWatchKey";
String anotherWatchKey = "anotherWatchKey";
String someCacheKey = "someCacheKey";
String anotherCacheKey = "anotherCacheKey";
String someValue = "someValue";
ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class);
when(someReleaseMessage.getMessage()).thenReturn(someWatchKey);
Cache<String, String> cache =
(Cache<String, String>) ReflectionTestUtils.getField(configFileController, "localCache");
cache.put(someCacheKey, someValue);
cache.put(anotherCacheKey, someValue);
watchedKeys2CacheKey.putAll(someWatchKey, Lists.newArrayList(someCacheKey, anotherCacheKey));
watchedKeys2CacheKey.putAll(anotherWatchKey, Lists.newArrayList(someCacheKey, anotherCacheKey));
cacheKey2WatchedKeys.putAll(someCacheKey, Lists.newArrayList(someWatchKey, anotherWatchKey));
cacheKey2WatchedKeys.putAll(anotherCacheKey, Lists.newArrayList(someWatchKey, anotherWatchKey));
configFileController.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC);
assertTrue(watchedKeys2CacheKey.isEmpty());
assertTrue(cacheKey2WatchedKeys.isEmpty());
}
}
......@@ -11,6 +11,7 @@ import com.ctrip.framework.apollo.biz.service.ReleaseMessageService;
import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil;
import com.ctrip.framework.apollo.common.entity.AppNamespace;
import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
......@@ -61,10 +62,13 @@ public class NotificationControllerTest {
@Before
public void setUp() throws Exception {
controller = new NotificationController();
ReflectionTestUtils.setField(controller, "appNamespaceService", appNamespaceService);
ReflectionTestUtils.setField(controller, "releaseMessageService", releaseMessageService);
ReflectionTestUtils.setField(controller, "entityManagerUtil", entityManagerUtil);
ReflectionTestUtils.setField(controller, "namespaceUtil", namespaceUtil);
WatchKeysUtil watchKeysUtil = new WatchKeysUtil();
ReflectionTestUtils.setField(watchKeysUtil, "appNamespaceService", appNamespaceService);
ReflectionTestUtils.setField(controller, "watchKeysUtil", watchKeysUtil);
someAppId = "someAppId";
someCluster = "someCluster";
......
package com.ctrip.framework.apollo.configservice.integration;
import com.google.gson.Gson;
import com.ctrip.framework.apollo.ConfigServiceTestConfiguration;
import com.ctrip.framework.apollo.biz.entity.Namespace;
import com.ctrip.framework.apollo.biz.entity.Release;
import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository;
import com.ctrip.framework.apollo.biz.repository.ReleaseRepository;
import com.ctrip.framework.apollo.biz.utils.ReleaseKeyGenerator;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
......@@ -13,6 +22,9 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
import java.util.Map;
import javax.annotation.PostConstruct;
/**
......@@ -22,6 +34,12 @@ import javax.annotation.PostConstruct;
@SpringApplicationConfiguration(classes = AbstractBaseIntegrationTest.TestConfiguration.class)
@WebIntegrationTest(randomPort = true)
public abstract class AbstractBaseIntegrationTest {
@Autowired
private ReleaseMessageRepository releaseMessageRepository;
@Autowired
private ReleaseRepository releaseRepository;
private Gson gson = new Gson();
RestTemplate restTemplate = new TestRestTemplate("user", "");
......@@ -40,8 +58,29 @@ public abstract class AbstractBaseIntegrationTest {
@Configuration
@Import(ConfigServiceTestConfiguration.class)
protected static class TestConfiguration {
}
protected void sendReleaseMessage(String message) {
ReleaseMessage releaseMessage = new ReleaseMessage(message);
releaseMessageRepository.save(releaseMessage);
}
public Release buildRelease(String name, String comment, Namespace namespace,
Map<String, String> configurations, String owner) {
Release release = new Release();
release.setReleaseKey(ReleaseKeyGenerator.generateReleaseKey(namespace));
release.setDataChangeCreatedTime(new Date());
release.setDataChangeCreatedBy(owner);
release.setDataChangeLastModifiedBy(owner);
release.setName(name);
release.setComment(comment);
release.setAppId(namespace.getAppId());
release.setClusterName(namespace.getClusterName());
release.setNamespaceName(namespace.getNamespaceName());
release.setConfigurations(gson.toJson(configurations));
release = releaseRepository.save(release);
return release;
}
}
package com.ctrip.framework.apollo.configservice.integration;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.ctrip.framework.apollo.biz.entity.Namespace;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.netflix.servo.util.Strings;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.jdbc.Sql;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class ConfigFileControllerIntegrationTest extends AbstractBaseIntegrationTest {
private String someAppId;
private String someCluster;
private String someNamespace;
private String somePublicNamespace;
private String someDC;
private String someDefaultCluster;
@Before
public void setUp() throws Exception {
someDefaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT;
someAppId = "someAppId";
someCluster = "someCluster";
someNamespace = "someNamespace";
somePublicNamespace = "somePublicNamespace";
someDC = "someDC";
}
@Test
@Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testQueryConfigAsFile() throws Exception {
ResponseEntity<String> response =
restTemplate
.getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
getHostUrl(), someAppId, someCluster, someNamespace);
String result = response.getBody();
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(result.contains("k2=v2"));
}
@Test
@Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testQueryPublicConfigAsFile() throws Exception {
ResponseEntity<String> response =
restTemplate
.getForEntity(
"{baseurl}/configfiles/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}",
String.class,
getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDC);
String result = response.getBody();
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(result.contains("k1=override-someDC-v1"));
assertTrue(result.contains("k2=someDC-v2"));
}
@Test
@Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testConfigChanged() throws Exception {
ResponseEntity<String> response =
restTemplate
.getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
getHostUrl(), someAppId, someCluster, someNamespace);
String result = response.getBody();
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(result.contains("k2=v2"));
String someReleaseName = "someReleaseName";
String someReleaseComment = "someReleaseComment";
Namespace namespace = new Namespace();
namespace.setAppId(someAppId);
namespace.setClusterName(someCluster);
namespace.setNamespaceName(someNamespace);
String someOwner = "someOwner";
Map<String, String> newConfigurations = ImmutableMap.of("k1", "v1-changed", "k2", "v2-changed");
buildRelease(someReleaseName, someReleaseComment, namespace, newConfigurations, someOwner);
ResponseEntity<String> anotherResponse =
restTemplate
.getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
getHostUrl(), someAppId, someCluster, someNamespace);
assertEquals(response.getBody(), anotherResponse.getBody());
List<String> keys = Lists.newArrayList(someAppId, someCluster, someNamespace);
String message = Strings.join(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR, keys.iterator());
sendReleaseMessage(message);
TimeUnit.MILLISECONDS.sleep(500);
ResponseEntity<String> newResponse =
restTemplate
.getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
getHostUrl(), someAppId, someCluster, someNamespace);
result = newResponse.getBody();
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(result.contains("k1=v1-changed"));
assertTrue(result.contains("k2=v2-changed"));
}
}
......@@ -29,8 +29,7 @@ import static org.junit.Assert.assertNotEquals;
public class NotificationControllerIntegrationTest extends AbstractBaseIntegrationTest {
@Autowired
private NotificationController notificationController;
@Autowired
private ReleaseMessageRepository releaseMessageRepository;
private String someAppId;
private String someCluster;
private String defaultNamespace;
......@@ -244,8 +243,7 @@ public class NotificationControllerIntegrationTest extends AbstractBaseIntegrati
break;
}
ReleaseMessage releaseMessage = new ReleaseMessage(message);
releaseMessageRepository.save(releaseMessage);
sendReleaseMessage(message);
}
});
}
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
package com.ctrip.framework.apollo.core.utils;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class PropertiesUtil {
/**
* Transform the properties to string format
* @param properties the properties object
* @return the string containing the properties
* @throws IOException
*/
public static String toString(Properties properties) throws IOException {
StringWriter writer = new StringWriter();
properties.store(writer, null);
StringBuffer stringBuffer = writer.getBuffer();
filterPropertiesComment(stringBuffer);
return stringBuffer.toString();
}
/**
* filter out the first comment line
* @param stringBuffer the string buffer
* @return true if filtered successfully, false otherwise
*/
static boolean filterPropertiesComment(StringBuffer stringBuffer) {
//check whether has comment in the first line
if (stringBuffer.charAt(0) != '#') {
return false;
}
int commentLineIndex = stringBuffer.indexOf("\n");
if (commentLineIndex == -1) {
return false;
}
stringBuffer.delete(0, commentLineIndex + 1);
return true;
}
}
......@@ -4,7 +4,7 @@
<parent>
<artifactId>apollo</artifactId>
<groupId>com.ctrip.framework.apollo</groupId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>apollo-demo</artifactId>
......
......@@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
......@@ -18,7 +20,7 @@ public class ApolloConfigFileDemo {
private String namespace = "application";
public ApolloConfigFileDemo() {
configFile = ConfigService.getConfigFile(namespace, ConfigFileFormat.XML);
configFile = ConfigService.getConfigFile(namespace, ConfigFileFormat.Properties);
}
private void print() {
......
......@@ -4,7 +4,7 @@
<parent>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
......
......@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo</artifactId>
<version>0.0.5</version>
<version>0.0.6-SNAPSHOT</version>
<name>Apollo</name>
<packaging>pom</packaging>
<description>Ctrip Configuration Center</description>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册