You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by mm...@apache.org on 2018/02/13 03:14:55 UTC

[incubator-pulsar] branch master updated: PIP-10: Removing cluster from topic name (#1150)

This is an automated email from the ASF dual-hosted git repository.

mmerli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new 35e84c3  PIP-10: Removing cluster from topic name (#1150)
35e84c3 is described below

commit 35e84c349ce2442592bde6f1bfc980dacf175f1e
Author: cckellogg <cc...@gmail.com>
AuthorDate: Mon Feb 12 19:14:52 2018 -0800

    PIP-10: Removing cluster from topic name (#1150)
    
    * Make DestinationName and NamespaceName classes to handle both formats
    
    * Refactor /namespaces and /resource-quotas handlers for new-style namespace names
    
    * Refactor topics APIs to accept new name formats
    
    * Separate the broker admin rest endpoints in different pacakges.
    
    v1 - legacy endpoints with clusters name
    v2 - endpoints without the cluster name
    
    The new admin v2 endpoints are mounted ad /admin/v2.
    
    * Remove refactor remnants.
    
    * Fix list namespaces to handle v1 and v2 formats.
    
    * Add default namespace policies on create if none are sent.
    
    * Fix internals that assumed a cluster in the path.
    
    * Fix merge compile issue.
    
    * Fix compile issues from merge with master.
---
 .../org/apache/pulsar/broker/PulsarService.java    |    6 +-
 .../apache/pulsar/broker/admin/AdminResource.java  |  135 +-
 .../org/apache/pulsar/broker/admin/Namespaces.java | 1757 --------------------
 .../BrokerStatsBase.java}                          |   13 +-
 .../admin/{Brokers.java => impl/BrokersBase.java}  |   14 +-
 .../{Clusters.java => impl/ClustersBase.java}      |   16 +-
 .../pulsar/broker/admin/impl/NamespacesBase.java   | 1246 ++++++++++++++
 .../PersistentTopicsBase.java}                     |  828 +++------
 .../{Properties.java => impl/PropertiesBase.java}  |   16 +-
 .../ResourceQuotasBase.java}                       |   97 +-
 .../apache/pulsar/broker/admin/v1/BrokerStats.java |   32 +
 .../org/apache/pulsar/broker/admin/v1/Brokers.java |   29 +
 .../apache/pulsar/broker/admin/v1/Clusters.java    |   32 +
 .../apache/pulsar/broker/admin/v1/Namespaces.java  |  711 ++++++++
 .../broker/admin/{ => v1}/NonPersistentTopics.java |  102 +-
 .../pulsar/broker/admin/v1/PersistentTopics.java   |  421 +++++
 .../apache/pulsar/broker/admin/v1/Properties.java  |   34 +
 .../pulsar/broker/admin/v1/ResourceQuotas.java     |   79 +
 .../apache/pulsar/broker/admin/v2/BrokerStats.java |   32 +
 .../org/apache/pulsar/broker/admin/v2/Brokers.java |   29 +
 .../apache/pulsar/broker/admin/v2/Clusters.java    |   32 +
 .../apache/pulsar/broker/admin/v2/Namespaces.java  |  519 ++++++
 .../broker/admin/v2/NonPersistentTopics.java       |  159 ++
 .../pulsar/broker/admin/v2/PersistentTopics.java   |  396 +++++
 .../apache/pulsar/broker/admin/v2/Properties.java  |   34 +
 .../pulsar/broker/admin/v2/ResourceQuotas.java     |   90 +
 .../broker/loadbalance/impl/LoadManagerShared.java |    6 +-
 .../pulsar/broker/lookup/DestinationLookup.java    |    4 +-
 .../pulsar/broker/namespace/NamespaceService.java  |   12 +-
 .../broker/namespace/ServiceUnitZkUtils.java       |   15 +-
 .../pulsar/broker/service/BrokerService.java       |    2 +-
 .../apache/pulsar/broker/service/ServerCnx.java    |    4 +-
 .../pulsar/broker/web/PulsarWebResource.java       |   22 +-
 .../utils/PulsarBrokerVersionStringUtils.java      |    4 +-
 .../apache/pulsar/broker/admin/AdminApiTest.java   |    4 +-
 .../org/apache/pulsar/broker/admin/AdminTest.java  |   27 +-
 .../apache/pulsar/broker/admin/NamespacesTest.java |   14 +-
 .../org/apache/pulsar/common/naming/Constants.java |   26 +
 .../pulsar/common/naming/DestinationName.java      |   73 +-
 .../apache/pulsar/common/naming/NamespaceName.java |   88 +-
 .../pulsar/common/naming/DestinationNameTest.java  |   31 +-
 .../pulsar/common/naming/NamespaceNameTest.java    |   31 +-
 42 files changed, 4572 insertions(+), 2650 deletions(-)

diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java
index 51e6ace..9a2b44a 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java
@@ -275,7 +275,8 @@ public class PulsarService implements AutoCloseable {
 
             this.webService = new WebService(this);
             this.webService.addRestResources("/", "org.apache.pulsar.broker.web", false);
-            this.webService.addRestResources("/admin", "org.apache.pulsar.broker.admin", true);
+            this.webService.addRestResources("/admin", "org.apache.pulsar.broker.admin.v1", true);
+            this.webService.addRestResources("/admin/v2", "org.apache.pulsar.broker.admin.v2", true);
             this.webService.addRestResources("/lookup", "org.apache.pulsar.broker.lookup", true);
 
             this.webService.addServlet("/metrics",
@@ -462,8 +463,7 @@ public class PulsarService implements AutoCloseable {
             List<CompletableFuture<Topic>> persistentTopics = Lists.newArrayList();
             long topicLoadStart = System.nanoTime();
 
-            for (String topic : getNamespaceService().getListOfDestinations(nsName.getProperty(), nsName.getCluster(),
-                    nsName.getLocalName())) {
+            for (String topic : getNamespaceService().getListOfDestinations(nsName)) {
                 try {
                     DestinationName dn = DestinationName.get(topic);
                     if (bundle.includes(dn)) {
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/AdminResource.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/AdminResource.java
index f04d15b..b91c63a 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/AdminResource.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/AdminResource.java
@@ -52,6 +52,7 @@ import org.apache.pulsar.common.policies.data.LocalPolicies;
 import org.apache.pulsar.common.policies.data.Policies;
 import org.apache.pulsar.common.policies.data.PropertyAdmin;
 import org.apache.pulsar.common.policies.impl.NamespaceIsolationPolicies;
+import org.apache.pulsar.common.util.Codec;
 import org.apache.pulsar.common.util.ObjectMapperFactory;
 import org.apache.pulsar.zookeeper.ZooKeeperCache;
 import org.apache.pulsar.zookeeper.ZooKeeperCache.Deserializer;
@@ -189,11 +190,22 @@ public abstract class AdminResource extends PulsarWebResource {
     protected List<String> getListOfNamespaces(String property) throws Exception {
         List<String> namespaces = Lists.newArrayList();
 
-        for (String cluster : globalZk().getChildren(path(POLICIES, property), false)) {
+        // this will return a cluster in v1 and a namespace in v2
+        for (String clusterOrNamespace : globalZk().getChildren(path(POLICIES, property), false)) {
             // Then get the list of namespaces
             try {
-                for (String namespace : globalZk().getChildren(path(POLICIES, property, cluster), false)) {
-                    namespaces.add(String.format("%s/%s/%s", property, cluster, namespace));
+                final List<String> children = globalZk().getChildren(path(POLICIES, property, clusterOrNamespace), false);
+                if (children == null || children.isEmpty()) {
+                    String namespace = NamespaceName.get(property, clusterOrNamespace).toString();
+                    // if the length is 0 then this is probably a leftover cluster from namespace created
+                    // with the v1 admin format (prop/cluster/ns) and then deleted, so no need to add it to the list
+                    if (globalZk().getData(path(POLICIES, namespace), false, null).length != 0) {
+                        namespaces.add(namespace);
+                    }
+                } else {
+                    children.forEach(ns -> {
+                        namespaces.add(NamespaceName.get(property, clusterOrNamespace, ns).toString());
+                    });
                 }
             } catch (KeeperException.NoNodeException e) {
                 // A cluster was deleted between the 2 getChildren() calls, ignoring
@@ -204,7 +216,56 @@ public abstract class AdminResource extends PulsarWebResource {
         return namespaces;
     }
 
-    
+    protected NamespaceName namespaceName;
+
+    protected void validateNamespaceName(String property, String namespace) {
+        try {
+            this.namespaceName = NamespaceName.get(property, namespace);
+        } catch (IllegalArgumentException e) {
+            log.warn("[{}] Failed to create namespace with invalid name {}", clientAppId(), namespace, e);
+            throw new RestException(Status.PRECONDITION_FAILED, "Namespace name is not valid");
+        }
+    }
+
+    @Deprecated
+    protected void validateNamespaceName(String property, String cluster, String namespace) {
+        try {
+            this.namespaceName = NamespaceName.get(property, cluster, namespace);
+        } catch (IllegalArgumentException e) {
+            log.warn("[{}] Failed to create namespace with invalid name {}", clientAppId(), namespace, e);
+            throw new RestException(Status.PRECONDITION_FAILED, "Namespace name is not valid");
+        }
+    }
+
+    protected DestinationName destinationName;
+
+    protected void validateDestinationName(String property, String namespace, String encodedTopic) {
+        String topic = Codec.decode(encodedTopic);
+        try {
+            this.namespaceName = NamespaceName.get(property, namespace);
+            this.destinationName = DestinationName.get(domain(), namespaceName, topic);
+        } catch (IllegalArgumentException e) {
+            log.warn("[{}] Failed to validate topic name {}://{}/{}/{}", clientAppId(), domain(), property, namespace,
+                    topic, e);
+            throw new RestException(Status.PRECONDITION_FAILED, "Topic name is not valid");
+        }
+
+        this.destinationName = DestinationName.get(domain(), namespaceName, topic);
+    }
+
+    @Deprecated
+    protected void validateDestinationName(String property, String cluster, String namespace, String encodedTopic) {
+        String topic = Codec.decode(encodedTopic);
+        try {
+            this.namespaceName = NamespaceName.get(property, cluster, namespace);
+            this.destinationName = DestinationName.get(domain(), namespaceName, topic);
+        } catch (IllegalArgumentException e) {
+            log.warn("[{}] Failed to validate topic name {}://{}/{}/{}/{}", clientAppId(), domain(), property, cluster,
+                    namespace, topic, e);
+            throw new RestException(Status.PRECONDITION_FAILED, "Topic name is not valid");
+        }
+    }
+
     /**
      * Redirect the call to the specified broker
      *
@@ -227,20 +288,20 @@ public abstract class AdminResource extends PulsarWebResource {
         }
     }
 
-    protected Policies getNamespacePolicies(String property, String cluster, String namespace) {
+    protected Policies getNamespacePolicies(NamespaceName namespaceName) {
         try {
-            Policies policies = policiesCache().get(AdminResource.path(POLICIES, property, cluster, namespace))
+            Policies policies = policiesCache().get(AdminResource.path(POLICIES, namespaceName.toString()))
                     .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace does not exist"));
             // fetch bundles from LocalZK-policies
             NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
-                    .getBundles(NamespaceName.get(property, cluster, namespace));
+                    .getBundles(namespaceName);
             BundlesData bundleData = NamespaceBundleFactory.getBundlesData(bundles);
             policies.bundles = bundleData != null ? bundleData : policies.bundles;
             return policies;
         } catch (RestException re) {
             throw re;
         } catch (Exception e) {
-            log.error("[{}] Failed to get namespace policies {}/{}/{}", clientAppId(), property, cluster, namespace, e);
+            log.error("[{}] Failed to get namespace policies {}", clientAppId(), namespaceName, e);
             throw new RestException(e);
         }
     }
@@ -249,27 +310,27 @@ public abstract class AdminResource extends PulsarWebResource {
         return ObjectMapperFactory.getThreadLocal();
     }
 
-    ZooKeeperDataCache<PropertyAdmin> propertiesCache() {
+    public ZooKeeperDataCache<PropertyAdmin> propertiesCache() {
         return pulsar().getConfigurationCache().propertiesCache();
     }
 
-    ZooKeeperDataCache<Policies> policiesCache() {
+    protected ZooKeeperDataCache<Policies> policiesCache() {
         return pulsar().getConfigurationCache().policiesCache();
     }
 
-    ZooKeeperDataCache<LocalPolicies> localPoliciesCache() {
+    protected ZooKeeperDataCache<LocalPolicies> localPoliciesCache() {
         return pulsar().getLocalZkCacheService().policiesCache();
     }
 
-    ZooKeeperDataCache<ClusterData> clustersCache() {
+    protected ZooKeeperDataCache<ClusterData> clustersCache() {
         return pulsar().getConfigurationCache().clustersCache();
     }
 
-    ZooKeeperChildrenCache managedLedgerListCache() {
+    protected ZooKeeperChildrenCache managedLedgerListCache() {
         return pulsar().getLocalZkCacheService().managedLedgerListCache();
     }
 
-    Set<String> clusters() {
+    protected Set<String> clusters() {
         try {
             return pulsar().getConfigurationCache().clustersListCache().get();
         } catch (Exception e) {
@@ -277,7 +338,7 @@ public abstract class AdminResource extends PulsarWebResource {
         }
     }
 
-    ZooKeeperChildrenCache clustersListCache() {
+    protected ZooKeeperChildrenCache clustersListCache() {
         return pulsar().getConfigurationCache().clustersListCache();
     }
 
@@ -297,32 +358,30 @@ public abstract class AdminResource extends PulsarWebResource {
         return pulsar().getConfigurationCache().failureDomainListCache();
     }
 
-    protected PartitionedTopicMetadata getPartitionedTopicMetadata(String property, String cluster, String namespace,
-            String destination, boolean authoritative) {
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateClusterOwnership(dn.getCluster());
+    protected PartitionedTopicMetadata getPartitionedTopicMetadata(DestinationName destinationName,
+            boolean authoritative) {
+        validateClusterOwnership(destinationName.getCluster());
         // validates global-namespace contains local/peer cluster: if peer/local cluster present then lookup can
         // serve/redirect request else fail partitioned-metadata-request so, client fails while creating
         // producer/consumer
-        validateGlobalNamespaceOwnership(dn.getNamespaceObject());
-        
+        validateGlobalNamespaceOwnership(destinationName.getNamespaceObject());
+
         try {
-            checkConnect(dn);
+            checkConnect(destinationName);
         } catch (WebApplicationException e) {
-            validateAdminAccessOnProperty(dn.getProperty());
+            validateAdminAccessOnProperty(destinationName.getProperty());
         } catch (Exception e) {
             // unknown error marked as internal server error
-            log.warn("Unexpected error while authorizing lookup. destination={}, role={}. Error: {}", destination,
+            log.warn("Unexpected error while authorizing lookup. destination={}, role={}. Error: {}", destinationName,
                     clientAppId(), e.getMessage(), e);
             throw new RestException(e);
         }
 
-        String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(),
-                dn.getEncodedLocalName());
+        String path = path(PARTITIONED_TOPIC_PATH_ZNODE, namespaceName.toString(), domain(), destinationName.getEncodedLocalName());
         PartitionedTopicMetadata partitionMetadata = fetchPartitionedTopicMetadata(pulsar(), path);
 
         if (log.isDebugEnabled()) {
-            log.debug("[{}] Total number of partitions for topic {} is {}", clientAppId(), dn,
+            log.debug("[{}] Total number of partitions for topic {} is {}", clientAppId(), destinationName,
                     partitionMetadata.partitions);
         }
         return partitionMetadata;
@@ -339,8 +398,8 @@ public abstract class AdminResource extends PulsarWebResource {
         }
     }
 
-    protected static CompletableFuture<PartitionedTopicMetadata> fetchPartitionedTopicMetadataAsync(PulsarService pulsar,
-            String path) {
+    protected static CompletableFuture<PartitionedTopicMetadata> fetchPartitionedTopicMetadataAsync(
+            PulsarService pulsar, String path) {
         CompletableFuture<PartitionedTopicMetadata> metadataFuture = new CompletableFuture<>();
         try {
             // gets the number of partitions from the zk cache
@@ -375,4 +434,22 @@ public abstract class AdminResource extends PulsarWebResource {
             throw new RestException(e);
         }
     }
+
+    protected Policies getNamespacePolicies(String property, String cluster, String namespace) {
+        try {
+            Policies policies = policiesCache().get(AdminResource.path(POLICIES, property, cluster, namespace))
+                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace does not exist"));
+            // fetch bundles from LocalZK-policies
+            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
+                    .getBundles(NamespaceName.get(property, cluster, namespace));
+            BundlesData bundleData = NamespaceBundleFactory.getBundlesData(bundles);
+            policies.bundles = bundleData != null ? bundleData : policies.bundles;
+            return policies;
+        } catch (RestException re) {
+            throw re;
+        } catch (Exception e) {
+            log.error("[{}] Failed to get namespace policies {}/{}/{}", clientAppId(), property, cluster, namespace, e);
+            throw new RestException(e);
+        }
+    }
 }
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Namespaces.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Namespaces.java
deleted file mode 100644
index e2467f9..0000000
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Namespaces.java
+++ /dev/null
@@ -1,1757 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.pulsar.broker.admin;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.commons.lang3.StringUtils.isBlank;
-import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
-import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES_ROOT;
-import static org.apache.pulsar.broker.cache.LocalZooKeeperCacheService.LOCAL_POLICIES_ROOT;
-
-import java.net.URI;
-import java.net.URL;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-
-import org.apache.pulsar.broker.PulsarServerException;
-import org.apache.pulsar.broker.ServiceConfiguration;
-import org.apache.pulsar.broker.service.BrokerServiceException.SubscriptionBusyException;
-import org.apache.pulsar.broker.service.Subscription;
-import org.apache.pulsar.broker.service.Topic;
-import org.apache.pulsar.broker.service.persistent.PersistentReplicator;
-import org.apache.pulsar.broker.service.persistent.PersistentTopic;
-import org.apache.pulsar.broker.web.RestException;
-import org.apache.pulsar.client.admin.PulsarAdminException;
-import org.apache.pulsar.common.naming.DestinationName;
-import org.apache.pulsar.common.naming.NamedEntity;
-import org.apache.pulsar.common.naming.NamespaceBundle;
-import org.apache.pulsar.common.naming.NamespaceBundleFactory;
-import org.apache.pulsar.common.naming.NamespaceBundles;
-import org.apache.pulsar.common.naming.NamespaceName;
-import org.apache.pulsar.common.policies.data.AuthAction;
-import org.apache.pulsar.common.policies.data.BacklogQuota;
-import org.apache.pulsar.common.policies.data.BacklogQuota.BacklogQuotaType;
-import org.apache.pulsar.common.policies.data.BundlesData;
-import org.apache.pulsar.common.policies.data.ClusterData;
-import org.apache.pulsar.common.policies.data.DispatchRate;
-import org.apache.pulsar.common.policies.data.PersistencePolicies;
-import org.apache.pulsar.common.policies.data.Policies;
-import org.apache.pulsar.common.policies.data.RetentionPolicies;
-import org.apache.pulsar.common.policies.data.SubscriptionAuthMode;
-import org.apache.pulsar.common.util.FutureUtil;
-import org.apache.zookeeper.KeeperException;
-import org.apache.zookeeper.KeeperException.NoNodeException;
-import org.apache.zookeeper.data.Stat;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Sets.SetView;
-
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-
-@Path("/namespaces")
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-@Api(value = "/namespaces", description = "Namespaces admin apis", tags = "namespaces")
-public class Namespaces extends AdminResource {
-
-    public static final String GLOBAL_CLUSTER = "global";
-    private static final long MAX_BUNDLES = ((long) 1) << 32;
-
-    @GET
-    @Path("/{property}")
-    @ApiOperation(value = "Get the list of all the namespaces for a certain property.", response = String.class, responseContainer = "Set")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property doesn't exist") })
-    public List<String> getPropertyNamespaces(@PathParam("property") String property) {
-        validateAdminAccessOnProperty(property);
-
-        try {
-            return getListOfNamespaces(property);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to get namespace list for propery: {} - Does not exist", clientAppId(), property);
-            throw new RestException(Status.NOT_FOUND, "Property does not exist");
-        } catch (Exception e) {
-            log.error("[{}] Failed to get namespaces list: {}", clientAppId(), e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}")
-    @ApiOperation(value = "Get the list of all the namespaces for a certain property on single cluster.", response = String.class, responseContainer = "Set")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster doesn't exist") })
-    public List<String> getNamespacesForCluster(@PathParam("property") String property,
-            @PathParam("cluster") String cluster) {
-        validateAdminAccessOnProperty(property);
-        List<String> namespaces = Lists.newArrayList();
-        if (!clusters().contains(cluster)) {
-            log.warn("[{}] Failed to get namespace list for property: {}/{} - Cluster does not exist", clientAppId(),
-                    property, cluster);
-            throw new RestException(Status.NOT_FOUND, "Cluster does not exist");
-        }
-
-        try {
-            for (String namespace : globalZk().getChildren(path(POLICIES, property, cluster), false)) {
-                namespaces.add(String.format("%s/%s/%s", property, cluster, namespace));
-            }
-        } catch (KeeperException.NoNodeException e) {
-            // NoNode means there are no namespaces for this property on the specified cluster, returning empty list
-        } catch (Exception e) {
-            log.error("[{}] Failed to get namespaces list: {}", clientAppId(), e);
-            throw new RestException(e);
-        }
-
-        namespaces.sort(null);
-        return namespaces;
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/destinations")
-    @ApiOperation(value = "Get the list of all the destinations under a certain namespace.", response = String.class, responseContainer = "Set")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
-    public List<String> getDestinations(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-
-        // Validate that namespace exists, throws 404 if it doesn't exist
-        getNamespacePolicies(property, cluster, namespace);
-
-        try {
-            return pulsar().getNamespaceService().getListOfDestinations(property, cluster, namespace);
-        } catch (Exception e) {
-            log.error("Failed to get topics list for namespace {}/{}/{}", property, cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}")
-    @ApiOperation(value = "Get the dump all the policies specified for a namespace.", response = Policies.class)
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
-    public Policies getPolicies(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-
-        return getNamespacePolicies(property, cluster, namespace);
-    }
-
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}")
-    @ApiOperation(value = "Creates a new empty namespace with no policies attached.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Namespace already exists"),
-            @ApiResponse(code = 412, message = "Namespace name is not valid") })
-    public void createNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, BundlesData initialBundles) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-        // If the namespace is non global, make sure property has the access on the cluster. For global namespace, same
-        // check is made at the time of setting replication.
-        if (!cluster.equals(GLOBAL_CLUSTER)) {
-            validateClusterForProperty(property, cluster);
-        }
-        if (!clusters().contains(cluster)) {
-            log.warn("[{}] Failed to create namespace. Cluster {} does not exist", clientAppId(), cluster);
-            throw new RestException(Status.NOT_FOUND, "Cluster does not exist");
-        }
-        try {
-            checkNotNull(propertiesCache().get(path(POLICIES, property)));
-        } catch (NoNodeException nne) {
-            log.warn("[{}] Failed to create namespace. Property {} does not exist", clientAppId(), property);
-            throw new RestException(Status.NOT_FOUND, "Property does not exist");
-        } catch (RestException e) {
-            throw e;
-        } catch (Exception e) {
-            throw new RestException(e);
-        }
-        try {
-            NamedEntity.checkName(namespace);
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            Policies policies = new Policies();
-            if (initialBundles != null && initialBundles.getNumBundles() > 0) {
-                if (initialBundles.getBoundaries() == null || initialBundles.getBoundaries().size() == 0) {
-                    policies.bundles = getBundles(initialBundles.getNumBundles());
-                } else {
-                    policies.bundles = validateBundlesData(initialBundles);
-                }
-            } else {
-                int defaultNumberOfBundles = config().getDefaultNumberOfNamespaceBundles();
-                policies.bundles = getBundles(defaultNumberOfBundles);
-            }
-
-            zkCreateOptimistic(path(POLICIES, property, cluster, namespace),
-                    jsonMapper().writeValueAsBytes(policies));
-            log.info("[{}] Created namespace {}/{}/{}", clientAppId(), property, cluster, namespace);
-        } catch (KeeperException.NodeExistsException e) {
-            log.warn("[{}] Failed to create namespace {}/{}/{} - already exists", clientAppId(), property, cluster,
-                    namespace);
-            throw new RestException(Status.CONFLICT, "Namespace already exists");
-        } catch (IllegalArgumentException e) {
-            log.warn("[{}] Failed to create namespace with invalid name {}", clientAppId(), property, e);
-            throw new RestException(Status.PRECONDITION_FAILED, "Namespace name is not valid");
-        } catch (Exception e) {
-            log.error("[{}] Failed to create namespace {}/{}/{}", clientAppId(), property, cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}")
-    @ApiOperation(value = "Delete a namespace and all the destinations under it.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Namespace is not empty") })
-    public void deleteNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        // ensure that non-global namespace is directed to the correct cluster
-        validateClusterOwnership(cluster);
-
-        Entry<Policies, Stat> policiesNode = null;
-        Policies policies = null;
-
-        // ensure the local cluster is the only cluster for the global namespace configuration
-        try {
-            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist."));
-
-            policies = policiesNode.getKey();
-            if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-                if (policies.replication_clusters.size() > 1) {
-                    // There are still more than one clusters configured for the global namespace
-                    throw new RestException(Status.PRECONDITION_FAILED, "Cannot delete the global namespace " + nsName
-                            + ". There are still more than one replication clusters configured.");
-                }
-                if (policies.replication_clusters.size() == 1
-                        && !policies.replication_clusters.contains(config().getClusterName())) {
-                    // the only replication cluster is other cluster, redirect
-                    String replCluster = policies.replication_clusters.get(0);
-                    ClusterData replClusterData = clustersCache().get(AdminResource.path("clusters", replCluster))
-                            .orElseThrow(() -> new RestException(Status.NOT_FOUND,
-                                    "Cluser " + replCluster + " does not exist"));
-                    URL replClusterUrl;
-                    if (!config().isTlsEnabled()) {
-                        replClusterUrl = new URL(replClusterData.getServiceUrl());
-                    } else if (!replClusterData.getServiceUrlTls().isEmpty()) {
-                        replClusterUrl = new URL(replClusterData.getServiceUrlTls());
-                    } else {
-                        throw new RestException(Status.PRECONDITION_FAILED,
-                                "The replication cluster does not provide TLS encrypted service");
-                    }
-                    URI redirect = UriBuilder.fromUri(uri.getRequestUri()).host(replClusterUrl.getHost())
-                            .port(replClusterUrl.getPort()).replaceQueryParam("authoritative", false).build();
-                    log.debug("[{}] Redirecting the rest call to {}: cluster={}", clientAppId(), redirect, cluster);
-                    throw new WebApplicationException(Response.temporaryRedirect(redirect).build());
-                }
-            }
-        } catch (WebApplicationException wae) {
-            throw wae;
-        } catch (Exception e) {
-            throw new RestException(e);
-        }
-
-        List<String> destinations = getDestinations(property, cluster, namespace);
-        if (!destinations.isEmpty()) {
-            log.info("Found destinations: {}", destinations);
-            throw new RestException(Status.CONFLICT, "Cannot delete non empty namespace");
-        }
-
-        // set the policies to deleted so that somebody else cannot acquire this namespace
-        try {
-            policies.deleted = true;
-            globalZk().setData(path(POLICIES, property, cluster, namespace), jsonMapper().writeValueAsBytes(policies),
-                    policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-        } catch (Exception e) {
-            log.error("[{}] Failed to delete namespace on global ZK {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
-            throw new RestException(e);
-        }
-
-        // remove from owned namespace map and ephemeral node from ZK
-        try {
-            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory().getBundles(nsName);
-            for (NamespaceBundle bundle : bundles.getBundles()) {
-                // check if the bundle is owned by any broker, if not then we do not need to delete the bundle
-                if (pulsar().getNamespaceService().getOwner(bundle).isPresent()) {
-                    pulsar().getAdminClient().namespaces().deleteNamespaceBundle(nsName.toString(),
-                            bundle.getBundleRange());
-                }
-            }
-
-            // we have successfully removed all the ownership for the namespace, the policies znode can be deleted now
-            final String globalZkPolicyPath = path(POLICIES, property, cluster, namespace);
-            final String lcaolZkPolicyPath = joinPath(LOCAL_POLICIES_ROOT, property, cluster, namespace);
-            globalZk().delete(globalZkPolicyPath, -1);
-            localZk().delete(lcaolZkPolicyPath, -1);
-            policiesCache().invalidate(globalZkPolicyPath);
-            localCacheService().policiesCache().invalidate(lcaolZkPolicyPath);
-        } catch (PulsarAdminException cae) {
-            throw new RestException(cae);
-        } catch (Exception e) {
-            log.error(String.format("[%s] Failed to remove owned namespace %s/%s/%s", clientAppId(), property, cluster,
-                    namespace), e);
-            // avoid throwing exception in case of the second failure
-        }
-
-    }
-
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/{bundle}")
-    @ApiOperation(value = "Delete a namespace bundle and all the destinations under it.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Namespace bundle is not empty") })
-    public void deleteNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        // ensure that non-global namespace is directed to the correct cluster
-        validateClusterOwnership(cluster);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        // ensure the local cluster is the only cluster for the global namespace configuration
-        try {
-            if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-                if (policies.replication_clusters.size() > 1) {
-                    // There are still more than one clusters configured for the global namespace
-                    throw new RestException(Status.PRECONDITION_FAILED, "Cannot delete the global namespace " + nsName
-                            + ". There are still more than one replication clusters configured.");
-                }
-                if (policies.replication_clusters.size() == 1
-                        && !policies.replication_clusters.contains(config().getClusterName())) {
-                    // the only replication cluster is other cluster, redirect
-                    String replCluster = policies.replication_clusters.get(0);
-                    ClusterData replClusterData = clustersCache().get(AdminResource.path("clusters", replCluster))
-                            .orElseThrow(() -> new RestException(Status.NOT_FOUND,
-                                    "Cluser " + replCluster + " does not exist"));
-                    URL replClusterUrl;
-                    if (!config().isTlsEnabled()) {
-                        replClusterUrl = new URL(replClusterData.getServiceUrl());
-                    } else if (!replClusterData.getServiceUrlTls().isEmpty()) {
-                        replClusterUrl = new URL(replClusterData.getServiceUrlTls());
-                    } else {
-                        throw new RestException(Status.PRECONDITION_FAILED,
-                                "The replication cluster does not provide TLS encrypted service");
-                    }
-                    URI redirect = UriBuilder.fromUri(uri.getRequestUri()).host(replClusterUrl.getHost())
-                            .port(replClusterUrl.getPort()).replaceQueryParam("authoritative", false).build();
-                    log.debug("[{}] Redirecting the rest call to {}: cluster={}", clientAppId(), redirect, cluster);
-                    throw new WebApplicationException(Response.temporaryRedirect(redirect).build());
-                }
-            }
-        } catch (WebApplicationException wae) {
-            throw wae;
-        } catch (Exception e) {
-            throw new RestException(e);
-        }
-
-        NamespaceBundle bundle = validateNamespaceBundleOwnership(nsName, policies.bundles, bundleRange, authoritative,
-                true);
-        try {
-            List<String> destinations = getDestinations(property, cluster, namespace);
-            for (String destination : destinations) {
-                NamespaceBundle destinationBundle = (NamespaceBundle) pulsar().getNamespaceService()
-                        .getBundle(DestinationName.get(destination));
-                if (bundle.equals(destinationBundle)) {
-                    throw new RestException(Status.CONFLICT, "Cannot delete non empty bundle");
-                }
-            }
-
-            // remove from owned namespace map and ephemeral node from ZK
-            pulsar().getNamespaceService().removeOwnedServiceUnit(bundle);
-        } catch (WebApplicationException wae) {
-            throw wae;
-        } catch (Exception e) {
-            log.error("[{}] Failed to remove namespace bundle {}/{}", clientAppId(), nsName.toString(), bundleRange, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/permissions")
-    @ApiOperation(value = "Retrieve the permissions for a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Namespace is not empty") })
-    public Map<String, Set<AuthAction>> getPermissions(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        return policies.auth_policies.namespace_auth;
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/permissions/{role}")
-    @ApiOperation(value = "Grant a new permission to a role on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void grantPermissionOnNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("role") String role, Set<AuthAction> actions) {
-        validateAdminAccessOnProperty(property);
-
-        NamespaceName namespaceName = NamespaceName.get(property, cluster, namespace);
-        try {
-            pulsar().getBrokerService().getAuthorizationService()
-                    .grantPermissionAsync(namespaceName, actions, role, null/*additional auth-data json*/)
-                    .get();
-        } catch (InterruptedException e) {
-            log.error("[{}] Failed to get permissions for namespace {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
-            throw new RestException(e);
-        } catch (ExecutionException e) {
-            if (e.getCause() instanceof IllegalArgumentException) {
-                log.warn("[{}] Failed to set permissions for namespace {}/{}/{}: does not exist", clientAppId(),
-                        property, cluster, namespace);
-                throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-            } else if (e.getCause() instanceof IllegalStateException) {
-                log.warn("[{}] Failed to set permissions for namespace {}/{}/{}: concurrent modification",
-                        clientAppId(), property, cluster, namespace);
-                throw new RestException(Status.CONFLICT, "Concurrent modification");
-            } else {
-                log.error("[{}] Failed to get permissions for namespace {}/{}/{}", clientAppId(), property, cluster,
-                        namespace, e);
-                throw new RestException(e);
-            }
-        }
-    }
-
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/permissions/{role}")
-    @ApiOperation(value = "Revoke all permissions to a role on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
-    public void revokePermissionsOnNamespace(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("role") String role) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        try {
-            Stat nodeStat = new Stat();
-            byte[] content = globalZk().getData(path(POLICIES, property, cluster, namespace), null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            policies.auth_policies.namespace_auth.remove(role);
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace), jsonMapper().writeValueAsBytes(policies),
-                    nodeStat.getVersion());
-
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully revoked access for role {} - namespace {}/{}/{}", clientAppId(), role, property,
-                    cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to revoke permissions for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to revoke permissions on namespace {}/{}/{}: concurrent modification", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to revoke permissions on namespace {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/replication")
-    @ApiOperation(value = "Get the replication clusters for a namespace.", response = String.class, responseContainer = "List")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 412, message = "Namespace is not global") })
-    public List<String> getNamespaceReplicationClusters(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-
-        if (!cluster.equals("global")) {
-            throw new RestException(Status.PRECONDITION_FAILED,
-                    "Cannot get the replication clusters for a non-global namespace");
-        }
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        return policies.replication_clusters;
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/replication")
-    @ApiOperation(value = "Set the replication clusters for a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Peer-cluster can't be part of replication-cluster"),
-            @ApiResponse(code = 412, message = "Namespace is not global or invalid cluster ids") })
-    public void setNamespaceReplicationClusters(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, List<String> clusterIds) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        Set<String> replicationClusterSet = Sets.newHashSet(clusterIds);
-        if (!cluster.equals("global")) {
-            throw new RestException(Status.PRECONDITION_FAILED, "Cannot set replication on a non-global namespace");
-        }
-
-        if (replicationClusterSet.contains("global")) {
-            throw new RestException(Status.PRECONDITION_FAILED,
-                    "Cannot specify global in the list of replication clusters");
-        }
-
-        Set<String> clusters = clusters();
-        for (String clusterId : replicationClusterSet) {
-            if (!clusters.contains(clusterId)) {
-                throw new RestException(Status.FORBIDDEN, "Invalid cluster id: " + clusterId);
-            }
-            validatePeerClusterConflict(clusterId, replicationClusterSet);
-        }
-
-        for (String clusterId : replicationClusterSet) {
-            validateClusterForProperty(property, clusterId);
-        }
-
-        Entry<Policies, Stat> policiesNode = null;
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-
-        try {
-            // Force to read the data s.t. the watch to the cache content is setup.
-            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
-            policiesNode.getKey().replication_clusters = clusterIds;
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace),
-                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-
-            log.info("[{}] Successfully updated the replication clusters on namespace {}/{}/{}", clientAppId(),
-                    property, cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update the replication clusters for namespace {}/{}/{}: does not exist",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn(
-                    "[{}] Failed to update the replication clusters on namespace {}/{}/{} expected policy node version={} : concurrent modification",
-                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
-
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to update the replication clusters on namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/messageTTL")
-    @ApiOperation(value = "Get the message TTL for the namespace")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
-    public int getNamespaceMessageTTL(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        return policies.message_ttl_in_seconds;
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/messageTTL")
-    @ApiOperation(value = "Set message TTL in seconds for namespace")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 412, message = "Invalid TTL") })
-    public void setNamespaceMessageTTL(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, int messageTTL) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        if (messageTTL < 0) {
-            throw new RestException(Status.PRECONDITION_FAILED, "Invalid value for message TTL");
-        }
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        Entry<Policies, Stat> policiesNode = null;
-
-        try {
-            // Force to read the data s.t. the watch to the cache content is setup.
-            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
-            policiesNode.getKey().message_ttl_in_seconds = messageTTL;
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace),
-                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-
-            log.info("[{}] Successfully updated the message TTL on namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update the message TTL for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn(
-                    "[{}] Failed to update the message TTL on namespace {}/{}/{} expected policy node version={} : concurrent modification",
-                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
-
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to update the message TTL on namespace {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/antiAffinity")
-    @ApiOperation(value = "Set anti-affinity group for a namespace")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 412, message = "Invalid antiAffinityGroup") })
-    public void setNamespaceAntiAffinityGroup(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, String antiAffinityGroup) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-        
-        log.info("[{}] Setting anti-affinity group {} for {}/{}/{}", clientAppId(), antiAffinityGroup, property,
-                cluster, namespace);
-
-        if (isBlank(antiAffinityGroup)) {
-            throw new RestException(Status.PRECONDITION_FAILED, "antiAffinityGroup can't be empty");
-        }
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        Entry<Policies, Stat> policiesNode = null;
-
-        try {
-            // Force to read the data s.t. the watch to the cache content is setup.
-            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
-            policiesNode.getKey().antiAffinityGroup = antiAffinityGroup;
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace),
-                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-
-            log.info("[{}] Successfully updated the antiAffinityGroup {} on namespace {}/{}/{}", clientAppId(),
-                    antiAffinityGroup, property, cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update the antiAffinityGroup for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn(
-                    "[{}] Failed to update the antiAffinityGroup on namespace {}/{}/{} expected policy node version={} : concurrent modification",
-                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
-
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to update the antiAffinityGroup on namespace {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/antiAffinity")
-    @ApiOperation(value = "Get anti-affinity group of a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
-    public String getNamespaceAntiAffinityGroup(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-        return getNamespacePolicies(property, cluster, namespace).antiAffinityGroup;
-    }
-
-    @GET
-    @Path("{cluster}/antiAffinity/{group}")
-    @ApiOperation(value = "Get all namespaces that are grouped by given anti-affinity group in a given cluster. api can be only accessed by admin of any of the existing property")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 412, message = "Cluster not exist/Anti-affinity group can't be empty.") })
-    public List<String> getAntiAffinityNamespaces(@PathParam("cluster") String cluster,
-            @PathParam("group") String antiAffinityGroup, @QueryParam("property") String property) {
-        validateAdminAccessOnProperty(property);
-
-        log.info("[{}]-{} Finding namespaces for {} in {}", clientAppId(), property, antiAffinityGroup, cluster);
-
-        if (isBlank(antiAffinityGroup)) {
-            throw new RestException(Status.PRECONDITION_FAILED, "anti-affinity group can't be empty.");
-        }
-        validateClusterExists(cluster);
-        List<String> namespaces = Lists.newArrayList();
-        try {
-            for (String prop : globalZk().getChildren(POLICIES_ROOT, false)) {
-                for (String namespace : globalZk().getChildren(path(POLICIES, prop, cluster), false)) {
-                    Optional<Policies> policies = policiesCache()
-                            .get(AdminResource.path(POLICIES, prop, cluster, namespace));
-                    if (policies.isPresent() && antiAffinityGroup.equalsIgnoreCase(policies.get().antiAffinityGroup)) {
-                        namespaces.add(String.format("%s/%s/%s", prop, cluster, namespace));
-                    }
-                }
-            }
-        } catch (Exception e) {
-            log.warn("Failed to list of properties/namespace from global-zk", e);
-        }
-        return namespaces;
-    }
-    
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/antiAffinity")
-    @ApiOperation(value = "Remove anti-affinity group of a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void removeNamespaceAntiAffinityGroup(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        log.info("[{}] Deleting anti-affinity group for {}/{}/{}", clientAppId(), property, cluster, namespace);
-        
-        try {
-            Stat nodeStat = new Stat();
-            final String path = path(POLICIES, property, cluster, namespace);
-            byte[] content = globalZk().getData(path, null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            policies.antiAffinityGroup = null;
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully removed anti-affinity group for a namespace={}/{}/{}", clientAppId(), property,
-                    cluster, namespace);
-
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to remove anti-affinity group for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to remove anti-affinity group for namespace {}/{}/{}: concurrent modification",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to remove anti-affinity group for namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/deduplication")
-    @ApiOperation(value = "Enable or disable broker side deduplication for all topics in a namespace")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
-    public void modifyDeduplication(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, boolean enableDeduplication) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        Entry<Policies, Stat> policiesNode = null;
-
-        try {
-            // Force to read the data s.t. the watch to the cache content is setup.
-            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
-            policiesNode.getKey().deduplicationEnabled = enableDeduplication;
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace),
-                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-
-            log.info("[{}] Successfully {} on namespace {}/{}/{}", clientAppId(),
-                    enableDeduplication ? "enabled" : "disabled", property, cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to modify deplication status for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn(
-                    "[{}] Failed to modify deplication status on namespace {}/{}/{} expected policy node version={} : concurrent modification",
-                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
-
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to modify deplication status on namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/bundles")
-    @ApiOperation(value = "Get the bundles split data.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 412, message = "Namespace is not setup to split in bundles") })
-    public BundlesData getBundlesData(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-
-        return policies.bundles;
-    }
-
-    private BundlesData validateBundlesData(BundlesData initialBundles) {
-        SortedSet<String> partitions = new TreeSet<String>();
-        for (String partition : initialBundles.getBoundaries()) {
-            Long partBoundary = Long.decode(partition);
-            partitions.add(String.format("0x%08x", partBoundary));
-        }
-        if (partitions.size() != initialBundles.getBoundaries().size()) {
-            log.debug("Input bundles included repeated partition points. Ignored.");
-        }
-        try {
-            NamespaceBundleFactory.validateFullRange(partitions);
-        } catch (IllegalArgumentException iae) {
-            throw new RestException(Status.BAD_REQUEST, "Input bundles do not cover the whole hash range. first:"
-                    + partitions.first() + ", last:" + partitions.last());
-        }
-        List<String> bundles = Lists.newArrayList();
-        bundles.addAll(partitions);
-        return new BundlesData(bundles);
-    }
-
-    private BundlesData getBundles(int numBundles) {
-        if (numBundles <= 0 || numBundles > MAX_BUNDLES) {
-            throw new RestException(Status.BAD_REQUEST,
-                    "Invalid number of bundles. Number of numbles has to be in the range of (0, 2^32].");
-        }
-        Long maxVal = ((long) 1) << 32;
-        Long segSize = maxVal / numBundles;
-        List<String> partitions = Lists.newArrayList();
-        partitions.add(String.format("0x%08x", 0l));
-        Long curPartition = segSize;
-        for (int i = 0; i < numBundles; i++) {
-            if (i != numBundles - 1) {
-                partitions.add(String.format("0x%08x", curPartition));
-            } else {
-                partitions.add(String.format("0x%08x", maxVal - 1));
-            }
-            curPartition += segSize;
-        }
-        return new BundlesData(partitions);
-    }
-
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}/unload")
-    @ApiOperation(value = "Unload namespace", notes = "Unload an active namespace from the current broker serving it. Performing this operation will let the broker"
-            + "removes all producers, consumers, and connections using this namespace, and close all destinations (including"
-            + "their persistent store). During that operation, the namespace is marked as tentatively unavailable until the"
-            + "broker completes the unloading action. This operation requires strictly super user privileges, since it would"
-            + "result in non-persistent message loss and unexpected connection closure to the clients.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 412, message = "Namespace is already unloaded or Namespace has bundles activated") })
-    public void unloadNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        log.info("[{}] Unloading namespace {}/{}/{}", clientAppId(), property, cluster, namespace);
-
-        validateSuperUserAccess();
-
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
-        } else {
-            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-
-        List<String> boundaries = policies.bundles.getBoundaries();
-        for (int i = 0; i < boundaries.size() - 1; i++) {
-            String bundle = String.format("%s_%s", boundaries.get(i), boundaries.get(i + 1));
-            try {
-                pulsar().getAdminClient().namespaces().unloadNamespaceBundle(nsName.toString(), bundle);
-            } catch (PulsarServerException | PulsarAdminException e) {
-                log.error(String.format("[%s] Failed to unload namespace %s/%s/%s", clientAppId(), property, cluster,
-                        namespace), e);
-                throw new RestException(e);
-            }
-        }
-        log.info("[{}] Successfully unloaded all the bundles in namespace {}/{}/{}", clientAppId(), property, cluster,
-                namespace);
-    }
-
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}/{bundle}/unload")
-    @ApiOperation(value = "Unload a namespace bundle")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
-    public void unloadNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        log.info("[{}] Unloading namespace bundle {}/{}/{}/{}", clientAppId(), property, cluster, namespace,
-                bundleRange);
-
-        validateSuperUserAccess();
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
-        } else {
-            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-
-        NamespaceName fqnn = NamespaceName.get(property, cluster, namespace);
-        validatePoliciesReadOnlyAccess();
-
-        if (!isBundleOwnedByAnyBroker(fqnn, policies.bundles, bundleRange)) {
-            log.info("[{}] Namespace bundle is not owned by any broker {}/{}/{}/{}", clientAppId(), property, cluster,
-                    namespace, bundleRange);
-            return;
-        }
-
-        NamespaceBundle nsBundle = validateNamespaceBundleOwnership(fqnn, policies.bundles, bundleRange, authoritative,
-                true);
-        try {
-            pulsar().getNamespaceService().unloadNamespaceBundle(nsBundle);
-            log.info("[{}] Successfully unloaded namespace bundle {}", clientAppId(), nsBundle.toString());
-        } catch (Exception e) {
-            log.error("[{}] Failed to unload namespace bundle {}/{}", clientAppId(), fqnn.toString(), bundleRange, e);
-            throw new RestException(e);
-        }
-    }
-
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}/{bundle}/split")
-    @ApiOperation(value = "Split a namespace bundle")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
-    public void splitNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative,
-            @QueryParam("unload") @DefaultValue("false") boolean unload) {
-        log.info("[{}] Split namespace bundle {}/{}/{}/{}", clientAppId(), property, cluster, namespace, bundleRange);
-
-        validateSuperUserAccess();
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
-        } else {
-            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-
-        NamespaceName fqnn = NamespaceName.get(property, cluster, namespace);
-        validatePoliciesReadOnlyAccess();
-        NamespaceBundle nsBundle = validateNamespaceBundleOwnership(fqnn, policies.bundles, bundleRange, authoritative,
-                true);
-
-        try {
-            pulsar().getNamespaceService().splitAndOwnBundle(nsBundle, unload).get();
-            log.info("[{}] Successfully split namespace bundle {}", clientAppId(), nsBundle.toString());
-        } catch (IllegalArgumentException e) {
-            log.error("[{}] Failed to split namespace bundle {}/{} due to {}", clientAppId(), fqnn.toString(),
-                    bundleRange, e.getMessage());
-            throw new RestException(Status.PRECONDITION_FAILED, "Split bundle failed due to invalid request");
-        } catch (Exception e) {
-            log.error("[{}] Failed to split namespace bundle {}/{}", clientAppId(), fqnn.toString(), bundleRange, e);
-            throw new RestException(e);
-        }
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/dispatchRate")
-    @ApiOperation(value = "Set dispatch-rate throttling for all topics of the namespace")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
-    public void setDispatchRate(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, DispatchRate dispatchRate) {
-        log.info("[{}] Set namespace dispatch-rate {}/{}/{}/{}", clientAppId(), property, cluster, namespace,
-                dispatchRate);
-        validateSuperUserAccess();
-
-        Entry<Policies, Stat> policiesNode = null;
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-
-        try {
-            final String path = path(POLICIES, property, cluster, namespace);
-            // Force to read the data s.t. the watch to the cache content is setup.
-            policiesNode = policiesCache().getWithStat(path)
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
-            policiesNode.getKey().clusterDispatchRate.put(pulsar().getConfiguration().getClusterName(), dispatchRate);
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policiesNode.getKey()),
-                    policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path);
-
-            log.info("[{}] Successfully updated the dispatchRate for cluster on namespace {}/{}/{}", clientAppId(),
-                    property, cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update the dispatchRate for cluster on namespace {}/{}/{}: does not exist",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn(
-                    "[{}] Failed to update the dispatchRate for cluster on namespace {}/{}/{} expected policy node version={} : concurrent modification",
-                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
-
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to update the dispatchRate for cluster on namespace {}/{}/{}", clientAppId(),
-                    property, cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/dispatchRate")
-    @ApiOperation(value = "Get dispatch-rate configured for the namespace, -1 represents not configured yet")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public DispatchRate getDispatchRate(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        DispatchRate dispatchRate = policies.clusterDispatchRate.get(pulsar().getConfiguration().getClusterName());
-        if (dispatchRate != null) {
-            return dispatchRate;
-        } else {
-            throw new RestException(Status.NOT_FOUND,
-                    "Dispatch-rate is not configured for cluster " + pulsar().getConfiguration().getClusterName());
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/backlogQuotaMap")
-    @ApiOperation(value = "Get backlog quota map on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public Map<BacklogQuotaType, BacklogQuota> getBacklogQuotaMap(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        return policies.backlog_quota_map;
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/backlogQuota")
-    @ApiOperation(value = " Set a backlog quota for all the destinations on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification"),
-            @ApiResponse(code = 412, message = "Specified backlog quota exceeds retention quota. Increase retention quota and retry request") })
-    public void setBacklogQuota(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @QueryParam("backlogQuotaType") BacklogQuotaType backlogQuotaType,
-            BacklogQuota backlogQuota) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        if (backlogQuotaType == null) {
-            backlogQuotaType = BacklogQuotaType.destination_storage;
-        }
-
-        try {
-            Stat nodeStat = new Stat();
-            final String path = path(POLICIES, property, cluster, namespace);
-            byte[] content = globalZk().getData(path, null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            RetentionPolicies r = policies.retention_policies;
-            if (r != null) {
-                Policies p = new Policies();
-                p.backlog_quota_map.put(backlogQuotaType, backlogQuota);
-                if (!checkQuotas(p, r)) {
-                    log.warn(
-                            "[{}] Failed to update backlog configuration for namespace {}/{}/{}: conflicts with retention quota",
-                            clientAppId(), property, cluster, namespace);
-                    throw new RestException(Status.PRECONDITION_FAILED,
-                            "Backlog Quota exceeds configured retention quota for namespace. Please increase retention quota and retry");
-                }
-            }
-            policies.backlog_quota_map.put(backlogQuotaType, backlogQuota);
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully updated backlog quota map: namespace={}/{}/{}, map={}", clientAppId(), property,
-                    cluster, namespace, jsonMapper().writeValueAsString(policies.backlog_quota_map));
-
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update backlog quota map for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to update backlog quota map for namespace {}/{}/{}: concurrent modification",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (RestException pfe) {
-            throw pfe;
-        } catch (Exception e) {
-            log.error("[{}] Failed to update backlog quota map for namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/backlogQuota")
-    @ApiOperation(value = "Remove a backlog quota policy from a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void removeBacklogQuota(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace,
-            @QueryParam("backlogQuotaType") BacklogQuotaType backlogQuotaType) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        if (backlogQuotaType == null) {
-            backlogQuotaType = BacklogQuotaType.destination_storage;
-        }
-
-        try {
-            Stat nodeStat = new Stat();
-            final String path = path(POLICIES, property, cluster, namespace);
-            byte[] content = globalZk().getData(path, null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            policies.backlog_quota_map.remove(backlogQuotaType);
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully removed backlog namespace={}/{}/{}, quota={}", clientAppId(), property, cluster,
-                    namespace, backlogQuotaType);
-
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update backlog quota map for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to update backlog quota map for namespace {}/{}/{}: concurrent modification",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to update backlog quota map for namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/retention")
-    @ApiOperation(value = "Get retention config on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public RetentionPolicies getRetention(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        if (policies.retention_policies == null) {
-            return new RetentionPolicies(config().getDefaultRetentionTimeInMinutes(),
-                    config().getDefaultRetentionSizeInMB());
-        } else {
-            return policies.retention_policies;
-        }
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/retention")
-    @ApiOperation(value = " Set retention configuration on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification"),
-            @ApiResponse(code = 412, message = "Retention Quota must exceed backlog quota") })
-    public void setRetention(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, RetentionPolicies retention) {
-        validatePoliciesReadOnlyAccess();
-
-        try {
-            Stat nodeStat = new Stat();
-            final String path = path(POLICIES, property, cluster, namespace);
-            byte[] content = globalZk().getData(path, null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            if (!checkQuotas(policies, retention)) {
-                log.warn(
-                        "[{}] Failed to update retention configuration for namespace {}/{}/{}: conflicts with backlog quota",
-                        clientAppId(), property, cluster, namespace);
-                throw new RestException(Status.PRECONDITION_FAILED,
-                        "Retention Quota must exceed configured backlog quota for namespace.");
-            }
-            policies.retention_policies = retention;
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully updated retention configuration: namespace={}/{}/{}, map={}", clientAppId(),
-                    property, cluster, namespace, jsonMapper().writeValueAsString(policies.retention_policies));
-
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update retention configuration for namespace {}/{}/{}: does not exist",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to update retention configuration for namespace {}/{}/{}: concurrent modification",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (RestException pfe) {
-            throw pfe;
-        } catch (Exception e) {
-            log.error("[{}] Failed to update retention configuration for namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-
-    }
-
-    private boolean checkQuotas(Policies policies, RetentionPolicies retention) {
-        Map<BacklogQuota.BacklogQuotaType, BacklogQuota> backlog_quota_map = policies.backlog_quota_map;
-        if (backlog_quota_map.isEmpty() || retention.getRetentionSizeInMB() == 0) {
-            return true;
-        }
-        BacklogQuota quota = backlog_quota_map.get(BacklogQuotaType.destination_storage);
-        if (quota == null) {
-            quota = pulsar().getBrokerService().getBacklogQuotaManager().getDefaultQuota();
-        }
-        if (quota.getLimit() >= ((long) retention.getRetentionSizeInMB() * 1024 * 1024)) {
-            return false;
-        }
-        return true;
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/persistence")
-    @ApiOperation(value = "Set the persistence configuration for all the destinations on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification"),
-            @ApiResponse(code = 400, message = "Invalid persistence policies") })
-    public void setPersistence(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, PersistencePolicies persistence) {
-        validatePoliciesReadOnlyAccess();
-        validatePersistencePolicies(persistence);
-
-        try {
-            Stat nodeStat = new Stat();
-            final String path = path(POLICIES, property, cluster, namespace);
-            byte[] content = globalZk().getData(path, null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            policies.persistence = persistence;
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully updated persistence configuration: namespace={}/{}/{}, map={}", clientAppId(),
-                    property, cluster, namespace, jsonMapper().writeValueAsString(policies.persistence));
-
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update persistence configuration for namespace {}/{}/{}: does not exist",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to update persistence configuration for namespace {}/{}/{}: concurrent modification",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to update persistence configuration for namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    private void validatePersistencePolicies(PersistencePolicies persistence) {
-        try {
-            checkNotNull(persistence);
-            final ServiceConfiguration config = pulsar().getConfiguration();
-            checkArgument(persistence.getBookkeeperEnsemble() <= config.getManagedLedgerMaxEnsembleSize(),
-                    "Bookkeeper-Ensemble must be <= %s", config.getManagedLedgerMaxEnsembleSize());
-            checkArgument(persistence.getBookkeeperWriteQuorum() <= config.getManagedLedgerMaxWriteQuorum(),
-                    "Bookkeeper-WriteQuorum must be <= %s", config.getManagedLedgerMaxWriteQuorum());
-            checkArgument(persistence.getBookkeeperAckQuorum() <= config.getManagedLedgerMaxAckQuorum(),
-                    "Bookkeeper-AckQuorum must be <= %s", config.getManagedLedgerMaxAckQuorum());
-            checkArgument(
-                    (persistence.getBookkeeperEnsemble() >= persistence.getBookkeeperWriteQuorum())
-                            && (persistence.getBookkeeperWriteQuorum() >= persistence.getBookkeeperAckQuorum()),
-                    "Bookkeeper Ensemble (%s) >= WriteQuorum (%s) >= AckQuoru (%s)", persistence.getBookkeeperEnsemble(),
-                    persistence.getBookkeeperWriteQuorum(), persistence.getBookkeeperAckQuorum());
-        }catch(NullPointerException | IllegalArgumentException e) {
-            throw new RestException(Status.PRECONDITION_FAILED, e.getMessage());
-        }
-    }
-
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/persistence")
-    @ApiOperation(value = "Get the persistence configuration for a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public PersistencePolicies getPersistence(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-        if (policies.persistence == null) {
-            return new PersistencePolicies(config().getManagedLedgerDefaultEnsembleSize(),
-                    config().getManagedLedgerDefaultWriteQuorum(), config().getManagedLedgerDefaultAckQuorum(), 0.0d);
-        } else {
-            return policies.persistence;
-        }
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/clearBacklog")
-    @ApiOperation(value = "Clear backlog for all destinations on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public void clearNamespaceBacklog(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        validateAdminAccessOnProperty(property);
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        try {
-            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory().getBundles(nsName);
-            Exception exception = null;
-            for (NamespaceBundle nsBundle : bundles.getBundles()) {
-                try {
-                    // check if the bundle is owned by any broker, if not then there is no backlog on this bundle to
-                    // clear
-                    if (pulsar().getNamespaceService().getOwner(nsBundle).isPresent()) {
-                        // TODO: make this admin call asynchronous
-                        pulsar().getAdminClient().namespaces().clearNamespaceBundleBacklog(nsName.toString(),
-                                nsBundle.getBundleRange());
-                    }
-                } catch (Exception e) {
-                    if (exception == null) {
-                        exception = e;
-                    }
-                }
-            }
-            if (exception != null) {
-                if (exception instanceof PulsarAdminException) {
-                    throw new RestException((PulsarAdminException) exception);
-                } else {
-                    throw new RestException(exception.getCause());
-                }
-            }
-        } catch (WebApplicationException wae) {
-            throw wae;
-        } catch (Exception e) {
-            throw new RestException(e);
-        }
-        log.info("[{}] Successfully cleared backlog on all the bundles for namespace {}", clientAppId(),
-                nsName.toString());
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{bundle}/clearBacklog")
-    @ApiOperation(value = "Clear backlog for all destinations on a namespace bundle.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public void clearNamespaceBundleBacklog(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("bundle") String bundleRange,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
-        } else {
-            // check cluster ownership  for a given global namespace: redirect if peer-cluster owns it
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        validateNamespaceBundleOwnership(nsName, policies.bundles, bundleRange, authoritative, true);
-
-        clearBacklog(nsName, bundleRange, null);
-        log.info("[{}] Successfully cleared backlog on namespace bundle {}/{}", clientAppId(), nsName.toString(),
-                bundleRange);
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/clearBacklog/{subscription}")
-    @ApiOperation(value = "Clear backlog for a given subscription on all destinations on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public void clearNamespaceBacklogForSubscription(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("subscription") String subscription,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        validateAdminAccessOnProperty(property);
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        try {
-            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory().getBundles(nsName);
-            Exception exception = null;
-            for (NamespaceBundle nsBundle : bundles.getBundles()) {
-                try {
-                    // check if the bundle is owned by any broker, if not then there is no backlog on this bundle to
-                    // clear
-                    if (pulsar().getNamespaceService().getOwner(nsBundle).isPresent()) {
-                        // TODO: make this admin call asynchronous
-                        pulsar().getAdminClient().namespaces().clearNamespaceBundleBacklogForSubscription(
-                                nsName.toString(), nsBundle.getBundleRange(), subscription);
-                    }
-                } catch (Exception e) {
-                    if (exception == null) {
-                        exception = e;
-                    }
-                }
-            }
-            if (exception != null) {
-                if (exception instanceof PulsarAdminException) {
-                    throw new RestException((PulsarAdminException) exception);
-                } else {
-                    throw new RestException(exception.getCause());
-                }
-            }
-        } catch (WebApplicationException wae) {
-            throw wae;
-        } catch (Exception e) {
-            throw new RestException(e);
-        }
-        log.info("[{}] Successfully cleared backlog for subscription {} on all the bundles for namespace {}",
-                clientAppId(), subscription, nsName.toString());
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{bundle}/clearBacklog/{subscription}")
-    @ApiOperation(value = "Clear backlog for a given subscription on all destinations on a namespace bundle.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public void clearNamespaceBundleBacklogForSubscription(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("subscription") String subscription, @PathParam("bundle") String bundleRange,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
-        } else {
-            // check cluster ownership  for a given global namespace: redirect if peer-cluster owns it
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        validateNamespaceBundleOwnership(nsName, policies.bundles, bundleRange, authoritative, true);
-
-        clearBacklog(nsName, bundleRange, subscription);
-        log.info("[{}] Successfully cleared backlog for subscription {} on namespace bundle {}/{}", clientAppId(),
-                subscription, nsName.toString(), bundleRange);
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/unsubscribe/{subscription}")
-    @ApiOperation(value = "Unsubscribes the given subscription on all destinations on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public void unsubscribeNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        validateAdminAccessOnProperty(property);
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        try {
-            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory().getBundles(nsName);
-            Exception exception = null;
-            for (NamespaceBundle nsBundle : bundles.getBundles()) {
-                try {
-                    // check if the bundle is owned by any broker, if not then there are no subscriptions
-                    if (pulsar().getNamespaceService().getOwner(nsBundle).isPresent()) {
-                        // TODO: make this admin call asynchronous
-                        pulsar().getAdminClient().namespaces().unsubscribeNamespaceBundle(nsName.toString(),
-                                nsBundle.getBundleRange(), subscription);
-                    }
-                } catch (Exception e) {
-                    if (exception == null) {
-                        exception = e;
-                    }
-                }
-            }
-            if (exception != null) {
-                if (exception instanceof PulsarAdminException) {
-                    throw new RestException((PulsarAdminException) exception);
-                } else {
-                    throw new RestException(exception.getCause());
-                }
-            }
-        } catch (WebApplicationException wae) {
-            throw wae;
-        } catch (Exception e) {
-            throw new RestException(e);
-        }
-        log.info("[{}] Successfully unsubscribed {} on all the bundles for namespace {}", clientAppId(), subscription,
-                nsName.toString());
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{bundle}/unsubscribe/{subscription}")
-    @ApiOperation(value = "Unsubscribes the given subscription on all destinations on a namespace bundle.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public void unsubscribeNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
-            @PathParam("bundle") String bundleRange,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        validateAdminAccessOnProperty(property);
-
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
-
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
-        } else {
-            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        validateNamespaceBundleOwnership(nsName, policies.bundles, bundleRange, authoritative, true);
-
-        unsubscribe(nsName, bundleRange, subscription);
-        log.info("[{}] Successfully unsubscribed {} on namespace bundle {}/{}", clientAppId(), subscription,
-                nsName.toString(), bundleRange);
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/subscriptionAuthMode")
-    @ApiOperation(value = " Set a subscription auth mode for all the destinations on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void setSubscriptionAuthMode(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, SubscriptionAuthMode subscriptionAuthMode) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        if (subscriptionAuthMode == null) {
-            subscriptionAuthMode = SubscriptionAuthMode.None;
-        }
-
-        try {
-            Stat nodeStat = new Stat();
-            final String path = path(POLICIES, property, cluster, namespace);
-            byte[] content = globalZk().getData(path, null, nodeStat);
-            Policies policies = jsonMapper().readValue(content, Policies.class);
-            policies.subscription_auth_mode = subscriptionAuthMode;
-            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-            log.info("[{}] Successfully updated subscription auth mode: namespace={}/{}/{}, map={}", clientAppId(), property,
-                    cluster, namespace, jsonMapper().writeValueAsString(policies.backlog_quota_map));
-
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to update subscription auth mode for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn("[{}] Failed to update subscription auth mode for namespace {}/{}/{}: concurrent modification",
-                    clientAppId(), property, cluster, namespace);
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (RestException pfe) {
-            throw pfe;
-        } catch (Exception e) {
-            log.error("[{}] Failed to update subscription auth mode for namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    private void clearBacklog(NamespaceName nsName, String bundleRange, String subscription) {
-        try {
-            List<Topic> topicList = pulsar().getBrokerService()
-                    .getAllTopicsFromNamespaceBundle(nsName.toString(), nsName.toString() + "/" + bundleRange);
-
-            List<CompletableFuture<Void>> futures = Lists.newArrayList();
-            if (subscription != null) {
-                if (subscription.startsWith(pulsar().getConfiguration().getReplicatorPrefix())) {
-                    subscription = PersistentReplicator.getRemoteCluster(subscription);
-                }
-                for (Topic topic : topicList) {
-                    if(topic instanceof PersistentTopic) {
-                        futures.add(((PersistentTopic)topic).clearBacklog(subscription));
-                    }
-                }
-            } else {
-                for (Topic topic : topicList) {
-                    if(topic instanceof PersistentTopic) {
-                        futures.add(((PersistentTopic)topic).clearBacklog());
-                    }
-                }
-            }
-
-            FutureUtil.waitForAll(futures).get();
-        } catch (Exception e) {
-            log.error("[{}] Failed to clear backlog for namespace {}/{}, subscription: {}", clientAppId(),
-                    nsName.toString(), bundleRange, subscription, e);
-            throw new RestException(e);
-        }
-    }
-
-    private void unsubscribe(NamespaceName nsName, String bundleRange, String subscription) {
-        try {
-            List<Topic> topicList = pulsar().getBrokerService()
-                    .getAllTopicsFromNamespaceBundle(nsName.toString(), nsName.toString() + "/" + bundleRange);
-            List<CompletableFuture<Void>> futures = Lists.newArrayList();
-            if (subscription.startsWith(pulsar().getConfiguration().getReplicatorPrefix())) {
-                throw new RestException(Status.PRECONDITION_FAILED, "Cannot unsubscribe a replication cursor");
-            } else {
-                for (Topic topic : topicList) {
-                    Subscription sub = topic.getSubscription(subscription);
-                    if (sub != null) {
-                        futures.add(sub.delete());
-                    }
-                }
-            }
-
-            FutureUtil.waitForAll(futures).get();
-        } catch (RestException re) {
-            throw re;
-        } catch (Exception e) {
-            log.error("[{}] Failed to unsubscribe {} for namespace {}/{}", clientAppId(), subscription,
-                    nsName.toString(), bundleRange, e);
-            if (e.getCause() instanceof SubscriptionBusyException) {
-                throw new RestException(Status.PRECONDITION_FAILED, "Subscription has active connected consumers");
-            }
-            throw new RestException(e.getCause());
-        }
-    }
-
-    /**
-     * It validates that peer-clusters can't coexist in replication-clusters
-     * 
-     * @param clusterName:
-     *            given cluster whose peer-clusters can't be present into replication-cluster list
-     * @param clusters:
-     *            replication-cluster list
-     */
-    private void validatePeerClusterConflict(String clusterName, Set<String> replicationClusters) {
-        try {
-            ClusterData clusterData = clustersCache().get(path("clusters", clusterName)).orElseThrow(
-                    () -> new RestException(Status.PRECONDITION_FAILED, "Invalid replication cluster " + clusterName));
-            Set<String> peerClusters = clusterData.getPeerClusterNames();
-            if (peerClusters != null && !peerClusters.isEmpty()) {
-                SetView<String> conflictPeerClusters = Sets.intersection(peerClusters, replicationClusters);
-                if (!conflictPeerClusters.isEmpty()) {
-                    log.warn("[{}] {}'s peer cluster can't be part of replication clusters {}", clientAppId(),
-                            clusterName, conflictPeerClusters);
-                    throw new RestException(Status.CONFLICT,
-                            String.format("%s's peer-clusters %s can't be part of replication-clusters %s", clusterName,
-                                    conflictPeerClusters, replicationClusters));
-                }
-            }
-        } catch (RestException re) {
-            throw re;
-        } catch (Exception e) {
-            log.warn("[{}] Failed to get cluster-data for {}", clientAppId(), clusterName, e);
-        }
-    }
-    
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/encryptionRequired")
-    @ApiOperation(value = "Message encryption is required or not for all topics in a namespace")
-    @ApiResponses(value = {
-            @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification"),
-    })
-    public void modifyEncryptionRequired(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, boolean encryptionRequired) {
-        validateAdminAccessOnProperty(property);
-        validatePoliciesReadOnlyAccess();
-
-        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
-        Entry<Policies, Stat> policiesNode = null;
-
-        try {
-            // Force to read the data s.t. the watch to the cache content is setup.
-            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
-                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
-            policiesNode.getKey().encryption_required = encryptionRequired;
-
-            // Write back the new policies into zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace),
-                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
-
-            log.info("[{}] Successfully {} on namespace {}/{}/{}", clientAppId(),
-                    encryptionRequired ? "true" : "false", property, cluster, namespace);
-        } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to modify encryption required status for namespace {}/{}/{}: does not exist", clientAppId(),
-                    property, cluster, namespace);
-            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
-        } catch (KeeperException.BadVersionException e) {
-            log.warn(
-                    "[{}] Failed to modify encryption required status on namespace {}/{}/{} expected policy node version={} : concurrent modification",
-                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
-
-            throw new RestException(Status.CONFLICT, "Concurrent modification");
-        } catch (Exception e) {
-            log.error("[{}] Failed to modify encryption required status on namespace {}/{}/{}", clientAppId(), property,
-                    cluster, namespace, e);
-            throw new RestException(e);
-        }
-    }
-
-    private static final Logger log = LoggerFactory.getLogger(Namespaces.class);
-}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/BrokerStats.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/BrokerStatsBase.java
similarity index 96%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/BrokerStats.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/BrokerStatsBase.java
index 8fdd313..549a033 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/BrokerStats.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/BrokerStatsBase.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
+package org.apache.pulsar.broker.admin.impl;
 
 import java.io.OutputStream;
 import java.util.Collection;
@@ -25,13 +25,12 @@ import java.util.Map;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
 import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.StreamingOutput;
 
 import org.apache.bookkeeper.mledger.proto.PendingBookieOpsStats;
+import org.apache.pulsar.broker.admin.AdminResource;
 import org.apache.pulsar.broker.loadbalance.LoadManager;
 import org.apache.pulsar.broker.loadbalance.ResourceUnit;
 import org.apache.pulsar.broker.loadbalance.impl.SimpleLoadManagerImpl;
@@ -47,16 +46,12 @@ import org.apache.pulsar.policies.data.loadbalancer.LoadReport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 
-@Path("/broker-stats")
-@Api(value = "/broker-stats", description = "Stats for broker", tags = "broker-stats")
-@Produces(MediaType.APPLICATION_JSON)
-public class BrokerStats extends AdminResource {
-    private static final Logger log = LoggerFactory.getLogger(BrokerStats.class);
+public class BrokerStatsBase extends AdminResource {
+    private static final Logger log = LoggerFactory.getLogger(BrokerStatsBase.class);
 
     @GET
     @Path("/metrics")
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Brokers.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/BrokersBase.java
similarity index 95%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Brokers.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/BrokersBase.java
index d4a538e..8098f97 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Brokers.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/BrokersBase.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
+package org.apache.pulsar.broker.admin.impl;
 
 import static org.apache.pulsar.broker.service.BrokerService.BROKER_SERVICE_CONFIGURATION_PATH;
 
@@ -28,14 +28,12 @@ import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.bookkeeper.util.ZkUtils;
 import org.apache.pulsar.broker.ServiceConfiguration;
+import org.apache.pulsar.broker.admin.AdminResource;
 import org.apache.pulsar.broker.loadbalance.LoadManager;
-import org.apache.pulsar.broker.loadbalance.impl.SimpleLoadManagerImpl;
 import org.apache.pulsar.broker.service.BrokerService;
 import org.apache.pulsar.broker.web.RestException;
 import org.apache.pulsar.common.policies.data.NamespaceOwnershipStatus;
@@ -48,17 +46,13 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Maps;
 
-import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 
 
-@Path("/brokers")
-@Api(value = "/brokers", description = "Brokers admin apis", tags = "brokers")
-@Produces(MediaType.APPLICATION_JSON)
-public class Brokers extends AdminResource {
-    private static final Logger LOG = LoggerFactory.getLogger(Brokers.class);
+public class BrokersBase extends AdminResource {
+    private static final Logger LOG = LoggerFactory.getLogger(BrokersBase.class);
     private int serviceConfigZkVersion = -1;
     
     @GET
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Clusters.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ClustersBase.java
similarity index 98%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Clusters.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ClustersBase.java
index 612e2e2..733105a 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Clusters.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ClustersBase.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
+package org.apache.pulsar.broker.admin.impl;
 
 import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
 
@@ -35,11 +35,10 @@ import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.bookkeeper.util.ZkUtils;
+import org.apache.pulsar.broker.admin.AdminResource;
 import org.apache.pulsar.broker.cache.ConfigurationCacheService;
 import org.apache.pulsar.broker.web.RestException;
 import org.apache.pulsar.common.naming.NamedEntity;
@@ -50,7 +49,6 @@ import org.apache.pulsar.common.policies.impl.NamespaceIsolationPolicies;
 import org.apache.pulsar.common.util.ObjectMapperFactory;
 import org.apache.zookeeper.CreateMode;
 import org.apache.zookeeper.KeeperException;
-import org.apache.zookeeper.KeeperException.NoNodeException;
 import org.apache.zookeeper.ZooDefs.Ids;
 import org.apache.zookeeper.data.Stat;
 import org.slf4j.Logger;
@@ -60,17 +58,11 @@ import com.fasterxml.jackson.core.JsonGenerationException;
 import com.fasterxml.jackson.databind.JsonMappingException;
 import com.google.common.collect.Maps;
 
-import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 
-import static org.apache.pulsar.broker.cache.ConfigurationCacheService.FAILURE_DOMAIN;
-
-@Path("/clusters")
-@Api(value = "/clusters", description = "Cluster admin apis", tags = "clusters")
-@Produces(MediaType.APPLICATION_JSON)
-public class Clusters extends AdminResource {
+public class ClustersBase extends AdminResource {
 
     @GET
     @ApiOperation(value = "Get the list of all the Pulsar clusters.", response = String.class, responseContainer = "Set")
@@ -623,6 +615,6 @@ public class Clusters extends AdminResource {
         }
     }
 
-    private static final Logger log = LoggerFactory.getLogger(Clusters.class);
+    private static final Logger log = LoggerFactory.getLogger(ClustersBase.class);
 
 }
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/NamespacesBase.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/NamespacesBase.java
new file mode 100644
index 0000000..0dda3ae
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/NamespacesBase.java
@@ -0,0 +1,1246 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.impl;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
+import static org.apache.pulsar.broker.cache.LocalZooKeeperCacheService.LOCAL_POLICIES_ROOT;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.pulsar.broker.PulsarServerException;
+import org.apache.pulsar.broker.ServiceConfiguration;
+import org.apache.pulsar.broker.admin.AdminResource;
+import org.apache.pulsar.broker.service.BrokerServiceException.SubscriptionBusyException;
+import org.apache.pulsar.broker.service.Subscription;
+import org.apache.pulsar.broker.service.Topic;
+import org.apache.pulsar.broker.service.persistent.PersistentReplicator;
+import org.apache.pulsar.broker.service.persistent.PersistentTopic;
+import org.apache.pulsar.broker.web.RestException;
+import org.apache.pulsar.client.admin.PulsarAdminException;
+import org.apache.pulsar.common.naming.DestinationName;
+import org.apache.pulsar.common.naming.NamespaceBundle;
+import org.apache.pulsar.common.naming.NamespaceBundleFactory;
+import org.apache.pulsar.common.naming.NamespaceBundles;
+import org.apache.pulsar.common.naming.NamespaceName;
+import org.apache.pulsar.common.policies.data.AuthAction;
+import org.apache.pulsar.common.policies.data.BacklogQuota;
+import org.apache.pulsar.common.policies.data.BacklogQuota.BacklogQuotaType;
+import org.apache.pulsar.common.policies.data.BundlesData;
+import org.apache.pulsar.common.policies.data.ClusterData;
+import org.apache.pulsar.common.policies.data.DispatchRate;
+import org.apache.pulsar.common.policies.data.PersistencePolicies;
+import org.apache.pulsar.common.policies.data.Policies;
+import org.apache.pulsar.common.policies.data.RetentionPolicies;
+import org.apache.pulsar.common.policies.data.SubscriptionAuthMode;
+import org.apache.pulsar.common.util.FutureUtil;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+
+public abstract class NamespacesBase extends AdminResource {
+
+    private static final long MAX_BUNDLES = ((long) 1) << 32;
+
+    protected List<String> internalGetPropertyNamespaces(String property) {
+        validateAdminAccessOnProperty(property);
+
+        try {
+            return getListOfNamespaces(property);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to get namespace list for propery: {} - Does not exist", clientAppId(), property);
+            throw new RestException(Status.NOT_FOUND, "Property does not exist");
+        } catch (Exception e) {
+            log.error("[{}] Failed to get namespaces list: {}", clientAppId(), e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalCreateNamespace(Policies policies) {
+        validatePoliciesReadOnlyAccess();
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        validatePolicies(namespaceName, policies);
+
+        try {
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+
+            zkCreateOptimistic(path(POLICIES, namespaceName.toString()), jsonMapper().writeValueAsBytes(policies));
+            log.info("[{}] Created namespace {}", clientAppId(), namespaceName);
+        } catch (KeeperException.NodeExistsException e) {
+            log.warn("[{}] Failed to create namespace {} - already exists", clientAppId(), namespaceName);
+            throw new RestException(Status.CONFLICT, "Namespace already exists");
+        } catch (Exception e) {
+            log.error("[{}] Failed to create namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalDeleteNamespace(boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        // ensure that non-global namespace is directed to the correct cluster
+        if (!namespaceName.isGlobal()) {
+            validateClusterOwnership(namespaceName.getCluster());
+        }
+
+        Entry<Policies, Stat> policiesNode = null;
+        Policies policies = null;
+
+        // ensure the local cluster is the only cluster for the global namespace configuration
+        try {
+            policiesNode = policiesCache().getWithStat(path(POLICIES, namespaceName.toString())).orElseThrow(
+                    () -> new RestException(Status.NOT_FOUND, "Namespace " + namespaceName + " does not exist."));
+
+            policies = policiesNode.getKey();
+            if (namespaceName.isGlobal()) {
+                if (policies.replication_clusters.size() > 1) {
+                    // There are still more than one clusters configured for the global namespace
+                    throw new RestException(Status.PRECONDITION_FAILED, "Cannot delete the global namespace "
+                            + namespaceName + ". There are still more than one replication clusters configured.");
+                }
+                if (policies.replication_clusters.size() == 1
+                        && !policies.replication_clusters.contains(config().getClusterName())) {
+                    // the only replication cluster is other cluster, redirect
+                    String replCluster = policies.replication_clusters.get(0);
+                    ClusterData replClusterData = clustersCache().get(AdminResource.path("clusters", replCluster))
+                            .orElseThrow(() -> new RestException(Status.NOT_FOUND,
+                                    "Cluster " + replCluster + " does not exist"));
+                    URL replClusterUrl;
+                    if (!config().isTlsEnabled()) {
+                        replClusterUrl = new URL(replClusterData.getServiceUrl());
+                    } else if (!replClusterData.getServiceUrlTls().isEmpty()) {
+                        replClusterUrl = new URL(replClusterData.getServiceUrlTls());
+                    } else {
+                        throw new RestException(Status.PRECONDITION_FAILED,
+                                "The replication cluster does not provide TLS encrypted service");
+                    }
+                    URI redirect = UriBuilder.fromUri(uri.getRequestUri()).host(replClusterUrl.getHost())
+                            .port(replClusterUrl.getPort()).replaceQueryParam("authoritative", false).build();
+                    if (log.isDebugEnabled()) {
+                        log.debug("[{}] Redirecting the rest call to {}: cluster={}", clientAppId(), redirect,
+                                replCluster);
+                    }
+                    throw new WebApplicationException(Response.temporaryRedirect(redirect).build());
+                }
+            }
+        } catch (WebApplicationException wae) {
+            throw wae;
+        } catch (Exception e) {
+            throw new RestException(e);
+        }
+
+        boolean isEmpty;
+        try {
+            isEmpty = pulsar().getNamespaceService().getListOfDestinations(namespaceName).isEmpty();
+        } catch (Exception e) {
+            throw new RestException(e);
+        }
+
+        if (!isEmpty) {
+            log.debug("Found destinations on namespace {}", namespaceName);
+            throw new RestException(Status.CONFLICT, "Cannot delete non empty namespace");
+        }
+
+        // set the policies to deleted so that somebody else cannot acquire this namespace
+        try {
+            policies.deleted = true;
+            globalZk().setData(path(POLICIES, namespaceName.toString()), jsonMapper().writeValueAsBytes(policies),
+                    policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+        } catch (Exception e) {
+            log.error("[{}] Failed to delete namespace on global ZK {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+
+        // remove from owned namespace map and ephemeral node from ZK
+        try {
+            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
+                    .getBundles(namespaceName);
+            for (NamespaceBundle bundle : bundles.getBundles()) {
+                // check if the bundle is owned by any broker, if not then we do not need to delete the bundle
+                if (pulsar().getNamespaceService().getOwner(bundle).isPresent()) {
+                    pulsar().getAdminClient().namespaces().deleteNamespaceBundle(namespaceName.toString(),
+                            bundle.getBundleRange());
+                }
+            }
+
+            // we have successfully removed all the ownership for the namespace, the policies znode can be deleted now
+            final String globalZkPolicyPath = path(POLICIES, namespaceName.toString());
+            final String lcaolZkPolicyPath = joinPath(LOCAL_POLICIES_ROOT, namespaceName.toString());
+            globalZk().delete(globalZkPolicyPath, -1);
+            localZk().delete(lcaolZkPolicyPath, -1);
+            policiesCache().invalidate(globalZkPolicyPath);
+            localCacheService().policiesCache().invalidate(lcaolZkPolicyPath);
+        } catch (PulsarAdminException cae) {
+            throw new RestException(cae);
+        } catch (Exception e) {
+            log.error("[{}] Failed to remove owned namespace {}", clientAppId(), namespaceName, e);
+            // avoid throwing exception in case of the second failure
+        }
+
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalDeleteNamespaceBundle(String bundleRange, boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        // ensure that non-global namespace is directed to the correct cluster
+        if (!namespaceName.isGlobal()) {
+            validateClusterOwnership(namespaceName.getCluster());
+        }
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        // ensure the local cluster is the only cluster for the global namespace configuration
+        try {
+            if (namespaceName.isGlobal()) {
+                if (policies.replication_clusters.size() > 1) {
+                    // There are still more than one clusters configured for the global namespace
+                    throw new RestException(Status.PRECONDITION_FAILED, "Cannot delete the global namespace "
+                            + namespaceName + ". There are still more than one replication clusters configured.");
+                }
+                if (policies.replication_clusters.size() == 1
+                        && !policies.replication_clusters.contains(config().getClusterName())) {
+                    // the only replication cluster is other cluster, redirect
+                    String replCluster = policies.replication_clusters.get(0);
+                    ClusterData replClusterData = clustersCache().get(AdminResource.path("clusters", replCluster))
+                            .orElseThrow(() -> new RestException(Status.NOT_FOUND,
+                                    "Cluser " + replCluster + " does not exist"));
+                    URL replClusterUrl;
+                    if (!config().isTlsEnabled()) {
+                        replClusterUrl = new URL(replClusterData.getServiceUrl());
+                    } else if (!replClusterData.getServiceUrlTls().isEmpty()) {
+                        replClusterUrl = new URL(replClusterData.getServiceUrlTls());
+                    } else {
+                        throw new RestException(Status.PRECONDITION_FAILED,
+                                "The replication cluster does not provide TLS encrypted service");
+                    }
+                    URI redirect = UriBuilder.fromUri(uri.getRequestUri()).host(replClusterUrl.getHost())
+                            .port(replClusterUrl.getPort()).replaceQueryParam("authoritative", false).build();
+                    log.debug("[{}] Redirecting the rest call to {}: cluster={}", clientAppId(), redirect, replCluster);
+                    throw new WebApplicationException(Response.temporaryRedirect(redirect).build());
+                }
+            }
+        } catch (WebApplicationException wae) {
+            throw wae;
+        } catch (Exception e) {
+            throw new RestException(e);
+        }
+
+        NamespaceBundle bundle = validateNamespaceBundleOwnership(namespaceName, policies.bundles, bundleRange,
+                authoritative, true);
+        try {
+            List<String> destinations = pulsar().getNamespaceService().getListOfDestinations(namespaceName);
+            for (String destination : destinations) {
+                NamespaceBundle destinationBundle = (NamespaceBundle) pulsar().getNamespaceService()
+                        .getBundle(DestinationName.get(destination));
+                if (bundle.equals(destinationBundle)) {
+                    throw new RestException(Status.CONFLICT, "Cannot delete non empty bundle");
+                }
+            }
+
+            // remove from owned namespace map and ephemeral node from ZK
+            pulsar().getNamespaceService().removeOwnedServiceUnit(bundle);
+        } catch (WebApplicationException wae) {
+            throw wae;
+        } catch (Exception e) {
+            log.error("[{}] Failed to remove namespace bundle {}/{}", clientAppId(), namespaceName.toString(),
+                    bundleRange, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalGrantPermissionOnNamespace(String role, Set<AuthAction> actions) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        try {
+            pulsar().getBrokerService().getAuthorizationService()
+                    .grantPermissionAsync(namespaceName, actions, role, null/*additional auth-data json*/)
+                    .get();
+        } catch (InterruptedException e) {
+            log.error("[{}] Failed to get permissions for namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        } catch (ExecutionException e) {
+            if (e.getCause() instanceof IllegalArgumentException) {
+                log.warn("[{}] Failed to set permissions for namespace {}: does not exist", clientAppId(),
+                        namespaceName);
+                throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+            } else if (e.getCause() instanceof IllegalStateException) {
+                log.warn("[{}] Failed to set permissions for namespace {}: concurrent modification",
+                        clientAppId(), namespaceName);
+                throw new RestException(Status.CONFLICT, "Concurrent modification");
+            } else {
+                log.error("[{}] Failed to get permissions for namespace {}", clientAppId(), namespaceName, e);
+                throw new RestException(e);
+            }
+        }
+    }
+
+    protected void internalRevokePermissionsOnNamespace(String role) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        try {
+            Stat nodeStat = new Stat();
+            byte[] content = globalZk().getData(path(POLICIES, namespaceName.toString()), null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            policies.auth_policies.namespace_auth.remove(role);
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path(POLICIES, namespaceName.toString()), jsonMapper().writeValueAsBytes(policies),
+                    nodeStat.getVersion());
+
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+            log.info("[{}] Successfully revoked access for role {} - namespace {}", clientAppId(), role, namespaceName);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to revoke permissions for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to revoke permissions on namespace {}: concurrent modification", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to revoke permissions on namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected List<String> internalGetNamespaceReplicationClusters() {
+        if (!namespaceName.isGlobal()) {
+            throw new RestException(Status.PRECONDITION_FAILED,
+                    "Cannot get the replication clusters for a non-global namespace");
+        }
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.replication_clusters;
+    }
+
+    protected void internalSetNamespaceReplicationClusters(List<String> clusterIds) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        Set<String> replicationClusterSet = Sets.newHashSet(clusterIds);
+        if (!namespaceName.isGlobal()) {
+            throw new RestException(Status.PRECONDITION_FAILED, "Cannot set replication on a non-global namespace");
+        }
+
+        if (replicationClusterSet.contains("global")) {
+            throw new RestException(Status.PRECONDITION_FAILED,
+                    "Cannot specify global in the list of replication clusters");
+        }
+
+        Set<String> clusters = clusters();
+        for (String clusterId : replicationClusterSet) {
+            if (!clusters.contains(clusterId)) {
+                throw new RestException(Status.FORBIDDEN, "Invalid cluster id: " + clusterId);
+            }
+            validatePeerClusterConflict(clusterId, replicationClusterSet);
+        }
+
+        for (String clusterId : replicationClusterSet) {
+            validateClusterForProperty(namespaceName.getProperty(), clusterId);
+        }
+
+        Entry<Policies, Stat> policiesNode = null;
+
+        try {
+            // Force to read the data s.t. the watch to the cache content is setup.
+            policiesNode = policiesCache().getWithStat(path(POLICIES, namespaceName.toString())).orElseThrow(
+                    () -> new RestException(Status.NOT_FOUND, "Namespace " + namespaceName + " does not exist"));
+            policiesNode.getKey().replication_clusters = clusterIds;
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path(POLICIES, namespaceName.toString()),
+                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+
+            log.info("[{}] Successfully updated the replication clusters on namespace {}", clientAppId(),
+                    namespaceName);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update the replication clusters for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn(
+                    "[{}] Failed to update the replication clusters on namespace {} expected policy node version={} : concurrent modification",
+                    clientAppId(), namespaceName, policiesNode.getValue().getVersion());
+
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to update the replication clusters on namespace {}", clientAppId(), namespaceName,
+                    e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalSetNamespaceMessageTTL(int messageTTL) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        if (messageTTL < 0) {
+            throw new RestException(Status.PRECONDITION_FAILED, "Invalid value for message TTL");
+        }
+
+        Entry<Policies, Stat> policiesNode = null;
+
+        try {
+            // Force to read the data s.t. the watch to the cache content is setup.
+            policiesNode = policiesCache().getWithStat(path(POLICIES, namespaceName.toString())).orElseThrow(
+                    () -> new RestException(Status.NOT_FOUND, "Namespace " + namespaceName + " does not exist"));
+            policiesNode.getKey().message_ttl_in_seconds = messageTTL;
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path(POLICIES, namespaceName.toString()),
+                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+
+            log.info("[{}] Successfully updated the message TTL on namespace {}", clientAppId(), namespaceName);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update the message TTL for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn(
+                    "[{}] Failed to update the message TTL on namespace {} expected policy node version={} : concurrent modification",
+                    clientAppId(), namespaceName, policiesNode.getValue().getVersion());
+
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to update the message TTL on namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalModifyDeduplication(boolean enableDeduplication) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        Entry<Policies, Stat> policiesNode = null;
+
+        try {
+            // Force to read the data s.t. the watch to the cache content is setup.
+            policiesNode = policiesCache().getWithStat(path(POLICIES, namespaceName.toString())).orElseThrow(
+                    () -> new RestException(Status.NOT_FOUND, "Namespace " + namespaceName + " does not exist"));
+            policiesNode.getKey().deduplicationEnabled = enableDeduplication;
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path(POLICIES, namespaceName.toString()),
+                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+
+            log.info("[{}] Successfully {} on namespace {}", clientAppId(),
+                    enableDeduplication ? "enabled" : "disabled", namespaceName);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to modify deplication status for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn(
+                    "[{}] Failed to modify deplication status on namespace {} expected policy node version={} : concurrent modification",
+                    clientAppId(), namespaceName, policiesNode.getValue().getVersion());
+
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to modify deplication status on namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalUnloadNamespace() {
+        log.info("[{}] Unloading namespace {}", clientAppId());
+
+        validateSuperUserAccess();
+
+        if (namespaceName.isGlobal()) {
+            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
+            validateGlobalNamespaceOwnership(namespaceName);
+        } else {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        List<String> boundaries = policies.bundles.getBoundaries();
+        for (int i = 0; i < boundaries.size() - 1; i++) {
+            String bundle = String.format("%s_%s", boundaries.get(i), boundaries.get(i + 1));
+            try {
+                pulsar().getAdminClient().namespaces().unloadNamespaceBundle(namespaceName.toString(), bundle);
+            } catch (PulsarServerException | PulsarAdminException e) {
+                log.error(String.format("[%s] Failed to unload namespace %s", clientAppId(), namespaceName), e);
+                throw new RestException(e);
+            }
+        }
+
+        log.info("[{}] Successfully unloaded all the bundles in namespace {}/{}/{}", clientAppId(), namespaceName);
+    }
+
+    @SuppressWarnings("deprecation")
+    public void internalUnloadNamespaceBundle(String bundleRange, boolean authoritative) {
+        log.info("[{}] Unloading namespace bundle {}/{}", clientAppId(), namespaceName, bundleRange);
+
+        validateSuperUserAccess();
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        if (namespaceName.isGlobal()) {
+            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
+            validateGlobalNamespaceOwnership(namespaceName);
+        } else {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        validatePoliciesReadOnlyAccess();
+
+        if (!isBundleOwnedByAnyBroker(namespaceName, policies.bundles, bundleRange)) {
+            log.info("[{}] Namespace bundle is not owned by any broker {}/{}", clientAppId(), namespaceName,
+                    bundleRange);
+            return;
+        }
+
+        NamespaceBundle nsBundle = validateNamespaceBundleOwnership(namespaceName, policies.bundles, bundleRange,
+                authoritative, true);
+        try {
+            pulsar().getNamespaceService().unloadNamespaceBundle(nsBundle);
+            log.info("[{}] Successfully unloaded namespace bundle {}", clientAppId(), nsBundle.toString());
+        } catch (Exception e) {
+            log.error("[{}] Failed to unload namespace bundle {}/{}", clientAppId(), namespaceName, bundleRange, e);
+            throw new RestException(e);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalSplitNamespaceBundle(String bundleRange, boolean authoritative, boolean unload) {
+        log.info("[{}] Split namespace bundle {}/{}", clientAppId(), namespaceName, bundleRange);
+
+        validateSuperUserAccess();
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        if (namespaceName.isGlobal()) {
+            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
+            validateGlobalNamespaceOwnership(namespaceName);
+        } else {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        validatePoliciesReadOnlyAccess();
+        NamespaceBundle nsBundle = validateNamespaceBundleOwnership(namespaceName, policies.bundles, bundleRange,
+                authoritative, true);
+
+        try {
+            pulsar().getNamespaceService().splitAndOwnBundle(nsBundle, unload).get();
+            log.info("[{}] Successfully split namespace bundle {}", clientAppId(), nsBundle.toString());
+        } catch (IllegalArgumentException e) {
+            log.error("[{}] Failed to split namespace bundle {}/{} due to {}", clientAppId(), namespaceName,
+                    bundleRange, e.getMessage());
+            throw new RestException(Status.PRECONDITION_FAILED, "Split bundle failed due to invalid request");
+        } catch (Exception e) {
+            log.error("[{}] Failed to split namespace bundle {}/{}", clientAppId(), namespaceName, bundleRange, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalSetDispatchRate(DispatchRate dispatchRate) {
+        log.info("[{}] Set namespace dispatch-rate {}/{}", clientAppId(), namespaceName, dispatchRate);
+        validateSuperUserAccess();
+
+        Entry<Policies, Stat> policiesNode = null;
+
+        try {
+            final String path = path(POLICIES, namespaceName.toString());
+            // Force to read the data s.t. the watch to the cache content is setup.
+            policiesNode = policiesCache().getWithStat(path).orElseThrow(
+                    () -> new RestException(Status.NOT_FOUND, "Namespace " + namespaceName + " does not exist"));
+            policiesNode.getKey().clusterDispatchRate.put(pulsar().getConfiguration().getClusterName(), dispatchRate);
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policiesNode.getKey()),
+                    policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path);
+
+            log.info("[{}] Successfully updated the dispatchRate for cluster on namespace {}", clientAppId(),
+                    namespaceName);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update the dispatchRate for cluster on namespace {}: does not exist",
+                    clientAppId(), namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn(
+                    "[{}] Failed to update the dispatchRate for cluster on namespace {} expected policy node version={} : concurrent modification",
+                    clientAppId(), namespaceName, policiesNode.getValue().getVersion());
+
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to update the dispatchRate for cluster on namespace {}", clientAppId(),
+                    namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected DispatchRate internalGetDispatchRate() {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        DispatchRate dispatchRate = policies.clusterDispatchRate.get(pulsar().getConfiguration().getClusterName());
+        if (dispatchRate != null) {
+            return dispatchRate;
+        } else {
+            throw new RestException(Status.NOT_FOUND,
+                    "Dispatch-rate is not configured for cluster " + pulsar().getConfiguration().getClusterName());
+        }
+    }
+
+    protected void internalSetBacklogQuota(BacklogQuotaType backlogQuotaType, BacklogQuota backlogQuota) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        if (backlogQuotaType == null) {
+            backlogQuotaType = BacklogQuotaType.destination_storage;
+        }
+
+        try {
+            Stat nodeStat = new Stat();
+            final String path = path(POLICIES, namespaceName.toString());
+            byte[] content = globalZk().getData(path, null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            RetentionPolicies r = policies.retention_policies;
+            if (r != null) {
+                Policies p = new Policies();
+                p.backlog_quota_map.put(backlogQuotaType, backlogQuota);
+                if (!checkQuotas(p, r)) {
+                    log.warn(
+                            "[{}] Failed to update backlog configuration for namespace {}: conflicts with retention quota",
+                            clientAppId(), namespaceName);
+                    throw new RestException(Status.PRECONDITION_FAILED,
+                            "Backlog Quota exceeds configured retention quota for namespace. Please increase retention quota and retry");
+                }
+            }
+            policies.backlog_quota_map.put(backlogQuotaType, backlogQuota);
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+            log.info("[{}] Successfully updated backlog quota map: namespace={}, map={}", clientAppId(), namespaceName,
+                    jsonMapper().writeValueAsString(policies.backlog_quota_map));
+
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update backlog quota map for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to update backlog quota map for namespace {}: concurrent modification", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (RestException pfe) {
+            throw pfe;
+        } catch (Exception e) {
+            log.error("[{}] Failed to update backlog quota map for namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalRemoveBacklogQuota(BacklogQuotaType backlogQuotaType) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        if (backlogQuotaType == null) {
+            backlogQuotaType = BacklogQuotaType.destination_storage;
+        }
+
+        try {
+            Stat nodeStat = new Stat();
+            final String path = path(POLICIES, namespaceName.toString());
+            byte[] content = globalZk().getData(path, null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            policies.backlog_quota_map.remove(backlogQuotaType);
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+            log.info("[{}] Successfully removed backlog namespace={}, quota={}", clientAppId(), namespaceName,
+                    backlogQuotaType);
+
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update backlog quota map for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to update backlog quota map for namespace {}: concurrent modification", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to update backlog quota map for namespace {}", clientAppId(), namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalSetRetention(RetentionPolicies retention) {
+        validatePoliciesReadOnlyAccess();
+
+        try {
+            Stat nodeStat = new Stat();
+            final String path = path(POLICIES, namespaceName.toString());
+            byte[] content = globalZk().getData(path, null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            if (!checkQuotas(policies, retention)) {
+                log.warn("[{}] Failed to update retention configuration for namespace {}: conflicts with backlog quota",
+                        clientAppId(), namespaceName);
+                throw new RestException(Status.PRECONDITION_FAILED,
+                        "Retention Quota must exceed configured backlog quota for namespace.");
+            }
+            policies.retention_policies = retention;
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+            log.info("[{}] Successfully updated retention configuration: namespace={}, map={}", clientAppId(),
+                    namespaceName, jsonMapper().writeValueAsString(policies.retention_policies));
+
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update retention configuration for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to update retention configuration for namespace {}: concurrent modification",
+                    clientAppId(), namespaceName);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (RestException pfe) {
+            throw pfe;
+        } catch (Exception e) {
+            log.error("[{}] Failed to update retention configuration for namespace {}", clientAppId(), namespaceName,
+                    e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalSetPersistence(PersistencePolicies persistence) {
+        validatePoliciesReadOnlyAccess();
+        validatePersistencePolicies(persistence);
+
+        try {
+            Stat nodeStat = new Stat();
+            final String path = path(POLICIES, namespaceName.toString());
+            byte[] content = globalZk().getData(path, null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            policies.persistence = persistence;
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+            log.info("[{}] Successfully updated persistence configuration: namespace={}, map={}", clientAppId(),
+                    namespaceName, jsonMapper().writeValueAsString(policies.persistence));
+
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update persistence configuration for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to update persistence configuration for namespace {}: concurrent modification",
+                    clientAppId(), namespaceName);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to update persistence configuration for namespace {}", clientAppId(), namespaceName,
+                    e);
+            throw new RestException(e);
+        }
+    }
+
+    protected PersistencePolicies internalGetPersistence() {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        if (policies.persistence == null) {
+            return new PersistencePolicies(config().getManagedLedgerDefaultEnsembleSize(),
+                    config().getManagedLedgerDefaultWriteQuorum(), config().getManagedLedgerDefaultAckQuorum(), 0.0d);
+        } else {
+            return policies.persistence;
+        }
+    }
+
+    protected void internalClearNamespaceBacklog(boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        try {
+            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
+                    .getBundles(namespaceName);
+            Exception exception = null;
+            for (NamespaceBundle nsBundle : bundles.getBundles()) {
+                try {
+                    // check if the bundle is owned by any broker, if not then there is no backlog on this bundle to
+                    // clear
+                    if (pulsar().getNamespaceService().getOwner(nsBundle).isPresent()) {
+                        // TODO: make this admin call asynchronous
+                        pulsar().getAdminClient().namespaces().clearNamespaceBundleBacklog(namespaceName.toString(),
+                                nsBundle.getBundleRange());
+                    }
+                } catch (Exception e) {
+                    if (exception == null) {
+                        exception = e;
+                    }
+                }
+            }
+            if (exception != null) {
+                if (exception instanceof PulsarAdminException) {
+                    throw new RestException((PulsarAdminException) exception);
+                } else {
+                    throw new RestException(exception.getCause());
+                }
+            }
+        } catch (WebApplicationException wae) {
+            throw wae;
+        } catch (Exception e) {
+            throw new RestException(e);
+        }
+        log.info("[{}] Successfully cleared backlog on all the bundles for namespace {}", clientAppId(), namespaceName);
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalClearNamespaceBundleBacklog(String bundleRange, boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        if (namespaceName.isGlobal()) {
+            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
+            validateGlobalNamespaceOwnership(namespaceName);
+        } else {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        validateNamespaceBundleOwnership(namespaceName, policies.bundles, bundleRange, authoritative, true);
+
+        clearBacklog(namespaceName, bundleRange, null);
+        log.info("[{}] Successfully cleared backlog on namespace bundle {}/{}", clientAppId(), namespaceName,
+                bundleRange);
+    }
+
+    protected void internalClearNamespaceBacklogForSubscription(String subscription, boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        try {
+            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
+                    .getBundles(namespaceName);
+            Exception exception = null;
+            for (NamespaceBundle nsBundle : bundles.getBundles()) {
+                try {
+                    // check if the bundle is owned by any broker, if not then there is no backlog on this bundle to
+                    // clear
+                    if (pulsar().getNamespaceService().getOwner(nsBundle).isPresent()) {
+                        // TODO: make this admin call asynchronous
+                        pulsar().getAdminClient().namespaces().clearNamespaceBundleBacklogForSubscription(
+                                namespaceName.toString(), nsBundle.getBundleRange(), subscription);
+                    }
+                } catch (Exception e) {
+                    if (exception == null) {
+                        exception = e;
+                    }
+                }
+            }
+            if (exception != null) {
+                if (exception instanceof PulsarAdminException) {
+                    throw new RestException((PulsarAdminException) exception);
+                } else {
+                    throw new RestException(exception.getCause());
+                }
+            }
+        } catch (WebApplicationException wae) {
+            throw wae;
+        } catch (Exception e) {
+            throw new RestException(e);
+        }
+        log.info("[{}] Successfully cleared backlog for subscription {} on all the bundles for namespace {}",
+                clientAppId(), subscription, namespaceName);
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalClearNamespaceBundleBacklogForSubscription(String subscription, String bundleRange,
+            boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        if (namespaceName.isGlobal()) {
+            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
+            validateGlobalNamespaceOwnership(namespaceName);
+        } else {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        validateNamespaceBundleOwnership(namespaceName, policies.bundles, bundleRange, authoritative, true);
+
+        clearBacklog(namespaceName, bundleRange, subscription);
+        log.info("[{}] Successfully cleared backlog for subscription {} on namespace bundle {}/{}", clientAppId(),
+                subscription, namespaceName, bundleRange);
+    }
+
+    protected void internalUnsubscribeNamespace(String subscription, boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        try {
+            NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
+                    .getBundles(namespaceName);
+            Exception exception = null;
+            for (NamespaceBundle nsBundle : bundles.getBundles()) {
+                try {
+                    // check if the bundle is owned by any broker, if not then there are no subscriptions
+                    if (pulsar().getNamespaceService().getOwner(nsBundle).isPresent()) {
+                        // TODO: make this admin call asynchronous
+                        pulsar().getAdminClient().namespaces().unsubscribeNamespaceBundle(namespaceName.toString(),
+                                nsBundle.getBundleRange(), subscription);
+                    }
+                } catch (Exception e) {
+                    if (exception == null) {
+                        exception = e;
+                    }
+                }
+            }
+            if (exception != null) {
+                if (exception instanceof PulsarAdminException) {
+                    throw new RestException((PulsarAdminException) exception);
+                } else {
+                    throw new RestException(exception.getCause());
+                }
+            }
+        } catch (WebApplicationException wae) {
+            throw wae;
+        } catch (Exception e) {
+            throw new RestException(e);
+        }
+        log.info("[{}] Successfully unsubscribed {} on all the bundles for namespace {}", clientAppId(), subscription,
+                namespaceName);
+    }
+
+    @SuppressWarnings("deprecation")
+    protected void internalUnsubscribeNamespaceBundle(String subscription, String bundleRange, boolean authoritative) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        if (namespaceName.isGlobal()) {
+            // check cluster ownership for a given global namespace: redirect if peer-cluster owns it
+            validateGlobalNamespaceOwnership(namespaceName);
+        } else {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        validateNamespaceBundleOwnership(namespaceName, policies.bundles, bundleRange, authoritative, true);
+
+        unsubscribe(namespaceName, bundleRange, subscription);
+        log.info("[{}] Successfully unsubscribed {} on namespace bundle {}/{}", clientAppId(), subscription,
+                namespaceName, bundleRange);
+    }
+
+    protected void internalSetSubscriptionAuthMode(SubscriptionAuthMode subscriptionAuthMode) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        if (subscriptionAuthMode == null) {
+            subscriptionAuthMode = SubscriptionAuthMode.None;
+        }
+
+        try {
+            Stat nodeStat = new Stat();
+            final String path = path(POLICIES, namespaceName.toString());
+            byte[] content = globalZk().getData(path, null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            policies.subscription_auth_mode = subscriptionAuthMode;
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+            log.info("[{}] Successfully updated subscription auth mode: namespace={}, map={}", clientAppId(),
+                    namespaceName, jsonMapper().writeValueAsString(policies.backlog_quota_map));
+
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update subscription auth mode for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to update subscription auth mode for namespace {}/{}/{}: concurrent modification",
+                    clientAppId(), namespaceName);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (RestException pfe) {
+            throw pfe;
+        } catch (Exception e) {
+            log.error("[{}] Failed to update subscription auth mode for namespace {}/{}/{}", clientAppId(),
+                    namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    protected void internalModifyEncryptionRequired(boolean encryptionRequired) {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+        validatePoliciesReadOnlyAccess();
+
+        Entry<Policies, Stat> policiesNode = null;
+
+        try {
+            // Force to read the data s.t. the watch to the cache content is setup.
+            policiesNode = policiesCache().getWithStat(path(POLICIES, namespaceName.toString())).orElseThrow(
+                    () -> new RestException(Status.NOT_FOUND, "Namespace " + namespaceName + " does not exist"));
+            policiesNode.getKey().encryption_required = encryptionRequired;
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path(POLICIES, namespaceName.toString()),
+                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
+
+            log.info("[{}] Successfully {} on namespace {}", clientAppId(), encryptionRequired ? "true" : "false",
+                    namespaceName);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to modify encryption required status for namespace {}: does not exist", clientAppId(),
+                    namespaceName);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn(
+                    "[{}] Failed to modify encryption required status on namespace {} expected policy node version={} : concurrent modification",
+                    clientAppId(), namespaceName, policiesNode.getValue().getVersion());
+
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to modify encryption required status on namespace {}", clientAppId(), namespaceName,
+                    e);
+            throw new RestException(e);
+        }
+    }
+
+    private void validatePersistencePolicies(PersistencePolicies persistence) {
+        try {
+            checkNotNull(persistence);
+            final ServiceConfiguration config = pulsar().getConfiguration();
+            checkArgument(persistence.getBookkeeperEnsemble() <= config.getManagedLedgerMaxEnsembleSize(),
+                    "Bookkeeper-Ensemble must be <= %s", config.getManagedLedgerMaxEnsembleSize());
+            checkArgument(persistence.getBookkeeperWriteQuorum() <= config.getManagedLedgerMaxWriteQuorum(),
+                    "Bookkeeper-WriteQuorum must be <= %s", config.getManagedLedgerMaxWriteQuorum());
+            checkArgument(persistence.getBookkeeperAckQuorum() <= config.getManagedLedgerMaxAckQuorum(),
+                    "Bookkeeper-AckQuorum must be <= %s", config.getManagedLedgerMaxAckQuorum());
+            checkArgument(
+                    (persistence.getBookkeeperEnsemble() >= persistence.getBookkeeperWriteQuorum())
+                            && (persistence.getBookkeeperWriteQuorum() >= persistence.getBookkeeperAckQuorum()),
+                    "Bookkeeper Ensemble (%s) >= WriteQuorum (%s) >= AckQuoru (%s)",
+                    persistence.getBookkeeperEnsemble(), persistence.getBookkeeperWriteQuorum(),
+                    persistence.getBookkeeperAckQuorum());
+        } catch (NullPointerException | IllegalArgumentException e) {
+            throw new RestException(Status.PRECONDITION_FAILED, e.getMessage());
+        }
+    }
+
+    protected RetentionPolicies internalGetRetention() {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        if (policies.retention_policies == null) {
+            return new RetentionPolicies(config().getDefaultRetentionTimeInMinutes(),
+                    config().getDefaultRetentionSizeInMB());
+        } else {
+            return policies.retention_policies;
+        }
+    }
+
+    private boolean checkQuotas(Policies policies, RetentionPolicies retention) {
+        Map<BacklogQuota.BacklogQuotaType, BacklogQuota> backlog_quota_map = policies.backlog_quota_map;
+        if (backlog_quota_map.isEmpty() || retention.getRetentionSizeInMB() == 0) {
+            return true;
+        }
+        BacklogQuota quota = backlog_quota_map.get(BacklogQuotaType.destination_storage);
+        if (quota == null) {
+            quota = pulsar().getBrokerService().getBacklogQuotaManager().getDefaultQuota();
+        }
+        if (quota.getLimit() >= ((long) retention.getRetentionSizeInMB() * 1024 * 1024)) {
+            return false;
+        }
+        return true;
+    }
+
+    private void clearBacklog(NamespaceName nsName, String bundleRange, String subscription) {
+        try {
+            List<Topic> topicList = pulsar().getBrokerService().getAllTopicsFromNamespaceBundle(nsName.toString(),
+                    nsName.toString() + "/" + bundleRange);
+
+            List<CompletableFuture<Void>> futures = Lists.newArrayList();
+            if (subscription != null) {
+                if (subscription.startsWith(pulsar().getConfiguration().getReplicatorPrefix())) {
+                    subscription = PersistentReplicator.getRemoteCluster(subscription);
+                }
+                for (Topic topic : topicList) {
+                    if (topic instanceof PersistentTopic) {
+                        futures.add(((PersistentTopic) topic).clearBacklog(subscription));
+                    }
+                }
+            } else {
+                for (Topic topic : topicList) {
+                    if (topic instanceof PersistentTopic) {
+                        futures.add(((PersistentTopic) topic).clearBacklog());
+                    }
+                }
+            }
+
+            FutureUtil.waitForAll(futures).get();
+        } catch (Exception e) {
+            log.error("[{}] Failed to clear backlog for namespace {}/{}, subscription: {}", clientAppId(),
+                    nsName.toString(), bundleRange, subscription, e);
+            throw new RestException(e);
+        }
+    }
+
+    private void unsubscribe(NamespaceName nsName, String bundleRange, String subscription) {
+        try {
+            List<Topic> topicList = pulsar().getBrokerService().getAllTopicsFromNamespaceBundle(nsName.toString(),
+                    nsName.toString() + "/" + bundleRange);
+            List<CompletableFuture<Void>> futures = Lists.newArrayList();
+            if (subscription.startsWith(pulsar().getConfiguration().getReplicatorPrefix())) {
+                throw new RestException(Status.PRECONDITION_FAILED, "Cannot unsubscribe a replication cursor");
+            } else {
+                for (Topic topic : topicList) {
+                    Subscription sub = topic.getSubscription(subscription);
+                    if (sub != null) {
+                        futures.add(sub.delete());
+                    }
+                }
+            }
+
+            FutureUtil.waitForAll(futures).get();
+        } catch (RestException re) {
+            throw re;
+        } catch (Exception e) {
+            log.error("[{}] Failed to unsubscribe {} for namespace {}/{}", clientAppId(), subscription,
+                    nsName.toString(), bundleRange, e);
+            if (e.getCause() instanceof SubscriptionBusyException) {
+                throw new RestException(Status.PRECONDITION_FAILED, "Subscription has active connected consumers");
+            }
+            throw new RestException(e.getCause());
+        }
+    }
+
+    /**
+     * It validates that peer-clusters can't coexist in replication-clusters
+     *
+     * @param clusterName:
+     *            given cluster whose peer-clusters can't be present into replication-cluster list
+     * @param clusters:
+     *            replication-cluster list
+     */
+    private void validatePeerClusterConflict(String clusterName, Set<String> replicationClusters) {
+        try {
+            ClusterData clusterData = clustersCache().get(path("clusters", clusterName)).orElseThrow(
+                    () -> new RestException(Status.PRECONDITION_FAILED, "Invalid replication cluster " + clusterName));
+            Set<String> peerClusters = clusterData.getPeerClusterNames();
+            if (peerClusters != null && !peerClusters.isEmpty()) {
+                SetView<String> conflictPeerClusters = Sets.intersection(peerClusters, replicationClusters);
+                if (!conflictPeerClusters.isEmpty()) {
+                    log.warn("[{}] {}'s peer cluster can't be part of replication clusters {}", clientAppId(),
+                            clusterName, conflictPeerClusters);
+                    throw new RestException(Status.CONFLICT,
+                            String.format("%s's peer-clusters %s can't be part of replication-clusters %s", clusterName,
+                                    conflictPeerClusters, replicationClusters));
+                }
+            }
+        } catch (RestException re) {
+            throw re;
+        } catch (Exception e) {
+            log.warn("[{}] Failed to get cluster-data for {}", clientAppId(), clusterName, e);
+        }
+    }
+
+    protected BundlesData validateBundlesData(BundlesData initialBundles) {
+        SortedSet<String> partitions = new TreeSet<String>();
+        for (String partition : initialBundles.getBoundaries()) {
+            Long partBoundary = Long.decode(partition);
+            partitions.add(String.format("0x%08x", partBoundary));
+        }
+        if (partitions.size() != initialBundles.getBoundaries().size()) {
+            log.debug("Input bundles included repeated partition points. Ignored.");
+        }
+        try {
+            NamespaceBundleFactory.validateFullRange(partitions);
+        } catch (IllegalArgumentException iae) {
+            throw new RestException(Status.BAD_REQUEST, "Input bundles do not cover the whole hash range. first:"
+                    + partitions.first() + ", last:" + partitions.last());
+        }
+        List<String> bundles = Lists.newArrayList();
+        bundles.addAll(partitions);
+        return new BundlesData(bundles);
+    }
+
+    protected BundlesData getBundles(int numBundles) {
+        if (numBundles <= 0 || numBundles > MAX_BUNDLES) {
+            throw new RestException(Status.BAD_REQUEST,
+                    "Invalid number of bundles. Number of numbles has to be in the range of (0, 2^32].");
+        }
+        Long maxVal = ((long) 1) << 32;
+        Long segSize = maxVal / numBundles;
+        List<String> partitions = Lists.newArrayList();
+        partitions.add(String.format("0x%08x", 0l));
+        Long curPartition = segSize;
+        for (int i = 0; i < numBundles; i++) {
+            if (i != numBundles - 1) {
+                partitions.add(String.format("0x%08x", curPartition));
+            } else {
+                partitions.add(String.format("0x%08x", maxVal - 1));
+            }
+            curPartition += segSize;
+        }
+        return new BundlesData(partitions);
+    }
+
+    private void validatePolicies(NamespaceName ns, Policies policies) {
+        // Validate cluster names and permissions
+        policies.replication_clusters.forEach(cluster -> validateClusterForProperty(ns.getProperty(), cluster));
+
+        if (policies.message_ttl_in_seconds < 0) {
+            throw new RestException(Status.PRECONDITION_FAILED, "Invalid value for message TTL");
+        }
+
+        if (policies.bundles != null && policies.bundles.getNumBundles() > 0) {
+            if (policies.bundles.getBoundaries() == null || policies.bundles.getBoundaries().size() == 0) {
+                policies.bundles = getBundles(policies.bundles.getNumBundles());
+            } else {
+                policies.bundles = validateBundlesData(policies.bundles);
+            }
+        } else {
+            int defaultNumberOfBundles = config().getDefaultNumberOfNamespaceBundles();
+            policies.bundles = getBundles(defaultNumberOfBundles);
+        }
+
+        if (policies.persistence != null) {
+            validatePersistencePolicies(policies.persistence);
+        }
+    }
+
+    private static final Logger log = LoggerFactory.getLogger(NamespacesBase.class);
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/PersistentTopics.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PersistentTopicsBase.java
similarity index 57%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/PersistentTopics.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PersistentTopicsBase.java
index 484a873..b459011 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/PersistentTopics.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PersistentTopicsBase.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
+package org.apache.pulsar.broker.admin.impl;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
@@ -35,20 +35,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.Encoded;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
 import javax.ws.rs.core.Response.Status;
@@ -65,6 +53,7 @@ import org.apache.bookkeeper.mledger.impl.PositionImpl;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.pulsar.broker.PulsarServerException;
 import org.apache.pulsar.broker.PulsarService;
+import org.apache.pulsar.broker.admin.AdminResource;
 import org.apache.pulsar.broker.authentication.AuthenticationDataSource;
 import org.apache.pulsar.broker.service.BrokerServiceException.NotAllowedException;
 import org.apache.pulsar.broker.service.BrokerServiceException.SubscriptionBusyException;
@@ -90,7 +79,6 @@ import org.apache.pulsar.common.compression.CompressionCodec;
 import org.apache.pulsar.common.compression.CompressionCodecProvider;
 import org.apache.pulsar.common.naming.DestinationDomain;
 import org.apache.pulsar.common.naming.DestinationName;
-import org.apache.pulsar.common.naming.NamespaceName;
 import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
 import org.apache.pulsar.common.policies.data.AuthAction;
 import org.apache.pulsar.common.policies.data.AuthPolicies;
@@ -113,60 +101,44 @@ import com.google.common.collect.Sets;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.PooledByteBufAllocator;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 
 /**
  */
-@Path("/persistent")
-@Produces(MediaType.APPLICATION_JSON)
-@Api(value = "/persistent", description = "Persistent topic admin apis", tags = "persistent topic")
-public class PersistentTopics extends AdminResource {
-    private static final Logger log = LoggerFactory.getLogger(PersistentTopics.class);
+public class PersistentTopicsBase extends AdminResource {
+    private static final Logger log = LoggerFactory.getLogger(PersistentTopicsBase.class);
 
     protected static final int PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS = 1000;
     private static final int OFFLINE_TOPIC_STAT_TTL_MINS = 10;
     private static final String DEPRECATED_CLIENT_VERSION_PREFIX = "Pulsar-CPP-v";
-    private static final Version LEAST_SUPPORTED_CLIENT_VERSION_PREFIX = Version.forIntegers(1,21);
+    private static final Version LEAST_SUPPORTED_CLIENT_VERSION_PREFIX = Version.forIntegers(1, 21);
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}")
-    @ApiOperation(value = "Get the list of destinations under a namespace.", response = String.class, responseContainer = "List")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
-    public List<String> getList(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
+    protected List<String> internalGetList() {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
 
         // Validate that namespace exists, throws 404 if it doesn't exist
         try {
-            policiesCache().get(path(POLICIES, property, cluster, namespace));
+            policiesCache().get(path(POLICIES, namespaceName.toString()));
         } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to get topic list {}/{}/{}: Namespace does not exist", clientAppId(), property,
-                    cluster, namespace);
+            log.warn("[{}] Failed to get topic list {}: Namespace does not exist", clientAppId(), namespaceName);
             throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
         } catch (Exception e) {
-            log.error("[{}] Failed to get topic list {}/{}/{}", clientAppId(), property, cluster, namespace, e);
+            log.error("[{}] Failed to get topic list {}", clientAppId(), namespaceName, e);
             throw new RestException(e);
         }
 
         List<String> destinations = Lists.newArrayList();
 
         try {
-            String path = String.format("/managed-ledgers/%s/%s/%s/%s", property, cluster, namespace, domain());
+            String path = String.format("/managed-ledgers/%s/%s", namespaceName.toString(), domain());
             for (String destination : managedLedgerListCache().get(path)) {
                 if (domain().equals(DestinationDomain.persistent.toString())) {
-                    destinations.add(DestinationName
-                            .get(domain(), property, cluster, namespace, decode(destination)).toString());
+                    destinations.add(DestinationName.get(domain(), namespaceName, decode(destination)).toString());
                 }
             }
         } catch (KeeperException.NoNodeException e) {
             // NoNode means there are no destination in this domain for this namespace
         } catch (Exception e) {
-            log.error("[{}] Failed to get destination list for namespace {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
+            log.error("[{}] Failed to get destination list for namespace {}", clientAppId(), namespaceName, e);
             throw new RestException(e);
         }
 
@@ -174,39 +146,34 @@ public class PersistentTopics extends AdminResource {
         return destinations;
     }
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/partitioned")
-    @ApiOperation(value = "Get the list of partitioned topics under a namespace.", response = String.class, responseContainer = "List")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
-    public List<String> getPartitionedTopicList(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
-        validateAdminAccessOnProperty(property);
+    protected List<String> internalGetPartitionedTopicList() {
+        validateAdminAccessOnProperty(namespaceName.getProperty());
 
         // Validate that namespace exists, throws 404 if it doesn't exist
         try {
-            policiesCache().get(path(POLICIES, property, cluster, namespace));
+            policiesCache().get(path(POLICIES, namespaceName.toString()));
         } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to get partitioned topic list {}/{}/{}: Namespace does not exist", clientAppId(), property,
-                    cluster, namespace);
+            log.warn("[{}] Failed to get partitioned topic list {}: Namespace does not exist", clientAppId(),
+                    namespaceName);
             throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
         } catch (Exception e) {
-            log.error("[{}] Failed to get partitioned topic list for namespace {}/{}/{}", clientAppId(), property, cluster, namespace, e);
+            log.error("[{}] Failed to get partitioned topic list for namespace {}", clientAppId(), namespaceName, e);
             throw new RestException(e);
         }
 
         List<String> partitionedTopics = Lists.newArrayList();
 
         try {
-            String partitionedTopicPath = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain());
+            String partitionedTopicPath = path(PARTITIONED_TOPIC_PATH_ZNODE, namespaceName.toString(), domain());
             List<String> destinations = globalZk().getChildren(partitionedTopicPath, false);
-            partitionedTopics = destinations.stream().map(s -> String.format("persistent://%s/%s/%s/%s", property, cluster, namespace, decode(s))).collect(
-                    Collectors.toList());
+            partitionedTopics = destinations.stream()
+                    .map(s -> String.format("persistent://%s/%s", namespaceName.toString(), decode(s)))
+                    .collect(Collectors.toList());
         } catch (KeeperException.NoNodeException e) {
             // NoNode means there are no partitioned topics in this domain for this namespace
         } catch (Exception e) {
-            log.error("[{}] Failed to get partitioned topic list for namespace {}/{}/{}", clientAppId(), property, cluster,
-                    namespace, e);
+            log.error("[{}] Failed to get partitioned topic list for namespace {}", clientAppId(),
+                    namespaceName.toString(), e);
             throw new RestException(e);
         }
 
@@ -214,23 +181,14 @@ public class PersistentTopics extends AdminResource {
         return partitionedTopics;
     }
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/{destination}/permissions")
-    @ApiOperation(value = "Get permissions on a destination.", notes = "Retrieve the effective permissions for a destination. These permissions are defined by the permissions set at the"
-            + "namespace level combined (union) with any eventual specific permission set on the destination.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
-    public Map<String, Set<AuthAction>> getPermissionsOnDestination(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination) {
+    protected Map<String, Set<AuthAction>> internalGetPermissionsOnDestination() {
         // This operation should be reading from zookeeper and it should be allowed without having admin privileges
-        destination = decode(destination);
-        validateAdminAccessOnProperty(property);
+        validateAdminAccessOnProperty(namespaceName.getProperty());
 
-        String destinationUri = DestinationName.get(domain(), property, cluster, namespace, destination).toString();
+        String destinationUri = destinationName.toString();
 
         try {
-            Policies policies = policiesCache().get(path(POLICIES, property, cluster, namespace))
+            Policies policies = policiesCache().get(path(POLICIES, namespaceName.toString()))
                     .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace does not exist"));
 
             Map<String, Set<AuthAction>> permissions = Maps.newTreeMap();
@@ -264,47 +222,38 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    protected void validateAdminAndClientPermission(DestinationName destination) {
+    protected void validateAdminAndClientPermission() {
         try {
-            validateAdminAccessOnProperty(destination.getProperty());
+            validateAdminAccessOnProperty(destinationName.getProperty());
         } catch (Exception ve) {
             try {
-                checkAuthorization(pulsar(), destination, clientAppId(), clientAuthData());
+                checkAuthorization(pulsar(), destinationName, clientAppId(), clientAuthData());
             } catch (RestException re) {
                 throw re;
             } catch (Exception e) {
                 // unknown error marked as internal server error
-                log.warn("Unexpected error while authorizing request. destination={}, role={}. Error: {}", destination,
-                        clientAppId(), e.getMessage(), e);
+                log.warn("Unexpected error while authorizing request. destination={}, role={}. Error: {}",
+                        destinationName, clientAppId(), e.getMessage(), e);
                 throw new RestException(e);
             }
         }
     }
 
-    protected void validateAdminOperationOnDestination(DestinationName fqdn, boolean authoritative) {
-        validateAdminAccessOnProperty(fqdn.getProperty());
-        validateDestinationOwnership(fqdn, authoritative);
+    public void validateAdminOperationOnDestination(boolean authoritative) {
+        validateAdminAccessOnProperty(destinationName.getProperty());
+        validateDestinationOwnership(destinationName, authoritative);
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/permissions/{role}")
-    @ApiOperation(value = "Grant a new permission to a role on a single destination.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace doesn't exist"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void grantPermissionsOnDestination(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination, @PathParam("role") String role, Set<AuthAction> actions) {
-        destination = decode(destination);
+    protected void internalGrantPermissionsOnDestination(String role, Set<AuthAction> actions) {
         // This operation should be reading from zookeeper and it should be allowed without having admin privileges
-        validateAdminAccessOnProperty(property);
+        validateAdminAccessOnProperty(namespaceName.getProperty());
         validatePoliciesReadOnlyAccess();
 
-        String destinationUri = DestinationName.get(domain(), property, cluster, namespace, destination).toString();
+        String destinationUri = destinationName.toString();
 
         try {
             Stat nodeStat = new Stat();
-            byte[] content = globalZk().getData(path(POLICIES, property, cluster, namespace), null, nodeStat);
+            byte[] content = globalZk().getData(path(POLICIES, namespaceName.toString()), null, nodeStat);
             Policies policies = jsonMapper().readValue(content, Policies.class);
 
             if (!policies.auth_policies.destination_auth.containsKey(destinationUri)) {
@@ -314,11 +263,11 @@ public class PersistentTopics extends AdminResource {
             policies.auth_policies.destination_auth.get(destinationUri).put(role, actions);
 
             // Write the new policies to zookeeper
-            globalZk().setData(path(POLICIES, property, cluster, namespace), jsonMapper().writeValueAsBytes(policies),
+            globalZk().setData(path(POLICIES, namespaceName.toString()), jsonMapper().writeValueAsBytes(policies),
                     nodeStat.getVersion());
 
             // invalidate the local cache to force update
-            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
+            policiesCache().invalidate(path(POLICIES, namespaceName.toString()));
 
             log.info("[{}] Successfully granted access for role {}: {} - destination {}", clientAppId(), role, actions,
                     destinationUri);
@@ -333,27 +282,17 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/{destination}/permissions/{role}")
-    @ApiOperation(value = "Revoke permissions on a destination.", notes = "Revoke permissions to a role on a single destination. If the permission was not set at the destination"
-            + "level, but rather at the namespace level, this operation will return an error (HTTP status code 412).")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace doesn't exist"),
-            @ApiResponse(code = 412, message = "Permissions are not set at the destination level") })
-    public void revokePermissionsOnDestination(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination, @PathParam("role") String role) {
-        destination = decode(destination);
+    protected void internalRevokePermissionsOnDestination(String role) {
         // This operation should be reading from zookeeper and it should be allowed without having admin privileges
-        validateAdminAccessOnProperty(property);
+        validateAdminAccessOnProperty(namespaceName.getProperty());
         validatePoliciesReadOnlyAccess();
 
-        String destinationUri = DestinationName.get(domain(), property, cluster, namespace, destination).toString();
+        String destinationUri = destinationName.toString();
         Stat nodeStat = new Stat();
         Policies policies;
 
         try {
-            byte[] content = globalZk().getData(path(POLICIES, property, cluster, namespace), null, nodeStat);
+            byte[] content = globalZk().getData(path(POLICIES, namespaceName.toString()), null, nodeStat);
             policies = jsonMapper().readValue(content, Policies.class);
         } catch (KeeperException.NoNodeException e) {
             log.warn("[{}] Failed to revoke permissions on destination {}: Namespace does not exist", clientAppId(),
@@ -375,7 +314,7 @@ public class PersistentTopics extends AdminResource {
 
         try {
             // Write the new policies to zookeeper
-            String namespacePath = path(POLICIES, property, cluster, namespace);
+            String namespacePath = path(POLICIES, namespaceName.toString());
             globalZk().setData(namespacePath, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
 
             // invalidate the local cache to force update
@@ -390,33 +329,24 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
-    @ApiOperation(value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 409, message = "Partitioned topic already exist") })
-    public void createPartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, int numPartitions,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAccessOnProperty(dn.getProperty());
+    protected void internalCreatePartitionedTopic(int numPartitions, boolean authoritative) {
+        validateAdminAccessOnProperty(destinationName.getProperty());
         if (numPartitions <= 1) {
             throw new RestException(Status.NOT_ACCEPTABLE, "Number of partitions should be more than 1");
         }
         try {
-            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(),
-                    dn.getEncodedLocalName());
+            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, namespaceName.toString(), domain(),
+                    destinationName.getEncodedLocalName());
             byte[] data = jsonMapper().writeValueAsBytes(new PartitionedTopicMetadata(numPartitions));
             zkCreateOptimistic(path, data);
             // we wait for the data to be synced in all quorums and the observers
             Thread.sleep(PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS);
-            log.info("[{}] Successfully created partitioned topic {}", clientAppId(), dn);
+            log.info("[{}] Successfully created partitioned topic {}", clientAppId(), destinationName);
         } catch (KeeperException.NodeExistsException e) {
-            log.warn("[{}] Failed to create already existing partitioned topic {}", clientAppId(), dn);
+            log.warn("[{}] Failed to create already existing partitioned topic {}", clientAppId(), destinationName);
             throw new RestException(Status.CONFLICT, "Partitioned topic already exist");
         } catch (Exception e) {
-            log.error("[{}] Failed to create partitioned topic {}", clientAppId(), dn, e);
+            log.error("[{}] Failed to create partitioned topic {}", clientAppId(), destinationName, e);
             throw new RestException(e);
         }
     }
@@ -430,77 +360,47 @@ public class PersistentTopics extends AdminResource {
      * recreate them at application so, newly created producers and consumers can connect to newly added partitions as
      * well. Therefore, it can violate partition ordering at producers until all producers are restarted at application.
      *
-     * @param property
-     * @param cluster
-     * @param namespace
-     * @param destination
      * @param numPartitions
      */
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
-    @ApiOperation(value = "Increment partitons of an existing partitioned topic.", notes = "It only increments partitions of existing non-global partitioned-topic")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 409, message = "Partitioned topic does not exist") })
-    public void updatePartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            int numPartitions) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAccessOnProperty(dn.getProperty());
-        if (dn.isGlobal()) {
-            log.error("[{}] Update partitioned-topic is forbidden on global namespace {}", clientAppId(), dn);
+    protected void internalUpdatePartitionedTopic(int numPartitions) {
+        validateAdminAccessOnProperty(destinationName.getProperty());
+        if (destinationName.isGlobal()) {
+            log.error("[{}] Update partitioned-topic is forbidden on global namespace {}", clientAppId(),
+                    destinationName);
             throw new RestException(Status.FORBIDDEN, "Update forbidden on global namespace");
         }
         if (numPartitions <= 1) {
             throw new RestException(Status.NOT_ACCEPTABLE, "Number of partitions should be more than 1");
         }
         try {
-            updatePartitionedTopic(dn, numPartitions).get();
+            updatePartitionedTopic(destinationName, numPartitions).get();
         } catch (Exception e) {
             if (e.getCause() instanceof RestException) {
                 throw (RestException) e.getCause();
             }
-            log.error("[{}] Failed to update partitioned topic {}", clientAppId(), dn, e.getCause());
+            log.error("[{}] Failed to update partitioned topic {}", clientAppId(), destinationName, e.getCause());
             throw new RestException(e.getCause());
         }
     }
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
-    @ApiOperation(value = "Get partitioned topic metadata.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
-    public PartitionedTopicMetadata getPartitionedMetadata(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        PartitionedTopicMetadata metadata = getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative);
+    protected PartitionedTopicMetadata internalGetPartitionedMetadata(boolean authoritative) {
+        PartitionedTopicMetadata metadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (metadata.partitions > 1) {
             validateClientVersion();
         }
         return metadata;
     }
 
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
-    @ApiOperation(value = "Delete a partitioned topic.", notes = "It will also delete all the partitions of the topic if it exists.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Partitioned topic does not exist") })
-    public void deletePartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAccessOnProperty(dn.getProperty());
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected void internalDeletePartitionedTopic(boolean authoritative) {
+        validateAdminAccessOnProperty(destinationName.getProperty());
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         int numPartitions = partitionMetadata.partitions;
         if (numPartitions > 0) {
             final CompletableFuture<Void> future = new CompletableFuture<>();
             final AtomicInteger count = new AtomicInteger(numPartitions);
             try {
                 for (int i = 0; i < numPartitions; i++) {
-                    DestinationName dn_partition = dn.getPartition(i);
+                    DestinationName dn_partition = destinationName.getPartition(i);
                     pulsar().getAdminClient().persistentTopics().deleteAsync(dn_partition.toString())
                             .whenComplete((r, ex) -> {
                                 if (ex != null) {
@@ -538,63 +438,45 @@ public class PersistentTopics extends AdminResource {
         }
 
         // Only tries to delete the znode for partitioned topic when all its partitions are successfully deleted
-        String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(),
-                dn.getEncodedLocalName());
+        String path = path(PARTITIONED_TOPIC_PATH_ZNODE, namespaceName.toString(), domain(),
+                destinationName.getEncodedLocalName());
         try {
             globalZk().delete(path, -1);
             globalZkCache().invalidate(path);
             // we wait for the data to be synced in all quorums and the observers
             Thread.sleep(PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS);
-            log.info("[{}] Deleted partitioned topic {}", clientAppId(), dn);
+            log.info("[{}] Deleted partitioned topic {}", clientAppId(), destinationName);
         } catch (KeeperException.NoNodeException nne) {
             throw new RestException(Status.NOT_FOUND, "Partitioned topic does not exist");
         } catch (Exception e) {
-            log.error("[{}] Failed to delete partitioned topic {}", clientAppId(), dn, e);
+            log.error("[{}] Failed to delete partitioned topic {}", clientAppId(), destinationName, e);
             throw new RestException(e);
         }
     }
 
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}/{destination}/unload")
-    @ApiOperation(value = "Unload a topic")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public void unloadTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        log.info("[{}] Unloading topic {}/{}/{}/{}", clientAppId(), property, cluster, namespace, destination);
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        unloadTopic(dn, authoritative);
+    protected void internalUnloadTopic(boolean authoritative) {
+        log.info("[{}] Unloading topic {}", clientAppId(), destinationName);
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        unloadTopic(destinationName, authoritative);
     }
 
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/{destination}")
-    @ApiOperation(value = "Delete a topic.", notes = "The topic cannot be deleted if there's any active subscription or producer connected to the it.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist"),
-            @ApiResponse(code = 412, message = "Topic has active producers/subscriptions") })
-    public void deleteTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminOperationOnDestination(dn, authoritative);
-        Topic topic = getTopicReference(dn);
-        if (dn.isGlobal()) {
+    protected void internalDeleteTopic(boolean authoritative) {
+        validateAdminOperationOnDestination(authoritative);
+        Topic topic = getTopicReference(destinationName);
+        if (destinationName.isGlobal()) {
             // Delete is disallowed on global topic
-            log.error("[{}] Delete topic is forbidden on global namespace {}", clientAppId(), dn);
+            log.error("[{}] Delete topic is forbidden on global namespace {}", clientAppId(), destinationName);
             throw new RestException(Status.FORBIDDEN, "Delete forbidden on global namespace");
         }
+
         try {
             topic.delete().get();
-            log.info("[{}] Successfully removed topic {}", clientAppId(), dn);
+            log.info("[{}] Successfully removed topic {}", clientAppId(), destinationName);
         } catch (Exception e) {
             Throwable t = e.getCause();
-            log.error("[{}] Failed to get delete topic {}", clientAppId(), dn, t);
+            log.error("[{}] Failed to get delete topic {}", clientAppId(), destinationName, t);
             if (t instanceof TopicBusyException) {
                 throw new RestException(Status.PRECONDITION_FAILED, "Topic has active producers/subscriptions");
             } else {
@@ -603,39 +485,30 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscriptions")
-    @ApiOperation(value = "Get the list of persistent subscriptions for a given topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public List<String> getSubscriptions(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
+    protected List<String> internalGetSubscriptions(boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
         }
+
         List<String> subscriptions = Lists.newArrayList();
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             try {
                 // get the subscriptions only from the 1st partition since all the other partitions will have the same
                 // subscriptions
-                subscriptions.addAll(
-                        pulsar().getAdminClient().persistentTopics().getSubscriptions(dn.getPartition(0).toString()));
+                subscriptions.addAll(pulsar().getAdminClient().persistentTopics()
+                        .getSubscriptions(destinationName.getPartition(0).toString()));
             } catch (Exception e) {
                 throw new RestException(e);
             }
         } else {
-            validateAdminOperationOnDestination(dn, authoritative);
-            Topic topic = getTopicReference(dn);
+            validateAdminOperationOnDestination(authoritative);
+            Topic topic = getTopicReference(destinationName);
 
             try {
                 topic.getSubscriptions().forEach((subName, sub) -> subscriptions.add(subName));
             } catch (Exception e) {
-                log.error("[{}] Failed to get list of subscriptions for {}", clientAppId(), dn);
+                log.error("[{}] Failed to get list of subscriptions for {}", clientAppId(), destinationName);
                 throw new RestException(e);
             }
         }
@@ -643,60 +516,32 @@ public class PersistentTopics extends AdminResource {
         return subscriptions;
     }
 
-    @GET
-    @Path("{property}/{cluster}/{namespace}/{destination}/stats")
-    @ApiOperation(value = "Get the stats for the topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public PersistentTopicStats getStats(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAndClientPermission(dn);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        validateDestinationOwnership(dn, authoritative);
-        Topic topic = getTopicReference(dn);
+    protected PersistentTopicStats internalGetStats(boolean authoritative) {
+        validateAdminAndClientPermission();
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        validateDestinationOwnership(destinationName, authoritative);
+        Topic topic = getTopicReference(destinationName);
         return topic.getStats();
     }
 
-    @GET
-    @Path("{property}/{cluster}/{namespace}/{destination}/internalStats")
-    @ApiOperation(value = "Get the internal stats for the topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public PersistentTopicInternalStats getInternalStats(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAndClientPermission(dn);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        validateDestinationOwnership(dn, authoritative);
-        Topic topic = getTopicReference(dn);
+    protected PersistentTopicInternalStats internalGetInternalStats(boolean authoritative) {
+        validateAdminAndClientPermission();
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        validateDestinationOwnership(destinationName, authoritative);
+        Topic topic = getTopicReference(destinationName);
         return topic.getInternalStats();
     }
 
-    @GET
-    @Path("{property}/{cluster}/{namespace}/{destination}/internal-info")
-    @ApiOperation(value = "Get the internal stats for the topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public void getManagedLedgerInfo(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @Suspended AsyncResponse asyncResponse) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAccessOnProperty(dn.getProperty());
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        String managedLedger = dn.getPersistenceNamingEncoding();
+    protected void internalGetManagedLedgerInfo(AsyncResponse asyncResponse) {
+        validateAdminAccessOnProperty(destinationName.getProperty());
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        String managedLedger = destinationName.getPersistenceNamingEncoding();
         pulsar().getManagedLedgerFactory().asyncGetManagedLedgerInfo(managedLedger, new ManagedLedgerInfoCallback() {
             @Override
             public void getInfoComplete(ManagedLedgerInfo info, Object ctx) {
@@ -712,32 +557,21 @@ public class PersistentTopics extends AdminResource {
         }, null);
     }
 
-    @GET
-    @Path("{property}/{cluster}/{namespace}/{destination}/partitioned-stats")
-    @ApiOperation(value = "Get the stats for the partitioned topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public PartitionedTopicStats getPartitionedStats(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected PartitionedTopicStats internalGetPartitionedStats(boolean authoritative) {
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions == 0) {
             throw new RestException(Status.NOT_FOUND, "Partitioned Topic not found");
         }
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
         }
         PartitionedTopicStats stats = new PartitionedTopicStats(partitionMetadata);
         try {
             for (int i = 0; i < partitionMetadata.partitions; i++) {
                 PersistentTopicStats partitionStats = pulsar().getAdminClient().persistentTopics()
-                        .getStats(dn.getPartition(i).toString());
+                        .getStats(destinationName.getPartition(i).toString());
                 stats.add(partitionStats);
-                stats.partitions.put(dn.getPartition(i).toString(), partitionStats);
+                stats.partitions.put(destinationName.getPartition(i).toString(), partitionStats);
             }
         } catch (Exception e) {
             throw new RestException(e);
@@ -745,28 +579,16 @@ public class PersistentTopics extends AdminResource {
         return stats;
     }
 
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}")
-    @ApiOperation(value = "Delete a subscription.", notes = "There should not be any active consumers on the subscription.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic does not exist"),
-            @ApiResponse(code = 412, message = "Subscription has active consumers") })
-    public void deleteSubscription(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected void internalDeleteSubscription(String subName, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             try {
                 for (int i = 0; i < partitionMetadata.partitions; i++) {
-                    pulsar().getAdminClient().persistentTopics().deleteSubscription(dn.getPartition(i).toString(),
-                            subName);
+                    pulsar().getAdminClient().persistentTopics()
+                            .deleteSubscription(destinationName.getPartition(i).toString(), subName);
                 }
             } catch (Exception e) {
                 if (e instanceof NotFoundException) {
@@ -774,18 +596,18 @@ public class PersistentTopics extends AdminResource {
                 } else if (e instanceof PreconditionFailedException) {
                     throw new RestException(Status.PRECONDITION_FAILED, "Subscription has active connected consumers");
                 } else {
-                    log.error("[{}] Failed to delete subscription {} {}", clientAppId(), dn, subName, e);
+                    log.error("[{}] Failed to delete subscription {} {}", clientAppId(), destinationName, subName, e);
                     throw new RestException(e);
                 }
             }
         } else {
-            validateAdminOperationOnDestination(dn, authoritative);
-            Topic topic = getTopicReference(dn);
+            validateAdminOperationOnDestination(authoritative);
+            Topic topic = getTopicReference(destinationName);
             try {
                 Subscription sub = topic.getSubscription(subName);
                 checkNotNull(sub);
                 sub.delete().get();
-                log.info("[{}][{}] Deleted subscription {}", clientAppId(), dn, subName);
+                log.info("[{}][{}] Deleted subscription {}", clientAppId(), destinationName, subName);
             } catch (Exception e) {
                 Throwable t = e.getCause();
                 if (e instanceof NullPointerException) {
@@ -793,43 +615,30 @@ public class PersistentTopics extends AdminResource {
                 } else if (t instanceof SubscriptionBusyException) {
                     throw new RestException(Status.PRECONDITION_FAILED, "Subscription has active connected consumers");
                 } else {
-                    log.error("[{}] Failed to delete subscription {} {}", clientAppId(), dn, subName, e);
+                    log.error("[{}] Failed to delete subscription {} {}", clientAppId(), destinationName, subName, e);
                     throw new RestException(t);
                 }
             }
         }
-
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/skip_all")
-    @ApiOperation(value = "Skip all messages on a topic subscription.", notes = "Completely clears the backlog on the subscription.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 405, message = "Operation not allowed on non-persistent topic"),
-            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
-    public void skipAllMessages(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected void internalSkipAllMessages(String subName, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             try {
                 for (int i = 0; i < partitionMetadata.partitions; i++) {
-                    pulsar().getAdminClient().persistentTopics().skipAllMessages(dn.getPartition(i).toString(),
-                            subName);
+                    pulsar().getAdminClient().persistentTopics()
+                            .skipAllMessages(destinationName.getPartition(i).toString(), subName);
                 }
             } catch (Exception e) {
                 throw new RestException(e);
             }
         } else {
-            validateAdminOperationOnDestination(dn, authoritative);
-            PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+            validateAdminOperationOnDestination(authoritative);
+            PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
             try {
                 if (subName.startsWith(topic.replicatorPrefix)) {
                     String remoteCluster = PersistentReplicator.getRemoteCluster(subName);
@@ -841,38 +650,26 @@ public class PersistentTopics extends AdminResource {
                     checkNotNull(sub);
                     sub.clearBacklog().get();
                 }
-                log.info("[{}] Cleared backlog on {} {}", clientAppId(), dn, subName);
+                log.info("[{}] Cleared backlog on {} {}", clientAppId(), destinationName, subName);
             } catch (NullPointerException npe) {
                 throw new RestException(Status.NOT_FOUND, "Subscription not found");
             } catch (Exception exception) {
-                log.error("[{}] Failed to skip all messages {} {}", clientAppId(), dn, subName, exception);
+                log.error("[{}] Failed to skip all messages {} {}", clientAppId(), destinationName, subName, exception);
                 throw new RestException(exception);
             }
         }
-
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/skip/{numMessages}")
-    @ApiOperation(value = "Skip messages on a topic subscription.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
-    public void skipMessages(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName, @PathParam("numMessages") int numMessages,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected void internalSkipMessages(String subName, int numMessages, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             throw new RestException(Status.METHOD_NOT_ALLOWED, "Skip messages on a partitioned topic is not allowed");
         }
-        validateAdminOperationOnDestination(dn, authoritative);
-        PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+        validateAdminOperationOnDestination(authoritative);
+        PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
         try {
             if (subName.startsWith(topic.replicatorPrefix)) {
                 String remoteCluster = PersistentReplicator.getRemoteCluster(subName);
@@ -884,85 +681,51 @@ public class PersistentTopics extends AdminResource {
                 checkNotNull(sub);
                 sub.skipMessages(numMessages).get();
             }
-            log.info("[{}] Skipped {} messages on {} {}", clientAppId(), numMessages, dn, subName);
+            log.info("[{}] Skipped {} messages on {} {}", clientAppId(), numMessages, destinationName, subName);
         } catch (NullPointerException npe) {
             throw new RestException(Status.NOT_FOUND, "Subscription not found");
         } catch (Exception exception) {
-            log.error("[{}] Failed to skip {} messages {} {}", clientAppId(), numMessages, dn, subName, exception);
+            log.error("[{}] Failed to skip {} messages {} {}", clientAppId(), numMessages, destinationName, subName,
+                    exception);
             throw new RestException(exception);
         }
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/expireMessages/{expireTimeInSeconds}")
-    @ApiOperation(value = "Expire messages on a topic subscription.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
-    public void expireTopicMessages(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName, @PathParam("expireTimeInSeconds") int expireTimeInSeconds,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        expireMessages(property, cluster, namespace, destination, subName, expireTimeInSeconds, authoritative);
-    }
-
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/all_subscription/expireMessages/{expireTimeInSeconds}")
-    @ApiOperation(value = "Expire messages on all subscriptions of topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
-    public void expireMessagesForAllSubscriptions(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destinationName, @PathParam("expireTimeInSeconds") int expireTimeInSeconds,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        final String destination = decode(destinationName);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected void internalExpireMessagesForAllSubscriptions(int expireTimeInSeconds, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             try {
                 // expire messages for each partition destination
                 for (int i = 0; i < partitionMetadata.partitions; i++) {
-                    pulsar().getAdminClient().persistentTopics()
-                            .expireMessagesForAllSubscriptions(dn.getPartition(i).toString(), expireTimeInSeconds);
+                    pulsar().getAdminClient().persistentTopics().expireMessagesForAllSubscriptions(
+                            destinationName.getPartition(i).toString(), expireTimeInSeconds);
                 }
             } catch (Exception e) {
-                log.error("[{}] Failed to expire messages up to {} on {} {}", clientAppId(), expireTimeInSeconds, dn,
-                        e);
+                log.error("[{}] Failed to expire messages up to {} on {} {}", clientAppId(), expireTimeInSeconds,
+                        destinationName, e);
                 throw new RestException(e);
             }
         } else {
             // validate ownership and redirect if current broker is not owner
-            validateAdminOperationOnDestination(dn, authoritative);
-            PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+            validateAdminOperationOnDestination(authoritative);
+            PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
             topic.getReplicators().forEach((subName, replicator) -> {
-                expireMessages(property, cluster, namespace, destination, subName, expireTimeInSeconds, authoritative);
+                internalExpireMessages(subName, expireTimeInSeconds, authoritative);
             });
             topic.getSubscriptions().forEach((subName, subscriber) -> {
-                expireMessages(property, cluster, namespace, destination, subName, expireTimeInSeconds, authoritative);
+                internalExpireMessages(subName, expireTimeInSeconds, authoritative);
             });
         }
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/resetcursor/{timestamp}")
-    @ApiOperation(value = "Reset subscription to message position closest to absolute timestamp (in ms).", notes = "It fence cursor and disconnects all active consumers before reseting cursor.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic/Subscription does not exist") })
-    public void resetCursor(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName, @PathParam("timestamp") long timestamp,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected void internalResetCursor(String subName, long timestamp, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
 
         if (partitionMetadata.partitions > 0) {
             int numParts = partitionMetadata.partitions;
@@ -970,8 +733,8 @@ public class PersistentTopics extends AdminResource {
             Exception partitionException = null;
             try {
                 for (int i = 0; i < numParts; i++) {
-                    pulsar().getAdminClient().persistentTopics().resetCursor(dn.getPartition(i).toString(), subName,
-                            timestamp);
+                    pulsar().getAdminClient().persistentTopics().resetCursor(destinationName.getPartition(i).toString(),
+                            subName, timestamp);
                 }
             } catch (PreconditionFailedException pfe) {
                 // throw the last exception if all partitions get this error
@@ -979,25 +742,25 @@ public class PersistentTopics extends AdminResource {
                 ++numPartException;
                 partitionException = pfe;
             } catch (Exception e) {
-                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(), dn, subName,
-                        timestamp, e);
+                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(),
+                        destinationName, subName, timestamp, e);
                 throw new RestException(e);
             }
             // report an error to user if unable to reset for all partitions
             if (numPartException == numParts) {
-                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(), dn, subName,
-                        timestamp, partitionException);
+                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(),
+                        destinationName, subName, timestamp, partitionException);
                 throw new RestException(Status.PRECONDITION_FAILED, partitionException.getMessage());
             } else if (numPartException > 0) {
                 log.warn("[{}][{}] partial errors for reset cursor on subscription {} to time {} - ", clientAppId(),
-                        destination, subName, timestamp, partitionException);
+                        destinationName, subName, timestamp, partitionException);
             }
 
         } else {
-            validateAdminOperationOnDestination(dn, authoritative);
-            log.info("[{}][{}] received reset cursor on subscription {} to time {}", clientAppId(), destination,
+            validateAdminOperationOnDestination(authoritative);
+            log.info("[{}][{}] received reset cursor on subscription {} to time {}", clientAppId(), destinationName,
                     subName, timestamp);
-            PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+            PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
             if (topic == null) {
                 throw new RestException(Status.NOT_FOUND, "Topic not found");
             }
@@ -1005,11 +768,12 @@ public class PersistentTopics extends AdminResource {
                 PersistentSubscription sub = topic.getSubscription(subName);
                 checkNotNull(sub);
                 sub.resetCursor(timestamp).get();
-                log.info("[{}][{}] reset cursor on subscription {} to time {}", clientAppId(), dn, subName, timestamp);
+                log.info("[{}][{}] reset cursor on subscription {} to time {}", clientAppId(), destinationName, subName,
+                        timestamp);
             } catch (Exception e) {
                 Throwable t = e.getCause();
-                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(), dn, subName,
-                        timestamp, e);
+                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to time {}", clientAppId(),
+                        destinationName, subName, timestamp, e);
                 if (e instanceof NullPointerException) {
                     throw new RestException(Status.NOT_FOUND, "Subscription not found");
                 } else if (e instanceof NotAllowedException) {
@@ -1024,26 +788,14 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @PUT
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subscriptionName}")
-    @ApiOperation(value = "Reset subscription to message position closest to given position.", notes = "Creates a subscription on the topic at the specified message id")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic/Subscription does not exist"),
-            @ApiResponse(code = 405, message = "Not supported for partitioned topics") })
-    public void createSubscription(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subscriptionName") String subscriptionName,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative, MessageIdImpl messageId) throws PulsarServerException {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        log.info("[{}][{}] Creating subscription {} at message id {}", clientAppId(), destination,
+    protected void internalCreateSubscription(String subscriptionName, MessageIdImpl messageId, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        log.info("[{}][{}] Creating subscription {} at message id {}", clientAppId(), destinationName,
                 subscriptionName, messageId);
 
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
 
         try {
             if (partitionMetadata.partitions > 0) {
@@ -1052,15 +804,16 @@ public class PersistentTopics extends AdminResource {
                 PulsarAdmin admin = pulsar().getAdminClient();
 
                 for (int i = 0; i < partitionMetadata.partitions; i++) {
-                    futures.add(admin.persistentTopics().createSubscriptionAsync(dn.getPartition(i).toString(),
+                    futures.add(admin.persistentTopics().createSubscriptionAsync(
+                            destinationName.getPartition(i).toString(),
                             subscriptionName, messageId));
                 }
 
                 FutureUtil.waitForAll(futures).join();
             } else {
-                validateAdminOperationOnDestination(dn, authoritative);
+                validateAdminOperationOnDestination(authoritative);
 
-                PersistentTopic topic = (PersistentTopic) getOrCreateTopic(dn);
+                PersistentTopic topic = (PersistentTopic) getOrCreateTopic(destinationName);
 
                 if (topic.getSubscriptions().containsKey(subscriptionName)) {
                     throw new RestException(Status.CONFLICT, "Subscription already exists for topic");
@@ -1069,13 +822,13 @@ public class PersistentTopics extends AdminResource {
                 PersistentSubscription subscription = (PersistentSubscription) topic
                         .createSubscription(subscriptionName).get();
                 subscription.resetCursor(PositionImpl.get(messageId.getLedgerId(), messageId.getEntryId())).get();
-                log.info("[{}][{}] Successfully created subscription {} at message id {}", clientAppId(), dn,
-                        subscriptionName, messageId);
+                log.info("[{}][{}] Successfully created subscription {} at message id {}", clientAppId(),
+                        destinationName, subscriptionName, messageId);
             }
         } catch (Exception e) {
             Throwable t = e.getCause();
-            log.warn("[{}] [{}] Failed to create subscription {} at message id {}", clientAppId(), dn, subscriptionName,
-                    messageId, e);
+            log.warn("[{}] [{}] Failed to create subscription {} at message id {}", clientAppId(),
+                    destinationName, subscriptionName, messageId, e);
             if (t instanceof SubscriptionInvalidCursorPosition) {
                 throw new RestException(Status.PRECONDITION_FAILED,
                         "Unable to find position for position specified: " + t.getMessage());
@@ -1085,34 +838,23 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/resetcursor")
-    @ApiOperation(value = "Reset subscription to message position closest to given position.", notes = "It fence cursor and disconnects all active consumers before reseting cursor.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic/Subscription does not exist"),
-            @ApiResponse(code = 405, message = "Not supported for partitioned topics") })
-    public void resetCursorOnPosition(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative, MessageIdImpl messageId) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        log.info("[{}][{}] received reset cursor on subscription {} to position {}", clientAppId(), destination,
+    protected void internalResetCursorOnPosition(String subName, boolean authoritative, MessageIdImpl messageId) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        log.info("[{}][{}] received reset cursor on subscription {} to position {}", clientAppId(), destinationName,
                 subName, messageId);
 
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
 
         if (partitionMetadata.partitions > 0) {
-            log.warn("[{}] Not supported operation on partitioned-topic {} {}", clientAppId(), dn, subName);
+            log.warn("[{}] Not supported operation on partitioned-topic {} {}", clientAppId(), destinationName,
+                    subName);
             throw new RestException(Status.METHOD_NOT_ALLOWED,
                     "Reset-cursor at position is not allowed for partitioned-topic");
         } else {
-            validateAdminOperationOnDestination(dn, authoritative);
-            PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+            validateAdminOperationOnDestination(authoritative);
+            PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
             if (topic == null) {
                 throw new RestException(Status.NOT_FOUND, "Topic not found");
             }
@@ -1120,12 +862,12 @@ public class PersistentTopics extends AdminResource {
                 PersistentSubscription sub = topic.getSubscription(subName);
                 checkNotNull(sub);
                 sub.resetCursor(PositionImpl.get(messageId.getLedgerId(), messageId.getEntryId())).get();
-                log.info("[{}][{}] successfully reset cursor on subscription {} to position {}", clientAppId(), dn,
-                        subName, messageId);
+                log.info("[{}][{}] successfully reset cursor on subscription {} to position {}", clientAppId(),
+                        destinationName, subName, messageId);
             } catch (Exception e) {
                 Throwable t = e.getCause();
-                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to position {}", clientAppId(), dn,
-                        subName, messageId, e);
+                log.warn("[{}] [{}] Failed to reset cursor on subscription {} to position {}", clientAppId(),
+                        destinationName, subName, messageId, e);
                 if (e instanceof NullPointerException) {
                     throw new RestException(Status.NOT_FOUND, "Subscription not found");
                 } else if (t instanceof SubscriptionInvalidCursorPosition) {
@@ -1138,32 +880,22 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/position/{messagePosition}")
-    @ApiOperation(value = "Peek nth message on a topic subscription.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Topic, subscription or the message position does not exist") })
-    public Response peekNthMessage(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @PathParam("subName") String subName, @PathParam("messagePosition") int messagePosition,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected Response internalPeekNthMessage(String subName, int messagePosition, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             throw new RestException(Status.METHOD_NOT_ALLOWED, "Peek messages on a partitioned topic is not allowed");
         }
-        validateAdminOperationOnDestination(dn, authoritative);
-        if (!(getTopicReference(dn) instanceof PersistentTopic)) {
-            log.error("[{}] Not supported operation of non-persistent topic {} {}", clientAppId(), dn, subName);
+        validateAdminOperationOnDestination(authoritative);
+        if (!(getTopicReference(destinationName) instanceof PersistentTopic)) {
+            log.error("[{}] Not supported operation of non-persistent topic {} {}", clientAppId(), destinationName,
+                    subName);
             throw new RestException(Status.METHOD_NOT_ALLOWED,
                     "Skip messages on a non-persistent topic is not allowed");
         }
-        PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+        PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
         PersistentReplicator repl = null;
         PersistentSubscription sub = null;
         Entry entry = null;
@@ -1223,8 +955,8 @@ public class PersistentTopics extends AdminResource {
         } catch (NullPointerException npe) {
             throw new RestException(Status.NOT_FOUND, "Message not found");
         } catch (Exception exception) {
-            log.error("[{}] Failed to get message at position {} from {} {}", clientAppId(), messagePosition, dn,
-                    subName, exception);
+            log.error("[{}] Failed to get message at position {} from {} {}", clientAppId(), messagePosition,
+                    destinationName, subName, exception);
             throw new RestException(exception);
         } finally {
             if (entry != null) {
@@ -1233,37 +965,26 @@ public class PersistentTopics extends AdminResource {
         }
     }
 
-    @GET
-    @Path("{property}/{cluster}/{namespace}/{destination}/backlog")
-    @ApiOperation(value = "Get estimated backlog for offline topic.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public PersistentOfflineTopicStats getBacklog(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        validateAdminAccessOnProperty(property);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
+    protected PersistentOfflineTopicStats internalGetBacklog(boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
         }
         // Validate that namespace exists, throw 404 if it doesn't exist
         // note that we do not want to load the topic and hence skip validateAdminOperationOnDestination()
         try {
-            policiesCache().get(path(POLICIES, property, cluster, namespace));
+            policiesCache().get(path(POLICIES, namespaceName.toString()));
         } catch (KeeperException.NoNodeException e) {
-            log.warn("[{}] Failed to get topic backlog {}/{}/{}: Namespace does not exist", clientAppId(), property,
-                    cluster, namespace);
+            log.warn("[{}] Failed to get topic backlog {}: Namespace does not exist", clientAppId(), namespaceName);
             throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
         } catch (Exception e) {
-            log.error("[{}] Failed to get topic backlog {}/{}/{}", clientAppId(), property, cluster, namespace, e);
+            log.error("[{}] Failed to get topic backlog {}", clientAppId(), namespaceName, e);
             throw new RestException(e);
         }
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
+
         PersistentOfflineTopicStats offlineTopicStats = null;
         try {
 
-            offlineTopicStats = pulsar().getBrokerService().getOfflineTopicStat(dn);
+            offlineTopicStats = pulsar().getBrokerService().getOfflineTopicStat(destinationName);
             if (offlineTopicStats != null) {
                 // offline topic stat has a cost - so use cached value until TTL
                 long elapsedMs = System.currentTimeMillis() - offlineTopicStats.statGeneratedAt.getTime();
@@ -1271,75 +992,62 @@ public class PersistentTopics extends AdminResource {
                     return offlineTopicStats;
                 }
             }
-            final ManagedLedgerConfig config = pulsar().getBrokerService().getManagedLedgerConfig(dn).get();
+            final ManagedLedgerConfig config = pulsar().getBrokerService().getManagedLedgerConfig(destinationName)
+                    .get();
             ManagedLedgerOfflineBacklog offlineTopicBacklog = new ManagedLedgerOfflineBacklog(config.getDigestType(),
                     config.getPassword(), pulsar().getAdvertisedAddress(), false);
-            offlineTopicStats = offlineTopicBacklog
-                    .estimateUnloadedTopicBacklog((ManagedLedgerFactoryImpl) pulsar().getManagedLedgerFactory(), dn);
-            pulsar().getBrokerService().cacheOfflineTopicStats(dn, offlineTopicStats);
+            offlineTopicStats = offlineTopicBacklog.estimateUnloadedTopicBacklog(
+                    (ManagedLedgerFactoryImpl) pulsar().getManagedLedgerFactory(), destinationName);
+            pulsar().getBrokerService().cacheOfflineTopicStats(destinationName, offlineTopicStats);
         } catch (Exception exception) {
             throw new RestException(exception);
         }
         return offlineTopicStats;
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{destination}/terminate")
-    @ApiOperation(value = "Terminate a topic. A topic that is terminated will not accept any more "
-            + "messages to be published and will let consumer to drain existing messages in backlog")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 405, message = "Operation not allowed on non-persistent topic"),
-            @ApiResponse(code = 404, message = "Topic does not exist") })
-    public MessageId terminate(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
-        }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+    protected MessageId internalTerminate(boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             throw new RestException(Status.METHOD_NOT_ALLOWED, "Termination of a partitioned topic is not allowed");
         }
-        validateAdminOperationOnDestination(dn, authoritative);
-        Topic topic = getTopicReference(dn);
+        validateAdminOperationOnDestination(authoritative);
+        Topic topic = getTopicReference(destinationName);
         try {
             return ((PersistentTopic) topic).terminate().get();
         } catch (Exception exception) {
-            log.error("[{}] Failed to terminated topic {}", clientAppId(), dn, exception);
+            log.error("[{}] Failed to terminated topic {}", clientAppId(), destinationName, exception);
             throw new RestException(exception);
         }
     }
 
-    public void expireMessages(String property, String cluster, String namespace, String destination, String subName,
-            int expireTimeInSeconds, boolean authoritative) {
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
+    protected void internalExpireMessages(String subName, int expireTimeInSeconds, boolean authoritative) {
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
         }
-        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(property, cluster, namespace,
-                destination, authoritative);
+        PartitionedTopicMetadata partitionMetadata = getPartitionedTopicMetadata(destinationName, authoritative);
         if (partitionMetadata.partitions > 0) {
             // expire messages for each partition destination
             try {
                 for (int i = 0; i < partitionMetadata.partitions; i++) {
-                    pulsar().getAdminClient().persistentTopics().expireMessages(dn.getPartition(i).toString(), subName,
-                            expireTimeInSeconds);
+                    pulsar().getAdminClient().persistentTopics()
+                            .expireMessages(destinationName.getPartition(i).toString(), subName, expireTimeInSeconds);
                 }
             } catch (Exception e) {
                 throw new RestException(e);
             }
         } else {
             // validate ownership and redirect if current broker is not owner
-            validateAdminOperationOnDestination(dn, authoritative);
-            if (!(getTopicReference(dn) instanceof PersistentTopic)) {
-                log.error("[{}] Not supported operation of non-persistent topic {} {}", clientAppId(), dn, subName);
+            validateAdminOperationOnDestination(authoritative);
+            if (!(getTopicReference(destinationName) instanceof PersistentTopic)) {
+                log.error("[{}] Not supported operation of non-persistent topic {} {}", clientAppId(), destinationName,
+                        subName);
                 throw new RestException(Status.METHOD_NOT_ALLOWED,
                         "Expire messages on a non-persistent topic is not allowed");
             }
-            PersistentTopic topic = (PersistentTopic) getTopicReference(dn);
+            PersistentTopic topic = (PersistentTopic) getTopicReference(destinationName);
             try {
                 if (subName.startsWith(topic.replicatorPrefix)) {
                     String remoteCluster = PersistentReplicator.getRemoteCluster(subName);
@@ -1351,13 +1059,13 @@ public class PersistentTopics extends AdminResource {
                     checkNotNull(sub);
                     sub.expireMessages(expireTimeInSeconds);
                 }
-                log.info("[{}] Message expire started up to {} on {} {}", clientAppId(), expireTimeInSeconds, dn,
-                        subName);
+                log.info("[{}] Message expire started up to {} on {} {}", clientAppId(), expireTimeInSeconds,
+                        destinationName, subName);
             } catch (NullPointerException npe) {
                 throw new RestException(Status.NOT_FOUND, "Subscription not found");
             } catch (Exception exception) {
                 log.error("[{}] Failed to expire messages up to {} on {} with subscription {} {}", clientAppId(),
-                        expireTimeInSeconds, dn, subName, exception);
+                        expireTimeInSeconds, destinationName, subName, exception);
                 throw new RestException(exception);
             }
         }
@@ -1386,8 +1094,8 @@ public class PersistentTopics extends AdminResource {
                 throw ex;
             }
 
-            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, dn.getProperty(), dn.getCluster(),
-                    dn.getNamespacePortion(), "persistent", dn.getEncodedLocalName());
+            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, dn.getNamespace(),
+                    "persistent", dn.getEncodedLocalName());
 
             // validates global-namespace contains local/peer cluster: if peer/local cluster present then lookup can
             // serve/redirect request else fail partitioned-metadata-request so, client fails while creating
@@ -1409,7 +1117,7 @@ public class PersistentTopics extends AdminResource {
         return metadataFuture;
     }
 
-	/**
+    /**
      * Get the Topic object reference from the Pulsar broker
      */
     private Topic getTopicReference(DestinationName dn) {
@@ -1485,8 +1193,10 @@ public class PersistentTopics extends AdminResource {
     /**
      * It creates subscriptions for new partitions of existing partitioned-topics
      *
-     * @param dn : topic-name: persistent://prop/cluster/ns/topic
-     * @param numPartitions : number partitions for the topics
+     * @param dn
+     *            : topic-name: persistent://prop/cluster/ns/topic
+     * @param numPartitions
+     *            : number partitions for the topics
      */
     private CompletableFuture<Void> createSubscriptions(DestinationName dn, int numPartitions) {
         String path = path(PARTITIONED_TOPIC_PATH_ZNODE, dn.getProperty(), dn.getCluster(), dn.getNamespacePortion(),
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Properties.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PropertiesBase.java
similarity index 95%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Properties.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PropertiesBase.java
index 359f300..6845e91 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/Properties.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PropertiesBase.java
@@ -16,22 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
+package org.apache.pulsar.broker.admin.impl;
 
-import java.util.Collections;
 import java.util.List;
 
-import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response.Status;
 
+import org.apache.pulsar.broker.admin.AdminResource;
 import org.apache.pulsar.broker.web.RestException;
 import org.apache.pulsar.common.naming.NamedEntity;
 import org.apache.pulsar.common.policies.data.PropertyAdmin;
@@ -41,17 +38,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Lists;
-import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
 
-@Path("/properties")
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-@Api(value = "/properties", description = "Properties admin apis", tags = "properties")
-public class Properties extends AdminResource {
+public class PropertiesBase extends AdminResource {
 
     @GET
     @ApiOperation(value = "Get the list of properties.", response = String.class, responseContainer = "List")
@@ -207,5 +199,5 @@ public class Properties extends AdminResource {
         }
     }
 
-    private static final Logger log = LoggerFactory.getLogger(Properties.class);
+    private static final Logger log = LoggerFactory.getLogger(PropertiesBase.class);
 }
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/ResourceQuotas.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ResourceQuotasBase.java
similarity index 53%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/ResourceQuotas.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ResourceQuotasBase.java
index 3eed023..9c5a54f 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/ResourceQuotas.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ResourceQuotasBase.java
@@ -16,43 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
+package org.apache.pulsar.broker.admin.impl;
+
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.pulsar.broker.web.RestException;
 import org.apache.pulsar.common.naming.NamespaceBundle;
-import org.apache.pulsar.common.naming.NamespaceName;
 import org.apache.pulsar.common.policies.data.Policies;
 import org.apache.pulsar.common.policies.data.ResourceQuota;
 import org.apache.zookeeper.KeeperException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-
-@Path("/resource-quotas")
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-@Api(value = "/resource-quotas", description = "Quota admin APIs", tags = "resource-quotas")
-public class ResourceQuotas extends AdminResource {
+public abstract class ResourceQuotasBase extends NamespacesBase {
 
-    private static final Logger log = LoggerFactory.getLogger(ResourceQuotas.class);
-
-    @GET
-    @ApiOperation(value = "Get the default quota", response = String.class, responseContainer = "Set")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
     public ResourceQuota getDefaultResourceQuota() throws Exception {
         validateSuperUserAccess();
         try {
@@ -64,9 +41,6 @@ public class ResourceQuotas extends AdminResource {
 
     }
 
-    @POST
-    @ApiOperation(value = "Set the default quota", response = String.class, responseContainer = "Set")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
     public void setDefaultResourceQuota(ResourceQuota quota) throws Exception {
         validateSuperUserAccess();
         validatePoliciesReadOnlyAccess();
@@ -78,25 +52,18 @@ public class ResourceQuotas extends AdminResource {
         }
     }
 
-    @GET
-    @Path("/{property}/{cluster}/{namespace}/{bundle}")
-    @ApiOperation(value = "Get resource quota of a namespace bundle.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 404, message = "Namespace does not exist") })
-    public ResourceQuota getNamespaceBundleResourceQuota(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("bundle") String bundleRange) {
+    @SuppressWarnings("deprecation")
+    protected ResourceQuota internalGetNamespaceBundleResourceQuota(String bundleRange) {
         validateSuperUserAccess();
 
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
+        Policies policies = getNamespacePolicies(namespaceName);
 
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
+        if (!namespaceName.isGlobal()) {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
         }
 
-        NamespaceName fqnn = NamespaceName.get(property, cluster, namespace);
-        NamespaceBundle nsBundle = validateNamespaceBundleRange(fqnn, policies.bundles, bundleRange);
+        NamespaceBundle nsBundle = validateNamespaceBundleRange(namespaceName, policies.bundles, bundleRange);
 
         try {
             return pulsar().getLocalZkCacheService().getResourceQuotaCache().getQuota(nsBundle);
@@ -106,26 +73,19 @@ public class ResourceQuotas extends AdminResource {
         }
     }
 
-    @POST
-    @Path("/{property}/{cluster}/{namespace}/{bundle}")
-    @ApiOperation(value = "Set resource quota on a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void setNamespaceBundleResourceQuota(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("bundle") String bundleRange, ResourceQuota quota) {
+    @SuppressWarnings("deprecation")
+    protected void internalSetNamespaceBundleResourceQuota(String bundleRange, ResourceQuota quota) {
         validateSuperUserAccess();
         validatePoliciesReadOnlyAccess();
 
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
+        Policies policies = getNamespacePolicies(namespaceName);
 
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
+        if (!namespaceName.isGlobal()) {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
         }
 
-        NamespaceName fqnn = NamespaceName.get(property, cluster, namespace);
-        NamespaceBundle nsBundle = validateNamespaceBundleRange(fqnn, policies.bundles, bundleRange);
+        NamespaceBundle nsBundle = validateNamespaceBundleRange(namespaceName, policies.bundles, bundleRange);
 
         try {
             pulsar().getLocalZkCacheService().getResourceQuotaCache().setQuota(nsBundle, quota);
@@ -142,26 +102,19 @@ public class ResourceQuotas extends AdminResource {
 
     }
 
-    @DELETE
-    @Path("/{property}/{cluster}/{namespace}/{bundle}")
-    @ApiOperation(value = "Remove resource quota for a namespace.")
-    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
-            @ApiResponse(code = 409, message = "Concurrent modification") })
-    public void removeNamespaceBundleResourceQuota(@PathParam("property") String property,
-            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("bundle") String bundleRange) {
+    @SuppressWarnings("deprecation")
+    protected void internalRemoveNamespaceBundleResourceQuota(String bundleRange) {
         validateSuperUserAccess();
         validatePoliciesReadOnlyAccess();
 
-        Policies policies = getNamespacePolicies(property, cluster, namespace);
+        Policies policies = getNamespacePolicies(namespaceName);
 
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateClusterOwnership(cluster);
-            validateClusterForProperty(property, cluster);
+        if (!namespaceName.isGlobal()) {
+            validateClusterOwnership(namespaceName.getCluster());
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
         }
 
-        NamespaceName fqnn = NamespaceName.get(property, cluster, namespace);
-        NamespaceBundle nsBundle = validateNamespaceBundleRange(fqnn, policies.bundles, bundleRange);
+        NamespaceBundle nsBundle = validateNamespaceBundleRange(namespaceName, policies.bundles, bundleRange);
 
         try {
             pulsar().getLocalZkCacheService().getResourceQuotaCache().unsetQuota(nsBundle);
@@ -177,4 +130,6 @@ public class ResourceQuotas extends AdminResource {
             throw new RestException(e);
         }
     }
+
+    private static final Logger log = LoggerFactory.getLogger(ResourceQuotasBase.class);
 }
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/BrokerStats.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/BrokerStats.java
new file mode 100644
index 0000000..97ebbd9
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/BrokerStats.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.BrokerStatsBase;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/broker-stats")
+@Api(value = "/broker-stats", description = "Stats for broker", tags = "broker-stats")
+@Produces(MediaType.APPLICATION_JSON)
+public class BrokerStats extends BrokerStatsBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Brokers.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Brokers.java
new file mode 100644
index 0000000..c5d712c
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Brokers.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.BrokersBase;
+
+import javax.ws.rs.Path;
+
+@Path("/brokers")
+@Api(value = "/brokers", description = "BrokersBase admin apis", tags = "brokers")
+public class Brokers extends BrokersBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Clusters.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Clusters.java
new file mode 100644
index 0000000..7cd5ccd
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Clusters.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.ClustersBase;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/clusters")
+@Api(value = "/clusters", description = "Cluster admin apis", tags = "clusters")
+@Produces(MediaType.APPLICATION_JSON)
+public class Clusters extends ClustersBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Namespaces.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Namespaces.java
new file mode 100644
index 0000000..3ae1d4b
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Namespaces.java
@@ -0,0 +1,711 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import static org.apache.commons.lang3.StringUtils.isBlank;
+import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
+import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES_ROOT;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.pulsar.broker.admin.AdminResource;
+import org.apache.pulsar.broker.admin.impl.NamespacesBase;
+import org.apache.pulsar.broker.web.RestException;
+import org.apache.pulsar.common.naming.NamespaceName;
+import org.apache.pulsar.common.policies.data.AuthAction;
+import org.apache.pulsar.common.policies.data.BacklogQuota;
+import org.apache.pulsar.common.policies.data.BacklogQuota.BacklogQuotaType;
+import org.apache.pulsar.common.policies.data.BundlesData;
+import org.apache.pulsar.common.policies.data.DispatchRate;
+import org.apache.pulsar.common.policies.data.PersistencePolicies;
+import org.apache.pulsar.common.policies.data.Policies;
+import org.apache.pulsar.common.policies.data.RetentionPolicies;
+import org.apache.pulsar.common.policies.data.SubscriptionAuthMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Lists;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+@Path("/namespaces")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Api(value = "/namespaces", description = "Namespaces admin apis", tags = "namespaces")
+public class Namespaces extends NamespacesBase {
+
+    @GET
+    @Path("/{property}")
+    @ApiOperation(value = "Get the list of all the namespaces for a certain property.", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property doesn't exist") })
+    public List<String> getPropertyNamespaces(@PathParam("property") String property) {
+        return internalGetPropertyNamespaces(property);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}")
+    @ApiOperation(hidden = true, value = "Get the list of all the namespaces for a certain property on single cluster.", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster doesn't exist") })
+    public List<String> getNamespacesForCluster(@PathParam("property") String property,
+            @PathParam("cluster") String cluster) {
+        validateAdminAccessOnProperty(property);
+        List<String> namespaces = Lists.newArrayList();
+        if (!clusters().contains(cluster)) {
+            log.warn("[{}] Failed to get namespace list for property: {}/{} - Cluster does not exist", clientAppId(),
+                    property, cluster);
+            throw new RestException(Status.NOT_FOUND, "Cluster does not exist");
+        }
+
+        try {
+            for (String namespace : globalZk().getChildren(path(POLICIES, property, cluster), false)) {
+                namespaces.add(String.format("%s/%s/%s", property, cluster, namespace));
+            }
+        } catch (KeeperException.NoNodeException e) {
+            // NoNode means there are no namespaces for this property on the specified cluster, returning empty list
+        } catch (Exception e) {
+            log.error("[{}] Failed to get namespaces list: {}", clientAppId(), e);
+            throw new RestException(e);
+        }
+
+        namespaces.sort(null);
+        return namespaces;
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/destinations")
+    @ApiOperation(hidden = true, value = "Get the list of all the destinations under a certain namespace.", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public List<String> getDestinations(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, cluster, namespace);
+
+        // Validate that namespace exists, throws 404 if it doesn't exist
+        getNamespacePolicies(namespaceName);
+
+        try {
+            return pulsar().getNamespaceService().getListOfDestinations(namespaceName);
+        } catch (Exception e) {
+            log.error("Failed to get topics list for namespace {}/{}/{}", property, cluster, namespace, e);
+            throw new RestException(e);
+        }
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}")
+    @ApiOperation(hidden = true, value = "Get the dump all the policies specified for a namespace.", response = Policies.class)
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public Policies getPolicies(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, cluster, namespace);
+        return getNamespacePolicies(namespaceName);
+    }
+
+    @SuppressWarnings("deprecation")
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}")
+    @ApiOperation(hidden = true, value = "Creates a new empty namespace with no policies attached.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace already exists"),
+            @ApiResponse(code = 412, message = "Namespace name is not valid") })
+    public void createNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, BundlesData initialBundles) {
+        validateNamespaceName(property, cluster, namespace);
+
+        if (!namespaceName.isGlobal()) {
+            // If the namespace is non global, make sure property has the access on the cluster. For global namespace,
+            // same check is made at the time of setting replication.
+            validateClusterForProperty(namespaceName.getProperty(), namespaceName.getCluster());
+        }
+
+        Policies policies = new Policies();
+        if (initialBundles != null && initialBundles.getNumBundles() > 0) {
+            if (initialBundles.getBoundaries() == null || initialBundles.getBoundaries().size() == 0) {
+                policies.bundles = getBundles(initialBundles.getNumBundles());
+            } else {
+                policies.bundles = validateBundlesData(initialBundles);
+            }
+        } else {
+            int defaultNumberOfBundles = config().getDefaultNumberOfNamespaceBundles();
+            policies.bundles = getBundles(defaultNumberOfBundles);
+        }
+
+        internalCreateNamespace(policies);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}")
+    @ApiOperation(hidden = true, value = "Delete a namespace and all the destinations under it.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace is not empty") })
+    public void deleteNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalDeleteNamespace(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/{bundle}")
+    @ApiOperation(hidden = true, value = "Delete a namespace bundle and all the destinations under it.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace bundle is not empty") })
+    public void deleteNamespaceBundle(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalDeleteNamespaceBundle(bundleRange, authoritative);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/permissions")
+    @ApiOperation(hidden = true, value = "Retrieve the permissions for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace is not empty") })
+    public Map<String, Set<AuthAction>> getPermissions(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, cluster, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.auth_policies.namespace_auth;
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/permissions/{role}")
+    @ApiOperation(hidden = true, value = "Grant a new permission to a role on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void grantPermissionOnNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("role") String role, Set<AuthAction> actions) {
+        validateNamespaceName(property, cluster, namespace);
+        internalGrantPermissionOnNamespace(role, actions);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/permissions/{role}")
+    @ApiOperation(hidden = true, value = "Revoke all permissions to a role on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public void revokePermissionsOnNamespace(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("role") String role) {
+        validateNamespaceName(property, cluster, namespace);
+        internalRevokePermissionsOnNamespace(role);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/replication")
+    @ApiOperation(hidden = true, value = "Get the replication clusters for a namespace.", response = String.class, responseContainer = "List")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Namespace is not global") })
+    public List<String> getNamespaceReplicationClusters(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, cluster, namespace);
+
+        return internalGetNamespaceReplicationClusters();
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/replication")
+    @ApiOperation(hidden = true, value = "Set the replication clusters for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Peer-cluster can't be part of replication-cluster"),
+            @ApiResponse(code = 412, message = "Namespace is not global or invalid cluster ids") })
+    public void setNamespaceReplicationClusters(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace, List<String> clusterIds) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetNamespaceReplicationClusters(clusterIds);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/messageTTL")
+    @ApiOperation(hidden = true, value = "Get the message TTL for the namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public int getNamespaceMessageTTL(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, cluster, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.message_ttl_in_seconds;
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/messageTTL")
+    @ApiOperation(hidden = true, value = "Set message TTL in seconds for namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Invalid TTL") })
+    public void setNamespaceMessageTTL(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, int messageTTL) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetNamespaceMessageTTL(messageTTL);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/antiAffinity")
+    @ApiOperation(value = "Set anti-affinity group for a namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Invalid antiAffinityGroup") })
+    public void setNamespaceAntiAffinityGroup(@PathParam("property") String property, @PathParam("cluster") String cluster,
+                                              @PathParam("namespace") String namespace, String antiAffinityGroup) {
+        validateAdminAccessOnProperty(property);
+        validatePoliciesReadOnlyAccess();
+
+        log.info("[{}] Setting anti-affinity group {} for {}/{}/{}", clientAppId(), antiAffinityGroup, property,
+                cluster, namespace);
+
+        if (isBlank(antiAffinityGroup)) {
+            throw new RestException(Status.PRECONDITION_FAILED, "antiAffinityGroup can't be empty");
+        }
+
+        NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
+        Map.Entry<Policies, Stat> policiesNode = null;
+
+        try {
+            // Force to read the data s.t. the watch to the cache content is setup.
+            policiesNode = policiesCache().getWithStat(path(POLICIES, property, cluster, namespace))
+                    .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace " + nsName + " does not exist"));
+            policiesNode.getKey().antiAffinityGroup = antiAffinityGroup;
+
+            // Write back the new policies into zookeeper
+            globalZk().setData(path(POLICIES, property, cluster, namespace),
+                    jsonMapper().writeValueAsBytes(policiesNode.getKey()), policiesNode.getValue().getVersion());
+            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
+
+            log.info("[{}] Successfully updated the antiAffinityGroup {} on namespace {}/{}/{}", clientAppId(),
+                    antiAffinityGroup, property, cluster, namespace);
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to update the antiAffinityGroup for namespace {}/{}/{}: does not exist", clientAppId(),
+                    property, cluster, namespace);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn(
+                    "[{}] Failed to update the antiAffinityGroup on namespace {}/{}/{} expected policy node version={} : concurrent modification",
+                    clientAppId(), property, cluster, namespace, policiesNode.getValue().getVersion());
+
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to update the antiAffinityGroup on namespace {}/{}/{}", clientAppId(), property, cluster,
+                    namespace, e);
+            throw new RestException(e);
+        }
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/antiAffinity")
+    @ApiOperation(value = "Get anti-affinity group of a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public String getNamespaceAntiAffinityGroup(@PathParam("property") String property, @PathParam("cluster") String cluster,
+                                                @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        return getNamespacePolicies(property, cluster, namespace).antiAffinityGroup;
+    }
+
+    @GET
+    @Path("{cluster}/antiAffinity/{group}")
+    @ApiOperation(value = "Get all namespaces that are grouped by given anti-affinity group in a given cluster. api can be only accessed by admin of any of the existing property")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 412, message = "Cluster not exist/Anti-affinity group can't be empty.") })
+    public List<String> getAntiAffinityNamespaces(@PathParam("cluster") String cluster,
+                                                  @PathParam("group") String antiAffinityGroup, @QueryParam("property") String property) {
+        validateAdminAccessOnProperty(property);
+
+        log.info("[{}]-{} Finding namespaces for {} in {}", clientAppId(), property, antiAffinityGroup, cluster);
+
+        if (isBlank(antiAffinityGroup)) {
+            throw new RestException(Status.PRECONDITION_FAILED, "anti-affinity group can't be empty.");
+        }
+        validateClusterExists(cluster);
+        List<String> namespaces = Lists.newArrayList();
+        try {
+            for (String prop : globalZk().getChildren(POLICIES_ROOT, false)) {
+                for (String namespace : globalZk().getChildren(path(POLICIES, prop, cluster), false)) {
+                    Optional<Policies> policies = policiesCache()
+                            .get(AdminResource.path(POLICIES, prop, cluster, namespace));
+                    if (policies.isPresent() && antiAffinityGroup.equalsIgnoreCase(policies.get().antiAffinityGroup)) {
+                        namespaces.add(String.format("%s/%s/%s", prop, cluster, namespace));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.warn("Failed to list of properties/namespace from global-zk", e);
+        }
+        return namespaces;
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/antiAffinity")
+    @ApiOperation(value = "Remove anti-affinity group of a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void removeNamespaceAntiAffinityGroup(@PathParam("property") String property,
+                                                 @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validatePoliciesReadOnlyAccess();
+
+        log.info("[{}] Deleting anti-affinity group for {}/{}/{}", clientAppId(), property, cluster, namespace);
+
+        try {
+            Stat nodeStat = new Stat();
+            final String path = path(POLICIES, property, cluster, namespace);
+            byte[] content = globalZk().getData(path, null, nodeStat);
+            Policies policies = jsonMapper().readValue(content, Policies.class);
+            policies.antiAffinityGroup = null;
+            globalZk().setData(path, jsonMapper().writeValueAsBytes(policies), nodeStat.getVersion());
+            policiesCache().invalidate(path(POLICIES, property, cluster, namespace));
+            log.info("[{}] Successfully removed anti-affinity group for a namespace={}/{}/{}", clientAppId(), property,
+                    cluster, namespace);
+
+        } catch (KeeperException.NoNodeException e) {
+            log.warn("[{}] Failed to remove anti-affinity group for namespace {}/{}/{}: does not exist", clientAppId(),
+                    property, cluster, namespace);
+            throw new RestException(Status.NOT_FOUND, "Namespace does not exist");
+        } catch (KeeperException.BadVersionException e) {
+            log.warn("[{}] Failed to remove anti-affinity group for namespace {}/{}/{}: concurrent modification",
+                    clientAppId(), property, cluster, namespace);
+            throw new RestException(Status.CONFLICT, "Concurrent modification");
+        } catch (Exception e) {
+            log.error("[{}] Failed to remove anti-affinity group for namespace {}/{}/{}", clientAppId(), property,
+                    cluster, namespace, e);
+            throw new RestException(e);
+        }
+    }
+
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/deduplication")
+    @ApiOperation(hidden = true, value = "Enable or disable broker side deduplication for all topics in a namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public void modifyDeduplication(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, boolean enableDeduplication) {
+        validateNamespaceName(property, cluster, namespace);
+        internalModifyDeduplication(enableDeduplication);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/bundles")
+    @ApiOperation(hidden = true, value = "Get the bundles split data.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Namespace is not setup to split in bundles") })
+    public BundlesData getBundlesData(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validatePoliciesReadOnlyAccess();
+        validateNamespaceName(property, cluster, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        return policies.bundles;
+    }
+
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}/unload")
+    @ApiOperation(hidden = true, value = "Unload namespace", notes = "Unload an active namespace from the current broker serving it. Performing this operation will let the broker"
+            + "removes all producers, consumers, and connections using this namespace, and close all destinations (including"
+            + "their persistent store). During that operation, the namespace is marked as tentatively unavailable until the"
+            + "broker completes the unloading action. This operation requires strictly super user privileges, since it would"
+            + "result in non-persistent message loss and unexpected connection closure to the clients.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Namespace is already unloaded or Namespace has bundles activated") })
+    public void unloadNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, cluster, namespace);
+        internalUnloadNamespace();
+    }
+
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}/{bundle}/unload")
+    @ApiOperation(hidden = true, value = "Unload a namespace bundle")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void unloadNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalUnloadNamespaceBundle(bundleRange, authoritative);
+    }
+
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}/{bundle}/split")
+    @ApiOperation(hidden = true, value = "Split a namespace bundle")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void splitNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative,
+            @QueryParam("unload") @DefaultValue("false") boolean unload) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSplitNamespaceBundle(bundleRange, authoritative, unload);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/dispatchRate")
+    @ApiOperation(hidden = true, value = "Set dispatch-rate throttling for all topics of the namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void setDispatchRate(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, DispatchRate dispatchRate) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetDispatchRate(dispatchRate);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/dispatchRate")
+    @ApiOperation(hidden = true, value = "Get dispatch-rate configured for the namespace, -1 represents not configured yet")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public DispatchRate getDispatchRate(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, cluster, namespace);
+        return internalGetDispatchRate();
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/backlogQuotaMap")
+    @ApiOperation(hidden = true, value = "Get backlog quota map on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public Map<BacklogQuotaType, BacklogQuota> getBacklogQuotaMap(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, cluster, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.backlog_quota_map;
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/backlogQuota")
+    @ApiOperation(hidden = true, value = " Set a backlog quota for all the destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"),
+            @ApiResponse(code = 412, message = "Specified backlog quota exceeds retention quota. Increase retention quota and retry request") })
+    public void setBacklogQuota(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @QueryParam("backlogQuotaType") BacklogQuotaType backlogQuotaType,
+            BacklogQuota backlogQuota) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetBacklogQuota(backlogQuotaType, backlogQuota);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/backlogQuota")
+    @ApiOperation(hidden = true, value = "Remove a backlog quota policy from a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void removeBacklogQuota(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace,
+            @QueryParam("backlogQuotaType") BacklogQuotaType backlogQuotaType) {
+        validateNamespaceName(property, cluster, namespace);
+        internalRemoveBacklogQuota(backlogQuotaType);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/retention")
+    @ApiOperation(hidden = true, value = "Get retention config on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public RetentionPolicies getRetention(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, cluster, namespace);
+        return internalGetRetention();
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/retention")
+    @ApiOperation(hidden = true, value = " Set retention configuration on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"),
+            @ApiResponse(code = 412, message = "Retention Quota must exceed backlog quota") })
+    public void setRetention(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, RetentionPolicies retention) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetRetention(retention);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/persistence")
+    @ApiOperation(hidden = true, value = "Set the persistence configuration for all the destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"),
+            @ApiResponse(code = 400, message = "Invalid persistence policies") })
+    public void setPersistence(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, PersistencePolicies persistence) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetPersistence(persistence);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/persistence")
+    @ApiOperation(hidden = true, value = "Get the persistence configuration for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public PersistencePolicies getPersistence(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, cluster, namespace);
+        return internalGetPersistence();
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/clearBacklog")
+    @ApiOperation(hidden = true, value = "Clear backlog for all destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBacklog(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalClearNamespaceBacklog(authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{bundle}/clearBacklog")
+    @ApiOperation(hidden = true, value = "Clear backlog for all destinations on a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBundleBacklog(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalClearNamespaceBundleBacklog(bundleRange, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/clearBacklog/{subscription}")
+    @ApiOperation(hidden = true, value = "Clear backlog for a given subscription on all destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBacklogForSubscription(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("subscription") String subscription,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalClearNamespaceBacklogForSubscription(subscription, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{bundle}/clearBacklog/{subscription}")
+    @ApiOperation(hidden = true, value = "Clear backlog for a given subscription on all destinations on a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBundleBacklogForSubscription(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("subscription") String subscription, @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalClearNamespaceBundleBacklogForSubscription(subscription, bundleRange, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/unsubscribe/{subscription}")
+    @ApiOperation(hidden = true, value = "Unsubscribes the given subscription on all destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void unsubscribeNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalUnsubscribeNamespace(subscription, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{bundle}/unsubscribe/{subscription}")
+    @ApiOperation(hidden = true, value = "Unsubscribes the given subscription on all destinations on a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void unsubscribeNamespaceBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, cluster, namespace);
+        internalUnsubscribeNamespaceBundle(subscription, bundleRange, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/subscriptionAuthMode")
+    @ApiOperation(value = " Set a subscription auth mode for all the destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void setSubscriptionAuthMode(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, SubscriptionAuthMode subscriptionAuthMode) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetSubscriptionAuthMode(subscriptionAuthMode);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/encryptionRequired")
+    @ApiOperation(hidden = true, value = "Message encryption is required or not for all topics in a namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"), })
+    public void modifyEncryptionRequired(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, boolean encryptionRequired) {
+        validateNamespaceName(property, cluster, namespace);
+        internalModifyEncryptionRequired(encryptionRequired);
+    }
+
+    private static final Logger log = LoggerFactory.getLogger(Namespaces.class);
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/NonPersistentTopics.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/NonPersistentTopics.java
similarity index 79%
rename from pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/NonPersistentTopics.java
rename to pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/NonPersistentTopics.java
index 68b4e83..c423f21 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/NonPersistentTopics.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/NonPersistentTopics.java
@@ -16,15 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pulsar.broker.admin;
+package org.apache.pulsar.broker.admin.v1;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES;
-import static org.apache.pulsar.common.util.Codec.decode;
-
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
 
 import javax.ws.rs.DefaultValue;
 import javax.ws.rs.Encoded;
@@ -37,12 +31,12 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response.Status;
 
+import com.google.common.collect.Lists;
 import org.apache.pulsar.broker.PulsarServerException;
 import org.apache.pulsar.broker.service.Topic;
 import org.apache.pulsar.broker.service.nonpersistent.NonPersistentTopic;
 import org.apache.pulsar.broker.web.RestException;
-import org.apache.pulsar.client.admin.PulsarAdminException;
-import org.apache.pulsar.common.naming.DestinationDomain;
+import org.apache.pulsar.common.naming.Constants;
 import org.apache.pulsar.common.naming.DestinationName;
 import org.apache.pulsar.common.naming.NamespaceBundle;
 import org.apache.pulsar.common.naming.NamespaceName;
@@ -55,13 +49,15 @@ import org.apache.zookeeper.KeeperException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Lists;
-
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
 /**
  */
 @Path("/non-persistent")
@@ -69,96 +65,94 @@ import io.swagger.annotations.ApiResponses;
 @Api(value = "/non-persistent", description = "Non-Persistent topic admin apis", tags = "non-persistent topic")
 public class NonPersistentTopics extends PersistentTopics {
     private static final Logger log = LoggerFactory.getLogger(NonPersistentTopics.class);
-    
+
     @GET
     @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
-    @ApiOperation(value = "Get partitioned topic metadata.")
+    @ApiOperation(hidden = true, value = "Get partitioned topic metadata.")
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
     public PartitionedTopicMetadata getPartitionedMetadata(@PathParam("property") String property,
             @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination,
+            @PathParam("destination") @Encoded String encodedTopic,
             @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        return getPartitionedTopicMetadata(property, cluster, namespace, destination, authoritative);
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return getPartitionedTopicMetadata(destinationName, authoritative);
     }
 
     @GET
     @Path("{property}/{cluster}/{namespace}/{destination}/stats")
-    @ApiOperation(value = "Get the stats for the topic.")
+    @ApiOperation(hidden = true, value = "Get the stats for the topic.")
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
             @ApiResponse(code = 404, message = "Topic does not exist") })
-    public NonPersistentTopicStats getStats(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
+    public NonPersistentTopicStats getStats(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
             @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminOperationOnDestination(dn, authoritative);
-        Topic topic = getTopicReference(dn);
-        return ((NonPersistentTopic)topic).getStats();
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        validateAdminOperationOnDestination(authoritative);
+        Topic topic = getTopicReference(destinationName);
+        return ((NonPersistentTopic) topic).getStats();
     }
 
     @GET
     @Path("{property}/{cluster}/{namespace}/{destination}/internalStats")
-    @ApiOperation(value = "Get the internal stats for the topic.")
+    @ApiOperation(hidden = true, value = "Get the internal stats for the topic.")
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
             @ApiResponse(code = 404, message = "Topic does not exist") })
     public PersistentTopicInternalStats getInternalStats(@PathParam("property") String property,
             @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
-            @PathParam("destination") @Encoded String destination,
+            @PathParam("destination") @Encoded String encodedTopic,
             @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminOperationOnDestination(dn, authoritative);
-        Topic topic = getTopicReference(dn);
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        validateAdminOperationOnDestination(authoritative);
+        Topic topic = getTopicReference(destinationName);
         return topic.getInternalStats();
     }
 
     @PUT
     @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
-    @ApiOperation(value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.")
+    @ApiOperation(hidden = true, value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.")
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
             @ApiResponse(code = 409, message = "Partitioned topic already exist") })
     public void createPartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination, int numPartitions,
-            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        validateAdminAccessOnProperty(dn.getProperty());
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            int numPartitions, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        validateAdminAccessOnProperty(destinationName.getProperty());
         if (numPartitions <= 1) {
             throw new RestException(Status.NOT_ACCEPTABLE, "Number of partitions should be more than 1");
         }
         try {
-            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, property, cluster, namespace, domain(),
-                    dn.getEncodedLocalName());
+            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, namespaceName.toString(), domain(),
+                    destinationName.getEncodedLocalName());
             byte[] data = jsonMapper().writeValueAsBytes(new PartitionedTopicMetadata(numPartitions));
             zkCreateOptimistic(path, data);
             // we wait for the data to be synced in all quorums and the observers
             Thread.sleep(PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS);
-            log.info("[{}] Successfully created partitioned topic {}", clientAppId(), dn);
+            log.info("[{}] Successfully created partitioned topic {}", clientAppId(), destinationName);
         } catch (KeeperException.NodeExistsException e) {
-            log.warn("[{}] Failed to create already existing partitioned topic {}", clientAppId(), dn);
+            log.warn("[{}] Failed to create already existing partitioned topic {}", clientAppId(), destinationName);
             throw new RestException(Status.CONFLICT, "Partitioned topic already exist");
         } catch (Exception e) {
-            log.error("[{}] Failed to create partitioned topic {}", clientAppId(), dn, e);
+            log.error("[{}] Failed to create partitioned topic {}", clientAppId(), destinationName, e);
             throw new RestException(e);
         }
     }
 
     @PUT
     @Path("/{property}/{cluster}/{namespace}/{destination}/unload")
-    @ApiOperation(value = "Unload a topic")
+    @ApiOperation(hidden = true, value = "Unload a topic")
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
             @ApiResponse(code = 404, message = "Topic does not exist") })
     public void unloadTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
             @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
-        log.info("[{}] Unloading topic {}/{}/{}/{}", clientAppId(), property, cluster, namespace, destination);
-        destination = decode(destination);
-        DestinationName dn = DestinationName.get(domain(), property, cluster, namespace, destination);
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
-            validateGlobalNamespaceOwnership(NamespaceName.get(property, cluster, namespace));
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        log.info("[{}] Unloading topic {}", clientAppId(), destinationName);
+
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
         }
-        unloadTopic(dn, authoritative);
+        unloadTopic(destinationName, authoritative);
     }
 
     @GET
@@ -167,13 +161,13 @@ public class NonPersistentTopics extends PersistentTopics {
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
             @ApiResponse(code = 404, message = "Namespace doesn't exist") })
     public List<String> getList(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace) {
+                                @PathParam("namespace") String namespace) {
         log.info("[{}] list of topics on namespace {}/{}/{}/{}", clientAppId(), property, cluster, namespace);
         validateAdminAccessOnProperty(property);
         Policies policies = getNamespacePolicies(property, cluster, namespace);
         NamespaceName nsName = NamespaceName.get(property, cluster, namespace);
 
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
+        if (!cluster.equals(Constants.GLOBAL_CLUSTER)) {
             validateClusterOwnership(cluster);
             validateClusterForProperty(property, cluster);
         } else {
@@ -213,19 +207,19 @@ public class NonPersistentTopics extends PersistentTopics {
         }
         return topics;
     }
-    
+
     @GET
     @Path("/{property}/{cluster}/{namespace}/{bundle}")
     @ApiOperation(value = "Get the list of non-persistent topics under a namespace bundle.", response = String.class, responseContainer = "List")
     @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
             @ApiResponse(code = 404, message = "Namespace doesn't exist") })
     public List<String> getListFromBundle(@PathParam("property") String property, @PathParam("cluster") String cluster,
-            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange) {
+                                          @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange) {
         log.info("[{}] list of topics on namespace bundle {}/{}/{}/{}", clientAppId(), property, cluster, namespace,
                 bundleRange);
         validateAdminAccessOnProperty(property);
         Policies policies = getNamespacePolicies(property, cluster, namespace);
-        if (!cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
+        if (!cluster.equals(Constants.GLOBAL_CLUSTER)) {
             validateClusterOwnership(cluster);
             validateClusterForProperty(property, cluster);
         } else {
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/PersistentTopics.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/PersistentTopics.java
new file mode 100644
index 0000000..624a3f3
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/PersistentTopics.java
@@ -0,0 +1,421 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.Encoded;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.pulsar.broker.admin.impl.PersistentTopicsBase;
+import org.apache.pulsar.client.api.MessageId;
+import org.apache.pulsar.client.impl.MessageIdImpl;
+import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
+import org.apache.pulsar.common.policies.data.AuthAction;
+import org.apache.pulsar.common.policies.data.PartitionedTopicStats;
+import org.apache.pulsar.common.policies.data.PersistentOfflineTopicStats;
+import org.apache.pulsar.common.policies.data.PersistentTopicInternalStats;
+import org.apache.pulsar.common.policies.data.PersistentTopicStats;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+/**
+ */
+@Path("/persistent")
+@Produces(MediaType.APPLICATION_JSON)
+@Api(value = "/persistent", description = "Persistent topic admin apis", tags = "persistent topic")
+public class PersistentTopics extends PersistentTopicsBase {
+    @GET
+    @Path("/{property}/{cluster}/{namespace}")
+    @ApiOperation(hidden = true, value = "Get the list of topics under a namespace.", response = String.class, responseContainer = "List")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
+    public List<String> getList(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, cluster, namespace);
+        return internalGetList();
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/partitioned")
+    @ApiOperation(hidden = true, value = "Get the list of partitioned topics under a namespace.", response = String.class, responseContainer = "List")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
+    public List<String> getPartitionedTopicList(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, cluster, namespace);
+        return internalGetPartitionedTopicList();
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/{destination}/permissions")
+    @ApiOperation(hidden = true, value = "Get permissions on a destination.", notes = "Retrieve the effective permissions for a destination. These permissions are defined by the permissions set at the"
+            + "namespace level combined (union) with any eventual specific permission set on the destination.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
+    public Map<String, Set<AuthAction>> getPermissionsOnDestination(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetPermissionsOnDestination();
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/permissions/{role}")
+    @ApiOperation(hidden = true, value = "Grant a new permission to a role on a single topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void grantPermissionsOnDestination(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("role") String role,
+            Set<AuthAction> actions) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalGrantPermissionsOnDestination(role, actions);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/{destination}/permissions/{role}")
+    @ApiOperation(hidden = true, value = "Revoke permissions on a destination.", notes = "Revoke permissions to a role on a single destination. If the permission was not set at the destination"
+            + "level, but rather at the namespace level, this operation will return an error (HTTP status code 412).")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Permissions are not set at the destination level") })
+    public void revokePermissionsOnDestination(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("role") String role) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalRevokePermissionsOnDestination(role);
+    }
+
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
+    @ApiOperation(hidden = true, value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Partitioned topic already exist") })
+    public void createPartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            int numPartitions, @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalCreatePartitionedTopic(numPartitions, authoritative);
+    }
+
+    /**
+     * It updates number of partitions of an existing non-global partitioned topic. It requires partitioned-topic to be
+     * already exist and number of new partitions must be greater than existing number of partitions. Decrementing
+     * number of partitions requires deletion of topic which is not supported.
+     *
+     * Already created partitioned producers and consumers can't see newly created partitions and it requires to
+     * recreate them at application so, newly created producers and consumers can connect to newly added partitions as
+     * well. Therefore, it can violate partition ordering at producers until all producers are restarted at application.
+     *
+     * @param property
+     * @param cluster
+     * @param namespace
+     * @param numPartitions
+     */
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
+    @ApiOperation(hidden = true, value = "Increment partitons of an existing partitioned topic.", notes = "It only increments partitions of existing non-global partitioned-topic")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Partitioned topic does not exist") })
+    public void updatePartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            int numPartitions) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalUpdatePartitionedTopic(numPartitions);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
+    @ApiOperation(hidden = true, value = "Get partitioned topic metadata.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public PartitionedTopicMetadata getPartitionedMetadata(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetPartitionedMetadata(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/{destination}/partitions")
+    @ApiOperation(hidden = true, value = "Delete a partitioned topic.", notes = "It will also delete all the partitions of the topic if it exists.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Partitioned topic does not exist") })
+    public void deletePartitionedTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalDeletePartitionedTopic(authoritative);
+    }
+
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}/{destination}/unload")
+    @ApiOperation(hidden = true, value = "Unload a topic")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public void unloadTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalUnloadTopic(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/{destination}")
+    @ApiOperation(hidden = true, value = "Delete a topic.", notes = "The topic cannot be deleted if there's any active subscription or producer connected to the it.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist"),
+            @ApiResponse(code = 412, message = "Topic has active producers/subscriptions") })
+    public void deleteTopic(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalDeleteTopic(authoritative);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscriptions")
+    @ApiOperation(hidden = true, value = "Get the list of persistent subscriptions for a given topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public List<String> getSubscriptions(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetSubscriptions(authoritative);
+    }
+
+    @GET
+    @Path("{property}/{cluster}/{namespace}/{destination}/stats")
+    @ApiOperation(hidden = true, value = "Get the stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PersistentTopicStats getStats(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetStats(authoritative);
+    }
+
+    @GET
+    @Path("{property}/{cluster}/{namespace}/{destination}/internalStats")
+    @ApiOperation(hidden = true, value = "Get the internal stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PersistentTopicInternalStats getInternalStats(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetInternalStats(authoritative);
+    }
+
+    @GET
+    @Path("{property}/{cluster}/{namespace}/{destination}/internal-info")
+    @ApiOperation(hidden = true, value = "Get the internal stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public void getManagedLedgerInfo(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @Suspended AsyncResponse asyncResponse) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalGetManagedLedgerInfo(asyncResponse);
+    }
+
+    @GET
+    @Path("{property}/{cluster}/{namespace}/{destination}/partitioned-stats")
+    @ApiOperation(hidden = true, value = "Get the stats for the partitioned topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PartitionedTopicStats getPartitionedStats(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetPartitionedStats(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}")
+    @ApiOperation(hidden = true, value = "Delete a subscription.", notes = "There should not be any active consumers on the subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist"),
+            @ApiResponse(code = 412, message = "Subscription has active consumers") })
+    public void deleteSubscription(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalDeleteSubscription(subName, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/skip_all")
+    @ApiOperation(hidden = true, value = "Skip all messages on a topic subscription.", notes = "Completely clears the backlog on the subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 405, message = "Operation not allowed on non-persistent topic"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void skipAllMessages(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalSkipAllMessages(subName, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/skip/{numMessages}")
+    @ApiOperation(hidden = true, value = "Skip messages on a topic subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void skipMessages(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName, @PathParam("numMessages") int numMessages,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalSkipMessages(subName, numMessages, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/expireMessages/{expireTimeInSeconds}")
+    @ApiOperation(hidden = true, value = "Expire messages on a topic subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void expireTopicMessages(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName, @PathParam("expireTimeInSeconds") int expireTimeInSeconds,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalExpireMessages(subName, expireTimeInSeconds, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/all_subscription/expireMessages/{expireTimeInSeconds}")
+    @ApiOperation(hidden = true, value = "Expire messages on all subscriptions of topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void expireMessagesForAllSubscriptions(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("expireTimeInSeconds") int expireTimeInSeconds,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalExpireMessagesForAllSubscriptions(expireTimeInSeconds, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/resetcursor/{timestamp}")
+    @ApiOperation(hidden = true, value = "Reset subscription to message position closest to absolute timestamp (in ms).", notes = "It fence cursor and disconnects all active consumers before reseting cursor.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic/Subscription does not exist") })
+    public void resetCursor(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName, @PathParam("timestamp") long timestamp,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalResetCursor(subName, timestamp, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/resetcursor")
+    @ApiOperation(hidden = true, value = "Reset subscription to message position closest to given position.", notes = "It fence cursor and disconnects all active consumers before reseting cursor.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic/Subscription does not exist"),
+            @ApiResponse(code = 405, message = "Not supported for partitioned topics") })
+    public void resetCursorOnPosition(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative, MessageIdImpl messageId) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        internalResetCursorOnPosition(subName, authoritative, messageId);
+    }
+
+    @PUT
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subscriptionName}")
+    @ApiOperation(value = "Reset subscription to message position closest to given position.", notes = "Creates a subscription on the topic at the specified message id")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic/Subscription does not exist"),
+            @ApiResponse(code = 405, message = "Not supported for partitioned topics") })
+    public void createSubscription(@PathParam("property") String property, @PathParam("cluster") String cluster,
+           @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String destination,
+           @PathParam("subscriptionName") String subscriptionName,
+           @QueryParam("authoritative") @DefaultValue("false") boolean authoritative, MessageIdImpl messageId) {
+        validateDestinationName(property, cluster, namespace, destination);
+        internalCreateSubscription(subscriptionName, messageId, authoritative);
+    }
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/{destination}/subscription/{subName}/position/{messagePosition}")
+    @ApiOperation(hidden = true, value = "Peek nth message on a topic subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic, subscription or the message position does not exist") })
+    public Response peekNthMessage(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("subName") String subName, @PathParam("messagePosition") int messagePosition,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalPeekNthMessage(subName, messagePosition, authoritative);
+    }
+
+    @GET
+    @Path("{property}/{cluster}/{namespace}/{destination}/backlog")
+    @ApiOperation(hidden = true, value = "Get estimated backlog for offline topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public PersistentOfflineTopicStats getBacklog(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalGetBacklog(authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{destination}/terminate")
+    @ApiOperation(hidden = true, value = "Terminate a topic. A topic that is terminated will not accept any more "
+            + "messages to be published and will let consumer to drain existing messages in backlog")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 405, message = "Operation not allowed on non-persistent topic"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public MessageId terminate(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, cluster, namespace, encodedTopic);
+        return internalTerminate(authoritative);
+    }
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Properties.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Properties.java
new file mode 100644
index 0000000..879a98d
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/Properties.java
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.PropertiesBase;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/properties")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Api(value = "/properties", description = "PropertiesBase admin apis", tags = "properties")
+public class Properties extends PropertiesBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/ResourceQuotas.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/ResourceQuotas.java
new file mode 100644
index 0000000..fb77ee0
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v1/ResourceQuotas.java
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v1;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.pulsar.broker.admin.impl.ResourceQuotasBase;
+import org.apache.pulsar.common.policies.data.ResourceQuota;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+@Path("/resource-quotas")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Api(value = "/resource-quotas", description = "Quota admin APIs", tags = "resource-quotas")
+public class ResourceQuotas extends ResourceQuotasBase {
+
+    @GET
+    @Path("/{property}/{cluster}/{namespace}/{bundle}")
+    @ApiOperation(hidden = true, value = "Get resource quota of a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public ResourceQuota getNamespaceBundleResourceQuota(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange) {
+        validateNamespaceName(property, cluster, namespace);
+        return internalGetNamespaceBundleResourceQuota(bundleRange);
+    }
+
+    @POST
+    @Path("/{property}/{cluster}/{namespace}/{bundle}")
+    @ApiOperation(hidden = true, value = "Set resource quota on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void setNamespaceBundleResourceQuota(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange, ResourceQuota quota) {
+        validateNamespaceName(property, cluster, namespace);
+        internalSetNamespaceBundleResourceQuota(bundleRange, quota);
+    }
+
+    @DELETE
+    @Path("/{property}/{cluster}/{namespace}/{bundle}")
+    @ApiOperation(hidden = true, value = "Remove resource quota for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void removeNamespaceBundleResourceQuota(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange) {
+        validateNamespaceName(property, cluster, namespace);
+        internalRemoveNamespaceBundleResourceQuota(bundleRange);
+    }
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/BrokerStats.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/BrokerStats.java
new file mode 100644
index 0000000..f0318d7
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/BrokerStats.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.BrokerStatsBase;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/broker-stats")
+@Api(value = "/broker-stats", description = "Stats for broker", tags = "broker-stats")
+@Produces(MediaType.APPLICATION_JSON)
+public class BrokerStats extends BrokerStatsBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Brokers.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Brokers.java
new file mode 100644
index 0000000..7a69bb8
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Brokers.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.BrokersBase;
+
+import javax.ws.rs.Path;
+
+@Path("/brokers")
+@Api(value = "/brokers", description = "BrokersBase admin apis", tags = "brokers")
+public class Brokers extends BrokersBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Clusters.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Clusters.java
new file mode 100644
index 0000000..f51dad8
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Clusters.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.ClustersBase;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/clusters")
+@Api(value = "/clusters", description = "Cluster admin apis", tags = "clusters")
+@Produces(MediaType.APPLICATION_JSON)
+public class Clusters extends ClustersBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Namespaces.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Namespaces.java
new file mode 100644
index 0000000..d00499a
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Namespaces.java
@@ -0,0 +1,519 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.pulsar.broker.admin.impl.NamespacesBase;
+import org.apache.pulsar.broker.web.RestException;
+import org.apache.pulsar.common.policies.data.AuthAction;
+import org.apache.pulsar.common.policies.data.BacklogQuota;
+import org.apache.pulsar.common.policies.data.BacklogQuota.BacklogQuotaType;
+import org.apache.pulsar.common.policies.data.BundlesData;
+import org.apache.pulsar.common.policies.data.DispatchRate;
+import org.apache.pulsar.common.policies.data.PersistencePolicies;
+import org.apache.pulsar.common.policies.data.Policies;
+import org.apache.pulsar.common.policies.data.RetentionPolicies;
+import org.apache.pulsar.common.policies.data.SubscriptionAuthMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+@Path("/namespaces")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Api(value = "/namespaces", description = "Namespaces admin apis", tags = "namespaces")
+public class Namespaces extends NamespacesBase {
+
+    @GET
+    @Path("/{property}")
+    @ApiOperation(value = "Get the list of all the namespaces for a certain property.", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property doesn't exist") })
+    public List<String> getPropertyNamespaces(@PathParam("property") String property) {
+        return internalGetPropertyNamespaces(property);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/topics")
+    @ApiOperation(value = "Get the list of all the topics under a certain namespace.", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public List<String> getDestinations(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, namespace);
+
+        // Validate that namespace exists, throws 404 if it doesn't exist
+        getNamespacePolicies(namespaceName);
+
+        try {
+            return pulsar().getNamespaceService().getListOfDestinations(namespaceName);
+        } catch (Exception e) {
+            log.error("Failed to get topics list for namespace {}", namespaceName, e);
+            throw new RestException(e);
+        }
+    }
+
+    @GET
+    @Path("/{property}/{namespace}")
+    @ApiOperation(value = "Get the dump all the policies specified for a namespace.", response = Policies.class)
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public Policies getPolicies(@PathParam("property") String property, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, namespace);
+        return getNamespacePolicies(namespaceName);
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}")
+    @ApiOperation(value = "Creates a new namespace with the specified policies")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace already exists"),
+            @ApiResponse(code = 412, message = "Namespace name is not valid") })
+    public void createNamespace(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            Policies policies) {
+        validateNamespaceName(property, namespace);
+
+        policies = getDefaultPolicesIfNull(policies);
+        internalCreateNamespace(policies);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}")
+    @ApiOperation(value = "Delete a namespace and all the topics under it.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace is not empty") })
+    public void deleteNamespace(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalDeleteNamespace(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/bundle/{bundle}")
+    @ApiOperation(value = "Delete a namespace bundle and all the topics under it.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace bundle is not empty") })
+    public void deleteNamespaceBundle(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalDeleteNamespaceBundle(bundleRange, authoritative);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/permissions")
+    @ApiOperation(value = "Retrieve the permissions for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Namespace is not empty") })
+    public Map<String, Set<AuthAction>> getPermissions(@PathParam("property") String property,
+            @PathParam("cluster") String cluster, @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.auth_policies.namespace_auth;
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/permissions/{role}")
+    @ApiOperation(value = "Grant a new permission to a role on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void grantPermissionOnNamespace(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("role") String role, Set<AuthAction> actions) {
+        validateNamespaceName(property, namespace);
+        internalGrantPermissionOnNamespace(role, actions);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/permissions/{role}")
+    @ApiOperation(value = "Revoke all permissions to a role on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public void revokePermissionsOnNamespace(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("role") String role) {
+        validateNamespaceName(property, namespace);
+        internalRevokePermissionsOnNamespace(role);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/replication")
+    @ApiOperation(value = "Get the replication clusters for a namespace.", response = String.class, responseContainer = "List")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Namespace is not global") })
+    public List<String> getNamespaceReplicationClusters(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, namespace);
+
+        return internalGetNamespaceReplicationClusters();
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/replication")
+    @ApiOperation(value = "Set the replication clusters for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Peer-cluster can't be part of replication-cluster"),
+            @ApiResponse(code = 412, message = "Namespace is not global or invalid cluster ids") })
+    public void setNamespaceReplicationClusters(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, List<String> clusterIds) {
+        validateNamespaceName(property, namespace);
+        internalSetNamespaceReplicationClusters(clusterIds);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/messageTTL")
+    @ApiOperation(value = "Get the message TTL for the namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public int getNamespaceMessageTTL(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.message_ttl_in_seconds;
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/messageTTL")
+    @ApiOperation(value = "Set message TTL in seconds for namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Invalid TTL") })
+    public void setNamespaceMessageTTL(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            int messageTTL) {
+        validateNamespaceName(property, namespace);
+        internalSetNamespaceMessageTTL(messageTTL);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/deduplication")
+    @ApiOperation(value = "Enable or disable broker side deduplication for all topics in a namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist") })
+    public void modifyDeduplication(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            boolean enableDeduplication) {
+        validateNamespaceName(property, namespace);
+        internalModifyDeduplication(enableDeduplication);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/bundles")
+    @ApiOperation(value = "Get the bundles split data.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Namespace is not setup to split in bundles") })
+    public BundlesData getBundlesData(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validatePoliciesReadOnlyAccess();
+        validateNamespaceName(property, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+
+        return policies.bundles;
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/unload")
+    @ApiOperation(value = "Unload namespace", notes = "Unload an active namespace from the current broker serving it. Performing this operation will let the broker"
+            + "removes all producers, consumers, and connections using this namespace, and close all destinations (including"
+            + "their persistent store). During that operation, the namespace is marked as tentatively unavailable until the"
+            + "broker completes the unloading action. This operation requires strictly super user privileges, since it would"
+            + "result in non-persistent message loss and unexpected connection closure to the clients.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Namespace is already unloaded or Namespace has bundles activated") })
+    public void unloadNamespace(@PathParam("property") String property, @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, namespace);
+        internalUnloadNamespace();
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/{bundle}/unload")
+    @ApiOperation(value = "Unload a namespace bundle")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void unloadNamespaceBundle(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalUnloadNamespaceBundle(bundleRange, authoritative);
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/{bundle}/split")
+    @ApiOperation(value = "Split a namespace bundle")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void splitNamespaceBundle(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative,
+            @QueryParam("unload") @DefaultValue("false") boolean unload) {
+        validateNamespaceName(property, namespace);
+        internalSplitNamespaceBundle(bundleRange, authoritative, unload);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/dispatchRate")
+    @ApiOperation(value = "Set dispatch-rate throttling for all topics of the namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void setDispatchRate(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            DispatchRate dispatchRate) {
+        validateNamespaceName(property, namespace);
+        internalSetDispatchRate(dispatchRate);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/dispatchRate")
+    @ApiOperation(value = "Get dispatch-rate configured for the namespace, -1 represents not configured yet")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public DispatchRate getDispatchRate(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, namespace);
+        return internalGetDispatchRate();
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/backlogQuotaMap")
+    @ApiOperation(value = "Get backlog quota map on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public Map<BacklogQuotaType, BacklogQuota> getBacklogQuotaMap(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateAdminAccessOnProperty(property);
+        validateNamespaceName(property, namespace);
+
+        Policies policies = getNamespacePolicies(namespaceName);
+        return policies.backlog_quota_map;
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/backlogQuota")
+    @ApiOperation(value = " Set a backlog quota for all the destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"),
+            @ApiResponse(code = 412, message = "Specified backlog quota exceeds retention quota. Increase retention quota and retry request") })
+    public void setBacklogQuota(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @QueryParam("backlogQuotaType") BacklogQuotaType backlogQuotaType, BacklogQuota backlogQuota) {
+        validateNamespaceName(property, namespace);
+        internalSetBacklogQuota(backlogQuotaType, backlogQuota);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/backlogQuota")
+    @ApiOperation(value = "Remove a backlog quota policy from a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void removeBacklogQuota(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @QueryParam("backlogQuotaType") BacklogQuotaType backlogQuotaType) {
+        validateNamespaceName(property, namespace);
+        internalRemoveBacklogQuota(backlogQuotaType);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/retention")
+    @ApiOperation(value = "Get retention config on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public RetentionPolicies getRetention(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, namespace);
+        return internalGetRetention();
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/retention")
+    @ApiOperation(value = " Set retention configuration on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"),
+            @ApiResponse(code = 412, message = "Retention Quota must exceed backlog quota") })
+    public void setRetention(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            RetentionPolicies retention) {
+        validateNamespaceName(property, namespace);
+        internalSetRetention(retention);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/persistence")
+    @ApiOperation(value = "Set the persistence configuration for all the destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"),
+            @ApiResponse(code = 400, message = "Invalid persistence policies") })
+    public void setPersistence(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            PersistencePolicies persistence) {
+        validateNamespaceName(property, namespace);
+        internalSetPersistence(persistence);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/persistence")
+    @ApiOperation(value = "Get the persistence configuration for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public PersistencePolicies getPersistence(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, namespace);
+        return internalGetPersistence();
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/clearBacklog")
+    @ApiOperation(value = "Clear backlog for all destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBacklog(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalClearNamespaceBacklog(authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{bundle}/clearBacklog")
+    @ApiOperation(value = "Clear backlog for all destinations on a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBundleBacklog(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalClearNamespaceBundleBacklog(bundleRange, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/clearBacklog/{subscription}")
+    @ApiOperation(value = "Clear backlog for a given subscription on all destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBacklogForSubscription(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalClearNamespaceBacklogForSubscription(subscription, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{bundle}/clearBacklog/{subscription}")
+    @ApiOperation(value = "Clear backlog for a given subscription on all destinations on a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void clearNamespaceBundleBacklogForSubscription(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalClearNamespaceBundleBacklogForSubscription(subscription, bundleRange, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/unsubscribe/{subscription}")
+    @ApiOperation(value = "Unsubscribes the given subscription on all destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void unsubscribeNamespace(@PathParam("property") String property, @PathParam("cluster") String cluster,
+            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalUnsubscribeNamespace(subscription, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{bundle}/unsubscribe/{subscription}")
+    @ApiOperation(value = "Unsubscribes the given subscription on all topics on a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public void unsubscribeNamespaceBundle(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("subscription") String subscription,
+            @PathParam("bundle") String bundleRange,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateNamespaceName(property, namespace);
+        internalUnsubscribeNamespaceBundle(subscription, bundleRange, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/subscriptionAuthMode")
+    @ApiOperation(value = " Set a subscription auth mode for all the destinations on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void setSubscriptionAuthMode(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, SubscriptionAuthMode subscriptionAuthMode) {
+        validateNamespaceName(property, namespace);
+        internalSetSubscriptionAuthMode(subscriptionAuthMode);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/encryptionRequired")
+    @ApiOperation(value = "Message encryption is required or not for all topics in a namespace")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Property or cluster or namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification"), })
+    public void modifyEncryptionRequired(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, boolean encryptionRequired) {
+        validateNamespaceName(property, namespace);
+        internalModifyEncryptionRequired(encryptionRequired);
+    }
+
+    private Policies getDefaultPolicesIfNull(Policies policies) {
+        if (policies != null) {
+            return policies;
+        }
+
+        Policies defaultPolicies = new Policies();
+        int defaultNumberOfBundles = config().getDefaultNumberOfNamespaceBundles();
+        defaultPolicies.bundles = getBundles(defaultNumberOfBundles);
+        return defaultPolicies;
+    }
+
+    private static final Logger log = LoggerFactory.getLogger(Namespaces.class);
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/NonPersistentTopics.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/NonPersistentTopics.java
new file mode 100644
index 0000000..55bd9be
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/NonPersistentTopics.java
@@ -0,0 +1,159 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.Encoded;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.pulsar.broker.service.Topic;
+import org.apache.pulsar.broker.service.nonpersistent.NonPersistentTopic;
+import org.apache.pulsar.broker.web.RestException;
+import org.apache.pulsar.common.naming.DestinationName;
+import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
+import org.apache.pulsar.common.policies.data.NonPersistentTopicStats;
+import org.apache.pulsar.common.policies.data.PersistentTopicInternalStats;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+/**
+ */
+@Path("/non-persistent")
+@Produces(MediaType.APPLICATION_JSON)
+@Api(value = "/non-persistent", description = "Non-Persistent topic admin apis", tags = "non-persistent topic")
+public class NonPersistentTopics extends PersistentTopics {
+    private static final Logger log = LoggerFactory.getLogger(NonPersistentTopics.class);
+
+    @GET
+    @Path("/{property}/{namespace}/{destination}/partitions")
+    @ApiOperation(value = "Get partitioned topic metadata.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public PartitionedTopicMetadata getPartitionedMetadata(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return getPartitionedTopicMetadata(destinationName, authoritative);
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/stats")
+    @ApiOperation(value = "Get the stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public NonPersistentTopicStats getStats(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        validateAdminOperationOnDestination(destinationName, authoritative);
+        Topic topic = getTopicReference(destinationName);
+        return ((NonPersistentTopic) topic).getStats();
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/internalStats")
+    @ApiOperation(value = "Get the internal stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PersistentTopicInternalStats getInternalStats(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        validateAdminOperationOnDestination(destinationName, authoritative);
+        Topic topic = getTopicReference(destinationName);
+        return topic.getInternalStats();
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/{destination}/partitions")
+    @ApiOperation(value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Partitioned topic already exist") })
+    public void createPartitionedTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, int numPartitions,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        validateAdminAccessOnProperty(destinationName.getProperty());
+        if (numPartitions <= 1) {
+            throw new RestException(Status.NOT_ACCEPTABLE, "Number of partitions should be more than 1");
+        }
+        try {
+            String path = path(PARTITIONED_TOPIC_PATH_ZNODE, namespaceName.toString(), domain(),
+                    destinationName.getEncodedLocalName());
+            byte[] data = jsonMapper().writeValueAsBytes(new PartitionedTopicMetadata(numPartitions));
+            zkCreateOptimistic(path, data);
+            // we wait for the data to be synced in all quorums and the observers
+            Thread.sleep(PARTITIONED_TOPIC_WAIT_SYNC_TIME_MS);
+            log.info("[{}] Successfully created partitioned topic {}", clientAppId(), destinationName);
+        } catch (KeeperException.NodeExistsException e) {
+            log.warn("[{}] Failed to create already existing partitioned topic {}", clientAppId(), destinationName);
+            throw new RestException(Status.CONFLICT, "Partitioned topic already exist");
+        } catch (Exception e) {
+            log.error("[{}] Failed to create partitioned topic {}", clientAppId(), destinationName, e);
+            throw new RestException(e);
+        }
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/{destination}/unload")
+    @ApiOperation(value = "Unload a topic")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public void unloadTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        log.info("[{}] Unloading topic {}", clientAppId(), destinationName);
+        if (destinationName.isGlobal()) {
+            validateGlobalNamespaceOwnership(namespaceName);
+        }
+        unloadTopic(destinationName, authoritative);
+    }
+
+    protected void validateAdminOperationOnDestination(DestinationName fqdn, boolean authoritative) {
+        validateAdminAccessOnProperty(fqdn.getProperty());
+        validateDestinationOwnership(fqdn, authoritative);
+    }
+
+    private Topic getTopicReference(DestinationName dn) {
+        try {
+            Topic topic = pulsar().getBrokerService().getTopicReference(dn.toString());
+            checkNotNull(topic);
+            return topic;
+        } catch (Exception e) {
+            throw new RestException(Status.NOT_FOUND, "Topic not found");
+        }
+    }
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/PersistentTopics.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/PersistentTopics.java
new file mode 100644
index 0000000..b52a5ad
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/PersistentTopics.java
@@ -0,0 +1,396 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.Encoded;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.pulsar.broker.admin.impl.PersistentTopicsBase;
+import org.apache.pulsar.client.api.MessageId;
+import org.apache.pulsar.client.impl.MessageIdImpl;
+import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
+import org.apache.pulsar.common.policies.data.AuthAction;
+import org.apache.pulsar.common.policies.data.PartitionedTopicStats;
+import org.apache.pulsar.common.policies.data.PersistentOfflineTopicStats;
+import org.apache.pulsar.common.policies.data.PersistentTopicInternalStats;
+import org.apache.pulsar.common.policies.data.PersistentTopicStats;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+/**
+ */
+@Path("/persistent")
+@Produces(MediaType.APPLICATION_JSON)
+@Api(value = "/persistent", description = "Persistent topic admin apis", tags = "persistent topic")
+public class PersistentTopics extends PersistentTopicsBase {
+
+    @GET
+    @Path("/{property}/{namespace}")
+    @ApiOperation(value = "Get the list of topics under a namespace.", response = String.class, responseContainer = "List")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
+    public List<String> getList(@PathParam("property") String property, @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, namespace);
+        return internalGetList();
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/partitioned")
+    @ApiOperation(value = "Get the list of partitioned topics under a namespace.", response = String.class, responseContainer = "List")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
+    public List<String> getPartitionedTopicList(@PathParam("property") String property,
+            @PathParam("namespace") String namespace) {
+        validateNamespaceName(property, namespace);
+        return internalGetPartitionedTopicList();
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/{destination}/permissions")
+    @ApiOperation(value = "Get permissions on a destination.", notes = "Retrieve the effective permissions for a destination. These permissions are defined by the permissions set at the"
+            + "namespace level combined (union) with any eventual specific permission set on the destination.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist") })
+    public Map<String, Set<AuthAction>> getPermissionsOnDestination(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetPermissionsOnDestination();
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/permissions/{role}")
+    @ApiOperation(value = "Grant a new permission to a role on a single topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void grantPermissionsOnDestination(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("role") String role, Set<AuthAction> actions) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalGrantPermissionsOnDestination(role, actions);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/{destination}/permissions/{role}")
+    @ApiOperation(value = "Revoke permissions on a destination.", notes = "Revoke permissions to a role on a single destination. If the permission was not set at the destination"
+            + "level, but rather at the namespace level, this operation will return an error (HTTP status code 412).")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace doesn't exist"),
+            @ApiResponse(code = 412, message = "Permissions are not set at the destination level") })
+    public void revokePermissionsOnDestination(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("role") String role) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalRevokePermissionsOnDestination(role);
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/{destination}/partitions")
+    @ApiOperation(value = "Create a partitioned topic.", notes = "It needs to be called before creating a producer on a partitioned topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Partitioned topic already exist") })
+    public void createPartitionedTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, int numPartitions,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalCreatePartitionedTopic(numPartitions, authoritative);
+    }
+
+    /**
+     * It updates number of partitions of an existing non-global partitioned topic. It requires partitioned-topic to be
+     * already exist and number of new partitions must be greater than existing number of partitions. Decrementing
+     * number of partitions requires deletion of topic which is not supported.
+     *
+     * Already created partitioned producers and consumers can't see newly created partitions and it requires to
+     * recreate them at application so, newly created producers and consumers can connect to newly added partitions as
+     * well. Therefore, it can violate partition ordering at producers until all producers are restarted at application.
+     *
+     * @param property
+     * @param cluster
+     * @param namespace
+     * @param destination
+     * @param numPartitions
+     */
+    @POST
+    @Path("/{property}/{namespace}/{destination}/partitions")
+    @ApiOperation(value = "Increment partitons of an existing partitioned topic.", notes = "It only increments partitions of existing non-global partitioned-topic")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Partitioned topic does not exist") })
+    public void updatePartitionedTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, int numPartitions) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalUpdatePartitionedTopic(numPartitions);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/{destination}/partitions")
+    @ApiOperation(value = "Get partitioned topic metadata.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public PartitionedTopicMetadata getPartitionedMetadata(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetPartitionedMetadata(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/{destination}/partitions")
+    @ApiOperation(value = "Delete a partitioned topic.", notes = "It will also delete all the partitions of the topic if it exists.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Partitioned topic does not exist") })
+    public void deletePartitionedTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalDeletePartitionedTopic(authoritative);
+    }
+
+    @PUT
+    @Path("/{property}/{namespace}/{destination}/unload")
+    @ApiOperation(value = "Unload a topic")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public void unloadTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalUnloadTopic(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/{destination}")
+    @ApiOperation(value = "Delete a topic.", notes = "The topic cannot be deleted if there's any active subscription or producer connected to the it.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist"),
+            @ApiResponse(code = 412, message = "Topic has active producers/subscriptions") })
+    public void deleteTopic(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalDeleteTopic(authoritative);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/{destination}/subscriptions")
+    @ApiOperation(value = "Get the list of persistent subscriptions for a given topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public List<String> getSubscriptions(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetSubscriptions(authoritative);
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/stats")
+    @ApiOperation(value = "Get the stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PersistentTopicStats getStats(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetStats(authoritative);
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/internalStats")
+    @ApiOperation(value = "Get the internal stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PersistentTopicInternalStats getInternalStats(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetInternalStats(authoritative);
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/internal-info")
+    @ApiOperation(value = "Get the internal stats for the topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public void getManagedLedgerInfo(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @Suspended AsyncResponse asyncResponse) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalGetManagedLedgerInfo(asyncResponse);
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/partitioned-stats")
+    @ApiOperation(value = "Get the stats for the partitioned topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public PartitionedTopicStats getPartitionedStats(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetPartitionedStats(authoritative);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}")
+    @ApiOperation(value = "Delete a subscription.", notes = "There should not be any active consumers on the subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic does not exist"),
+            @ApiResponse(code = 412, message = "Subscription has active consumers") })
+    public void deleteSubscription(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalDeleteSubscription(subName, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}/skip_all")
+    @ApiOperation(value = "Skip all messages on a topic subscription.", notes = "Completely clears the backlog on the subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 405, message = "Operation not allowed on non-persistent topic"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void skipAllMessages(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalSkipAllMessages(subName, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}/skip/{numMessages}")
+    @ApiOperation(value = "Skip messages on a topic subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void skipMessages(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @PathParam("numMessages") int numMessages,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalSkipMessages(subName, numMessages, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}/expireMessages/{expireTimeInSeconds}")
+    @ApiOperation(value = "Expire messages on a topic subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void expireTopicMessages(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @PathParam("expireTimeInSeconds") int expireTimeInSeconds,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalExpireMessages(subName, expireTimeInSeconds, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/all_subscription/expireMessages/{expireTimeInSeconds}")
+    @ApiOperation(value = "Expire messages on all subscriptions of topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic or subscription does not exist") })
+    public void expireMessagesForAllSubscriptions(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @PathParam("expireTimeInSeconds") int expireTimeInSeconds,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalExpireMessagesForAllSubscriptions(expireTimeInSeconds, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}/resetcursor/{timestamp}")
+    @ApiOperation(value = "Reset subscription to message position closest to absolute timestamp (in ms).", notes = "It fence cursor and disconnects all active consumers before reseting cursor.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic/Subscription does not exist") })
+    public void resetCursor(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @PathParam("timestamp") long timestamp,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalResetCursor(subName, timestamp, authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}/resetcursor")
+    @ApiOperation(value = "Reset subscription to message position closest to given position.", notes = "It fence cursor and disconnects all active consumers before reseting cursor.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic/Subscription does not exist"),
+            @ApiResponse(code = 405, message = "Not supported for partitioned topics") })
+    public void resetCursorOnPosition(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative, MessageIdImpl messageId) {
+        validateDestinationName(property, namespace, encodedTopic);
+        internalResetCursorOnPosition(subName, authoritative, messageId);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/{destination}/subscription/{subName}/position/{messagePosition}")
+    @ApiOperation(value = "Peek nth message on a topic subscription.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Topic, subscription or the message position does not exist") })
+    public Response peekNthMessage(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic, @PathParam("subName") String subName,
+            @PathParam("messagePosition") int messagePosition,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalPeekNthMessage(subName, messagePosition, authoritative);
+    }
+
+    @GET
+    @Path("{property}/{namespace}/{destination}/backlog")
+    @ApiOperation(value = "Get estimated backlog for offline topic.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public PersistentOfflineTopicStats getBacklog(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalGetBacklog(authoritative);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{destination}/terminate")
+    @ApiOperation(value = "Terminate a topic. A topic that is terminated will not accept any more "
+            + "messages to be published and will let consumer to drain existing messages in backlog")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 405, message = "Operation not allowed on non-persistent topic"),
+            @ApiResponse(code = 404, message = "Topic does not exist") })
+    public MessageId terminate(@PathParam("property") String property, @PathParam("namespace") String namespace,
+            @PathParam("destination") @Encoded String encodedTopic,
+            @QueryParam("authoritative") @DefaultValue("false") boolean authoritative) {
+        validateDestinationName(property, namespace, encodedTopic);
+        return internalTerminate(authoritative);
+    }
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Properties.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Properties.java
new file mode 100644
index 0000000..19d3652
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Properties.java
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import io.swagger.annotations.Api;
+import org.apache.pulsar.broker.admin.impl.PropertiesBase;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/properties")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Api(value = "/properties", description = "PropertiesBase admin apis", tags = "properties")
+public class Properties extends PropertiesBase {
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ResourceQuotas.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ResourceQuotas.java
new file mode 100644
index 0000000..b84a14f
--- /dev/null
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ResourceQuotas.java
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.admin.v2;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.pulsar.broker.admin.impl.ResourceQuotasBase;
+import org.apache.pulsar.common.policies.data.ResourceQuota;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+@Path("/resource-quotas")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Api(value = "/resource-quotas", description = "Quota admin APIs", tags = "resource-quotas")
+public class ResourceQuotas extends ResourceQuotasBase {
+
+    @GET
+    @ApiOperation(value = "Get the default quota", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public ResourceQuota getDefaultResourceQuota() throws Exception {
+        return super.getDefaultResourceQuota();
+    }
+
+    @POST
+    @ApiOperation(value = "Set the default quota", response = String.class, responseContainer = "Set")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") })
+    public void setDefaultResourceQuota(ResourceQuota quota) throws Exception {
+        super.setDefaultResourceQuota(quota);
+    }
+
+    @GET
+    @Path("/{property}/{namespace}/{bundle}")
+    @ApiOperation(value = "Get resource quota of a namespace bundle.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 404, message = "Namespace does not exist") })
+    public ResourceQuota getNamespaceBundleResourceQuota(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange) {
+        validateNamespaceName(property, namespace);
+        return internalGetNamespaceBundleResourceQuota(bundleRange);
+    }
+
+    @POST
+    @Path("/{property}/{namespace}/{bundle}")
+    @ApiOperation(value = "Set resource quota on a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void setNamespaceBundleResourceQuota(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange, ResourceQuota quota) {
+        validateNamespaceName(property, namespace);
+        internalSetNamespaceBundleResourceQuota(bundleRange, quota);
+    }
+
+    @DELETE
+    @Path("/{property}/{namespace}/{bundle}")
+    @ApiOperation(value = "Remove resource quota for a namespace.")
+    @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"),
+            @ApiResponse(code = 409, message = "Concurrent modification") })
+    public void removeNamespaceBundleResourceQuota(@PathParam("property") String property,
+            @PathParam("namespace") String namespace, @PathParam("bundle") String bundleRange) {
+        validateNamespaceName(property, namespace);
+        internalRemoveNamespaceBundleResourceQuota(bundleRange);
+    }
+}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/loadbalance/impl/LoadManagerShared.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/loadbalance/impl/LoadManagerShared.java
index 19d3f34..80cb31c 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/loadbalance/impl/LoadManagerShared.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/loadbalance/impl/LoadManagerShared.java
@@ -253,7 +253,7 @@ public class LoadManagerShared {
      * @param assignedBundleName
      *            Name of bundle to be assigned.
      * @param candidates
-     *            Brokers available for placement.
+     *            BrokersBase available for placement.
      * @param brokerToNamespaceToBundleRange
      *            Map from brokers to namespaces to bundle ranges.
      */
@@ -302,14 +302,14 @@ public class LoadManagerShared {
      * eg.
      * <pre>
      * Before:
-     * Domain-count  Brokers-count
+     * Domain-count  BrokersBase-count
      * ____________  ____________
      * d1-3          b1-2,b2-1
      * d2-3          b3-2,b4-1
      * d3-4          b5-2,b6-2
      *
      * After filtering: "candidates" brokers
-     * Domain-count  Brokers-count
+     * Domain-count  BrokersBase-count
      * ____________  ____________
      * d1-3          b2-1
      * d2-3          b4-1
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/lookup/DestinationLookup.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/lookup/DestinationLookup.java
index 0393d6a..300bd84 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/lookup/DestinationLookup.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/lookup/DestinationLookup.java
@@ -52,6 +52,7 @@ import org.apache.pulsar.common.lookup.data.LookupData;
 import org.apache.pulsar.common.naming.DestinationDomain;
 import org.apache.pulsar.common.naming.DestinationName;
 import org.apache.pulsar.common.naming.NamespaceBundle;
+import org.apache.pulsar.common.naming.NamespaceName;
 import org.apache.pulsar.common.util.Codec;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -211,7 +212,8 @@ public class DestinationLookup extends PulsarWebResource {
             if (differentClusterData != null) {
                 if (log.isDebugEnabled()) {
                     log.debug("[{}] Redirecting the lookup call to {}/{} cluster={}", clientAppId,
-                            differentClusterData.getBrokerServiceUrl(), differentClusterData.getBrokerServiceUrlTls(), cluster);
+                            differentClusterData.getBrokerServiceUrl(), differentClusterData.getBrokerServiceUrlTls(),
+                            cluster);
                 }
                 validationFuture.complete(newLookupResponse(differentClusterData.getBrokerServiceUrl(),
                         differentClusterData.getBrokerServiceUrlTls(), true, LookupType.Redirect, requestId, false));
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/NamespaceService.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/NamespaceService.java
index e7b885a..5319344 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/NamespaceService.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/NamespaceService.java
@@ -726,16 +726,18 @@ public class NamespaceService {
         return getBundle(destinationName);
     }
 
-    public List<String> getListOfDestinations(String property, String cluster, String namespace) throws Exception {
+    public List<String> getListOfDestinations(NamespaceName namespaceName) throws Exception {
         List<String> destinations = Lists.newArrayList();
 
         // For every topic there will be a managed ledger created.
         try {
-            String path = String.format("/managed-ledgers/%s/%s/%s/persistent", property, cluster, namespace);
-            LOG.debug("Getting children from managed-ledgers now: {}", path);
+            String path = String.format("/managed-ledgers/%s/persistent", namespaceName);
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Getting children from managed-ledgers now: {}", path);
+            }
+
             for (String destination : pulsar.getLocalZkCacheService().managedLedgerListCache().get(path)) {
-                destinations.add(String.format("persistent://%s/%s/%s/%s", property, cluster, namespace,
-                        Codec.decode(destination)));
+                destinations.add(String.format("persistent://%s/%s", namespaceName, Codec.decode(destination)));
             }
         } catch (KeeperException.NoNodeException e) {
             // NoNode means there are no persistent topics for this namespace
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/ServiceUnitZkUtils.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/ServiceUnitZkUtils.java
index 210b335..a925857 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/ServiceUnitZkUtils.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/namespace/ServiceUnitZkUtils.java
@@ -75,10 +75,17 @@ public final class ServiceUnitZkUtils {
         String[] parts = path.split("/");
         checkArgument(parts.length > 2);
         checkArgument(parts[1].equals("namespace"));
-        checkArgument(parts.length > 5);
-
-        Range<Long> range = getHashRange(parts[5]);
-        return factory.getBundle(NamespaceName.get(parts[2], parts[3], parts[4]), range);
+        checkArgument(parts.length > 4);
+
+        if (parts.length > 5) {
+            // this is a V1 path prop/cluster/namespace/hash
+            Range<Long> range = getHashRange(parts[5]);
+            return factory.getBundle(NamespaceName.get(parts[2], parts[3], parts[4]), range);
+        } else {
+            // this is a V2 path prop/namespace/hash
+            Range<Long> range = getHashRange(parts[4]);
+            return factory.getBundle(NamespaceName.get(parts[2], parts[3]), range);
+        }
     }
 
     private static Range<Long> getHashRange(String rangePathPart) {
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/BrokerService.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/BrokerService.java
index 2f3b27a..1eff83b 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/BrokerService.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/BrokerService.java
@@ -638,7 +638,7 @@ public class BrokerService implements Closeable, ZooKeeperCacheListener<Policies
             try {
                 policies = pulsar
                         .getConfigurationCache().policiesCache().get(AdminResource.path(POLICIES,
-                                namespace.getProperty(), namespace.getCluster(), namespace.getLocalName()));
+                                namespace.toString()));
             } catch (Throwable t) {
                 // Ignoring since if we don't have policies, we fallback on the default
                 log.warn("Got exception when reading persistence policy for {}: {}", topicName, t.getMessage(), t);
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java
index b50eee6..0b8d512 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java
@@ -20,7 +20,7 @@ package org.apache.pulsar.broker.service;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static org.apache.commons.lang3.StringUtils.isNotBlank;
-import static org.apache.pulsar.broker.admin.PersistentTopics.getPartitionedTopicMetadata;
+import static org.apache.pulsar.broker.admin.impl.PersistentTopicsBase.getPartitionedTopicMetadata;
 import static org.apache.pulsar.broker.lookup.DestinationLookup.lookupDestinationAsync;
 import static org.apache.pulsar.common.api.Commands.newLookupErrorResponse;
 import static org.apache.pulsar.common.api.proto.PulsarApi.ProtocolVersion.v5;
@@ -278,7 +278,7 @@ public class ServerCnx extends PulsarHandler {
                 return null;
             }).exceptionally(ex -> {
                 final String msg = "Exception occured while trying to authorize lookup";
-                log.warn("[{}] {} with role {} on topic {}", remoteAddress, msg, authRole, topicName);
+                log.warn("[{}] {} with role {} on topic {}", remoteAddress, msg, authRole, topicName, ex);
                 ctx.writeAndFlush(newLookupErrorResponse(ServerError.AuthorizationError, msg, requestId));
                 lookupSemaphore.release();
                 return null;
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java
index c17182c..60031fc 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java
@@ -45,14 +45,10 @@ import javax.ws.rs.core.UriInfo;
 import org.apache.pulsar.broker.PulsarService;
 import org.apache.pulsar.broker.ServiceConfiguration;
 import org.apache.pulsar.broker.admin.AdminResource;
-import org.apache.pulsar.broker.admin.Namespaces;
 import org.apache.pulsar.broker.authentication.AuthenticationDataHttps;
 import org.apache.pulsar.broker.authentication.AuthenticationDataSource;
 import org.apache.pulsar.broker.namespace.NamespaceService;
-import org.apache.pulsar.common.naming.DestinationName;
-import org.apache.pulsar.common.naming.NamespaceBundle;
-import org.apache.pulsar.common.naming.NamespaceBundles;
-import org.apache.pulsar.common.naming.NamespaceName;
+import org.apache.pulsar.common.naming.*;
 import org.apache.pulsar.common.policies.data.BundlesData;
 import org.apache.pulsar.common.policies.data.ClusterData;
 import org.apache.pulsar.common.policies.data.Policies;
@@ -277,12 +273,13 @@ public abstract class PulsarWebResource {
     }
 
     protected static CompletableFuture<ClusterData> getClusterDataIfDifferentCluster(PulsarService pulsar,
-            String cluster, String clientAppId) {
+         String cluster, String clientAppId) {
 
         CompletableFuture<ClusterData> clusterDataFuture = new CompletableFuture<>();
 
         if (!isValidCluster(pulsar, cluster)) {
             try {
+                // this code should only happen with a v1 namespace format prop/cluster/namespaces
                 if (!pulsar.getConfiguration().getClusterName().equals(cluster)) {
                     // redirect to the cluster requested
                     pulsar.getConfigurationCache().clustersCache().getAsync(path("clusters", cluster))
@@ -310,15 +307,15 @@ public abstract class PulsarWebResource {
         return clusterDataFuture;
     }
 
-    protected static boolean isValidCluster(PulsarService pulsarSevice, String cluster) {// If the cluster name is
-        // "global", don't validate the
-        // cluster ownership.
+    static boolean isValidCluster(PulsarService pulsarService, String cluster) {// If the cluster name is
+        // cluster == null or "global", don't validate the
+        // cluster ownership. Cluster will be null in v2 naming.
         // The validation will be done by checking the namespace configuration
-        if (cluster.equals(Namespaces.GLOBAL_CLUSTER)) {
+        if (cluster == null || Constants.GLOBAL_CLUSTER.equals(cluster)) {
             return true;
         }
 
-        if (!pulsarSevice.getConfiguration().isAuthorizationEnabled()) {
+        if (!pulsarService.getConfiguration().isAuthorizationEnabled()) {
             // Without authorization, any cluster name should be valid and accepted by the broker
             return true;
         }
@@ -565,8 +562,7 @@ public abstract class PulsarWebResource {
         }
         final CompletableFuture<ClusterData> validationFuture = new CompletableFuture<>();
         final String localCluster = pulsarService.getConfiguration().getClusterName();
-        final String path = AdminResource.path(POLICIES, namespace.getProperty(), namespace.getCluster(),
-                namespace.getLocalName());
+        final String path = AdminResource.path(POLICIES, namespace.toString());
 
         pulsarService.getConfigurationCache().policiesCache().getAsync(path).thenAccept(policiesResult -> {
             if (policiesResult.isPresent()) {
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/utils/PulsarBrokerVersionStringUtils.java b/pulsar-broker/src/main/java/org/apache/pulsar/utils/PulsarBrokerVersionStringUtils.java
index 27e3992..fd4d7ed 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/utils/PulsarBrokerVersionStringUtils.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/utils/PulsarBrokerVersionStringUtils.java
@@ -70,11 +70,11 @@ public class PulsarBrokerVersionStringUtils {
     }
 
     /**
-     * Looks for a resource in the jar which is expected to be a java.util.Properties, then
+     * Looks for a resource in the jar which is expected to be a java.util.PropertiesBase, then
      * extract a specific property value.
      *
      * @return the property value, or null if the resource does not exist or the resource
-     *         is not a valid java.util.Properties or the resource does not contain the
+     *         is not a valid java.util.PropertiesBase or the resource does not contain the
      *         named property
      */
     private static String getPropertyFromResource(String resource, String propertyName) {
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiTest.java
index 444d224..5038116 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiTest.java
@@ -1493,7 +1493,7 @@ public class AdminApiTest extends MockedPulsarServiceBaseTest {
 
     /**
      * <pre>
-     * Verify: PersistentTopics.expireMessages()/expireMessagesForAllSubscriptions()
+     * Verify: PersistentTopicsBase.expireMessages()/expireMessagesForAllSubscriptions()
      * 1. Created multiple shared subscriptions and publisher on topic
      * 2. Publish messages on the topic
      * 3. expire message on sub-1 : backlog for sub-1 must be 0
@@ -1553,7 +1553,7 @@ public class AdminApiTest extends MockedPulsarServiceBaseTest {
     }
 
     /**
-     * Verify: PersistentTopics.expireMessages()/expireMessagesForAllSubscriptions() for PartitionTopic
+     * Verify: PersistentTopicsBase.expireMessages()/expireMessagesForAllSubscriptions() for PartitionTopic
      *
      * @throws Exception
      */
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java
index c587033..52d1186 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java
@@ -48,11 +48,18 @@ import javax.ws.rs.core.UriInfo;
 
 import org.apache.bookkeeper.mledger.proto.PendingBookieOpsStats;
 import org.apache.bookkeeper.util.ZkUtils;
+import org.apache.pulsar.broker.admin.v1.BrokerStats;
+import org.apache.pulsar.broker.admin.v1.Brokers;
+import org.apache.pulsar.broker.admin.v1.Clusters;
+import org.apache.pulsar.broker.admin.v1.Properties;
+import org.apache.pulsar.broker.admin.v1.Namespaces;
+import org.apache.pulsar.broker.admin.v1.PersistentTopics;
+import org.apache.pulsar.broker.admin.v1.ResourceQuotas;
 import org.apache.pulsar.broker.auth.MockedPulsarServiceBaseTest;
 import org.apache.pulsar.broker.cache.ConfigurationCacheService;
-import org.apache.pulsar.broker.loadbalance.ResourceUnit;
 import org.apache.pulsar.broker.web.PulsarWebResource;
 import org.apache.pulsar.broker.web.RestException;
+import org.apache.pulsar.common.naming.NamespaceName;
 import org.apache.pulsar.common.policies.data.AuthAction;
 import org.apache.pulsar.common.policies.data.AutoFailoverPolicyData;
 import org.apache.pulsar.common.policies.data.AutoFailoverPolicyType;
@@ -90,7 +97,6 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
     private BrokerStats brokerStats;
 
     private Field uriField;
-    private UriInfo uriInfo;
     private final String configClusterName = "use";
 
     public AdminTest() {
@@ -146,7 +152,6 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
 
         uriField = PulsarWebResource.class.getDeclaredField("uri");
         uriField.setAccessible(true);
-        uriInfo = mock(UriInfo.class);
 
         persistentTopics = spy(new PersistentTopics());
         persistentTopics.setServletContext(new MockServletContext());
@@ -511,7 +516,7 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
         String namespace = "ns";
         String bundleRange = "0x00000000_0xffffffff";
         Policies policies = new Policies();
-        doReturn(policies).when(resourceQuotas).getNamespacePolicies(property, cluster, namespace);
+        doReturn(policies).when(resourceQuotas).getNamespacePolicies(NamespaceName.get(property, cluster, namespace));
         doReturn("client-id").when(resourceQuotas).clientAppId();
 
         try {
@@ -570,8 +575,7 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
         StreamingOutput destination = brokerStats.getDestinations2();
         assertNotNull(destination);
         try {
-            Map<Long, Collection<ResourceUnit>> resource = brokerStats.getBrokerResourceAvailability("prop", "use",
-                    "ns2");
+            brokerStats.getBrokerResourceAvailability("prop", "use", "ns2");
             fail("should have failed as ModularLoadManager doesn't support it");
         } catch (RestException re) {
             // Ok
@@ -586,13 +590,12 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
         final String namespace = "ns";
         final String destination = "ds1";
         Policies policies = new Policies();
-        doReturn(policies).when(resourceQuotas).getNamespacePolicies(property, cluster, namespace);
+        doReturn(policies).when(resourceQuotas).getNamespacePolicies(NamespaceName.get(property, cluster, namespace));
         doReturn("client-id").when(resourceQuotas).clientAppId();
         // create policies
         PropertyAdmin admin = new PropertyAdmin();
         admin.getAllowedClusters().add(cluster);
-        ZkUtils.createFullPathOptimistic(mockZookKeeper,
-                PulsarWebResource.path(POLICIES, property, cluster, namespace),
+        ZkUtils.createFullPathOptimistic(mockZookKeeper, PulsarWebResource.path(POLICIES, property, cluster, namespace),
                 ObjectMapperFactory.getThreadLocal().writeValueAsBytes(new Policies()), ZooDefs.Ids.OPEN_ACL_UNSAFE,
                 CreateMode.PERSISTENT);
 
@@ -601,8 +604,8 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
         // create destination
         assertEquals(persistentTopics.getPartitionedTopicList(property, cluster, namespace), Lists.newArrayList());
         persistentTopics.createPartitionedTopic(property, cluster, namespace, destination, 5, false);
-        assertEquals(persistentTopics.getPartitionedTopicList(property, cluster, namespace), Lists.newArrayList(
-                String.format("persistent://%s/%s/%s/%s", property, cluster, namespace, destination)));
+        assertEquals(persistentTopics.getPartitionedTopicList(property, cluster, namespace), Lists
+                .newArrayList(String.format("persistent://%s/%s/%s/%s", property, cluster, namespace, destination)));
 
         CountDownLatch notificationLatch = new CountDownLatch(2);
         configurationCache.policiesCache().registerListener((path, data, stat) -> {
@@ -635,5 +638,5 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
         assertEquals(exception.getMessage(), message);
 
     }
-    
+
 }
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java
index 36c9ec2..f9ff652 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java
@@ -47,6 +47,8 @@ import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 
 import org.apache.bookkeeper.util.ZkUtils;
+import org.apache.pulsar.broker.admin.v1.Namespaces;
+import org.apache.pulsar.broker.admin.v1.PersistentTopics;
 import org.apache.pulsar.broker.auth.MockedPulsarServiceBaseTest;
 import org.apache.pulsar.broker.namespace.NamespaceEphemeralData;
 import org.apache.pulsar.broker.namespace.NamespaceService;
@@ -1064,20 +1066,16 @@ public class NamespacesTest extends MockedPulsarServiceBaseTest {
             doReturn(false).when(topics).isRequestHttps();
             doReturn("test").when(topics).clientAppId();
             mockWebUrl(localWebServiceUrl, testNs);
+            doReturn("persistent").when(topics).domain();
 
             try {
-                topics.validateAdminOperationOnDestination(topicName, false);
+                topics.validateDestinationName(topicName.getProperty(), topicName.getCluster(),
+                        topicName.getNamespacePortion(), topicName.getEncodedLocalName());
+                topics.validateAdminOperationOnDestination(false);
             } catch (RestException e) {
                 fail("validateAdminAccessOnProperty failed");
             }
 
-            try {
-                topics.validateAdminOperationOnDestination(DestinationName.get(""), false);
-                fail("validateAdminAccessOnProperty failed");
-            } catch (Exception e) {
-                // OK
-            }
-
         } catch (RestException e) {
             fail("validateAdminAccessOnProperty failed");
         }
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/naming/Constants.java b/pulsar-common/src/main/java/org/apache/pulsar/common/naming/Constants.java
new file mode 100644
index 0000000..aed20fa
--- /dev/null
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/naming/Constants.java
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.common.naming;
+
+public class Constants {
+
+    public static final String GLOBAL_CLUSTER = "global";
+
+    private Constants() {}
+}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/naming/DestinationName.java b/pulsar-common/src/main/java/org/apache/pulsar/common/naming/DestinationName.java
index 3505010..ef40ced 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/naming/DestinationName.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/naming/DestinationName.java
@@ -18,8 +18,6 @@
  */
 package org.apache.pulsar.common.naming;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -64,6 +62,16 @@ public class DestinationName implements ServiceUnitId {
                 }
             });
 
+    public static DestinationName get(String domain, NamespaceName namespaceName, String destination) {
+        String name = domain + "://" + namespaceName.toString() + '/' + destination;
+        return DestinationName.get(name);
+    }
+
+    public static DestinationName get(String domain, String property, String namespace, String destination) {
+        String name = domain + "://" + property + '/' + namespace + '/' + destination;
+        return DestinationName.get(name);
+    }
+
     public static DestinationName get(String domain, String property, String cluster, String namespace,
             String destination) {
         String name = domain + "://" + property + '/' + cluster + '/' + namespace + '/' + destination;
@@ -92,7 +100,9 @@ public class DestinationName implements ServiceUnitId {
     private DestinationName(String destination) {
         this.destination = destination;
         try {
-            // persistent://property/cluster/namespace/topic
+            // The topic name can be in two different forms:
+            // new:    persistent://property/namespace/topic
+            // legacy: persistent://property/cluster/namespace/topic
             if (!destination.contains("://")) {
                 throw new IllegalArgumentException(
                         "Invalid destination name: " + destination + " -- Domain is missing");
@@ -102,29 +112,44 @@ public class DestinationName implements ServiceUnitId {
             this.domain = DestinationDomain.getEnum(parts.get(0));
 
             String rest = parts.get(1);
-            // property/cluster/namespace/<localName>
+
+            // The rest of the name can be in different forms:
+            // new:    property/namespace/<localName>
+            // legacy: property/cluster/namespace/<localName>
             // Examples of localName:
             // 1. some/name/xyz//
             // 2. /xyz-123/feeder-2
+
+
             parts = Splitter.on("/").limit(4).splitToList(rest);
-            if (parts.size() != 4) {
+            if (parts.size() == 3) {
+                // New topic name without cluster name
+                this.property = parts.get(0);
+                this.cluster = null;
+                this.namespacePortion = parts.get(1);
+                this.localName = parts.get(2);
+                this.partitionIndex = getPartitionIndex(destination);
+                this.namespaceName = NamespaceName.get(property, namespacePortion);
+            } else if (parts.size() == 4) {
+                // Legacy topic name that includes cluster name
+                this.property = parts.get(0);
+                this.cluster = parts.get(1);
+                this.namespacePortion = parts.get(2);
+                this.localName = parts.get(3);
+                this.partitionIndex = getPartitionIndex(destination);
+                this.namespaceName = NamespaceName.get(property, cluster, namespacePortion);
+            } else {
                 throw new IllegalArgumentException("Invalid destination name: " + destination);
             }
 
-            this.property = parts.get(0);
-            this.cluster = parts.get(1);
-            this.namespacePortion = parts.get(2);
-            this.localName = parts.get(3);
-            this.partitionIndex = getPartitionIndex(destination);
 
-            NamespaceName.validateNamespaceName(property, cluster, namespacePortion);
-            if (checkNotNull(localName).isEmpty()) {
+            if (localName == null || localName.isEmpty()) {
                 throw new IllegalArgumentException("Invalid destination name: " + destination);
             }
         } catch (NullPointerException e) {
             throw new IllegalArgumentException("Invalid destination name: " + destination, e);
         }
-        namespaceName = NamespaceName.get(property, cluster, namespacePortion);
+
     }
 
     /**
@@ -156,6 +181,7 @@ public class DestinationName implements ServiceUnitId {
         return property;
     }
 
+    @Deprecated
     public String getCluster() {
         return cluster;
     }
@@ -229,9 +255,16 @@ public class DestinationName implements ServiceUnitId {
      * @return the relative path to be used in persistence
      */
     public String getPersistenceNamingEncoding() {
-        // The convention is: domain://property/cluster/namespace/destination
-        // We want to persist in the order: property/cluster/namespace/domain/destination
-        return String.format("%s/%s/%s/%s/%s", property, cluster, namespacePortion, domain, getEncodedLocalName());
+        // The convention is: domain://property/namespace/topic
+        // We want to persist in the order: property/namespace/domain/topic
+
+        // For legacy naming scheme, the convention is: domain://property/cluster/namespace/topic
+        // We want to persist in the order: property/cluster/namespace/domain/topic
+        if (cluster == null) {
+            return String.format("%s/%s/%s/%s", property, namespacePortion, domain, getEncodedLocalName());
+        } else {
+            return String.format("%s/%s/%s/%s/%s", property, cluster, namespacePortion, domain, getEncodedLocalName());
+        }
     }
 
     /**
@@ -244,11 +277,15 @@ public class DestinationName implements ServiceUnitId {
      * @return
      */
     public String getLookupName() {
-        return String.format("%s/%s/%s/%s/%s", domain, property, cluster, namespacePortion, getEncodedLocalName());
+        if (cluster == null) {
+            return String.format("%s/%s/%s/%s", domain, property, namespacePortion, getEncodedLocalName());
+        } else {
+            return String.format("%s/%s/%s/%s/%s", domain, property, cluster, namespacePortion, getEncodedLocalName());
+        }
     }
 
     public boolean isGlobal() {
-        return "global".equals(cluster);
+        return cluster == null || Constants.GLOBAL_CLUSTER.equalsIgnoreCase(cluster);
     }
 
     @Override
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/naming/NamespaceName.java b/pulsar-common/src/main/java/org/apache/pulsar/common/naming/NamespaceName.java
index a179f9a..8ff474d 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/naming/NamespaceName.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/naming/NamespaceName.java
@@ -20,8 +20,6 @@ package org.apache.pulsar.common.naming;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
@@ -35,10 +33,10 @@ public class NamespaceName implements ServiceUnitId {
 
     private final String namespace;
 
-    private String property;
-    private String cluster;
-    private String localName;
-    
+    private final String property;
+    private final String cluster;
+    private final String localName;
+
     private static final LoadingCache<String, NamespaceName> cache = CacheBuilder.newBuilder().maximumSize(100000)
             .expireAfterAccess(30, TimeUnit.MINUTES).build(new CacheLoader<String, NamespaceName>() {
                 @Override
@@ -47,6 +45,11 @@ public class NamespaceName implements ServiceUnitId {
                 }
             });
 
+    public static NamespaceName get(String property, String namespace) {
+        validateNamespaceName(property, namespace);
+        return get(property + '/' + namespace);
+    }
+
     public static NamespaceName get(String property, String cluster, String namespace) {
         validateNamespaceName(property, cluster, namespace);
         return get(property + '/' + cluster + '/' + namespace);
@@ -68,14 +71,37 @@ public class NamespaceName implements ServiceUnitId {
     }
 
     private NamespaceName(String namespace) {
-        try {
-            checkNotNull(namespace);
-        } catch (NullPointerException e) {
+        if (namespace == null || namespace.isEmpty()) {
             throw new IllegalArgumentException("Invalid null namespace: " + namespace);
         }
 
         // Verify it's a proper namespace
-        validateNamespaceName(namespace);
+        // The namespace name is composed of <property>/<namespace>
+        // or in the legacy format with the cluster name:
+        // <property>/<cluster>/<namespace>
+        try {
+
+            String[] parts = namespace.split("/");
+            if (parts.length == 2) {
+                // New style namespace : <property>/<namespace>
+                validateNamespaceName(parts[0], parts[1]);
+
+                property = parts[0];
+                cluster = null;
+                localName = parts[1];
+            } else if (parts.length == 3) {
+                // Old style namespace: <property>/<cluster>/<namespace>
+                validateNamespaceName(parts[0], parts[1], parts[2]);
+
+                property = parts[0];
+                cluster = parts[1];
+                localName = parts[2];
+            } else {
+                throw new IllegalArgumentException("Invalid namespace format. namespace: " + namespace);
+            }
+        } catch (NullPointerException e) {
+            throw new IllegalArgumentException("Invalid namespace format. namespace: " + namespace, e);
+        }
         this.namespace = namespace;
     }
 
@@ -83,6 +109,7 @@ public class NamespaceName implements ServiceUnitId {
         return property;
     }
 
+    @Deprecated
     public String getCluster() {
         return cluster;
     }
@@ -92,7 +119,7 @@ public class NamespaceName implements ServiceUnitId {
     }
 
     public boolean isGlobal() {
-        return "global".equals(cluster);
+        return cluster == null || Constants.GLOBAL_CLUSTER.equalsIgnoreCase(cluster);
     }
 
     public String getPersistentTopicName(String localTopic) {
@@ -136,46 +163,37 @@ public class NamespaceName implements ServiceUnitId {
         return namespace.hashCode();
     }
 
-    public static void validateNamespaceName(String property, String cluster, String namespace) {
+    public static void validateNamespaceName(String property, String namespace) {
         try {
             checkNotNull(property);
-            checkNotNull(cluster);
             checkNotNull(namespace);
-            if (property.isEmpty() || cluster.isEmpty() || namespace.isEmpty()) {
+            if (property.isEmpty() || namespace.isEmpty()) {
                 throw new IllegalArgumentException(
-                        String.format("Invalid namespace format. namespace: %s/%s/%s", property, cluster, namespace));
+                        String.format("Invalid namespace format. namespace: %s/%s", property, namespace));
             }
             NamedEntity.checkName(property);
-            NamedEntity.checkName(cluster);
             NamedEntity.checkName(namespace);
         } catch (NullPointerException e) {
             throw new IllegalArgumentException(
-                    String.format("Invalid namespace format. namespace: %s/%s/%s", property, cluster, namespace), e);
+                    String.format("Invalid namespace format. namespace: %s/%s/%s", property, namespace), e);
         }
     }
 
-    private void validateNamespaceName(String namespace) {
-        // assume the namespace is in the form of <property>/<cluster>/<namespace>
+    public static void validateNamespaceName(String property, String cluster, String namespace) {
         try {
+            checkNotNull(property);
+            checkNotNull(cluster);
             checkNotNull(namespace);
-            String testUrl = String.format("http://%s", namespace);
-            URI uri = new URI(testUrl);
-            checkNotNull(uri.getPath());
-            NamedEntity.checkURI(uri, testUrl);
-
-            String[] parts = uri.getPath().split("/");
-            if (parts.length != 3) {
-                throw new IllegalArgumentException("Invalid namespace format. namespace: " + namespace);
+            if (property.isEmpty() || cluster.isEmpty() || namespace.isEmpty()) {
+                throw new IllegalArgumentException(
+                        String.format("Invalid namespace format. namespace: %s/%s/%s", property, cluster, namespace));
             }
-            validateNamespaceName(uri.getHost(), parts[1], parts[2]);
-
-            property = uri.getHost();
-            cluster = parts[1];
-            localName = parts[2];
-        } catch (URISyntaxException e) {
-            throw new IllegalArgumentException("Invalid namespace format. namespace: " + namespace, e);
+            NamedEntity.checkName(property);
+            NamedEntity.checkName(cluster);
+            NamedEntity.checkName(namespace);
         } catch (NullPointerException e) {
-            throw new IllegalArgumentException("Invalid namespace format. namespace: " + namespace, e);
+            throw new IllegalArgumentException(
+                    String.format("Invalid namespace format. namespace: %s/%s/%s", property, cluster, namespace), e);
         }
     }
 
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/naming/DestinationNameTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/naming/DestinationNameTest.java
index 74574d8..caa16fa 100644
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/naming/DestinationNameTest.java
+++ b/pulsar-common/src/test/java/org/apache/pulsar/common/naming/DestinationNameTest.java
@@ -119,13 +119,6 @@ public class DestinationNameTest {
         }
 
         try {
-            DestinationName.get("persistent://property/cluster/namespace");
-            fail("Should have raised exception");
-        } catch (IllegalArgumentException e) {
-            // Ok
-        }
-
-        try {
             DestinationName.get("property/cluster/namespace/destination");
             fail("Should have raised exception");
         } catch (IllegalArgumentException e) {
@@ -223,4 +216,28 @@ public class DestinationNameTest {
         assertEquals(name.getEncodedLocalName(), encodedName);
         assertEquals(name.getPersistenceNamingEncoding(), "prop/colo/ns/persistent/" + encodedName);
     }
+
+    @Test
+    public void testTopicNameWithoutCluster() throws Exception {
+        DestinationName dn = DestinationName.get("persistent://property/namespace/destination");
+
+        assertEquals(dn.getNamespace(), "property/namespace");
+
+        assertEquals(dn, DestinationName.get("persistent", "property", "namespace", "destination"));
+
+        assertEquals(dn.hashCode(),
+                DestinationName.get("persistent", "property", "namespace", "destination").hashCode());
+
+        assertEquals(dn.toString(), "persistent://property/namespace/destination");
+        assertEquals(dn.getDomain(), DestinationDomain.persistent);
+        assertEquals(dn.getProperty(), "property");
+        assertEquals(dn.getCluster(), null);
+        assertEquals(dn.getNamespacePortion(), "namespace");
+        assertEquals(dn.getNamespace(), "property/namespace");
+        assertEquals(dn.getLocalName(), "destination");
+
+        assertEquals(dn.getEncodedLocalName(), "destination");
+        assertEquals(dn.getPartitionedTopicName(), "persistent://property/namespace/destination");
+        assertEquals(dn.getPersistenceNamingEncoding(), "property/namespace/persistent/destination");
+    }
 }
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/naming/NamespaceNameTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/naming/NamespaceNameTest.java
index 7d6a7cf..8d13c24 100644
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/naming/NamespaceNameTest.java
+++ b/pulsar-common/src/test/java/org/apache/pulsar/common/naming/NamespaceNameTest.java
@@ -60,13 +60,6 @@ public class NamespaceNameTest {
         }
 
         try {
-            NamespaceName.get("property/namespace");
-            fail("Should have raised exception");
-        } catch (IllegalArgumentException e) {
-            // Ok
-        }
-
-        try {
             NamespaceName.get("property/cluster/namespace/destination");
             fail("Should have raised exception");
         } catch (IllegalArgumentException e) {
@@ -128,13 +121,6 @@ public class NamespaceNameTest {
         }
 
         try {
-            NamespaceName.get("_pulsar/cluster/namespace");
-            fail("Should have raised exception");
-        } catch (IllegalArgumentException e) {
-            // Ok
-        }
-
-        try {
             NamespaceName.get(null, "cluster", "namespace");
             fail("Should have raised exception");
         } catch (IllegalArgumentException e) {
@@ -177,13 +163,6 @@ public class NamespaceNameTest {
         }
 
         try {
-            NamespaceName.get("pulsar/cluster/");
-            fail("Should have raised exception");
-        } catch (IllegalArgumentException e) {
-            // Ok
-        }
-
-        try {
             NamespaceName.get("pulsar", "cluster", null);
             fail("Should have raised exception");
         } catch (IllegalArgumentException e) {
@@ -202,4 +181,14 @@ public class NamespaceNameTest {
         assertEquals(v2Namespace.getCluster(), "colo1");
         assertEquals(v2Namespace.getLocalName(), "testns-1");
     }
+
+    @Test
+    void testNewScheme() {
+        NamespaceName ns = NamespaceName.get("my-tenant/my-namespace");
+        assertEquals(ns.getProperty(), "my-tenant");
+        assertEquals(ns.getLocalName(), "my-namespace");
+        assertEquals(ns.isGlobal(), true);
+        assertEquals(ns.getCluster(), null);
+        assertEquals(ns.getPersistentTopicName("my-topic"), "persistent://my-tenant/my-namespace/my-topic");
+    }
 }

-- 
To stop receiving notification emails like this one, please contact
mmerli@apache.org.