Bladeren bron

add public config support

Jason Song 9 jaren geleden
bovenliggende
commit
a76372f073
19 gewijzigde bestanden met toevoegingen van 596 en 114 verwijderingen
  1. 1 0
      .gitignore
  2. 13 2
      apollo-biz/src/main/java/com/ctrip/apollo/biz/message/RedisMessageSender.java
  3. 2 0
      apollo-biz/src/main/java/com/ctrip/apollo/biz/repository/AppNamespaceRepository.java
  4. 7 0
      apollo-biz/src/main/java/com/ctrip/apollo/biz/service/AppNamespaceService.java
  5. 25 11
      apollo-client/src/main/java/com/ctrip/apollo/internals/RemoteConfigRepository.java
  6. 2 2
      apollo-client/src/main/java/com/ctrip/apollo/model/ConfigChangeEvent.java
  7. 8 0
      apollo-client/src/main/java/com/ctrip/apollo/util/ConfigUtil.java
  8. 7 0
      apollo-client/src/test/java/com/ctrip/apollo/BaseIntegrationTest.java
  9. 1 2
      apollo-client/src/test/java/com/ctrip/apollo/integration/ConfigIntegrationTest.java
  10. 5 0
      apollo-client/src/test/java/com/ctrip/apollo/internals/RemoteConfigRepositoryTest.java
  11. 43 11
      apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/ConfigController.java
  12. 35 10
      apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/NotificationController.java
  13. 177 30
      apollo-configservice/src/test/java/com/ctrip/apollo/configservice/controller/ConfigControllerTest.java
  14. 82 13
      apollo-configservice/src/test/java/com/ctrip/apollo/configservice/controller/NotificationControllerTest.java
  15. 76 0
      apollo-configservice/src/test/java/com/ctrip/apollo/configservice/integration/ConfigControllerIntegrationTest.java
  16. 80 8
      apollo-configservice/src/test/java/com/ctrip/apollo/configservice/integration/NotificationControllerIntegrationTest.java
  17. 20 4
      apollo-configservice/src/test/resources/integration-test/test-release.sql
  18. 1 21
      apollo-core/src/main/java/com/ctrip/apollo/core/dto/ApolloConfigNotification.java
  19. 11 0
      apollo-demo/Demo.md

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 *.class
+.DS_Store
 
 # Mobile Tools for Java (J2ME)
 .mtj.tmp/

+ 13 - 2
apollo-biz/src/main/java/com/ctrip/apollo/biz/message/RedisMessageSender.java

@@ -1,11 +1,18 @@
 package com.ctrip.apollo.biz.message;
 
+import com.dianping.cat.Cat;
+import com.dianping.cat.message.Message;
+import com.dianping.cat.message.Transaction;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
  */
 public class RedisMessageSender implements MessageSender {
+  private static final Logger logger = LoggerFactory.getLogger(RedisMessageSender.class);
   private RedisTemplate<String, String> redisTemplate;
 
   public RedisMessageSender(
@@ -15,12 +22,16 @@ public class RedisMessageSender implements MessageSender {
 
   @Override
   public void sendMessage(String message, String channel) {
+    logger.info("Sending message {} to channel {}", message, channel);
+    Transaction transaction = Cat.newTransaction("Apollo.AdminService", "RedisMessageSender");
     try {
       redisTemplate.convertAndSend(channel, message);
+      transaction.setStatus(Message.SUCCESS);
     } catch (Throwable ex) {
-
+      logger.error("Sending message to redis failed", ex);
+      transaction.setStatus(ex);
     } finally {
-
+      transaction.complete();
     }
   }
 }

+ 2 - 0
apollo-biz/src/main/java/com/ctrip/apollo/biz/repository/AppNamespaceRepository.java

@@ -8,4 +8,6 @@ public interface AppNamespaceRepository extends PagingAndSortingRepository<AppNa
 
   AppNamespace findByAppIdAndName(String appId, String namespaceName);
 
+  AppNamespace findByName(String namespaceName);
+
 }

+ 7 - 0
apollo-biz/src/main/java/com/ctrip/apollo/biz/service/AppNamespaceService.java

@@ -1,5 +1,7 @@
 package com.ctrip.apollo.biz.service;
 
+import com.google.common.base.Preconditions;
+
 import java.util.Objects;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -27,6 +29,11 @@ public class AppNamespaceService {
     return Objects.isNull(appNamespaceRepository.findByAppIdAndName(appId, namespaceName));
   }
 
+  public AppNamespace findByNamespaceName(String namespaceName) {
+    Preconditions.checkArgument(namespaceName != null, "Namespace must not be null");
+    return appNamespaceRepository.findByName(namespaceName);
+  }
+
   @Transactional
   public void createDefaultAppNamespace(String appId, String createBy) {
     if (!isAppNamespaceNameUnique(appId, appId)) {

+ 25 - 11
apollo-client/src/main/java/com/ctrip/apollo/internals/RemoteConfigRepository.java

@@ -134,6 +134,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
   private ApolloConfig loadApolloConfig() {
     String appId = m_configUtil.getAppId();
     String cluster = m_configUtil.getCluster();
+    String dataCenter = m_configUtil.getDataCenter();
     Cat.logEvent("Apollo.Client.ConfigInfo",
         String.format("%s-%s-%s", appId, cluster, m_namespace));
     int maxRetries = 2;
@@ -147,7 +148,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
       for (ServiceDTO configService : randomConfigServices) {
         String url =
             assembleQueryConfigUrl(configService.getHomepageUrl(), appId, cluster, m_namespace,
-                m_configCache.get());
+                dataCenter, m_configCache.get());
 
         logger.debug("Loading config from {}", url);
         HttpRequest request = new HttpRequest(url);
@@ -191,21 +192,30 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
   }
 
   private String assembleQueryConfigUrl(String uri, String appId, String cluster, String namespace,
-                                        ApolloConfig previousConfig) {
+                                        String dataCenter, ApolloConfig previousConfig) {
     Escaper escaper = UrlEscapers.urlPathSegmentEscaper();
     String path = "configs/%s/%s";
-    List<String> params = Lists.newArrayList(escaper.escape(appId), escaper.escape(cluster));
+    List<String> pathParams = Lists.newArrayList(escaper.escape(appId), escaper.escape(cluster));
+    Map<String, String> queryParams = Maps.newHashMap();
 
     if (!Strings.isNullOrEmpty(namespace)) {
       path = path + "/%s";
-      params.add(escaper.escape(namespace));
+      pathParams.add(escaper.escape(namespace));
     }
+
     if (previousConfig != null) {
-      path = path + "?releaseId=%s";
-      params.add(escaper.escape(String.valueOf(previousConfig.getReleaseId())));
+      queryParams.put("releaseId", escaper.escape(String.valueOf(previousConfig.getReleaseId())));
+    }
+
+    if (!Strings.isNullOrEmpty(dataCenter)) {
+      queryParams.put("dataCenter", escaper.escape(dataCenter));
     }
 
-    String pathExpanded = String.format(path, params.toArray());
+    String pathExpanded = String.format(path, pathParams.toArray());
+
+    if (!queryParams.isEmpty()) {
+      pathExpanded += "?" + Joiner.on("&").withKeyValueSeparator("=").join(queryParams);
+    }
     if (!uri.endsWith("/")) {
       uri += "/";
     }
@@ -215,18 +225,19 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
   private void scheduleLongPollingRefresh() {
     final String appId = m_configUtil.getAppId();
     final String cluster = m_configUtil.getCluster();
+    final String dataCenter = m_configUtil.getDataCenter();
     final ExecutorService longPollingService =
         Executors.newFixedThreadPool(2,
             ApolloThreadFactory.create("RemoteConfigRepository-LongPolling", true));
     longPollingService.submit(new Runnable() {
       @Override
       public void run() {
-        doLongPollingRefresh(appId, cluster, longPollingService);
+        doLongPollingRefresh(appId, cluster, dataCenter, longPollingService);
       }
     });
   }
 
-  private void doLongPollingRefresh(String appId, String cluster,
+  private void doLongPollingRefresh(String appId, String cluster, String dataCenter,
                                     ExecutorService longPollingService) {
     final Random random = new Random();
     ServiceDTO lastServiceDto = null;
@@ -240,7 +251,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
 
         String url =
             assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster,
-                m_namespace, m_configCache.get());
+                m_namespace, dataCenter, m_configCache.get());
 
         logger.debug("Long polling from {}", url);
         HttpRequest request = new HttpRequest(url);
@@ -286,7 +297,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
   }
 
   private String assembleLongPollRefreshUrl(String uri, String appId, String cluster,
-                                            String namespace,
+                                            String namespace, String dataCenter,
                                             ApolloConfig previousConfig) {
     Escaper escaper = UrlEscapers.urlPathSegmentEscaper();
     Map<String, String> queryParams = Maps.newHashMap();
@@ -296,6 +307,9 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
     if (!Strings.isNullOrEmpty(namespace)) {
       queryParams.put("namespace", escaper.escape(namespace));
     }
+    if (!Strings.isNullOrEmpty(dataCenter)) {
+      queryParams.put("dataCenter", escaper.escape(dataCenter));
+    }
     if (previousConfig != null) {
       queryParams.put("releaseId", escaper.escape(previousConfig.getReleaseId()));
     }

+ 2 - 2
apollo-client/src/main/java/com/ctrip/apollo/model/ConfigChangeEvent.java

@@ -18,8 +18,8 @@ public class ConfigChangeEvent {
    */
   public ConfigChangeEvent(String namespace,
                            Map<String, ConfigChange> changes) {
-    this.m_namespace = namespace;
-    this.m_changes = changes;
+    m_namespace = namespace;
+    m_changes = changes;
   }
 
   /**

+ 8 - 0
apollo-client/src/main/java/com/ctrip/apollo/util/ConfigUtil.java

@@ -34,6 +34,14 @@ public class ConfigUtil {
     return appId;
   }
 
+  /**
+   * Get the data center info for the current application.
+   * @return the current data center, null if there is no such info.
+   */
+  public String getDataCenter() {
+    return Foundation.server().getDataCenter();
+  }
+
   /**
    * Get the cluster name for the current application.
    * @return the cluster name, or "default" if not specified

+ 7 - 0
apollo-client/src/test/java/com/ctrip/apollo/BaseIntegrationTest.java

@@ -42,6 +42,7 @@ public abstract class BaseIntegrationTest extends ComponentTestCase {
   private static final String configServiceURL = "http://localhost:" + PORT;
   protected static String someAppId;
   protected static String someClusterName;
+  protected static String someDataCenter;
   protected static int refreshInterval;
   protected static TimeUnit refreshTimeUnit;
   private Server server;
@@ -59,6 +60,7 @@ public abstract class BaseIntegrationTest extends ComponentTestCase {
     super.setUp();
     someAppId = "1003171";
     someClusterName = "someClusterName";
+    someDataCenter = "someDC";
     refreshInterval = 5;
     refreshTimeUnit = TimeUnit.MINUTES;
 
@@ -160,6 +162,11 @@ public abstract class BaseIntegrationTest extends ComponentTestCase {
     public Env getApolloEnv() {
       return Env.LOCAL;
     }
+
+    @Override
+    public String getDataCenter() {
+      return someDataCenter;
+    }
   }
 
   /**

+ 1 - 2
apollo-client/src/test/java/com/ctrip/apollo/integration/ConfigIntegrationTest.java

@@ -241,8 +241,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
     ContextHandler configHandler = mockConfigServerHandler(HttpServletResponse.SC_OK, apolloConfig);
     ContextHandler pollHandler =
         mockPollNotificationHandler(pollTimeoutInMS, HttpServletResponse.SC_OK,
-            new ApolloConfigNotification(apolloConfig.getAppId(), apolloConfig.getCluster(),
-                apolloConfig.getNamespace()), false);
+            new ApolloConfigNotification(apolloConfig.getNamespace()), false);
 
     startServerWithHandlers(configHandler, pollHandler);
 

+ 5 - 0
apollo-client/src/test/java/com/ctrip/apollo/internals/RemoteConfigRepositoryTest.java

@@ -186,6 +186,11 @@ public class RemoteConfigRepositoryTest extends ComponentTestCase {
     public String getCluster() {
       return "someCluster";
     }
+
+    @Override
+    public String getDataCenter() {
+      return null;
+    }
   }
 
   public static class MockConfigServiceLocator extends ConfigServiceLocator {

+ 43 - 11
apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/ConfigController.java

@@ -8,7 +8,9 @@ import com.google.common.collect.Maps;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 
+import com.ctrip.apollo.biz.entity.AppNamespace;
 import com.ctrip.apollo.biz.entity.Release;
+import com.ctrip.apollo.biz.service.AppNamespaceService;
 import com.ctrip.apollo.biz.service.ConfigService;
 import com.ctrip.apollo.core.ConfigConsts;
 import com.ctrip.apollo.core.dto.ApolloConfig;
@@ -37,6 +39,8 @@ import javax.servlet.http.HttpServletResponse;
 public class ConfigController {
   @Autowired
   private ConfigService configService;
+  @Autowired
+  private AppNamespaceService appNamespaceService;
 
   private Gson gson = new Gson();
   private Type configurationTypeReference =
@@ -45,17 +49,17 @@ public class ConfigController {
 
   @RequestMapping(value = "/{appId}/{clusterName}", method = RequestMethod.GET)
   public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,
-                                  @RequestParam(value = "datacenter", required = false) String datacenter,
+                                  @RequestParam(value = "dataCenter", required = false) String dataCenter,
                                   @RequestParam(value = "releaseId", defaultValue = "-1") String clientSideReleaseId,
                                   HttpServletResponse response) throws IOException {
-    return this.queryConfig(appId, clusterName, ConfigConsts.NAMESPACE_DEFAULT, datacenter,
+    return this.queryConfig(appId, clusterName, ConfigConsts.NAMESPACE_DEFAULT, dataCenter,
         clientSideReleaseId, response);
   }
 
   @RequestMapping(value = "/{appId}/{clusterName}/{namespace}", method = RequestMethod.GET)
   public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,
                                   @PathVariable String namespace,
-                                  @RequestParam(value = "datacenter", required = false) String datacenter,
+                                  @RequestParam(value = "dataCenter", required = false) String dataCenter,
                                   @RequestParam(value = "releaseId", defaultValue = "-1") String clientSideReleaseId,
                                   HttpServletResponse response) throws IOException {
     List<Release> releases = Lists.newLinkedList();
@@ -66,13 +70,12 @@ public class ConfigController {
       releases.add(currentAppRelease);
     }
 
-    //if namespace is not appId itself, should check if it has its own configurations
+    //if namespace is not 'application', should check if it's a public configuration
     if (!Objects.equals(ConfigConsts.NAMESPACE_DEFAULT, namespace)) {
-      //TODO find id for this particular namespace, if not equal to current app id, then do more
-      if (!Objects.isNull(datacenter)) {
-        //TODO load newAppId+datacenter+namespace configurations
+      Release publicRelease = this.findPublicConfig(appId, namespace, dataCenter);
+      if (!Objects.isNull(publicRelease)) {
+        releases.add(publicRelease);
       }
-      //TODO if load from DC failed, then load newAppId+defaultCluster+namespace configurations
     }
 
     if (releases.isEmpty()) {
@@ -81,7 +84,7 @@ public class ConfigController {
               "Could not load configurations with appId: %s, clusterName: %s, namespace: %s",
               appId, clusterName, namespace));
       Cat.logEvent("Apollo.Config.NotFound",
-          assembleKey(appId, clusterName, namespace, datacenter));
+          assembleKey(appId, clusterName, namespace, dataCenter));
       return null;
     }
 
@@ -92,17 +95,46 @@ public class ConfigController {
       // Client side configuration is the same with server side, return 304
       response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
       Cat.logEvent("Apollo.Config.NotModified",
-          assembleKey(appId, clusterName, namespace, datacenter));
+          assembleKey(appId, clusterName, namespace, dataCenter));
       return null;
     }
 
     ApolloConfig apolloConfig = new ApolloConfig(appId, clusterName, namespace, mergedReleaseId);
     apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));
 
-    Cat.logEvent("Apollo.Config.Found", assembleKey(appId, clusterName, namespace, datacenter));
+    Cat.logEvent("Apollo.Config.Found", assembleKey(appId, clusterName, namespace, dataCenter));
     return apolloConfig;
   }
 
+  /**
+   * @param applicationId the application which uses public config
+   * @param namespace     the namespace
+   * @param dataCenter    the datacenter
+   */
+  private Release findPublicConfig(String applicationId, String namespace, String dataCenter) {
+    AppNamespace appNamespace = appNamespaceService.findByNamespaceName(namespace);
+
+    //check whether the namespace's appId equals to current one
+    if (Objects.isNull(appNamespace) || Objects.equals(applicationId, appNamespace.getAppId())) {
+      return null;
+    }
+
+    String publicConfigAppId = appNamespace.getAppId();
+
+    //try to load via data center
+    if (!Objects.isNull(dataCenter)) {
+      Release dataCenterRelease =
+          configService.findRelease(publicConfigAppId, dataCenter, namespace);
+      if (!Objects.isNull(dataCenterRelease)) {
+        return dataCenterRelease;
+      }
+    }
+
+    //fallback to default release
+    return configService
+        .findRelease(publicConfigAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace);
+  }
+
   /**
    * Merge configurations of releases.
    * Release in lower index override those in higher index

+ 35 - 10
apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/NotificationController.java

@@ -6,14 +6,17 @@ import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 
+import com.ctrip.apollo.biz.entity.AppNamespace;
 import com.ctrip.apollo.biz.message.MessageListener;
 import com.ctrip.apollo.biz.message.Topics;
+import com.ctrip.apollo.biz.service.AppNamespaceService;
 import com.ctrip.apollo.core.ConfigConsts;
 import com.ctrip.apollo.core.dto.ApolloConfigNotification;
 import com.dianping.cat.Cat;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -40,23 +43,22 @@ public class NotificationController implements MessageListener {
       deferredResults =
       Multimaps.synchronizedSetMultimap(HashMultimap.create());
 
+  @Autowired
+  private AppNamespaceService appNamespaceService;
+
   @RequestMapping(method = RequestMethod.GET)
   public DeferredResult<ResponseEntity<ApolloConfigNotification>> pollNotification(
       @RequestParam(value = "appId") String appId,
       @RequestParam(value = "cluster") String cluster,
       @RequestParam(value = "namespace", defaultValue = ConfigConsts.NAMESPACE_DEFAULT) String namespace,
-      @RequestParam(value = "datacenter", required = false) String datacenter,
+      @RequestParam(value = "dataCenter", required = false) String dataCenter,
       @RequestParam(value = "releaseId", defaultValue = "-1") String clientSideReleaseId,
       HttpServletResponse response) {
     List<String> watchedKeys = Lists.newArrayList(assembleKey(appId, cluster, namespace));
 
-    //Listen more namespaces, since it's not the default namespace
+    //Listen on more namespaces, since it's not the default namespace
     if (!Objects.equals(ConfigConsts.NAMESPACE_DEFAULT, namespace)) {
-      //TODO find id for this particular namespace, if not equal to current app id, then do more
-      if (!Objects.isNull(datacenter)) {
-        //TODO add newAppId+datacenter+namespace to listened keys
-      }
-      //TODO add newAppId+defaultCluster+namespace to listened keys
+      watchedKeys.addAll(this.findPublicConfigWatchKey(appId, namespace, dataCenter));
     }
 
     ResponseEntity<ApolloConfigNotification> body = new ResponseEntity<>(
@@ -77,7 +79,7 @@ public class NotificationController implements MessageListener {
     });
 
     logger.info("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
-        watchedKeys, appId, cluster, namespace, datacenter);
+        watchedKeys, appId, cluster, namespace, dataCenter);
     return deferredResult;
   }
 
@@ -85,6 +87,30 @@ public class NotificationController implements MessageListener {
     return String.format("%s-%s-%s", appId, cluster, namespace);
   }
 
+  private List<String> findPublicConfigWatchKey(String applicationId, String namespace,
+                                                String dataCenter) {
+    List<String> publicWatchedKeys = Lists.newArrayList();
+    AppNamespace appNamespace = appNamespaceService.findByNamespaceName(namespace);
+
+    //check whether the namespace's appId equals to current one
+    if (Objects.isNull(appNamespace) || Objects.equals(applicationId, appNamespace.getAppId())) {
+      return publicWatchedKeys;
+    }
+
+    String publicConfigAppId = appNamespace.getAppId();
+
+    //watch data center config change
+    if (!Objects.isNull(dataCenter)) {
+      publicWatchedKeys.add(assembleKey(publicConfigAppId, dataCenter, namespace));
+    }
+
+    //watch default cluster config change
+    publicWatchedKeys
+        .add(assembleKey(publicConfigAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace));
+
+    return publicWatchedKeys;
+  }
+
   @Override
   public void handleMessage(String message, String channel) {
     logger.info("message received - channel: {}, message: {}", channel, message);
@@ -101,8 +127,7 @@ public class NotificationController implements MessageListener {
 
     ResponseEntity<ApolloConfigNotification> notification =
         new ResponseEntity<>(
-            new ApolloConfigNotification(keys[0], keys[1], keys[2]),
-            HttpStatus.OK);
+            new ApolloConfigNotification(keys[2]), HttpStatus.OK);
 
     Collection<DeferredResult<ResponseEntity<ApolloConfigNotification>>>
         results = deferredResults.get(message);

+ 177 - 30
apollo-configservice/src/test/java/com/ctrip/apollo/configservice/controller/ConfigControllerTest.java

@@ -1,17 +1,16 @@
 package com.ctrip.apollo.configservice.controller;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.Map;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
 
-import javax.servlet.http.HttpServletResponse;
+import com.ctrip.apollo.biz.entity.AppNamespace;
+import com.ctrip.apollo.biz.entity.Release;
+import com.ctrip.apollo.biz.service.AppNamespaceService;
+import com.ctrip.apollo.biz.service.ConfigService;
+import com.ctrip.apollo.core.ConfigConsts;
+import com.ctrip.apollo.core.dto.ApolloConfig;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -20,13 +19,18 @@ import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 import org.springframework.test.util.ReflectionTestUtils;
 
-import com.ctrip.apollo.biz.entity.Release;
-import com.ctrip.apollo.biz.service.ConfigService;
-import com.ctrip.apollo.core.dto.ApolloConfig;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.gson.Gson;
-import com.google.gson.JsonSyntaxException;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+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)
@@ -36,26 +40,34 @@ public class ConfigControllerTest {
   private ConfigController configController;
   @Mock
   private ConfigService configService;
+  @Mock
+  private AppNamespaceService appNamespaceService;
   private String someAppId;
   private String someClusterName;
-  private String someNamespaceName;
+  private String defaultNamespaceName;
+  private String somePublicNamespaceName;
   private String someDataCenter;
-  private String someValidConfiguration;
   @Mock
   private Release someRelease;
+  @Mock
+  private Release somePublicRelease;
 
   @Before
   public void setUp() throws Exception {
     configController = new ConfigController();
     ReflectionTestUtils.setField(configController, "configService", configService);
+    ReflectionTestUtils.setField(configController, "appNamespaceService", appNamespaceService);
 
     someAppId = "1";
     someClusterName = "someClusterName";
-    someNamespaceName = "someNamespaceName";
+    defaultNamespaceName = ConfigConsts.NAMESPACE_DEFAULT;
+    somePublicNamespaceName = "somePublicNamespace";
     someDataCenter = "someDC";
-    someValidConfiguration = "{\"apollo.bar\": \"foo\"}";
+    String someValidConfiguration = "{\"apollo.bar\": \"foo\"}";
+    String somePublicConfiguration = "{\"apollo.public.bar\": \"foo\"}";
 
     when(someRelease.getConfigurations()).thenReturn(someValidConfiguration);
+    when(somePublicRelease.getConfigurations()).thenReturn(somePublicConfiguration);
   }
 
   @Test
@@ -64,17 +76,18 @@ public class ConfigControllerTest {
     long someServerSideNewReleaseId = 2;
     HttpServletResponse someResponse = mock(HttpServletResponse.class);
 
-    when(configService.findRelease(someAppId, someClusterName, someNamespaceName))
+    when(configService.findRelease(someAppId, someClusterName, defaultNamespaceName))
         .thenReturn(someRelease);
     when(someRelease.getId()).thenReturn(someServerSideNewReleaseId);
 
     ApolloConfig result = configController.queryConfig(someAppId, someClusterName,
-        someNamespaceName, someDataCenter, String.valueOf(someClientSideReleaseId), someResponse);
+        defaultNamespaceName, someDataCenter, String.valueOf(someClientSideReleaseId),
+        someResponse);
 
-    verify(configService, times(1)).findRelease(someAppId, someClusterName, someNamespaceName);
+    verify(configService, times(1)).findRelease(someAppId, someClusterName, defaultNamespaceName);
     assertEquals(someAppId, result.getAppId());
     assertEquals(someClusterName, result.getCluster());
-    assertEquals(someNamespaceName, result.getNamespace());
+    assertEquals(defaultNamespaceName, result.getNamespace());
     assertEquals(String.valueOf(someServerSideNewReleaseId), result.getReleaseId());
   }
 
@@ -84,10 +97,12 @@ public class ConfigControllerTest {
     long someClientSideReleaseId = 1;
     HttpServletResponse someResponse = mock(HttpServletResponse.class);
 
-    when(configService.findRelease(someAppId, someClusterName, someNamespaceName)).thenReturn(null);
+    when(configService.findRelease(someAppId, someClusterName, defaultNamespaceName))
+        .thenReturn(null);
 
     ApolloConfig result = configController.queryConfig(someAppId, someClusterName,
-        someNamespaceName, someDataCenter, String.valueOf(someClientSideReleaseId), someResponse);
+        defaultNamespaceName, someDataCenter, String.valueOf(someClientSideReleaseId),
+        someResponse);
 
     assertNull(result);
     verify(someResponse, times(1)).sendError(eq(HttpServletResponse.SC_NOT_FOUND), anyString());
@@ -99,19 +114,144 @@ public class ConfigControllerTest {
     long someServerSideReleaseId = someClientSideReleaseId;
     HttpServletResponse someResponse = mock(HttpServletResponse.class);
 
-    when(configService.findRelease(someAppId, someClusterName, someNamespaceName))
+    when(configService.findRelease(someAppId, someClusterName, defaultNamespaceName))
         .thenReturn(someRelease);
     when(someRelease.getId()).thenReturn(someServerSideReleaseId);
 
     ApolloConfig
         result =
-        configController.queryConfig(someAppId, someClusterName, someNamespaceName,
+        configController.queryConfig(someAppId, someClusterName, defaultNamespaceName,
             someDataCenter, String.valueOf(someClientSideReleaseId), someResponse);
 
     assertNull(result);
     verify(someResponse, times(1)).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
   }
 
+  @Test
+  public void testQueryConfigWithAppOwnNamespace() throws Exception {
+    String someClientSideReleaseId = "1";
+    String someServerSideReleaseId = "2";
+    String someAppOwnNamespaceName = "someAppOwn";
+    HttpServletResponse someResponse = mock(HttpServletResponse.class);
+    AppNamespace someAppOwnNamespace =
+        assmbleAppNamespace(someAppId, someAppOwnNamespaceName);
+
+    when(configService.findRelease(someAppId, someClusterName, someAppOwnNamespaceName))
+        .thenReturn(someRelease);
+    when(appNamespaceService.findByNamespaceName(someAppOwnNamespaceName))
+        .thenReturn(someAppOwnNamespace);
+    when(someRelease.getId()).thenReturn(Long.valueOf(someServerSideReleaseId));
+
+    ApolloConfig result =
+        configController
+            .queryConfig(someAppId, someClusterName, someAppOwnNamespaceName, someDataCenter,
+                someClientSideReleaseId, someResponse);
+
+    assertEquals(someServerSideReleaseId, result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someClusterName, result.getCluster());
+    assertEquals(someAppOwnNamespaceName, result.getNamespace());
+    assertEquals("foo", result.getConfigurations().get("apollo.bar"));
+  }
+
+  @Test
+  public void testQueryConfigWithPubicNamespaceAndNoAppOverride() throws Exception {
+    String someClientSideReleaseId = "1";
+    String someServerSideReleaseId = "2";
+    HttpServletResponse someResponse = mock(HttpServletResponse.class);
+    String somePublicAppId = "somePublicAppId";
+    AppNamespace somePublicAppNamespace =
+        assmbleAppNamespace(somePublicAppId, somePublicNamespaceName);
+
+    when(configService.findRelease(someAppId, someClusterName, somePublicNamespaceName))
+        .thenReturn(null);
+    when(appNamespaceService.findByNamespaceName(somePublicNamespaceName))
+        .thenReturn(somePublicAppNamespace);
+    when(configService.findRelease(somePublicAppId, someDataCenter, somePublicNamespaceName))
+        .thenReturn(somePublicRelease);
+    when(somePublicRelease.getId()).thenReturn(Long.valueOf(someServerSideReleaseId));
+
+    ApolloConfig result =
+        configController
+            .queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter,
+                someClientSideReleaseId, someResponse);
+
+    assertEquals(someServerSideReleaseId, result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someClusterName, result.getCluster());
+    assertEquals(somePublicNamespaceName, result.getNamespace());
+    assertEquals("foo", result.getConfigurations().get("apollo.public.bar"));
+  }
+
+  @Test
+  public void testQueryConfigWithPublicNamespaceAndNoAppOverrideAndNoDataCenter() throws Exception {
+    String someClientSideReleaseId = "1";
+    String someServerSideReleaseId = "2";
+    HttpServletResponse someResponse = mock(HttpServletResponse.class);
+    String somePublicAppId = "somePublicAppId";
+    AppNamespace somePublicAppNamespace =
+        assmbleAppNamespace(somePublicAppId, somePublicNamespaceName);
+
+    when(configService.findRelease(someAppId, someClusterName, somePublicNamespaceName))
+        .thenReturn(null);
+    when(appNamespaceService.findByNamespaceName(somePublicNamespaceName))
+        .thenReturn(somePublicAppNamespace);
+    when(configService.findRelease(somePublicAppId, someDataCenter, somePublicNamespaceName))
+        .thenReturn(null);
+    when(configService
+        .findRelease(somePublicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespaceName))
+        .thenReturn(somePublicRelease);
+    when(somePublicRelease.getId()).thenReturn(Long.valueOf(someServerSideReleaseId));
+
+    ApolloConfig result =
+        configController
+            .queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter,
+                someClientSideReleaseId, someResponse);
+
+    assertEquals(someServerSideReleaseId, result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someClusterName, result.getCluster());
+    assertEquals(somePublicNamespaceName, result.getNamespace());
+    assertEquals("foo", result.getConfigurations().get("apollo.public.bar"));
+  }
+
+  @Test
+  public void testQueryConfigWithPublicNamespaceAndAppOverride() throws Exception {
+    String someAppSideReleaseId = "1";
+    String somePublicAppSideReleaseId = "2";
+
+    HttpServletResponse someResponse = mock(HttpServletResponse.class);
+    String somePublicAppId = "somePublicAppId";
+    AppNamespace somePublicAppNamespace =
+        assmbleAppNamespace(somePublicAppId, somePublicNamespaceName);
+
+    when(someRelease.getConfigurations()).thenReturn("{\"apollo.public.foo\": \"foo-override\"}");
+    when(somePublicRelease.getConfigurations())
+        .thenReturn("{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}");
+
+    when(configService.findRelease(someAppId, someClusterName, somePublicNamespaceName))
+        .thenReturn(someRelease);
+    when(someRelease.getId()).thenReturn(Long.valueOf(someAppSideReleaseId));
+    when(appNamespaceService.findByNamespaceName(somePublicNamespaceName))
+        .thenReturn(somePublicAppNamespace);
+    when(configService.findRelease(somePublicAppId, someDataCenter, somePublicNamespaceName))
+        .thenReturn(somePublicRelease);
+    when(somePublicRelease.getId()).thenReturn(Long.valueOf(somePublicAppSideReleaseId));
+
+    ApolloConfig result =
+        configController
+            .queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter,
+                someAppSideReleaseId, someResponse);
+
+    assertEquals(String.format("%s|%s", someAppSideReleaseId, somePublicAppSideReleaseId),
+        result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someClusterName, result.getCluster());
+    assertEquals(somePublicNamespaceName, result.getNamespace());
+    assertEquals("foo-override", result.getConfigurations().get("apollo.public.foo"));
+    assertEquals("bar", result.getConfigurations().get("apollo.public.bar"));
+  }
+
   @Test
   public void testMergeConfigurations() throws Exception {
     Gson gson = new Gson();
@@ -148,4 +288,11 @@ public class ConfigControllerTest {
 
     configController.mergeReleaseConfigurations(Lists.newArrayList(someRelease));
   }
+
+  private AppNamespace assmbleAppNamespace(String appId, String namespace) {
+    AppNamespace appNamespace = new AppNamespace();
+    appNamespace.setAppId(appId);
+    appNamespace.setName(namespace);
+    return appNamespace;
+  }
 }

+ 82 - 13
apollo-configservice/src/test/java/com/ctrip/apollo/configservice/controller/NotificationControllerTest.java

@@ -1,8 +1,12 @@
 package com.ctrip.apollo.configservice.controller;
 
+import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 
+import com.ctrip.apollo.biz.entity.AppNamespace;
 import com.ctrip.apollo.biz.message.Topics;
+import com.ctrip.apollo.biz.service.AppNamespaceService;
+import com.ctrip.apollo.core.ConfigConsts;
 import com.ctrip.apollo.core.dto.ApolloConfigNotification;
 
 import org.junit.Before;
@@ -15,10 +19,13 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.web.context.request.async.DeferredResult;
 
+import java.util.List;
+
 import javax.servlet.http.HttpServletResponse;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
@@ -28,19 +35,26 @@ public class NotificationControllerTest {
   private NotificationController controller;
   private String someAppId;
   private String someCluster;
-  private String someNamespace;
+  private String defaultNamespace;
+  private String somePublicNamespace;
   private String someDataCenter;
   private String someReleaseId;
   @Mock
   private HttpServletResponse response;
-  private Multimap<String, DeferredResult<ResponseEntity<ApolloConfigNotification>>> deferredResults;
+  @Mock
+  private AppNamespaceService appNamespaceService;
+  private Multimap<String, DeferredResult<ResponseEntity<ApolloConfigNotification>>>
+      deferredResults;
 
   @Before
   public void setUp() throws Exception {
     controller = new NotificationController();
+    ReflectionTestUtils.setField(controller, "appNamespaceService", appNamespaceService);
+
     someAppId = "someAppId";
     someCluster = "someCluster";
-    someNamespace = "someNamespace";
+    defaultNamespace = ConfigConsts.NAMESPACE_DEFAULT;
+    somePublicNamespace = "somePublicNamespace";
     someDataCenter = "someDC";
     someReleaseId = "someRelease";
 
@@ -51,29 +65,78 @@ public class NotificationControllerTest {
 
   @Test
   public void testPollNotificationWithDefaultNamespace() throws Exception {
-    someNamespace = someAppId; //default namespace
-
     DeferredResult<ResponseEntity<ApolloConfigNotification>>
         deferredResult = controller
-        .pollNotification(someAppId, someCluster, someNamespace, someDataCenter, someReleaseId,
+        .pollNotification(someAppId, someCluster, defaultNamespace, someDataCenter, someReleaseId,
             response);
 
-    String key = String.format("%s-%s-%s", someAppId, someCluster, someNamespace);
+    String key = String.format("%s-%s-%s", someAppId, someCluster, defaultNamespace);
     assertEquals(1, deferredResults.size());
     assertTrue(deferredResults.get(key).contains(deferredResult));
+  }
 
+  @Test
+  public void testPollNotificationWithPublicNamespace() throws Exception {
+    String somePublicAppId = "somePublicAppId";
+    AppNamespace somePublicAppNamespace =
+        assmbleAppNamespace(somePublicAppId, somePublicNamespace);
+
+    when(appNamespaceService.findByNamespaceName(somePublicNamespace))
+        .thenReturn(somePublicAppNamespace);
+
+    DeferredResult<ResponseEntity<ApolloConfigNotification>>
+        deferredResult = controller
+        .pollNotification(someAppId, someCluster, somePublicNamespace, someDataCenter,
+            someReleaseId,
+            response);
+
+    List<String> publicClusters =
+        Lists.newArrayList(someDataCenter, ConfigConsts.CLUSTER_NAME_DEFAULT);
+
+    assertEquals(3, deferredResults.size());
+    String key = String.format("%s-%s-%s", someAppId, someCluster, somePublicNamespace);
+    assertTrue(deferredResults.get(key).contains(deferredResult));
+
+    for (String cluster : publicClusters) {
+      String publicKey = String.format("%s-%s-%s", somePublicAppId, cluster, somePublicNamespace);
+      assertTrue(deferredResults.get(publicKey).contains(deferredResult));
+    }
   }
 
   @Test
   public void testPollNotificationWithDefaultNamespaceAndHandleMessage() throws Exception {
-    someNamespace = someAppId; //default namespace
+    DeferredResult<ResponseEntity<ApolloConfigNotification>>
+        deferredResult = controller
+        .pollNotification(someAppId, someCluster, defaultNamespace, someDataCenter, someReleaseId,
+            response);
+
+    String key = String.format("%s-%s-%s", someAppId, someCluster, defaultNamespace);
+
+    controller.handleMessage(key, Topics.APOLLO_RELEASE_TOPIC);
+
+    ResponseEntity<ApolloConfigNotification> response =
+        (ResponseEntity<ApolloConfigNotification>) deferredResult.getResult();
+    ApolloConfigNotification notification = response.getBody();
+
+    assertEquals(HttpStatus.OK, response.getStatusCode());
+    assertEquals(defaultNamespace, notification.getNamespace());
+  }
+
+  @Test
+  public void testPollNotificationWithPublicNamespaceAndHandleMessage() throws Exception {
+    String somePublicAppId = "somePublicAppId";
+    AppNamespace somePublicAppNamespace =
+        assmbleAppNamespace(somePublicAppId, somePublicNamespace);
+
+    when(appNamespaceService.findByNamespaceName(somePublicNamespace))
+        .thenReturn(somePublicAppNamespace);
 
     DeferredResult<ResponseEntity<ApolloConfigNotification>>
         deferredResult = controller
-        .pollNotification(someAppId, someCluster, someNamespace, someDataCenter, someReleaseId,
+        .pollNotification(someAppId, someCluster, somePublicNamespace, someDataCenter, someReleaseId,
             response);
 
-    String key = String.format("%s-%s-%s", someAppId, someCluster, someNamespace);
+    String key = String.format("%s-%s-%s", somePublicAppId, someDataCenter, somePublicNamespace);
 
     controller.handleMessage(key, Topics.APOLLO_RELEASE_TOPIC);
 
@@ -82,8 +145,14 @@ public class NotificationControllerTest {
     ApolloConfigNotification notification = response.getBody();
 
     assertEquals(HttpStatus.OK, response.getStatusCode());
-    assertEquals(someAppId, notification.getAppId());
-    assertEquals(someCluster, notification.getCluster());
-    assertEquals(someNamespace, notification.getNamespace());
+    assertEquals(somePublicNamespace, notification.getNamespace());
+
+  }
+
+  private AppNamespace assmbleAppNamespace(String appId, String namespace) {
+    AppNamespace appNamespace = new AppNamespace();
+    appNamespace.setAppId(appId);
+    appNamespace.setName(namespace);
+    return appNamespace;
   }
 }

+ 76 - 0
apollo-configservice/src/test/java/com/ctrip/apollo/configservice/integration/ConfigControllerIntegrationTest.java

@@ -19,12 +19,18 @@ public class ConfigControllerIntegrationTest 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 {
     someAppId = "someAppId";
     someCluster = "someCluster";
     someNamespace = "someNamespace";
+    somePublicNamespace = "somePublicNamespace";
+    someDC = "someDC";
+    someDefaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT;
   }
 
   @Test
@@ -83,4 +89,74 @@ public class ConfigControllerIntegrationTest extends AbstractBaseIntegrationTest
     assertEquals(HttpStatus.NOT_MODIFIED, response.getStatusCode());
   }
 
+  @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 testQueryPublicConfigWithDataCenterFoundAndNoOverride() throws Exception {
+    ResponseEntity<ApolloConfig> response = restTemplate
+        .getForEntity("{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class,
+            getHostUrl(), someAppId, someCluster, somePublicNamespace, someDC);
+    ApolloConfig result = response.getBody();
+
+    assertEquals("993", result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someCluster, result.getCluster());
+    assertEquals(somePublicNamespace, result.getNamespace());
+    assertEquals("someDC-v1", result.getConfigurations().get("k1"));
+    assertEquals("someDC-v2", result.getConfigurations().get("k2"));
+  }
+
+  @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 testQueryPublicConfigWithDataCenterFoundAndOverride() throws Exception {
+    ResponseEntity<ApolloConfig> response = restTemplate
+        .getForEntity("{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class,
+            getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDC);
+    ApolloConfig result = response.getBody();
+
+    assertEquals("994|993", result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someDefaultCluster, result.getCluster());
+    assertEquals(somePublicNamespace, result.getNamespace());
+    assertEquals("override-v1", result.getConfigurations().get("k1"));
+    assertEquals("someDC-v2", result.getConfigurations().get("k2"));
+  }
+
+  @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 testQueryPublicConfigWithDataCenterNotFoundAndNoOverride() throws Exception {
+    String someDCNotFound = "someDCNotFound";
+    ResponseEntity<ApolloConfig> response = restTemplate
+        .getForEntity("{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class,
+            getHostUrl(), someAppId, someCluster, somePublicNamespace, someDCNotFound);
+    ApolloConfig result = response.getBody();
+
+    assertEquals("992", result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someCluster, result.getCluster());
+    assertEquals(somePublicNamespace, result.getNamespace());
+    assertEquals("default-v1", result.getConfigurations().get("k1"));
+    assertEquals("default-v2", result.getConfigurations().get("k2"));
+  }
+
+  @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 testQueryPublicConfigWithDataCenterNotFoundAndOverride() throws Exception {
+    String someDCNotFound = "someDCNotFound";
+    ResponseEntity<ApolloConfig> response = restTemplate
+        .getForEntity("{baseurl}/configs/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}", ApolloConfig.class,
+            getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDCNotFound);
+    ApolloConfig result = response.getBody();
+
+    assertEquals("994|992", result.getReleaseId());
+    assertEquals(someAppId, result.getAppId());
+    assertEquals(someDefaultCluster, result.getCluster());
+    assertEquals(somePublicNamespace, result.getNamespace());
+    assertEquals("override-v1", result.getConfigurations().get("k1"));
+    assertEquals("default-v2", result.getConfigurations().get("k2"));
+
+  }
 }

+ 80 - 8
apollo-configservice/src/test/java/com/ctrip/apollo/configservice/integration/NotificationControllerIntegrationTest.java

@@ -10,11 +10,13 @@ import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.jdbc.Sql;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.Assert.assertEquals;
 
@@ -26,40 +28,110 @@ public class NotificationControllerIntegrationTest extends AbstractBaseIntegrati
   private NotificationController notificationController;
   private String someAppId;
   private String someCluster;
-  private String someNamespace;
+  private String defaultNamespace;
+  private String somePublicNamespace;
   private ExecutorService executorService;
 
   @Before
   public void setUp() throws Exception {
     someAppId = "someAppId";
     someCluster = ConfigConsts.CLUSTER_NAME_DEFAULT;
-    someNamespace = "someNamespace";
+    defaultNamespace = ConfigConsts.NAMESPACE_DEFAULT;
+    somePublicNamespace = "somePublicNamespace";
     executorService = Executors.newSingleThreadExecutor();
   }
 
   @Test
-  public void testPollNotification() throws Exception {
+  public void testPollNotificationWithDefaultNamespace() throws Exception {
     Future<ResponseEntity<ApolloConfigNotification>> future =
         executorService.submit(() -> restTemplate
             .getForEntity(
                 "{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}",
                 ApolloConfigNotification.class,
-                getHostUrl(), someAppId, someCluster, someNamespace));
+                getHostUrl(), someAppId, someCluster, defaultNamespace));
 
     //wait for the request connected to server
     TimeUnit.MILLISECONDS.sleep(500);
 
-    notificationController.handleMessage(assembleKey(someAppId, someCluster, someNamespace),
+    notificationController.handleMessage(assembleKey(someAppId, someCluster, defaultNamespace),
         Topics.APOLLO_RELEASE_TOPIC);
 
     ResponseEntity<ApolloConfigNotification> result = future.get(500, TimeUnit.MILLISECONDS);
     ApolloConfigNotification notification = result.getBody();
     assertEquals(HttpStatus.OK, result.getStatusCode());
-    assertEquals(someAppId, notification.getAppId());
-    assertEquals(someCluster, notification.getCluster());
-    assertEquals(someNamespace, notification.getNamespace());
+    assertEquals(defaultNamespace, notification.getNamespace());
   }
 
+  @Test(timeout = 5000L)
+  @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 testPollNotificationWthPublicNamespaceAndNoDataCenter() throws Exception {
+    String publicAppId = "somePublicAppId";
+
+    AtomicBoolean stop = new AtomicBoolean();
+    executorService.submit((Runnable) () -> {
+      //wait for the request connected to server
+      while (!stop.get() && !Thread.currentThread().isInterrupted()) {
+        try {
+          TimeUnit.MILLISECONDS.sleep(100);
+        } catch (InterruptedException e) {
+        }
+
+        notificationController.handleMessage(
+            assembleKey(publicAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, somePublicNamespace),
+            Topics.APOLLO_RELEASE_TOPIC);
+      }
+    });
+
+    ResponseEntity<ApolloConfigNotification> result = restTemplate
+        .getForEntity(
+            "{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}",
+            ApolloConfigNotification.class,
+            getHostUrl(), someAppId, someCluster, somePublicNamespace);
+
+    stop.set(true);
+
+    ApolloConfigNotification notification = result.getBody();
+    assertEquals(HttpStatus.OK, result.getStatusCode());
+    assertEquals(somePublicNamespace, notification.getNamespace());
+  }
+
+  @Test(timeout = 5000L)
+  @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 testPollNotificationWthPublicNamespaceAndDataCenter() throws Exception {
+    String publicAppId = "somePublicAppId";
+    String someDC = "someDC";
+
+    AtomicBoolean stop = new AtomicBoolean();
+    executorService.submit((Runnable) () -> {
+      //wait for the request connected to server
+      while (!stop.get() && !Thread.currentThread().isInterrupted()) {
+        try {
+          TimeUnit.MILLISECONDS.sleep(100);
+        } catch (InterruptedException e) {
+        }
+
+        notificationController.handleMessage(
+            assembleKey(publicAppId, someDC, somePublicNamespace),
+            Topics.APOLLO_RELEASE_TOPIC);
+      }
+    });
+
+    ResponseEntity<ApolloConfigNotification> result = restTemplate
+        .getForEntity(
+            "{baseurl}/notifications?appId={appId}&cluster={clusterName}&namespace={namespace}&dataCenter={dataCenter}",
+            ApolloConfigNotification.class,
+            getHostUrl(), someAppId, someCluster, somePublicNamespace, someDC);
+
+    stop.set(true);
+
+    ApolloConfigNotification notification = result.getBody();
+    assertEquals(HttpStatus.OK, result.getStatusCode());
+    assertEquals(somePublicNamespace, notification.getNamespace());
+  }
+
+
   private String assembleKey(String appId, String cluster, String namespace) {
     return String.format("%s-%s-%s", appId, cluster, namespace);
   }

+ 20 - 4
apollo-configservice/src/test/resources/integration-test/test-release.sql

@@ -1,13 +1,29 @@
 INSERT INTO App (AppId, Name, OwnerName, OwnerEmail) VALUES ('someAppId','someAppName','someOwnerName','someOwnerName@ctrip.com');
+INSERT INTO App (AppId, Name, OwnerName, OwnerEmail) VALUES ('somePublicAppId','somePublicAppName','someOwnerName','someOwnerName@ctrip.com');
 
 INSERT INTO Cluster (AppId, Name) VALUES ('someAppId', 'default');
 INSERT INTO Cluster (AppId, Name) VALUES ('someAppId', 'someCluster');
+INSERT INTO Cluster (AppId, Name) VALUES ('somePublicAppId', 'default');
+INSERT INTO Cluster (AppId, Name) VALUES ('somePublicAppId', 'someDC');
 
-INSERT INTO AppNamespace (AppId, Name) VALUES ('someAppId', 'someAppId');
+INSERT INTO AppNamespace (AppId, Name) VALUES ('someAppId', 'application');
 INSERT INTO AppNamespace (AppId, Name) VALUES ('someAppId', 'someNamespace');
+INSERT INTO AppNamespace (AppId, Name) VALUES ('somePublicAppId', 'application');
+INSERT INTO AppNamespace (AppId, Name) VALUES ('somePublicAppId', 'somePublicNamespace');
 
-INSERT INTO Namespace (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'someAppId');
+INSERT INTO Namespace (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'application');
 INSERT INTO Namespace (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'someCluster', 'someNamespace');
+INSERT INTO Namespace (AppId, ClusterName, NamespaceName) VALUES ('somePublicAppId', 'default', 'application');
+INSERT INTO Namespace (AppId, ClusterName, NamespaceName) VALUES ('somePublicAppId', 'someDC', 'somePublicNamespace');
+INSERT INTO Namespace (AppId, ClusterName, NamespaceName) VALUES ('someAppId', 'default', 'somePublicNamespace');
 
-INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (990, 'INTEGRATION-TEST-DEFAULT','First Release','someAppId', 'default', 'application', '{"k1":"v1"}');
-INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations) VALUES (991, 'INTEGRATION-TEST-NAMESPACE','First Release','someAppId', 'someCluster', 'someNamespace', '{"k2":"v2"}');
+INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations)
+  VALUES (990, 'INTEGRATION-TEST-DEFAULT','First Release','someAppId', 'default', 'application', '{"k1":"v1"}');
+INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations)
+  VALUES (991, 'INTEGRATION-TEST-NAMESPACE','First Release','someAppId', 'someCluster', 'someNamespace', '{"k2":"v2"}');
+INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations)
+  VALUES (992, 'INTEGRATION-TEST-PUBLIC-DEFAULT','First Release','somePublicAppId', 'default', 'somePublicNamespace', '{"k1":"default-v1", "k2":"default-v2"}');
+INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations)
+ VALUES (993, 'INTEGRATION-TEST-PUBLIC-NAMESPACE','First Release','somePublicAppId', 'someDC', 'somePublicNamespace', '{"k1":"someDC-v1", "k2":"someDC-v2"}');
+INSERT INTO RELEASE (id, Name, Comment, AppId, ClusterName, NamespaceName, Configurations)
+ VALUES (994, 'INTEGRATION-TEST-DEFAULT-OVERRIDE-PUBLIC','First Release','someAppId', 'default', 'somePublicNamespace', '{"k1":"override-v1"}');

+ 1 - 21
apollo-core/src/main/java/com/ctrip/apollo/core/dto/ApolloConfigNotification.java

@@ -4,40 +4,20 @@ package com.ctrip.apollo.core.dto;
  * @author Jason Song(song_s@ctrip.com)
  */
 public class ApolloConfigNotification {
-  private String appId;
-  private String cluster;
   private String namespace;
 
   //for json converter
   public ApolloConfigNotification() {
   }
 
-  public ApolloConfigNotification(String appId, String cluster, String namespace) {
-    this.appId = appId;
-    this.cluster = cluster;
+  public ApolloConfigNotification(String namespace) {
     this.namespace = namespace;
   }
 
-  public String getAppId() {
-    return appId;
-  }
-
-  public String getCluster() {
-    return cluster;
-  }
-
   public String getNamespace() {
     return namespace;
   }
 
-  public void setAppId(String appId) {
-    this.appId = appId;
-  }
-
-  public void setCluster(String cluster) {
-    this.cluster = cluster;
-  }
-
   public void setNamespace(String namespace) {
     this.namespace = namespace;
   }

+ 11 - 0
apollo-demo/Demo.md

@@ -0,0 +1,11 @@
+#Demo
+
+## 1. 本地没有缓存文件,从服务端正常读取
+
+## 2. 本地有缓存文件,从服务端正常读取
+
+## 3. 本地没有缓存文件,从服务端读取失败
+
+## 4. 本地有缓存文件,从服务端读取失败
+
+## 5. 本地启动后,配置有新的发布